feat(runtime): state persistence + proposals + supervisor hot-restart #6

Merged
halofourteen merged 1 commits from feat/state-persist-proposals into main 2026-05-25 16:20:10 +03:00
halofourteen commented 2026-05-25 16:20:02 +03:00 (Migrated from github.com)

Summary

Closes the self-improvement loop:

```
reflex fails 3× → proposal markdown file → operator approves in TUI →
npm run propose:apply → spawn Pi on a feature branch →
Pi commits the patch → supervisor restarts the child on file change
```

Operator is in the loop at the approval step and the diff-review step — Pi never pushes or merges on its own.

Pieces

  • `runtime/state-store.js` — atomic write-rename for `current-task.json`, daily diary append, proposals/ + proposals/approved/ helpers.
  • `runtime/bot.js`:
    • `ctx.dispatch` writes `current-task.json` on start, updates on completion / failure / throw.
    • Failure tracker: 3 consecutive same-label failures → `writeProposal()` with the snapshot, label history, and a "next-step" section. 30 min cooldown.
    • Startup logs previous task + pending-proposal count.
    • On death: clears current-task, writes diary line.
    • New IPC commands: `PROPOSAL_LATEST`, `PROPOSAL_APPROVE`.
  • `tui/tui.tsx` — status bar badge `[proposals N, press y]`. Hotkey `y` opens the latest proposal; `y` approves, `n`/Esc closes.
  • `scripts/propose-apply.js` — given an approved filename, creates `feat/proposal-` branch, spawns `pi -p` with proposal + repo-conventions prompt. Refuses on dirty tree.
  • `runtime/supervisor.js` — forks `bot.js` as a child, watches `runtime/*.js`, restarts on file change or exit code 42. Rate-limited 5 restarts/min. Clean SIGINT/SIGTERM. `npm run bot` goes through supervisor; `npm run bot:bare` is the unsupervised escape hatch.

Why supervisor instead of true ESM hot-reload

True module hot-reload in Node ESM means dealing with import-cache poisoning, the open mineflayer client, the IPC socket, the path-finder plugin's WeakMap. Restart-on-change is the same outcome with none of the gotchas — the bot loses a TCP connection it would reconnect after a kick anyway.

Smoke test

  • `npm run bot` brings up supervisor + child, child connects + AuthMe login + spawn at (623, 85, 95).
  • Diary line written: `13:18:46 spawned at 623,85,95`.
  • SIGTERM forwards cleanly, IPC socket removed, current-task.json reset to `{}`.

Test plan

  • Trigger a real proposal: park the bot in dark_forest, let `flee from zombie` fail 3×, verify a proposal markdown lands under `state//proposals/`.
  • Open TUI, see `[proposals 1, press y]` badge, press `y`, read the proposal, press `y` to approve.
  • Run `npm run propose:apply ` and verify Pi gets the prompt and a new feature branch.
  • Edit `runtime/reflex.js` while supervisor is up → supervisor logs restart → bot reconnects.

🤖 Generated with Claude Code

## Summary Closes the self-improvement loop: \`\`\` reflex fails 3× → proposal markdown file → operator approves in TUI → npm run propose:apply <file> → spawn Pi on a feature branch → Pi commits the patch → supervisor restarts the child on file change \`\`\` Operator is in the loop at the approval step and the diff-review step — Pi never pushes or merges on its own. ## Pieces - **\`runtime/state-store.js\`** — atomic write-rename for \`current-task.json\`, daily diary append, proposals/ + proposals/approved/ helpers. - **\`runtime/bot.js\`**: - \`ctx.dispatch\` writes \`current-task.json\` on start, updates on completion / failure / throw. - Failure tracker: 3 consecutive same-label failures → \`writeProposal()\` with the snapshot, label history, and a "next-step" section. 30 min cooldown. - Startup logs previous task + pending-proposal count. - On death: clears current-task, writes diary line. - New IPC commands: \`PROPOSAL_LATEST\`, \`PROPOSAL_APPROVE\`. - **\`tui/tui.tsx\`** — status bar badge \`[proposals N, press y]\`. Hotkey \`y\` opens the latest proposal; \`y\` approves, \`n\`/Esc closes. - **\`scripts/propose-apply.js\`** — given an approved filename, creates \`feat/proposal-<slug>\` branch, spawns \`pi -p\` with proposal + repo-conventions prompt. Refuses on dirty tree. - **\`runtime/supervisor.js\`** — forks \`bot.js\` as a child, watches \`runtime/*.js\`, restarts on file change or exit code 42. Rate-limited 5 restarts/min. Clean SIGINT/SIGTERM. \`npm run bot\` goes through supervisor; \`npm run bot:bare\` is the unsupervised escape hatch. ## Why supervisor instead of true ESM hot-reload True module hot-reload in Node ESM means dealing with import-cache poisoning, the open mineflayer client, the IPC socket, the path-finder plugin's WeakMap. Restart-on-change is the same outcome with none of the gotchas — the bot loses a TCP connection it would reconnect after a kick anyway. ## Smoke test - \`npm run bot\` brings up supervisor + child, child connects + AuthMe login + spawn at (623, 85, 95). - Diary line written: \`13:18:46 spawned at 623,85,95\`. - SIGTERM forwards cleanly, IPC socket removed, current-task.json reset to \`{}\`. ## Test plan - [ ] Trigger a real proposal: park the bot in dark_forest, let \`flee from zombie\` fail 3×, verify a proposal markdown lands under \`state/<host>/proposals/\`. - [ ] Open TUI, see \`[proposals 1, press y]\` badge, press \`y\`, read the proposal, press \`y\` to approve. - [ ] Run \`npm run propose:apply <filename>\` and verify Pi gets the prompt and a new feature branch. - [ ] Edit \`runtime/reflex.js\` while supervisor is up → supervisor logs restart → bot reconnects. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.