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,6 +1,7 @@
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
- import { appendLedger, 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 { makeGoal, reviseCriterion, reviseWording, splitOrBlock } from "./steering-mutations.js";
5
6
  import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
6
7
  import { iso, ULW_LOOP_STEERING_MUTATION_KINDS, ULW_LOOP_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
@@ -223,11 +224,10 @@ export async function steerUlwLoop(repoRoot, proposal, scope) {
223
224
  }
224
225
  const at = proposal.now?.toISOString() ?? iso();
225
226
  const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
226
- if (accepted)
227
- await writePlan(repoRoot, next, scope);
228
- await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
227
+ const entries = [ledgerEntry(proposal, finalAudit, at)];
229
228
  if (batchEntry !== null)
230
- await appendLedger(repoRoot, batchEntry, scope);
229
+ entries.push(batchEntry);
230
+ await commit(repoRoot, scope, { plan: next, entries });
231
231
  return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
232
232
  });
233
233
  }
@@ -1,6 +1,8 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { isAbsolute, join, resolve, sep } from "node:path";
3
+ import { readLedgerAt } from "./ledger.js";
3
4
  import { normalizeUlwLoopSessionId, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
5
+ import { readUlwLoopPlanSync } from "./plan-io.js";
4
6
  import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
5
7
  // Turn-death recovery only: Codex emits Stop when a turn ends, so a run that
6
8
  // dies mid-turn (crash, kill, context blowup before any Stop) never reaches
@@ -27,7 +29,7 @@ export function runStopResumeHook(input) {
27
29
  return "";
28
30
  const scope = { sessionId: payload.session_id };
29
31
  const stateDir = ulwLoopDir(payload.cwd, scope);
30
- const plan = readPlan(join(stateDir, "goals.json"));
32
+ const plan = readPlan(payload.cwd, payload.session_id);
31
33
  if (plan === null || plan.aggregateCompletion?.status === "complete")
32
34
  return "";
33
35
  const goal = resumableGoal(plan);
@@ -76,11 +78,10 @@ function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
76
78
  throw error;
77
79
  }
78
80
  }
79
- // Two-strike cap keyed on ledger movement: an unchanged ledger.jsonl line
80
- // count across resumes means the loop is not progressing. The stuck marker is
81
- // a separate file — a ledger append would change the count and self-reset.
81
+ // Hook budget counters are exempt from plan/audit commits. Count reconciled
82
+ // entries, not cache lines: a writer dying after link still made progress.
82
83
  function consumeResumeBudget(stateDir, goalId) {
83
- const ledgerLineCount = countLedgerLines(join(stateDir, "ledger.jsonl"));
84
+ const ledgerLineCount = readLedgerAt(stateDir).length;
84
85
  const counterPath = resolve(stateDir, `auto-resume-${goalId}.json`);
85
86
  const stuckPath = resolve(stateDir, `auto-resume-${goalId}.stuck`);
86
87
  // goals.json is untrusted input: a crafted goal id (e.g. `../../x`) must
@@ -111,9 +112,9 @@ function renderResumeDirective(plan, goal, sessionId) {
111
112
  "If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead.",
112
113
  ].join("\n");
113
114
  }
114
- function readPlan(goalsPath) {
115
+ function readPlan(repoRoot, sessionId) {
115
116
  try {
116
- return JSON.parse(readFileSync(goalsPath, "utf8"));
117
+ return readUlwLoopPlanSync(repoRoot, { sessionId });
117
118
  }
118
119
  catch (error) {
119
120
  if (error instanceof Error)
@@ -121,16 +122,6 @@ function readPlan(goalsPath) {
121
122
  throw error;
122
123
  }
123
124
  }
124
- function countLedgerLines(ledgerPath) {
125
- try {
126
- return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
127
- }
128
- catch (error) {
129
- if (error instanceof Error)
130
- return 0;
131
- throw error;
132
- }
133
- }
134
125
  function readCounter(counterPath) {
135
126
  try {
136
127
  if (!existsSync(counterPath))
@@ -7,20 +7,20 @@
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.60) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 5.0.0-beta.63) Checking Ulw-Loop Steering"
11
11
  }
12
12
  ]
13
13
  }
14
14
  ],
15
15
  "PostToolUse": [
16
16
  {
17
- "matcher": "^(spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
17
+ "matcher": "^(spawn_agent|multi_agent_v1\\.spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
18
18
  "hooks": [
19
19
  {
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.60) Recording Spawn Admission"
23
+ "statusMessage": "(OmO 5.0.0-beta.63) Recording Spawn Admission"
24
24
  }
25
25
  ]
26
26
  }
@@ -33,18 +33,18 @@
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.60) Enforcing Unlimited Ulw-Loop Budget"
36
+ "statusMessage": "(OmO 5.0.0-beta.63) Enforcing Unlimited Ulw-Loop Budget"
37
37
  }
38
38
  ]
39
39
  },
40
40
  {
41
- "matcher": "^(spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
41
+ "matcher": "^(spawn_agent|multi_agent_v1\\.spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
42
42
  "hooks": [
43
43
  {
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.60) 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.60) 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.60",
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
  }