oh-my-opencode 5.0.0-beta.60 → 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 (227) hide show
  1. package/THIRD-PARTY-NOTICES.md +8 -6
  2. package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
  3. package/dist/cli/index.js +465 -348
  4. package/dist/cli-node/index.js +465 -348
  5. package/dist/config/schema/agent-names.d.ts +0 -1
  6. package/dist/config/schema/browser-automation.d.ts +0 -2
  7. package/dist/config/schema/oh-my-opencode-config.d.ts +0 -1
  8. package/dist/hooks/comment-checker/__fixtures__/download-probe.fixture.d.ts +1 -0
  9. package/dist/index.js +61 -535
  10. package/dist/oh-my-opencode.schema.json +0 -1
  11. package/dist/skills/ast-grep/SKILL.md +12 -0
  12. package/dist/skills/debugging/references/tools/playwright-cli.md +79 -161
  13. package/dist/skills/frontend/SKILL.md +1 -1
  14. package/dist/skills/frontend/references/design/aside.md +1 -1
  15. package/dist/skills/frontend/references/design/clone-from-url.md +1 -1
  16. package/dist/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  17. package/dist/skills/review-work/SKILL.md +1 -1
  18. package/dist/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  19. package/dist/skills/ultimate-browsing/SKILL.md +17 -29
  20. package/dist/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  21. package/dist/skills/ultimate-browsing/engine/templates/package.json +2 -2
  22. package/dist/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  23. package/dist/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  24. package/dist/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  25. package/dist/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  26. package/dist/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  27. package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  28. package/dist/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  29. package/dist/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  30. package/dist/skills/ulw-execute/SKILL.md +1 -1
  31. package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  32. package/dist/skills/ulw-research/SKILL.md +3 -3
  33. package/dist/skills/visual-qa/AGENTS.md +1 -1
  34. package/dist/skills/visual-qa/SKILL.md +1 -1
  35. package/dist/skills/visual-qa/references/browser-setup.md +75 -0
  36. package/dist/tui.js +12 -5
  37. package/package.json +16 -23
  38. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  39. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +6887 -249
  40. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  41. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +7 -1
  43. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  46. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  48. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  49. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  50. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  51. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  52. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  53. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  54. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  55. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  56. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  57. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  58. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
  59. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +25 -15
  60. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  61. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  63. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  64. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  65. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  66. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +15 -0
  67. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +25 -15
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +5 -4
  69. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +640 -327
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +5 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +16 -12
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +2 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +48 -22
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +3 -4
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +29 -6
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
  88. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +1 -0
  89. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +29 -66
  90. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.d.ts +2 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.js +22 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
  93. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
  94. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
  95. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
  96. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
  97. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +5 -5
  98. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
  99. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +7 -7
  100. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  101. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
  102. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +5 -4
  103. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +5 -0
  104. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +16 -16
  105. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
  106. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
  107. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +58 -51
  108. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
  109. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
  111. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +3 -3
  112. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +55 -6
  113. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
  114. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +29 -62
  115. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-role-guard.ts +22 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
  117. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
  118. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
  119. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +5 -4
  120. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
  121. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
  127. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
  130. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
  131. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
  133. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +145 -0
  134. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
  135. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +7 -1
  136. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-role-matrix.test.ts +113 -0
  137. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
  138. package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
  139. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
  140. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
  141. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  157. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  158. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  159. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  161. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  162. package/packages/omo-codex/plugin/package-lock.json +12 -12
  163. package/packages/omo-codex/plugin/package.json +1 -1
  164. package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
  165. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +18 -10
  166. package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
  167. package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
  168. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  169. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
  170. package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  171. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  172. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +3 -3
  173. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +3 -3
  174. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +3 -3
  175. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +4 -4
  176. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  177. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
  178. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  179. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  180. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  181. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  182. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  183. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  184. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  185. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  186. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  187. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  188. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +25 -15
  189. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +4 -4
  190. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +3 -3
  191. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  192. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +6 -6
  193. package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
  194. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -4
  195. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
  196. package/packages/omo-codex/plugin/test/bootstrap-default-role.test.mjs +66 -0
  197. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +13 -1
  198. package/packages/omo-codex/scripts/install-dist/install-local.mjs +7060 -417
  199. package/packages/prompts-core/prompts/ultrawork/codex.md +25 -15
  200. package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
  201. package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
  202. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  203. package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
  204. package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
  205. package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  206. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  207. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  208. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
  209. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  210. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
  211. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  212. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  213. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  214. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  215. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  216. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  217. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  218. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  219. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  220. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  221. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
  222. package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
  223. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  224. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
  225. package/dist/skills/visual-qa/references/agent-browser-setup.md +0 -45
  226. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
  227. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -1,36 +1,45 @@
1
1
  import { hasAllCriteriaPass, isFinalRunCompletionCandidate } from "./goal-status.js";
2
- import { ULW_LOOP_CREATE_GOALS_COMMAND } from "./plan-missing-recovery.js";
2
+ import { createGoalsAction } from "./plan-missing-recovery.js";
3
+ import type { UlwLoopToolkitSurface } from "./surface.js";
3
4
  import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
4
5
 
5
6
  /**
6
- * `status --json` is the one call every agent already makes between steps, so it is
7
+ * Status is the one call every agent already makes between steps, so it is
7
8
  * also the cheapest place to answer "what now?" without a second round trip.
8
9
  */
9
- export function statusNextActions(plan: UlwLoopPlan): readonly string[] {
10
+ export function statusNextActions(plan: UlwLoopPlan, surface: UlwLoopToolkitSurface = "lazycodex"): readonly string[] {
10
11
  const actions: string[] = [];
11
12
  const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
12
- if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
13
- else if (active !== undefined) actions.push(...activeGoalActions(plan, active));
13
+ if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
14
+ else if (active !== undefined) actions.push(...activeGoalActions(plan, active, surface));
14
15
  for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
15
16
  actions.push(
16
- `${goal.id} is review_blocked: capture the reviewer verdict with \`omo-agent-toolkit ulw-loop record-review-blockers --goal-id ${goal.id} --title "<title>" --objective "<objective>" --evidence "<verdict>" --codex-goal-json '<get_goal json>'\`.`,
17
+ surface === "omo-senpi"
18
+ ? `${goal.id} is review_blocked: capture the reviewer verdict with \`agentToolkit.recordReviewBlockers({ goalId: "${goal.id}", title: "<title>", objective: "<objective>", evidence: "<verdict>" })\`.`
19
+ : `${goal.id} is review_blocked: capture the reviewer verdict with \`omo-agent-toolkit ulw-loop record-review-blockers --goal-id ${goal.id} --title "<title>" --objective "<objective>" --evidence "<verdict>" --codex-goal-json '<get_goal json>'\`.`,
17
20
  );
18
21
  if (plan.evidenceLayoutVersion !== 2 || active === undefined)
19
22
  actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
20
23
  return actions;
21
24
  }
22
25
 
23
- function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem): readonly string[] {
26
+ function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem, surface: UlwLoopToolkitSurface): readonly string[] {
24
27
  const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
25
28
  if (unresolved.length > 0)
26
29
  return [
27
- `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`omo-agent-toolkit ulw-loop record-evidence --goal-id ${active.id} --criterion-id <id> --status pass --evidence "<observable proof>"\`.`,
30
+ surface === "omo-senpi"
31
+ ? `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`agentToolkit.recordEvidence({ goalId: "${active.id}", criterionId: "<id>", status: "pass", evidence: "<observable proof>" })\`.`
32
+ : `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`omo-agent-toolkit ulw-loop record-evidence --goal-id ${active.id} --criterion-id <id> --status pass --evidence "<observable proof>"\`.`,
28
33
  ];
29
34
  if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
30
35
  return [
31
- `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
36
+ surface === "omo-senpi"
37
+ ? `${active.id} passes every criterion and is the final story: complete the driver goal, then call \`agentToolkit.checkpoint({ goalId: "${active.id}", printTemplate: true })\` to build the final quality gate. Use status().result.currentAttemptDir for all quality-gate artifacts.`
38
+ : `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
32
39
  ];
33
40
  return [
34
- `${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`. Put quality-gate artifacts under the currentAttemptDir shown by status --json.`,
41
+ surface === "omo-senpi"
42
+ ? `${active.id} passes every criterion: close it with \`agentToolkit.checkpoint({ goalId: "${active.id}", status: "complete", evidence: "<proof>" })\`. Put quality-gate artifacts under status().result.currentAttemptDir.`
43
+ : `${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`. Put quality-gate artifacts under the currentAttemptDir shown by status --json.`,
35
44
  ];
36
45
  }
@@ -1,6 +1,7 @@
1
1
  // biome-ignore-all format: compact batch steering module stays below the pure LOC budget.
2
2
  import type { UlwLoopScope } from "./paths.js";
3
- import { appendLedger, appendLedgerEntries, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
3
+ import { commit } from "./plan-commit.js";
4
+ import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
4
5
  import { applySteeringMutation, validateUlwLoopSteeringProposal } from "./steering.js";
5
6
  import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
6
7
  import type { UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit, UlwLoopSteeringProposal } from "./types.js";
@@ -43,17 +44,16 @@ export async function steerUlwLoopBatch(
43
44
  const failed = prepared.results.find((item) => !item.accepted);
44
45
  if (failed !== undefined) {
45
46
  const entry = rejectedLedgerEntry(prepared.results);
46
- await appendLedger(repoRoot, entry, scope);
47
+ await commit(repoRoot, scope, { plan, entries: [entry] });
47
48
  return rejected(plan, prepared.results, failed.rejectedReasons);
48
49
  }
49
50
  let next = plan;
50
51
  for (const item of prepared.items) if (item.kind === "fresh") next = item.prepared.next;
51
52
  const fresh = prepared.items.filter((item): item is Extract<PreparedItem, { readonly kind: "fresh" }> => item.kind === "fresh");
52
53
  if (fresh.length > 0) {
53
- await writePlan(repoRoot, next, scope);
54
54
  const entries = fresh.map((item) => ledgerEntry(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
55
55
  const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
56
- await appendLedgerEntries(repoRoot, batchEntry === null ? entries : [...entries, batchEntry], scope);
56
+ await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
57
57
  }
58
58
  return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
59
59
  });
@@ -1,7 +1,8 @@
1
1
  // biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget
2
2
  import { isUlwLoopDone } from "./goal-status.js";
3
3
  import type { UlwLoopScope } from "./paths.js";
4
- import { appendLedger, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
4
+ import { commit } from "./plan-commit.js";
5
+ import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
5
6
  import { makeGoal, reviseCriterion, reviseWording, splitOrBlock } from "./steering-mutations.js";
6
7
  import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
7
8
  import type {
@@ -209,9 +210,9 @@ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringPr
209
210
  }
210
211
  const at = proposal.now?.toISOString() ?? iso();
211
212
  const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
212
- if (accepted) await writePlan(repoRoot, next, scope);
213
- await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
214
- if (batchEntry !== null) await appendLedger(repoRoot, batchEntry, scope);
213
+ const entries = [ledgerEntry(proposal, finalAudit, at)];
214
+ if (batchEntry !== null) entries.push(batchEntry);
215
+ await commit(repoRoot, scope, { plan: next, entries });
215
216
  return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
216
217
  });
217
218
  }
@@ -1,7 +1,9 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { isAbsolute, join, resolve, sep } from "node:path";
3
3
 
4
+ import { readLedgerAt } from "./ledger.js";
4
5
  import { normalizeUlwLoopSessionId, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
6
+ import { readUlwLoopPlanSync } from "./plan-io.js";
5
7
  import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
6
8
  import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
7
9
 
@@ -37,7 +39,7 @@ export function runStopResumeHook(input: unknown): string {
37
39
  if (boulderContinuationWillFire(payload.cwd, payload.session_id)) return "";
38
40
  const scope = { sessionId: payload.session_id } as const;
39
41
  const stateDir = ulwLoopDir(payload.cwd, scope);
40
- const plan = readPlan(join(stateDir, "goals.json"));
42
+ const plan = readPlan(payload.cwd, payload.session_id);
41
43
  if (plan === null || plan.aggregateCompletion?.status === "complete") return "";
42
44
  const goal = resumableGoal(plan);
43
45
  if (goal === undefined) return "";
@@ -81,11 +83,10 @@ function consumeResumeBudgetLocked(lockPath: string, stateDir: string, goalId: s
81
83
  }
82
84
  }
83
85
 
84
- // Two-strike cap keyed on ledger movement: an unchanged ledger.jsonl line
85
- // count across resumes means the loop is not progressing. The stuck marker is
86
- // a separate file — a ledger append would change the count and self-reset.
86
+ // Hook budget counters are exempt from plan/audit commits. Count reconciled
87
+ // entries, not cache lines: a writer dying after link still made progress.
87
88
  function consumeResumeBudget(stateDir: string, goalId: string): boolean {
88
- const ledgerLineCount = countLedgerLines(join(stateDir, "ledger.jsonl"));
89
+ const ledgerLineCount = readLedgerAt(stateDir).length;
89
90
  const counterPath = resolve(stateDir, `auto-resume-${goalId}.json`);
90
91
  const stuckPath = resolve(stateDir, `auto-resume-${goalId}.stuck`);
91
92
  // goals.json is untrusted input: a crafted goal id (e.g. `../../x`) must
@@ -119,24 +120,15 @@ function renderResumeDirective(plan: UlwLoopPlan, goal: UlwLoopItem, sessionId:
119
120
  ].join("\n");
120
121
  }
121
122
 
122
- function readPlan(goalsPath: string): UlwLoopPlan | null {
123
+ function readPlan(repoRoot: string, sessionId: string): UlwLoopPlan | null {
123
124
  try {
124
- return JSON.parse(readFileSync(goalsPath, "utf8")) as UlwLoopPlan;
125
+ return readUlwLoopPlanSync(repoRoot, { sessionId });
125
126
  } catch (error) {
126
127
  if (error instanceof Error) return null;
127
128
  throw error;
128
129
  }
129
130
  }
130
131
 
131
- function countLedgerLines(ledgerPath: string): number {
132
- try {
133
- return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
134
- } catch (error) {
135
- if (error instanceof Error) return 0;
136
- throw error;
137
- }
138
- }
139
-
140
132
  function readCounter(counterPath: string): { count: number; ledgerLineCount: number } | null {
141
133
  try {
142
134
  if (!existsSync(counterPath)) return null;
@@ -0,0 +1,13 @@
1
+ export function barrier() {
2
+ let resolve: () => void = () => {
3
+ throw new Error("barrier not initialized");
4
+ };
5
+ const promise = new Promise<void>((done, reject) => {
6
+ const timeout = setTimeout(() => reject(new Error("barrier timed out")), 5_000);
7
+ resolve = () => {
8
+ clearTimeout(timeout);
9
+ done();
10
+ };
11
+ });
12
+ return { promise, resolve };
13
+ }
@@ -0,0 +1,65 @@
1
+ import { fork } from "node:child_process";
2
+ import { join } from "node:path";
3
+
4
+ const running = new Set<ReturnType<typeof childWriter>>();
5
+ export async function cleanupWriters(): Promise<void> {
6
+ await Promise.all(
7
+ [...running].map(async (writer) => {
8
+ writer.child.kill("SIGKILL");
9
+ await writer.exited;
10
+ }),
11
+ );
12
+ }
13
+ export function childWriter(rootPath: string, mode: string, now = Date.now()) {
14
+ const child = fork(join(import.meta.dirname, "commit-writer.ts"), [rootPath, mode, String(now)], {
15
+ execPath: "bun",
16
+ execArgv: [],
17
+ stdio: ["ignore", "inherit", "inherit", "ipc"],
18
+ });
19
+ const pending = new Map<string, { resolve: () => void; reject: (error: Error) => void }>();
20
+ const seen = new Set<string>();
21
+ child.on("message", (message) => {
22
+ if (typeof message !== "string") return;
23
+ seen.add(message);
24
+ pending.get(message)?.resolve();
25
+ });
26
+ const exited = new Promise<number | null>((resolve, reject) => {
27
+ const timeout = setTimeout(() => {
28
+ child.kill("SIGKILL");
29
+ reject(new Error("child exit timed out"));
30
+ }, 15_000);
31
+ child.once("error", (error) => {
32
+ clearTimeout(timeout);
33
+ reject(error);
34
+ });
35
+ child.once("exit", (code) => {
36
+ clearTimeout(timeout);
37
+ running.delete(writer);
38
+ resolve(code);
39
+ for (const [name, waiter] of pending)
40
+ if (!seen.has(name)) waiter.reject(new Error(`child exited before ${name}: ${code}`));
41
+ });
42
+ });
43
+ const writer = {
44
+ child,
45
+ exited,
46
+ wait(name: string) {
47
+ if (seen.has(name)) return Promise.resolve();
48
+ return new Promise<void>((resolve, reject) => {
49
+ const timeout = setTimeout(() => reject(new Error(`IPC timeout: ${name}`)), 10_000);
50
+ pending.set(name, {
51
+ resolve: () => {
52
+ clearTimeout(timeout);
53
+ resolve();
54
+ },
55
+ reject: (error) => {
56
+ clearTimeout(timeout);
57
+ reject(error);
58
+ },
59
+ });
60
+ });
61
+ },
62
+ };
63
+ running.add(writer);
64
+ return writer;
65
+ }
@@ -0,0 +1,76 @@
1
+ import assert from "node:assert/strict";
2
+ import { fork, spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import { withCommitHooks } from "../../dist/plan-commit.js";
7
+ import { commit } from "../../dist/plan-commit.js";
8
+ import { readLedger, readUlwLoopPlan, withUlwLoopMutationLock } from "../../dist/plan-io.js";
9
+ const cli = resolve("dist/cli.js");
10
+ const root = mkdtempSync(join(tmpdir(), "ulw-cli-qa-"));
11
+ const legacyRoot = mkdtempSync(join(tmpdir(), "ulw-legacy-qa-"));
12
+ const scope = { sessionId: "commit-log" };
13
+ const dir = join(root, ".omo", "ulw-loop", scope.sessionId);
14
+ const json = (path) => JSON.parse(readFileSync(path, "utf8"));
15
+ function run(cwd, ...args) {
16
+ const argv = [...args, "--session-id", scope.sessionId, "--json"];
17
+ const result = spawnSync(process.execPath, [cli, ...argv], { cwd, encoding: "utf8", timeout: 15_000, env: { ...process.env, PI_SESSION_ID: "", CODEX_SESSION_ID: "", CODEX_THREAD_ID: "", OMO_ULW_LOOP_SESSION_ID: "" } });
18
+ console.log(`command=${process.execPath} ${cli} ${argv.map((arg) => JSON.stringify(arg)).join(" ")} cwd=${cwd}\n${result.stdout}${result.stderr}\nexit=${result.status}`);
19
+ assert.equal(result.status, 0);
20
+ return JSON.parse(result.stdout);
21
+ }
22
+ try {
23
+ run(root, "create-goals", "--brief", "Build durable state");
24
+ const original = json(join(dir, "goals.json"));
25
+ assert.equal(run(root, "status").plan.revision, 1);
26
+ rmSync(join(dir, "goals.json"));
27
+ assert.equal(run(root, "status").plan.revision, 1);
28
+ assert.equal(existsSync(join(dir, "goals.json")), false);
29
+ const goal = original.goals[0];
30
+ run(root, "record-evidence", "--goal-id", goal.id, "--criterion-id", goal.successCriteria[0].id, "--status", "pass", "--evidence", "Observed real CLI publication");
31
+ assert.equal(json(join(dir, "revisions", "00000002.json")).ledger[0].kind, "evidence_captured");
32
+ assert.equal(json(join(dir, "goals.json")).revision, 2);
33
+ console.log("QA=PASS log-only status is read-only; CLI evidence repairs and commits plan plus audit");
34
+ const clock = { now: () => 0, schedule: () => { throw new Error("Unexpected scheduler use"); } };
35
+ await assert.rejects(withCommitHooks({ beforeCommit: async () => {
36
+ const child = fork(resolve("test/fixtures/commit-writer.ts"), [root, "write", "201"], { execPath: "bun", execArgv: [], stdio: ["ignore", "inherit", "inherit", "ipc"] });
37
+ const exit = await new Promise((done, reject) => {
38
+ const timeout = setTimeout(() => { child.kill("SIGKILL"); reject(new Error("writer B timed out")); }, 10_000);
39
+ child.once("error", (error) => { clearTimeout(timeout); reject(error); });
40
+ child.once("exit", (code) => { clearTimeout(timeout); done(code); });
41
+ });
42
+ assert.equal(exit, 0);
43
+ } }, () => withUlwLoopMutationLock(root, scope, async () => {
44
+ const plan = await readUlwLoopPlan(root, scope);
45
+ plan.brief = "zombie";
46
+ await commit(root, scope, { plan, entries: [{ at: "A", kind: "goal_added", message: "A" }] });
47
+ }, { clock, leaseMs: 200, heartbeatMs: 0 })), { code: "ULW_LOOP_LOCK_LOST" });
48
+ assert.equal(readLedger(root, scope).at(-1).message, "B");
49
+ assert.equal(readLedger(root, scope).some((entry) => entry.message === "A"), false);
50
+ assert.deepEqual(readdirSync(join(dir, "revisions")), ["00000001.json", "00000002.json", "00000003.json"]);
51
+ console.log(`QA=PASS real fork zombie fencing ULW_LOOP_LOCK_LOST record=${JSON.stringify(json(join(dir, "revisions", "00000003.json")))}`);
52
+ run(root, "create-goals", "--brief", "Replacement brief", "--force");
53
+ const recreated = json(join(dir, "goals.json"));
54
+ assert.equal(recreated.revision, 4);
55
+ assert.equal(recreated.ledgerResetRevision, 4);
56
+ assert.equal(readFileSync(join(dir, "brief.md"), "utf8"), recreated.brief);
57
+ assert.equal(readLedger(root, scope).length, 1);
58
+ console.log("QA=PASS force recreation retains revision history and resets the logical audit");
59
+ const legacyDir = join(legacyRoot, ".omo", "ulw-loop", scope.sessionId);
60
+ mkdirSync(legacyDir, { recursive: true });
61
+ delete original.revision; delete original.brief; delete original.ledgerResetRevision;
62
+ writeFileSync(join(legacyDir, "goals.json"), JSON.stringify(original));
63
+ writeFileSync(join(legacyDir, "brief.md"), "Legacy brief\n");
64
+ writeFileSync(join(legacyDir, "ledger.jsonl"), '{"at":"legacy","kind":"goal_started"}\n');
65
+ assert.equal(run(legacyRoot, "status").plan.revision ?? 0, 0);
66
+ assert.equal(existsSync(join(legacyDir, "revisions")), false);
67
+ run(legacyRoot, "add-goal", "--title", "Extra", "--objective", "Verify legacy hydration");
68
+ assert.equal(json(join(legacyDir, "revisions", "00000001.json")).plan.brief, "Legacy brief\n");
69
+ assert.equal(readLedger(legacyRoot, scope)[0].id, "legacy-1");
70
+ console.log("QA=PASS legacy CLI status and first commit preserve brief and legacy audit");
71
+ } finally {
72
+ rmSync(root, { recursive: true, force: true });
73
+ rmSync(legacyRoot, { recursive: true, force: true });
74
+ assert.equal(existsSync(root) || existsSync(legacyRoot), false);
75
+ console.log(`cleanup=REMOVED ${root} ${legacyRoot}; real home directories not accessed by this driver`);
76
+ }
@@ -0,0 +1,23 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { type UlwLoopScope, ulwLoopDir } from "../../src/paths.js";
5
+ import { createUlwLoopPlan } from "../../src/plan-crud.js";
6
+
7
+ export async function seedCrash(root: string, scope: UlwLoopScope, first: boolean) {
8
+ const dir = ulwLoopDir(root, scope);
9
+ const plan = await createUlwLoopPlan(root, { brief: "original" }, scope);
10
+ const next = { ...plan, revision: (plan.revision ?? 0) + 1, brief: "committed\n", codexObjective: "committed" };
11
+ mkdirSync(join(dir, "revisions"), { recursive: true });
12
+ writeFileSync(
13
+ join(dir, "revisions", `${String(next.revision).padStart(8, "0")}.json`),
14
+ JSON.stringify({
15
+ version: 1,
16
+ revision: next.revision,
17
+ plan: next,
18
+ ledger: [{ at: "now", kind: "goal_added", revision: next.revision, id: `${next.revision}-0` }],
19
+ }),
20
+ );
21
+ if (first) await rm(join(dir, "goals.json"));
22
+ return next;
23
+ }
@@ -0,0 +1,58 @@
1
+ // Run only in an isolated remote test checkout. Each mutant is restored before the next.
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ function replace(source, before, after) {
7
+ if (source.split(before).length !== 2) throw new Error(`Mutation anchor is not unique: ${before}`);
8
+ return source.replace(before, after);
9
+ }
10
+ const publication = '(hooks.getStore()?.link ?? linkSync)(temp, join(dir, "revisions", `${String(next).padStart(8, "0")}.json`));';
11
+ const logTest = "test/plan-commit-log.test.ts";
12
+ const recoveryTest = "test/plan-commit-recovery.test.ts";
13
+ const mutations = [
14
+ { name: "heartbeat-no-refresh", file: "src/state-lock.ts", test: "test/state-lock.test.ts", pattern: "\\(3\\)", change: (s) => replace(s, "holder.record.leaseUntil = clock.now()", "holder.record.leaseUntil ??= clock.now()") },
15
+ { name: "heartbeat-by-path", file: "src/state-lock.ts", test: "test/state-lock.test.ts", pattern: "\\(2\\)", change: (s) => 'import { writeFileSync } from "node:fs";\n' + replace(s, "writeRecord(holder);\n\t\t\tschedule();", "writeFileSync(lockPath, JSON.stringify(holder.record));\n\t\t\tschedule();") },
16
+ { name: "rename-over-goals", file: "src/plan-commit.ts", test: logTest, pattern: "B wins write", change: (s) => replace(s, publication, `if (next === 1) { ${publication} } else { writeFileSync(temp, JSON.stringify(record.plan)); renameSync(temp, join(dir, "goals.json")); }`) },
17
+ { name: "delete-published-record", file: "src/plan-commit.ts", test: logTest, pattern: "B wins write", change: (s) => replace(s, "await materialize(dir);", 'await materialize(dir);\n if (next > 1) rmSync(join(dir, "revisions", `${String(next).padStart(8, "0")}.json`));') },
18
+ { name: "next-from-directory", file: "src/plan-commit.ts", test: logTest, pattern: "\\(6\\)", change: (s) => replace(s, "const next = (plan.revision ?? 0) + 1;", "const next = Math.max(plan.revision ?? 0, ...readRecords(dir).map((record) => record.revision)) + 1;") },
19
+ { name: "no-holder-retry", file: "src/plan-io.ts", test: logTest, pattern: "\\(6\\)", change: (s) => replace(s, "if (attempt !== 0) throw error;", "throw error;") },
20
+ { name: "no-plan-reconciliation", file: "src/plan-log.ts", test: logTest, pattern: "\\(7a\\)", change: (s) => replace(s, "const latest = readNewestRecord(dir);", "const latest = cached === undefined ? readNewestRecord(dir) : undefined;") },
21
+ { name: "no-ledger-reconciliation", file: "src/ledger.ts", test: logTest, pattern: "\\(7a\\)", change: (s) => replace(s, "for (const record of readRecords(dir))", "for (const record of [])") },
22
+ { name: "cache-only-existence", file: "src/plan-log.ts", test: recoveryTest, pattern: "\\(7b\\)", change: (s) => replace(s, 'return existsSync(join(dir, "goals.json")) || logNames(dir).length > 0;', 'return existsSync(join(dir, "goals.json"));') },
23
+ { name: "in-place-ledger-append", file: "src/plan-commit.ts", test: logTest, pattern: "\\(7c\\)", change: (s) => {
24
+ const start = s.indexOf("export async function materialize(dir:");
25
+ if (start < 0) throw new Error("materializer not found");
26
+ const body = replace(s.slice(start), 'const temp = join(dir, "tmp", `${process.pid}-${randomUUID()}-${name}`);', 'const temp = name === "ledger.jsonl" && readRecords(dir).length > 1 ? join(dir, name) : join(dir, "tmp", `${process.pid}-${randomUUID()}-${name}`);');
27
+ return 'import { appendFile } from "node:fs/promises";\n' + s.slice(0, start) + replace(body, 'await writeViewFile(temp, content);', 'if (name === "ledger.jsonl" && readRecords(dir).length > 1) await (hooks.getStore()?.writeView ?? appendFile)(temp, content); else await writeViewFile(temp, content);');
28
+ } },
29
+ { name: "brief-before-publication", file: "src/plan-crud.ts", test: recoveryTest, pattern: "\\(9\\)", change: (s) => replace(replace(s, 'import { beforePlanMutation, commit }', 'import { beforePlanMutation, commit, writeViewFile }'), "await beforePlanMutation();", 'await beforePlanMutation();\n await writeViewFile(`${repoRoot}/${plan.briefPath}`, plan.brief ?? "");') },
30
+ { name: "steering-audit-outside-record", file: "src/steering.ts", test: logTest, pattern: "\\(11\\)", change: (s) => replace(s, "await commit(repoRoot, scope, { plan: next, entries });", 'await commit(repoRoot, scope, { plan: next, entries: [] });\n await (await import("node:fs/promises")).appendFile(`${repoRoot}/${next.ledgerPath}`, entries.map((entry) => JSON.stringify(entry)).join("\\n") + "\\n");') },
31
+ { name: "copy-fallback", file: "src/plan-commit.ts", test: logTest, pattern: "\\(12\\)", change: (s) => 'import { copyFileSync } from "node:fs";\n' + replace(s, publication, `try { ${publication} } catch (error) { if (hasCode(error, "EPERM")) copyFileSync(temp, join(dir, "revisions", \`\${String(next).padStart(8, "0")}.json\`)); else throw error; }`) },
32
+ ];
33
+ const digest = (content) => createHash("sha256").update(content).digest("hex");
34
+ let failures = 0;
35
+ for (const mutation of mutations) {
36
+ const original = readFileSync(mutation.file, "utf8");
37
+ console.log(`\n## MUTATION ${mutation.name}\nfile=${mutation.file}\nbefore_sha256=${digest(original)}`);
38
+ try {
39
+ writeFileSync(mutation.file, mutation.change(original));
40
+ const args = ["run", "test", "--", mutation.test, "-t", mutation.pattern];
41
+ console.log(`command=bun ${args.map((arg) => JSON.stringify(arg)).join(" ")}`);
42
+ const result = spawnSync("bun", args, { encoding: "utf8", timeout: 60_000 });
43
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
44
+ console.log(output);
45
+ console.log(`mutation_exit=${result.status}`);
46
+ if (result.status !== 1 || !/[1-9]\d* failed/.test(output.replace(/\x1b\[[0-9;]*m/g, "")) || !output.includes("AssertionError")) {
47
+ failures += 1;
48
+ console.log(`PROOF=FAILED ${mutation.name}`);
49
+ } else console.log(`PROOF=RED ${mutation.name}`);
50
+ } finally {
51
+ writeFileSync(mutation.file, original);
52
+ const restored = readFileSync(mutation.file, "utf8");
53
+ if (restored !== original) throw new Error(`Failed to restore ${mutation.file}`);
54
+ console.log(`restored_sha256=${digest(restored)} cleanup=RESTORED`);
55
+ }
56
+ }
57
+ console.log(`mutation_proofs=${mutations.length} failed_proofs=${failures}`);
58
+ process.exitCode = failures === 0 ? 0 : 1;
@@ -0,0 +1,65 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { commit, withCommitHooks } from "../../src/plan-commit.js";
3
+ import { createUlwLoopPlan } from "../../src/plan-crud.js";
4
+ import { readUlwLoopPlan, withMutationLockOptions, withUlwLoopMutationLock } from "../../src/plan-io.js";
5
+ import { steerUlwLoop } from "../../src/steering.js";
6
+ import { leaseClock } from "./lease-clock.js";
7
+
8
+ const root = process.argv[2];
9
+ const mode = process.argv[3];
10
+ if (root === undefined) throw new Error("root required");
11
+ const scope = { sessionId: "commit-log" };
12
+ function barrier(name: string): Promise<void> {
13
+ return new Promise((resolve, reject) => {
14
+ const timeout = setTimeout(() => reject(new Error(`barrier timed out: ${name}`)), 10_000);
15
+ process.once("message", () => {
16
+ clearTimeout(timeout);
17
+ resolve();
18
+ });
19
+ process.send?.(name);
20
+ });
21
+ }
22
+ let calls = 0;
23
+ await withMutationLockOptions({ clock: leaseClock(Number(process.argv[4])), heartbeatMs: 0, timeoutMs: 0 }, () =>
24
+ withCommitHooks(
25
+ {
26
+ beforeCommit: async () => {
27
+ calls += 1;
28
+ if (mode === "mirror" && calls === 1) await barrier("before");
29
+ },
30
+ afterCommit: async () => {
31
+ if (mode === "accepted" || mode === "rejected") await barrier("linked");
32
+ },
33
+ writeView: async (path, content) => {
34
+ if (mode === "partial" && path.endsWith("ledger.jsonl")) {
35
+ await writeFile(path, content.slice(0, Math.max(1, Math.floor(content.length / 2))));
36
+ await barrier("partial");
37
+ }
38
+ await writeFile(path, content);
39
+ },
40
+ },
41
+ async () => {
42
+ if (mode === "force") await createUlwLoopPlan(root, { brief: "B\n", force: true }, scope);
43
+ else if (mode === "accepted" || mode === "rejected")
44
+ await steerUlwLoop(
45
+ root,
46
+ {
47
+ kind: "annotate_ledger",
48
+ source: "cli",
49
+ evidence: mode === "rejected" ? "" : "observed",
50
+ rationale: "audit",
51
+ idempotencyKey: "crash-key",
52
+ },
53
+ scope,
54
+ );
55
+ else
56
+ await withUlwLoopMutationLock(root, scope, async () => {
57
+ const plan = await readUlwLoopPlan(root, scope);
58
+ plan.codexObjective = `${plan.codexObjective}|B`;
59
+ await commit(root, scope, { plan, entries: [{ at: "B", kind: "goal_added", message: "B" }] });
60
+ });
61
+ },
62
+ ),
63
+ );
64
+ process.send?.("done");
65
+ process.disconnect?.();
@@ -0,0 +1,23 @@
1
+ export function leaseClock(initial = 0) {
2
+ let now = initial;
3
+ const tasks = new Set<{ at: number; fn: () => void }>();
4
+ return {
5
+ now: () => now,
6
+ schedule(fn: () => void, ms: number) {
7
+ const task = { at: now + ms, fn };
8
+ tasks.add(task);
9
+ return { unref() {}, cancel: () => tasks.delete(task) };
10
+ },
11
+ advance(ms: number) {
12
+ const end = now + ms;
13
+ for (;;) {
14
+ const next = [...tasks].filter((task) => task.at <= end).sort((a, b) => a.at - b.at)[0];
15
+ if (next === undefined) break;
16
+ now = next.at;
17
+ tasks.delete(next);
18
+ next.fn();
19
+ }
20
+ now = end;
21
+ },
22
+ };
23
+ }