lazycodex-ai 5.0.0-beta.62 → 5.0.0-beta.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
  2. package/dist/cli/index.js +64 -35
  3. package/dist/cli-node/index.js +64 -35
  4. package/package.json +2 -6
  5. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  6. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -0
  7. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  8. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  9. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  10. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  11. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  12. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  13. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  14. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  15. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  16. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  17. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  19. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
  25. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +5 -4
  26. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  29. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  30. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +13 -0
  33. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +5 -4
  34. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +5 -4
  35. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +582 -322
  36. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +5 -0
  37. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +16 -12
  38. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
  39. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
  40. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +2 -1
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +48 -22
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
  45. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
  46. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
  47. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
  48. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
  49. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
  50. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +3 -4
  51. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +12 -5
  52. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
  53. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +8 -62
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
  56. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
  57. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
  58. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
  59. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
  60. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +5 -5
  61. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
  62. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  63. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  64. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
  65. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +5 -4
  66. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +5 -0
  67. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +16 -16
  68. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
  69. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
  70. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +58 -51
  71. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
  72. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
  73. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
  74. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +3 -3
  75. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +30 -5
  76. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
  77. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +8 -59
  78. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
  79. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
  80. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
  81. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +5 -4
  82. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
  83. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
  84. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
  85. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
  86. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
  88. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
  89. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
  90. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
  93. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
  94. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
  95. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
  96. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
  97. package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
  99. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
  100. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  102. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  103. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  104. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  105. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  106. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  116. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  117. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  118. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  119. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  120. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  121. package/packages/omo-codex/plugin/package-lock.json +12 -12
  122. package/packages/omo-codex/plugin/package.json +1 -1
  123. package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
  124. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +15 -7
  125. package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
  126. package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
  127. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  128. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
  129. package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  130. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  131. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
  132. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  133. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
  134. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  135. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  136. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  137. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  138. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  139. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  140. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  141. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  142. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  143. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  144. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +5 -4
  145. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +1 -1
  146. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  147. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
  148. package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
  149. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
  150. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
  151. package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
  152. package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
  153. package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
  154. package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
  155. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  156. package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
  157. package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
  158. package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  159. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  160. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  161. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
  162. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  163. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
  164. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  165. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  166. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  167. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  168. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  169. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  170. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  171. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  172. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  173. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  174. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
  175. package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
  176. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  177. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
  178. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
  179. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.62) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 5.0.0-beta.63) Checking Ulw-Loop Steering"
11
11
  }
12
12
  ]
13
13
  }
@@ -20,7 +20,7 @@
20
20
  "type": "command",
21
21
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use-spawn",
22
22
  "timeout": 5,
23
- "statusMessage": "(OmO 5.0.0-beta.62) Recording Spawn Admission"
23
+ "statusMessage": "(OmO 5.0.0-beta.63) Recording Spawn Admission"
24
24
  }
25
25
  ]
26
26
  }
@@ -33,7 +33,7 @@
33
33
  "type": "command",
34
34
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
35
35
  "timeout": 5,
36
- "statusMessage": "(OmO 5.0.0-beta.62) Enforcing Unlimited Ulw-Loop Budget"
36
+ "statusMessage": "(OmO 5.0.0-beta.63) Enforcing Unlimited Ulw-Loop Budget"
37
37
  }
38
38
  ]
39
39
  },
@@ -44,7 +44,7 @@
44
44
  "type": "command",
45
45
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
46
46
  "timeout": 5,
47
- "statusMessage": "(OmO 5.0.0-beta.62) Guarding Ulw-Loop Spawns"
47
+ "statusMessage": "(OmO 5.0.0-beta.63) Guarding Ulw-Loop Spawns"
48
48
  }
49
49
  ]
50
50
  }
@@ -56,7 +56,7 @@
56
56
  "type": "command",
57
57
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
58
58
  "timeout": 10,
59
- "statusMessage": "(OmO 5.0.0-beta.62) Checking Ulw-Loop Resume"
59
+ "statusMessage": "(OmO 5.0.0-beta.63) Checking Ulw-Loop Resume"
60
60
  }
61
61
  ]
62
62
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ulw-loop",
3
- "version": "5.0.0-beta.62",
3
+ "version": "5.0.0-beta.63",
4
4
  "description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -20,7 +20,7 @@ Run each criterion's real-surface proof yourself through the channel that faithf
20
20
 
21
21
  1. **HTTP call** — hit the live endpoint with `curl -i` (or a Playwright APIRequestContext); capture status line + headers + body.
22
22
  2. **Terminal / TUI** - prove it through the xterm.js web terminal; tmux `send-keys` is fine for a boot smoke, but NEVER `tmux capture-pane` for color/layout/CJK evidence (it degrades truecolor).
23
- 3. **Browser use** — in Codex, use `browser:control-in-app-browser` first when available and the scenario does not need an authenticated or persistent user browser profile. Otherwise use Chrome to drive the REAL page; if unavailable, use agent-browser. Capture action log + screenshot path. Never downgrade a browser-facing criterion.
23
+ 3. **Browser use** — in Codex, prefer `browser:control-in-app-browser`. Otherwise, or for Chrome semantics, stealth, trace, or auth, WRITE a `playwright-core` script and run it from js eval against local Chrome (`channel: "chrome"`; persistent context on a CLONED profile, never the live one). Capture action log + screenshot path. Never downgrade a browser-facing criterion.
24
24
  4. **Computer use** — for desktop/GUI apps, drive the running app via OS automation (computer-use, AppleScript, xdotool, etc.); capture action log + screenshot.
25
25
 
26
26
  For TUI visual QA (mandatory when a PR or review must inspect the terminal screen),
@@ -165,7 +165,7 @@ Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures a
165
165
  4. INTEGRATE + CRITICAL SELF-QA + GIT CHECKPOINT (EVERY WORKER RETURN): do NOT trust the worker's report. Read the diff yourself, re-run its tests, and run LSP diagnostics on the changed files. Treat "done" as a claim to disprove. If the diff drifts, the test is hollow, or evidence is missing, RESPAWN the worker with the specific failure context. Once the work unit is verified, use `git-master` before staging: inspect recent repository commits and touched-path history to infer commit language, Conventional Commit scope, message shape, and unit size. Stage only that unit's files and commit in the observed style; do not carry verified work forward into a later omnibus commit. If no git-tracked files changed or committing is unsafe, record the no-commit reason as evidence. Forward every finding/learning to subsequent workers.
166
166
  5. EXECUTE-AS-SCENARIO: ACTUALLY run the Manual-QA scenario the criterion named (channel table above). Run it yourself for the orchestrator check; for heavier flows dispatch a dedicated QA execution worker (`lazycodex-worker-medium` by default; `lazycodex-worker-high` when the QA flow itself is hard) whose ONLY job is to drive the channel and write the artifact to the named evidence path. If the scenario FAILS, respawn the implementing worker with the captured failure — do not hand-patch around it.
167
167
  6. CAPTURE: collect the observable artifact path: transcript, stdout, screenshot, assertion, status+body, diff, or parsed dump. No artifact written at the evidence path — not done; record BLOCKED and respawn QA.
168
- 7. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 5 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars, AND close every finished worker (v1 `close_agent`; on V2 finished workers end on their own — `interrupt_agent` any still running). Register each teardown as its own todo the moment the QA spawns the resource (scripts, tmux assets, browsers / agent-browser sessions, PIDs, ports) so none is forgotten. Embed a one-line cleanup receipt in the evidence string, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo; rm -rf /tmp/ulw.aB12cD; interrupt_agent w-3`. Missing receipt → record BLOCKED, not PASS.
168
+ 7. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 5 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars, AND close every finished worker (v1 `close_agent`; on V2 finished workers end on their own — `interrupt_agent` any still running). Register each teardown as its own todo the moment the QA spawns the resource (scripts, tmux assets, browser contexts, PIDs, ports) so none is forgotten. Embed a one-line cleanup receipt in the evidence string, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo; rm -rf /tmp/ulw.aB12cD; interrupt_agent w-3`. Missing receipt → record BLOCKED, not PASS.
169
169
  8. RECORD one result immediately from the artifact you just wrote — never from memory or a later turn — stamping the capture tree `$(git rev-parse --short "HEAD^{tree}")` into the evidence:
170
170
  - PASS: `omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass --evidence "<observable> @tree:<short-tree> | <cleanup receipt>" --json`
171
171
  - FAIL: `omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status fail --evidence "<observable> @tree:<short-tree> | <cleanup receipt>" --notes "<diagnosis>" --json`
@@ -10,7 +10,8 @@ import { requireAllCriteriaPass, requireAllPlanCriteriaPass, requireEssentialCri
10
10
  import { codexGoalMode, isFinalRunCompletionCandidate } from "./goal-status.js";
11
11
  import type { UlwLoopScope } from "./paths.js";
12
12
  import { ulwLoopAttemptEvidenceDir } from "./paths.js";
13
- import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
13
+ import { commit } from "./plan-commit.js";
14
+ import { readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
14
15
  import {
15
16
  classifyExternalAuthorizationBlocker,
16
17
  clearGoalBlockerFields,
@@ -226,11 +227,11 @@ export async function checkpointUlwLoop(
226
227
  if (aggregateCompletion !== undefined) plan.aggregateCompletion = aggregateCompletion;
227
228
  if (aggregateCompletion !== undefined) nextActions = [...nextActions, 'aggregate complete — now update_goal({status:"complete"})'];
228
229
  plan.updatedAt = now;
229
- await writePlan(repoRoot, plan, scope);
230
230
  const ledgerEntry = buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion);
231
- await appendLedger(repoRoot, ledgerEntry, scope);
231
+ const entries = [ledgerEntry];
232
232
  const closedBatch = args.status === "complete" ? batchClosedBy(plan, goal.id) : undefined;
233
- if (closedBatch !== undefined) await appendLedger(repoRoot, { at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId }, scope);
233
+ if (closedBatch !== undefined) entries.push({ at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId });
234
+ await commit(repoRoot, scope, { plan, entries });
234
235
  return aggregateCompletion === undefined
235
236
  ? { plan, goal, ledgerEntry, nextActions, warnings }
236
237
  : { plan, goal, ledgerEntry, aggregateCompletion, nextActions, warnings };
@@ -63,6 +63,9 @@ export interface UlwLoopValidationBatch {
63
63
 
64
64
  export interface UlwLoopPlan {
65
65
  version: 1;
66
+ revision?: number;
67
+ brief?: string;
68
+ ledgerResetRevision?: number;
66
69
  evidenceLayoutVersion?: 2;
67
70
  createdAt: string;
68
71
  updatedAt: string;
@@ -158,6 +161,8 @@ export interface UlwLoopQualityGateSenpi extends UlwLoopQualityGateCommon {
158
161
  export type UlwLoopQualityGate = UlwLoopQualityGateLazycodex | UlwLoopQualityGateSenpi;
159
162
 
160
163
  export interface UlwLoopLedgerEntry {
164
+ revision?: number;
165
+ id?: string;
161
166
  at: string;
162
167
  kind: UlwLoopLedgerEventKind;
163
168
  goalId?: string;
@@ -1,6 +1,7 @@
1
1
  import { essentialCriteriaOf, hasAllCriteriaPass, hasEssentialCriteriaPass } from "./goal-status.js";
2
2
  import type { UlwLoopScope } from "./paths.js";
3
- import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
3
+ import { commit } from "./plan-commit.js";
4
+ import { readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
4
5
  import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js";
5
6
  import { iso, UlwLoopError } from "./types.js";
6
7
 
@@ -75,7 +76,6 @@ export async function recordEvidence(
75
76
  if (args.notes !== undefined) criterion.notes = args.notes;
76
77
  goal.updatedAt = capturedAt;
77
78
  plan.updatedAt = capturedAt;
78
- await writePlan(repoRoot, plan, scope);
79
79
  const ledgerEntry: UlwLoopLedgerEntry = {
80
80
  at: capturedAt,
81
81
  kind,
@@ -87,7 +87,7 @@ export async function recordEvidence(
87
87
  before: { status: prevStatus },
88
88
  after: { goalId: goal.id, criterionId: criterion.id, status: args.status, evidence, capturedAt, prevStatus },
89
89
  };
90
- await appendLedger(repoRoot, ledgerEntry, scope);
90
+ await commit(repoRoot, scope, { plan, entries: [ledgerEntry] });
91
91
  return { plan, goal, criterion, ledgerEntry };
92
92
  });
93
93
  }
@@ -115,19 +115,19 @@ export async function markCriteriaPendingResetForGoal(
115
115
  }
116
116
  goal.updatedAt = now;
117
117
  plan.updatedAt = now;
118
- await writePlan(repoRoot, plan, scope);
119
- await appendLedger(
120
- repoRoot,
121
- {
122
- at: now,
123
- kind: "criteria_revised",
124
- goalId,
125
- message: `Reset ${goal.successCriteria.length} criteria to pending.`,
126
- before,
127
- after: { resetCount: goal.successCriteria.length },
128
- },
129
- scope,
130
- );
118
+ await commit(repoRoot, scope, {
119
+ plan,
120
+ entries: [
121
+ {
122
+ at: now,
123
+ kind: "criteria_revised",
124
+ goalId,
125
+ message: `Reset ${goal.successCriteria.length} criteria to pending.`,
126
+ before,
127
+ after: { resetCount: goal.successCriteria.length },
128
+ },
129
+ ],
130
+ });
131
131
  return { plan, resetCount: goal.successCriteria.length };
132
132
  });
133
133
  }
@@ -0,0 +1,33 @@
1
+ import { join } from "node:path";
2
+ import { type UlwLoopScope, ulwLoopDir } from "./paths.js";
3
+ import { readOptional, readRecords, reconcilePlan } from "./plan-log.js";
4
+ import type { UlwLoopLedgerEntry } from "./types.js";
5
+
6
+ export function readLedgerAt(dir: string): UlwLoopLedgerEntry[] {
7
+ const entries = new Map<string, UlwLoopLedgerEntry>();
8
+ const lines = (readOptional(join(dir, "ledger.jsonl")) ?? "").split(/\r?\n/);
9
+ for (const [index, line] of lines.entries()) {
10
+ if (line.trim().length === 0) continue;
11
+ try {
12
+ const entry: UlwLoopLedgerEntry = JSON.parse(line);
13
+ entry.revision ??= 0;
14
+ entry.id ??= `legacy-${index + 1}`;
15
+ entries.set(entry.id, entry);
16
+ } catch (error) {
17
+ if (!(error instanceof SyntaxError)) throw error;
18
+ }
19
+ }
20
+ for (const record of readRecords(dir))
21
+ for (const [seq, entry] of record.ledger.entries()) {
22
+ const id = entry.id ?? `${record.revision}-${seq}`;
23
+ entries.set(id, { ...entry, revision: record.revision, id });
24
+ }
25
+ const reset = reconcilePlan(dir)?.ledgerResetRevision ?? 0;
26
+ const sequence = (entry: UlwLoopLedgerEntry) => Number(entry.id?.split("-").at(-1) ?? 0);
27
+ return [...entries.values()]
28
+ .filter((entry) => (entry.revision ?? 0) >= reset)
29
+ .sort((a, b) => (a.revision ?? 0) - (b.revision ?? 0) || sequence(a) - sequence(b));
30
+ }
31
+ export function readLedger(repoRoot: string, scope?: UlwLoopScope): UlwLoopLedgerEntry[] {
32
+ return readLedgerAt(ulwLoopDir(repoRoot, scope));
33
+ }
@@ -0,0 +1,124 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
3
+ import { linkSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { rename, writeFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { readLedgerAt } from "./ledger.js";
7
+ import { type UlwLoopScope, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
8
+ import { assertStateLockOwned, migrationEntries } from "./plan-io.js";
9
+ import { hasCode, type PlanCommitRecord, readOptional, readRecords, reconcilePlan } from "./plan-log.js";
10
+ import { UlwLoopError, type UlwLoopLedgerEntry, type UlwLoopPlan } from "./types.js";
11
+
12
+ interface CommitHooks {
13
+ readonly beforeMutation?: () => void | Promise<void>;
14
+ readonly beforeCommit?: () => void | Promise<void>;
15
+ readonly afterCommit?: () => void | Promise<void>;
16
+ readonly link?: (source: string, destination: string) => void;
17
+ readonly writeView?: (path: string, content: string) => Promise<void>;
18
+ }
19
+ const hooks = new AsyncLocalStorage<CommitHooks>();
20
+ export function withCommitHooks<T>(value: CommitHooks, fn: () => Promise<T>): Promise<T> {
21
+ return hooks.run(value, fn);
22
+ }
23
+
24
+ export async function beforePlanMutation(): Promise<void> {
25
+ await hooks.getStore()?.beforeMutation?.();
26
+ }
27
+ export async function writeViewFile(path: string, content: string): Promise<void> {
28
+ await (hooks.getStore()?.writeView ?? writeFile)(path, content);
29
+ }
30
+
31
+ // link is the sole commit point; no copy fallback (Node copyFile is not atomic).
32
+ // A resumed zombie can publish first, but never overwrite an existing revision.
33
+ // Logical audit order is (revision, sequence), not materialization completion time.
34
+ export async function commit(
35
+ repoRoot: string,
36
+ scope: UlwLoopScope | undefined,
37
+ mutation: {
38
+ readonly plan: UlwLoopPlan;
39
+ readonly entries: readonly UlwLoopLedgerEntry[];
40
+ },
41
+ ): Promise<void> {
42
+ const { plan } = mutation;
43
+ const dir = ulwLoopDir(repoRoot, scope);
44
+ assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
45
+ await hooks.getStore()?.beforeCommit?.();
46
+ const next = (plan.revision ?? 0) + 1;
47
+ const brief = plan.brief ?? readOptional(join(dir, "brief.md")) ?? "";
48
+ const entries = [...migrationEntries(plan), ...mutation.entries].map((entry, seq) => ({
49
+ ...entry,
50
+ revision: next,
51
+ id: `${next}-${seq}`,
52
+ }));
53
+ const record: PlanCommitRecord = {
54
+ version: 1,
55
+ revision: next,
56
+ plan: { ...plan, revision: next, brief },
57
+ ledger: entries,
58
+ };
59
+ mkdirSync(join(dir, "revisions"), { recursive: true });
60
+ mkdirSync(join(dir, "tmp"), { recursive: true });
61
+ const temp = join(dir, "tmp", `${process.pid}-${randomUUID()}.json`);
62
+ try {
63
+ writeFileSync(temp, `${JSON.stringify(record)}\n`, { flag: "wx" });
64
+ try {
65
+ (hooks.getStore()?.link ?? linkSync)(temp, join(dir, "revisions", `${String(next).padStart(8, "0")}.json`));
66
+ } catch (error) {
67
+ if (hasCode(error, "EEXIST"))
68
+ throw new UlwLoopError("Another writer published the read revision.", "ULW_LOOP_PUBLISH_CONFLICT", {
69
+ cause: error,
70
+ });
71
+ if (["EPERM", "ENOTSUP", "EXDEV"].some((code) => hasCode(error, code)))
72
+ throw new UlwLoopError(
73
+ "Immutable ulw-loop publication requires atomic hard links.",
74
+ "ULW_LOOP_PUBLISH_UNSUPPORTED_FS",
75
+ { cause: error },
76
+ );
77
+ throw error;
78
+ }
79
+ } finally {
80
+ rmSync(temp, { force: true });
81
+ }
82
+ plan.revision = next;
83
+ plan.brief = brief;
84
+ for (const [index, entry] of mutation.entries.entries())
85
+ Object.assign(entry, entries[index + entries.length - mutation.entries.length]);
86
+ await hooks.getStore()?.afterCommit?.();
87
+ await materialize(dir);
88
+ }
89
+
90
+ function viewContents(dir: string): [string, string][] {
91
+ const plan = reconcilePlan(dir);
92
+ if (plan === undefined || readRecords(dir).length === 0) return [];
93
+ const ledger = readLedgerAt(dir);
94
+ const views: [string, string][] = [
95
+ ["goals.json", `${JSON.stringify(plan, null, 2)}\n`],
96
+ ["ledger.jsonl", ledger.length === 0 ? "" : `${ledger.map((entry) => JSON.stringify(entry)).join("\n")}\n`],
97
+ ["brief.md", plan.brief ?? readOptional(join(dir, "brief.md")) ?? ""],
98
+ ];
99
+ return views.filter(([name, content]) => readOptional(join(dir, name)) !== content);
100
+ }
101
+ export function materializeSync(dir: string): void {
102
+ for (const [name, content] of viewContents(dir)) {
103
+ mkdirSync(join(dir, "tmp"), { recursive: true });
104
+ const temp = join(dir, "tmp", `${process.pid}-${randomUUID()}-${name}`);
105
+ try {
106
+ writeFileSync(temp, content, { flag: "wx" });
107
+ renameSync(temp, join(dir, name));
108
+ } finally {
109
+ rmSync(temp, { force: true });
110
+ }
111
+ }
112
+ }
113
+ export async function materialize(dir: string): Promise<void> {
114
+ for (const [name, content] of viewContents(dir)) {
115
+ mkdirSync(join(dir, "tmp"), { recursive: true });
116
+ const temp = join(dir, "tmp", `${process.pid}-${randomUUID()}-${name}`);
117
+ try {
118
+ await writeViewFile(temp, content);
119
+ await rename(temp, join(dir, name));
120
+ } finally {
121
+ rmSync(temp, { force: true });
122
+ }
123
+ }
124
+ }
@@ -1,20 +1,21 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
-
4
1
  import { aggregateCodexObjectiveForScope, isUlwLoopDone } from "./goal-status.js";
5
2
  import {
6
3
  type UlwLoopScope,
7
- ulwLoopBriefPath,
8
4
  ulwLoopBriefRelativePath,
9
- ulwLoopDir,
10
- ulwLoopGoalsPath,
11
5
  ulwLoopGoalsRelativePath,
12
- ulwLoopLedgerPath,
13
6
  ulwLoopLedgerRelativePath,
14
7
  } from "./paths.js";
8
+ import { beforePlanMutation, commit } from "./plan-commit.js";
15
9
  import { appendGoalToPlan, deriveGoalCandidates, makeGoal } from "./plan-goal-factory.js";
16
- import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
17
- import type { UlwLoopCodexGoalMode, UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js";
10
+ import { planExists, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
11
+ import type { UlwLoopToolkitSurface } from "./surface.js";
12
+ import type {
13
+ UlwLoopCodexGoalMode,
14
+ UlwLoopItem,
15
+ UlwLoopLedgerEntry,
16
+ UlwLoopPlan,
17
+ UlwLoopSuccessCriterion,
18
+ } from "./types.js";
18
19
  import { iso, UlwLoopError } from "./types.js";
19
20
  import { parseValidationBatches } from "./validation-batch.js";
20
21
 
@@ -60,11 +61,19 @@ export async function createUlwLoopPlan(
60
61
  repoRoot: string,
61
62
  args: { brief: string; codexGoalMode?: UlwLoopCodexGoalMode; force?: boolean; validationBatchesJson?: string },
62
63
  scope?: UlwLoopScope,
64
+ surface: UlwLoopToolkitSurface = "lazycodex",
63
65
  ): Promise<UlwLoopPlan> {
64
66
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
65
- if (!args.force && existsSync(ulwLoopGoalsPath(repoRoot, scope))) {
66
- const existing = await readUlwLoopPlan(repoRoot, scope);
67
- if (isUlwLoopDone(existing)) throw completedPlanExistsError(scope);
67
+ let existing: UlwLoopPlan | undefined;
68
+ if (planExists(repoRoot, scope)) {
69
+ try {
70
+ existing = await readUlwLoopPlan(repoRoot, scope);
71
+ } catch (error) {
72
+ if (!args.force) throw error;
73
+ }
74
+ }
75
+ if (!args.force && existing !== undefined) {
76
+ if (isUlwLoopDone(existing)) throw completedPlanExistsError(scope, surface);
68
77
  throw new UlwLoopError(
69
78
  `Refusing to overwrite existing ${ulwLoopGoalsRelativePath(scope)}; pass --force to recreate it.`,
70
79
  "ULW_LOOP_PLAN_EXISTS",
@@ -76,6 +85,9 @@ export async function createUlwLoopPlan(
76
85
  );
77
86
  const plan: UlwLoopPlan = {
78
87
  version: 1,
88
+ revision: existing?.revision ?? 0,
89
+ ledgerResetRevision: (existing?.revision ?? 0) + 1,
90
+ brief: args.brief.endsWith("\n") ? args.brief : `${args.brief}\n`,
79
91
  evidenceLayoutVersion: 2,
80
92
  createdAt: now,
81
93
  updatedAt: now,
@@ -88,29 +100,27 @@ export async function createUlwLoopPlan(
88
100
  const validationBatches = await parseValidationBatches(args.validationBatchesJson, goals);
89
101
  if (validationBatches !== undefined) plan.validationBatches = validationBatches;
90
102
  if (plan.codexGoalMode === "aggregate") plan.codexObjective = aggregateCodexObjectiveForScope(scope);
91
- await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
92
- await writeFile(
93
- ulwLoopBriefPath(repoRoot, scope),
94
- args.brief.endsWith("\n") ? args.brief : `${args.brief}\n`,
95
- "utf8",
96
- );
97
- await writePlan(repoRoot, plan, scope);
98
- await writeFile(ulwLoopLedgerPath(repoRoot, scope), "", "utf8");
99
- await appendLedger(
100
- repoRoot,
101
- { at: now, kind: "plan_created", message: `${goals.length} goal(s) created` },
102
- scope,
103
- );
103
+ await beforePlanMutation();
104
+ await commit(repoRoot, scope, {
105
+ plan,
106
+ entries: [{ at: now, kind: "plan_created", message: `${goals.length} goal(s) created` }],
107
+ });
104
108
  return plan;
105
109
  });
106
110
  }
107
111
 
108
- function completedPlanExistsError(scope?: UlwLoopScope): UlwLoopError {
112
+ function completedPlanExistsError(scope: UlwLoopScope | undefined, surface: UlwLoopToolkitSurface): UlwLoopError {
109
113
  return new UlwLoopError(
110
114
  [
111
115
  `Existing ulw-loop aggregate is already complete at ${ulwLoopGoalsRelativePath(scope)}.`,
112
- "Start a new run with `omo-agent-toolkit ulw-loop create-goals --session-id <new-id> ...` to isolate fresh state.",
113
- "Use --force only when you intentionally want to overwrite the completed evidence.",
116
+ ...(surface === "omo-senpi"
117
+ ? [
118
+ "Start a new run under a fresh session id (a new senpi session) or call agentToolkit.createGoals({ brief, force: true }) to recreate.",
119
+ ]
120
+ : [
121
+ "Start a new run with `omo-agent-toolkit ulw-loop create-goals --session-id <new-id> ...` to isolate fresh state.",
122
+ "Use --force only when you intentionally want to overwrite the completed evidence.",
123
+ ]),
114
124
  ].join(" "),
115
125
  "ULW_LOOP_PLAN_EXISTS_COMPLETE",
116
126
  );
@@ -125,12 +135,10 @@ export async function addUlwLoopGoal(
125
135
  const plan = await readUlwLoopPlan(repoRoot, scope);
126
136
  const now = iso();
127
137
  const goal = appendGoalToPlan(plan, args.title, args.objective, now);
128
- await writePlan(repoRoot, plan, scope);
129
- await appendLedger(
130
- repoRoot,
131
- { at: now, kind: "goal_added", goalId: goal.id, status: goal.status, message: goal.title },
132
- scope,
133
- );
138
+ await commit(repoRoot, scope, {
139
+ plan,
140
+ entries: [{ at: now, kind: "goal_added", goalId: goal.id, status: goal.status, message: goal.title }],
141
+ });
134
142
  return { plan, goal };
135
143
  });
136
144
  }
@@ -146,21 +154,18 @@ export async function startNextUlwLoop(
146
154
  if (plan.aggregateCompletion?.status === "complete") return { done: true, plan };
147
155
  const existing = plan.goals.find((goal) => goal.status === "in_progress" && isScheduleEligible(goal));
148
156
  if (existing) return { plan, goal: existing, resumed: true };
157
+ const entries: UlwLoopLedgerEntry[] = [];
149
158
  let next = plan.goals.find((goal) => goal.status === "pending" && isScheduleEligible(goal));
150
159
  if (!next && args.retryFailed) {
151
160
  next = plan.goals.find((goal) => goal.status === "failed" && !goal.nonRetriable && isScheduleEligible(goal));
152
161
  if (next)
153
- await appendLedger(
154
- repoRoot,
155
- {
156
- at: now,
157
- kind: "goal_retried",
158
- goalId: next.id,
159
- status: "pending",
160
- ...(next.failureReason ? { message: next.failureReason } : {}),
161
- },
162
- scope,
163
- );
162
+ entries.push({
163
+ at: now,
164
+ kind: "goal_retried",
165
+ goalId: next.id,
166
+ status: "pending",
167
+ ...(next.failureReason ? { message: next.failureReason } : {}),
168
+ });
164
169
  }
165
170
  if (!next) return { done: true, plan };
166
171
  next.status = "in_progress";
@@ -170,12 +175,14 @@ export async function startNextUlwLoop(
170
175
  next.updatedAt = now;
171
176
  plan.activeGoalId = next.id;
172
177
  plan.updatedAt = now;
173
- await writePlan(repoRoot, plan, scope);
174
- await appendLedger(
175
- repoRoot,
176
- { at: now, kind: "goal_started", goalId: next.id, status: next.status, message: `Attempt ${next.attempt}` },
177
- scope,
178
- );
178
+ entries.push({
179
+ at: now,
180
+ kind: "goal_started",
181
+ goalId: next.id,
182
+ status: next.status,
183
+ message: `Attempt ${next.attempt}`,
184
+ });
185
+ await commit(repoRoot, scope, { plan, entries });
179
186
  return { plan, goal: next, resumed: false };
180
187
  });
181
188
  }