pmo: cutover complete — container drives Beta, 521 closed, 497 parked

This commit is contained in:
2026-08-29 07:50:23 -05:00
parent 9042159698
commit b17ea8ae8f
3 changed files with 152 additions and 1 deletions
+142
View File
@@ -0,0 +1,142 @@
# REPORT — 2026-08-29 15:00 — Bootstrap: runtime container + tool-widening (Redmine 521 + 523)
- turn: FINAL crush turn on the harness repo (q15), Charles directive
"alpha asap, then kill the screen/crush stack; bootstrap the container FIRST"
- commits pushed to `ukrrs/MOPAC` main: `064aed0` (brief 495 landing),
`e1b519d` (file tools), `e905515` (loop wiring + bound + heartbeat),
`02013d3` (runtime container), `19d61a0` (docs)
- the `mopac-loop` compose service is RUNNING and healthy as of writing;
it processed alpha task 523 autonomously end-to-end (proof below)
## 1. Runtime container (Part A)
Files: `deploy/Dockerfile.runtime`, `deploy/compose.yaml` (runbook appended
to `deploy/runbook.md`, README section added).
- Image: digest-pinned `alpine@sha256:143583...` (alpine 3.22) + the
STATIC release binary (`make release`, CGO off, built in the
digest-pinned golang builder — never on the host) + `bash` (the bash
tool / dev.sh), `git` (turn commits), `docker-cli` (dev.sh builder
client). No ENTRYPOINT: SIGINT graceful stop is Go's; zombie reaping is
docker's built-in init (`init: true` in compose — docker's tini, zero
extra packages, image stays digest-pure).
- Same-path bind trick: the repo is mounted at its HOST path
(`HARNESS_REPO`, default `/home/reachableceo/projects/meta/MOPAC/harness`)
so a turn's `./dev.sh build` inside the container bind-mounts a path the
HOST docker daemon understands — builders remain digest-pinned
siblings; nothing toolchain-shaped touches the host. `harness.toml`
overlays read-only (turns can edit the repo, not their own gate).
- Secrets: `~/.coordinate/secrets/mopac-loop/` (0700): `loop.env` (0600,
HARNESS_REDMINE_KEY + HARNESS_LITELLM_KEY), container `.gitconfig` +
`git-credentials` (0600, gitea token via credential store — turn pushes
authenticate; commits attributed to the vertical).
- Placement knobs: `HARNESS_REPO`, `HARNESS_SECRETS`, `HARNESS_UID/GID`
(1001 on this host — the first boot caught the 1000 default and was
fixed by parameterizing).
### Healthcheck design (no port)
`harness loop --status-file state/loop/status.json` maintains a heartbeat
JSON — beats on EVERY scan plus a safety ticker at min(poll/3, 30s), so a
long turn (up to the 1800s bound) never stales it past the threshold. The
compose healthcheck is pure mtime age: unhealthy when older than 360s
(= 3x the default 120s poll; keep in sync if the poll changes). Content:
`{vertical, pid, started_at, running, scans, dispatched, reports,
last_scan_at, last_scan_error, last_beat}``running:false` on clean
stop, `last_scan_error` set while Redmine is down but the daemon lives
(that is deliberately NOT unhealthy: scan errors retry next tick).
### Container instructions
```sh
cd ~/projects/meta/MOPAC/harness
make release # static binary
docker compose -f deploy/compose.yaml up -d --build # up
docker compose -f deploy/compose.yaml logs -f mopac-loop
docker inspect mopac-loop --format '{{.State.Health.Status}}'
docker compose -f deploy/compose.yaml down # KILL SWITCH
```
Verified live: `state=running restart=unless-stopped health=healthy`.
## 2. Tool-widening (Part B, Redmine 521)
- File tools (`internal/tools/files.go`): `read` (numbered lines,
offset/limit, binary refusal, byte cap), `edit` (exact-match; unique
unless `replace_all`; never writes on failure), `write` (NEW files only
— no clobbering), `ls`, `glob` (`**` support, mtime-sorted, capped),
`grep` (RE2, mtime-sorted file list, capped). Deny-first like the bash
gate: anything outside `work_root` — absolute, `..`, or symlink escapes
(canonicalized via deepest existing ancestor) — returns `ErrDenied`,
counted and fed back to the model with the same semantics as bash
denials. Writes are temp+rename atomic. Every call leaves one audit
line on stdout (tool name + outcome only, never arguments).
- Bash gate: allow-list extended MINIMALLY to the build surface —
`./dev.sh *`, `make check`, `make release`, `git add *`, `git commit *`,
`git push *` (both `harness.toml.example` and live `harness.toml`).
Enforced by `TestShippedAllowListCoversDevAndGit`, which parses
`harness.toml.example` itself so the preset cannot drift from the
acceptance (dev.sh build/vet/test/check + git commit/push pass;
sudo/curl/docker run/python3/go run still default-deny).
- Turn budget: `[loop] turn_timeout_secs` (default 1800, 0 = off). An
expired turn keeps partial content, writes a partial REPORT with
stop_reason `turn_timeout`, and releases the task (the pre-turn dedup
marker prevents hot-looping; PMO re-releases by updating the issue).
## 3. Test results (`./dev.sh check` — build + vet + test, docker builder)
All green: brief, config, events, intake, llm, loop, models, quota,
serve, tools, writeback. New coverage:
- file-tool gate denials (absolute/`..`/symlink escapes → ErrDenied;
missing file = plain error, not denial), edit exact-match semantics
(not-found no-write, ambiguity count, replace_all, old==new refusal),
write new-files-only, read offset/limit + truncation, ls/glob/grep
scoping + include filter + invalid regexp
- timeout expiry: partial REPORT written with `turn_timeout`, err chain
carries ErrTurnTimeout, zero disables the bound
- allow-list acceptance vs the shipped preset (see above)
- heartbeat: file appears + fields, `running:false` after clean stop,
mtime keeps refreshing while scans cycle (poll=1s), scan errors
recorded without flipping running
- END-TO-END fake-Redmine loop turn: reads a file, EDITS it (verified on
disk), runs an allow-listed no-op bash command, REPORTs — note lands on
the issue, status transitions per the map, tool audit lines present;
a denied write outside the root still completes the turn with the note
## 4. Live autonomous proof (the alpha queue)
Old dev.sh loop container drained its queue (519 writebacks recovered,
523 turn completed 12:19Z), then was stopped BY NAME and replaced:
`docker stop mopac-loop``docker compose up -d`. The new container's
scan 2 dispatched re-queued task **523** (the exit-gate ticket itself) at
12:22Z and completed it autonomously at 12:31Z on glm-4.7-flash:
- tools used live: bash (2 ok), glob (2 ok), read (2 ok) — the new file
palette in production
- gate denials live: `docker ps -a` and `screen -ls` refused with
model-facing feedback (default deny held)
- REPORT `reports/REPORT-demo-523-20260829-123112.md` (rounds=8,
tool_calls=8, denied=2, stop=round_limit), Redmine note posted,
status New → Resolved
- heartbeat counted it: `{"scans":2,"dispatched":1,"reports":1}`,
health=healthy throughout
## 5. Notes / handoff
- The killed 495 turn's completed-but-uncommitted work (brief surface,
discourse client vendored) was landed first as its own commit so 521
started from a green tree; LIVE discourse delivery on 495 remains open
(queue task 519's follow-up belongs to the loop now — it has the tools).
- 523's flash turn hit the 8-round bound while exploring (stop=
round_limit, 23.8k tokens). If turns routinely saturate rounds on
study-class tasks, consider `[loop] max_rounds` 8 → 12 for the alpha
queue; not changed unilaterally.
- `harness.toml` (live) updated with `turn_timeout_secs = 1800`,
`[tools.files] enabled = true`, extended allow-list — file is
gitignored, mirrored into `harness.toml.example` (committed).
- Kill switch documented in compose header, README, runbook:
`docker compose -f deploy/compose.yaml down`. No broad pkill was used
at any point (`docker stop mopac-loop` by name only).
- The crush/screen stack can now be retired per the exit gate: the loop
is containerized, restart-policied, healthchecked, and its turns can
do build work through the same digest-pinned docker builder discipline.