Files
mopac-pmo/archive/REPORT-20260828-2131-selfhost-core.md

5.2 KiB

REPORT — self-host core: harness loop replaces the bash stack — 2026-08-28 21:31

Deliverable for "get to be self hosting asap — smallest core that can bootstrap itself (redmine/gitea interaction fine, discourse later)". Pushed to ukrrs/MOPAC main as 86c39c8 (module rename) → c869ac1 (keyproxy/config) → ecc0ee8 (loop daemon) → 2901bb8 (docs + dev.sh loop runner).

What runs now

The MOPAC harness (Go, stdlib only, built in the digest-pinned Docker builder — host stays toolchain-free) drives ITSELF. Redmine is the SoR, the conductor is the worker. Per issue in the released scope whose (id, updated_on) has not been processed:

  1. one bounded turn (same conductor harness once uses; sequential, one at a time — v0, no concurrency knob yet);
  2. REPORT file lands in reports/ (as before);
  3. REPORT body is POSTed back onto the issue as a Redmine journal note (the previously-stubbed note writeback is now implemented);
  4. status transitions per [redmine.status_map] (default shipped: "In Progress" = "Done"; names resolved via /issue_statuses.json);
  5. optional Gitea REPORT commit ([gitea] commit_reports, OFF by default);
  6. dedup marker refreshed to the post-writeback updated_on, so the loop's own note never re-triggers itself (self-retrigger hazard handled + test-asserted).

State: append-only state/loop/loop.jsonl — one JSON line per action (dispatch / report / note / status / commit / refresh / error), dedup index rebuilt at startup, restarts keep exactly-once-reaction semantics. One line per action goes to stdout as well. A FAILED turn is recorded, NOT retried (update the issue to re-release it) — a down proxy cannot hot-loop the poll.

Secrets: key_ref = "mpk:mpk-redmine" style refs resolve through mopac-keyproxy POST /v1/resolve (bearer from [keyproxy] token_ref, 60s in-memory cache). env:/file:/literal: refs keep working, so the loop runs with or without keyproxy up. No hosts hardcoded — all in harness.toml. Also folded in: module renamed git.knownelement.com/reachableceo/MOPAC/harnessukrrs.com/mopac/harness (the doc-rot finding), verified build+vet+test in the builder.

Tests: loop end-to-end against a fake Redmine (mutable: notes bump updated_on like the real SoR) + fake OpenAI server — dispatch/note/ status happy path, dedup on rescan, re-dispatch on issue update, failed turn without hot-loop, dry-run zero-trace, restart dedup survival, status-map miss, gitea create/update, keyproxy 401/404/cache. Full ./dev.sh check (build + vet + test) clean on go1.26.

Exact command to start the loop

From /home/reachableceo/projects/meta/MOPAC/harness (harness.toml is already in place; it is gitignored):

./dev.sh build
HARNESS_REDMINE_KEY=<key> HARNESS_LITELLM_KEY=<vertical virtual key> \
  ./dev.sh loop

That runs the daemon in the Docker builder container with the repo bind-mounted (REPORTs + state land in the repo). Verify first with a zero-cost scan:

HARNESS_REDMINE_KEY=<key> HARNESS_LITELLM_KEY=<key> \
  ./dev.sh loop --dry-run        # prints "would dispatch <id>" per new issue, no turns, no state
./dev.sh loop --once             # single scan (cron-able), exit 2 if the intake itself fails

Stop: Ctrl-C (SIGINT) in the foreground, or docker stop mopac-loop. Interval default 120s ([loop] poll_interval_secs), override with -interval 60s. With keyproxy up instead of env keys: [keyproxy] url/token_ref in harness.toml + mpk: refs (see harness.toml.example, all commented).

What it replaces — the die list (NOT deleted yet, pending your sign-off)

  1. /home/_crossfeed/tooling/agent-stack/semaphore.sh + slot-file concurrency cap → replaced by the loop's in-process discipline (v0: one turn at a time; concurrency knob later if load justifies).
  2. ~/.coordinate/scripts/queue-next.sh, queue-after.sh, pmo-heartbeat.sh → replaced by the loop + its own wake discipline (poll interval; --once for cron). The heartbeat screen (pmo-heartbeat, PID 2924085) can be killed whenever you say so.
  3. dispatch-turn.sh doorbell screen -S reachableceo-PMO -X stuff — note: ALREADY dead. The screen is named RCEO-PMO, so the message was silently dropped all along. Mentioned for the record; deletion after your sign-off.

Nothing in /home/_crossfeed or ~/.coordinate/scripts was touched this turn. harness once (manual/chained path) also unchanged in behavior — file-only writeback, no notes.

Rollback

Just stop the loop (Ctrl-C / docker stop mopac-loop). The bash stack is fully present and untouched — relaunch it exactly as before. The only new artifacts are state/loop/loop.jsonl (delete to forget all dedup history) and REPORT files. No config the old stack reads was modified.

Suggested verification (PMO)

  1. ./dev.sh loop --dry-run with the real Redmine — should list the released-scope issues it would dispatch, zero LLM calls.
  2. Pick one low-stakes issue, drop it to "In Progress" in scope, run ./dev.sh loop --once with keys: expect turn → REPORT file → journal note on the issue → status → Done.
  3. Check state/loop/loop.jsonl for the one-line-per-action trail.

— Work stack, 2026-08-28 ~22:30