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

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 (215) hide show
  1. package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
  2. package/dist/cli/index.js +70 -41
  3. package/dist/cli-node/index.js +70 -41
  4. package/package.json +2 -6
  5. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  6. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +4 -2
  7. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  8. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  9. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  10. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  11. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  12. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  13. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  14. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  15. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  16. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  17. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  19. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
  25. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +5 -4
  26. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  29. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  30. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +13 -0
  33. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +5 -4
  34. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +7 -5
  35. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +7 -2
  36. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -4
  37. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +931 -442
  38. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +6 -0
  39. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +20 -12
  40. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +8 -0
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.d.ts +4 -0
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.js +18 -0
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.d.ts +1 -0
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.js +37 -0
  45. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.d.ts +1 -0
  46. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +24 -12
  47. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
  48. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
  49. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +1 -0
  50. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +6 -0
  51. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
  52. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
  53. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +6 -2
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +54 -25
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.d.ts +10 -3
  56. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.js +38 -31
  57. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
  58. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
  59. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
  60. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
  61. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
  62. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
  63. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +11 -8
  64. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +28 -12
  65. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.d.ts +335 -0
  66. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.js +109 -10
  67. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.d.ts +3 -0
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
  69. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +8 -62
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.d.ts +3 -2
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +4 -4
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +9 -1
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.d.ts +3 -2
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +11 -11
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.d.ts +9 -0
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.js +41 -0
  84. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  85. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  86. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
  87. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +15 -3
  88. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +7 -4
  89. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +26 -14
  90. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +8 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/src/driver-objective-ack.ts +20 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence-artifacts.ts +45 -0
  93. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +23 -16
  94. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
  95. package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +7 -0
  96. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
  97. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +67 -54
  98. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts +63 -31
  99. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
  100. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
  101. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
  102. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +11 -7
  103. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +47 -12
  104. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/manifest.ts +124 -10
  105. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/types.ts +3 -0
  106. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
  107. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +8 -59
  108. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
  109. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
  110. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
  111. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-mutations.ts +5 -4
  112. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +13 -1
  113. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +12 -10
  114. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
  115. package/packages/omo-codex/plugin/components/ulw-loop/src/success-criteria-input.ts +56 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +37 -1
  117. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
  118. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
  119. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
  120. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
  121. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
  127. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +33 -4
  130. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-dx.test.ts +219 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
  133. package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
  134. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
  135. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
  136. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  137. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  138. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  139. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  140. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  141. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  157. package/packages/omo-codex/plugin/package-lock.json +12 -12
  158. package/packages/omo-codex/plugin/package.json +1 -1
  159. package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
  160. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +15 -7
  161. package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
  162. package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
  163. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  164. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
  165. package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  166. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  167. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
  168. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  169. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
  170. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  171. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  172. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  173. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  174. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  175. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  176. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  177. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  178. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  179. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  180. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +5 -4
  181. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +1 -1
  182. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  183. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
  184. package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
  185. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
  186. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
  187. package/packages/omo-codex/scripts/install-dist/install-local.mjs +6 -4
  188. package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
  189. package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
  190. package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
  191. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  192. package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
  193. package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
  194. package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  195. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  196. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  197. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
  198. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  199. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
  200. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  201. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  202. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  203. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  204. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  205. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  206. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  207. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  208. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  209. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  210. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
  211. package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
  212. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  213. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
  214. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
  215. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // components/ulw-loop/src/checkpoint.ts
4
- import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
5
- import { readFile as readFile4 } from "node:fs/promises";
6
- import { resolve as resolve3 } from "node:path";
4
+ import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
5
+ import { readFile as readFile3 } from "node:fs/promises";
6
+ import { resolve as resolve4 } from "node:path";
7
7
 
8
8
  // components/ulw-loop/src/codex-goal-snapshot.ts
9
9
  import { existsSync } from "node:fs";
@@ -80,33 +80,58 @@ function reconcileCodexGoalSnapshot(snapshot, options) {
80
80
  const effectiveSnapshot = snapshot ?? { available: false, raw: null };
81
81
  const errors = [];
82
82
  const warnings = [];
83
+ const nextActions = [];
83
84
  const expected = options.expectedObjective;
84
- const normalizedExpected = normalizeObjective(expected);
85
85
  if (!effectiveSnapshot.available) {
86
- warnings.push(`call get_goal; if none, create_goal with codexObjective "${expected}" verbatim`);
87
- return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
86
+ nextActions.push(`call get_goal; if none, create_goal with codexObjective "${expected}" verbatim`);
87
+ return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, nextActions, errors };
88
88
  }
89
- const accepted = new Set([
90
- normalizedExpected,
91
- ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective))
92
- ].filter(Boolean));
89
+ const normalized = (objectives) => new Set(objectives.map(normalizeObjective).filter(Boolean));
90
+ const accepted = normalized([expected, ...options.acceptedObjectives ?? []]);
91
+ const acknowledged = normalized(options.acknowledgedObjectives ?? []);
93
92
  const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
94
- if (actual && !accepted.has(normalizeObjective(actual))) {
93
+ let unacknowledgedObjective;
94
+ if (actual && !accepted.has(actual) && !acknowledged.has(actual)) {
95
95
  warnings.push(`driver_objective_differs: expected "${expected}", got "${actual}".`);
96
+ unacknowledgedObjective = actual;
96
97
  }
97
98
  const actualStatus = effectiveSnapshot.status ?? "unknown";
98
99
  if (actualStatus === "paused" || actualStatus === "usage_limited" || actualStatus === "budget_limited") {
99
- warnings.push("/goal resume or raise the budget");
100
+ nextActions.push("/goal resume or raise the budget");
100
101
  }
101
102
  if (actualStatus === "complete")
102
- warnings.push(`driver closed early: call create_goal with codexObjective "${expected}" verbatim`);
103
- return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
103
+ nextActions.push(`driver closed early: call create_goal with codexObjective "${expected}" verbatim`);
104
+ return {
105
+ ok: errors.length === 0,
106
+ snapshot: effectiveSnapshot,
107
+ warnings,
108
+ nextActions,
109
+ errors,
110
+ ...unacknowledgedObjective === undefined ? {} : { unacknowledgedObjective }
111
+ };
104
112
  }
105
113
  function formatCodexGoalReconciliation(reconciliation) {
106
- const parts = [...reconciliation.errors, ...reconciliation.warnings];
114
+ const parts = [...reconciliation.errors, ...reconciliation.nextActions, ...reconciliation.warnings];
107
115
  return parts.join(" ");
108
116
  }
109
117
 
118
+ // components/ulw-loop/src/driver-objective-ack.ts
119
+ function normalizeDriverObjective(value) {
120
+ return value.replace(/\s+/g, " ").trim();
121
+ }
122
+ function acknowledgedDriverObjectives(plan) {
123
+ return plan.acknowledgedDriverObjectives ?? [];
124
+ }
125
+ function acknowledgeDriverObjective(plan, objective) {
126
+ if (objective === undefined)
127
+ return false;
128
+ const normalized = normalizeDriverObjective(objective);
129
+ if (!normalized || acknowledgedDriverObjectives(plan).includes(normalized))
130
+ return false;
131
+ plan.acknowledgedDriverObjectives = [...acknowledgedDriverObjectives(plan), normalized];
132
+ return true;
133
+ }
134
+
110
135
  // components/ulw-loop/src/paths.ts
111
136
  import { isAbsolute, join, relative, sep } from "node:path";
112
137
  // components/ulw-loop/src/constants.ts
@@ -178,15 +203,9 @@ function ulwLoopGoalsRelativePath(scope) {
178
203
  function ulwLoopLedgerRelativePath(scope) {
179
204
  return `${ulwLoopRelativeDir(scope)}/${ULW_LOOP_LEDGER}`;
180
205
  }
181
- function ulwLoopBriefPath(repoRoot, scope) {
182
- return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_BRIEF);
183
- }
184
206
  function ulwLoopGoalsPath(repoRoot, scope) {
185
207
  return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_GOALS);
186
208
  }
187
- function ulwLoopLedgerPath(repoRoot, scope) {
188
- return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_LEDGER);
189
- }
190
209
  function ulwLoopStateLockPath(repoRoot, scope) {
191
210
  return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_STATE_LOCK);
192
211
  }
@@ -199,6 +218,10 @@ function repoRelative(absolutePath, repoRoot) {
199
218
  return absolutePath.slice(backslashPrefix.length).split("\\").join("/");
200
219
  return absolutePath.split("\\").join("/");
201
220
  }
221
+ function ulwLoopEvidenceRoot(scope) {
222
+ const sessionId = normalizeUlwLoopSessionId(scope?.sessionId);
223
+ return sessionId === null ? ".omo/evidence" : `.omo/evidence/ulw/${sessionId}`;
224
+ }
202
225
  function ulwLoopAttemptEvidenceDir(goalId, attempt, scope) {
203
226
  const sessionId = normalizeUlwLoopSessionId(scope?.sessionId);
204
227
  if (sessionId === null) {
@@ -305,14 +328,16 @@ async function validateCheckpointCodexGoal(input) {
305
328
  const expected = expectedCodexObjective(input.plan, input.goal);
306
329
  const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
307
330
  expectedObjective: expected,
331
+ acknowledgedObjectives: acknowledgedDriverObjectives(input.plan),
308
332
  ...codexGoalMode(input.plan) === "aggregate" ? { acceptedObjectives: compatibleCodexObjectives(input.plan) } : {}
309
333
  });
310
334
  if (!reconciliation.ok)
311
335
  throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
312
336
  return {
313
337
  raw: snapshot?.raw,
314
- nextActions: reconciliation.warnings,
315
- warnings: reconciliation.warnings.filter((warning) => warning.startsWith("driver_objective_differs"))
338
+ nextActions: reconciliation.nextActions,
339
+ warnings: reconciliation.warnings,
340
+ ...reconciliation.unacknowledgedObjective === undefined ? {} : { unacknowledgedObjective: reconciliation.unacknowledgedObjective }
316
341
  };
317
342
  }
318
343
  function combineCheckpointValidationErrors(codexError, gateError) {
@@ -322,23 +347,177 @@ ${gateError.message}`, "ULW_LOOP_QUALITY_GATE_INVALID", {
322
347
  });
323
348
  }
324
349
 
350
+ // components/ulw-loop/src/evidence-artifacts.ts
351
+ import { existsSync as existsSync2 } from "node:fs";
352
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
353
+ function fail(message, code, details) {
354
+ throw new UlwLoopError(message, code, { details });
355
+ }
356
+ function stored(repoRoot, absolute) {
357
+ const rel = relative2(repoRoot, absolute);
358
+ const inside = rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
359
+ return inside ? rel.split(sep2).join("/") : absolute;
360
+ }
361
+ function resolveEvidenceArtifacts(repoRoot, artifacts) {
362
+ if (artifacts === undefined)
363
+ return;
364
+ const seen = new Set;
365
+ const resolved = [];
366
+ for (const [index, candidate] of artifacts.entries()) {
367
+ const path = typeof candidate === "string" ? candidate.trim() : "";
368
+ if (!path)
369
+ fail(`Artifact ${index + 1} must be a non-empty path.`, "ULW_LOOP_ARGUMENT_INVALID", { index });
370
+ const absolute = resolve2(repoRoot, path);
371
+ if (!existsSync2(absolute))
372
+ fail(`Evidence artifact does not exist: ${path} (resolved to ${absolute}).`, "ULW_LOOP_EVIDENCE_ARTIFACT_MISSING", {
373
+ path,
374
+ resolved: absolute
375
+ });
376
+ const value = stored(repoRoot, absolute);
377
+ if (!seen.has(value)) {
378
+ seen.add(value);
379
+ resolved.push(value);
380
+ }
381
+ }
382
+ return resolved;
383
+ }
384
+
385
+ // components/ulw-loop/src/plan-commit.ts
386
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
387
+ import { randomUUID as randomUUID2 } from "node:crypto";
388
+ import { linkSync, mkdirSync as mkdirSync2, renameSync, rmSync, writeFileSync } from "node:fs";
389
+ import { rename, writeFile } from "node:fs/promises";
390
+ import { join as join4 } from "node:path";
391
+
392
+ // components/ulw-loop/src/ledger.ts
393
+ import { join as join3 } from "node:path";
394
+
395
+ // components/ulw-loop/src/plan-log.ts
396
+ import { existsSync as existsSync3, readdirSync, readFileSync } from "node:fs";
397
+ import { join as join2 } from "node:path";
398
+ function hasCode(error, code) {
399
+ return error instanceof Error && "code" in error && error.code === code;
400
+ }
401
+ function readOptional(path) {
402
+ try {
403
+ return readFileSync(path, "utf8");
404
+ } catch (error) {
405
+ if (hasCode(error, "ENOENT"))
406
+ return;
407
+ throw error;
408
+ }
409
+ }
410
+ function logNames(dir) {
411
+ try {
412
+ return readdirSync(join2(dir, "revisions")).filter((name) => /^\d{8,}\.json$/.test(name)).sort();
413
+ } catch (error) {
414
+ if (hasCode(error, "ENOENT"))
415
+ return [];
416
+ throw error;
417
+ }
418
+ }
419
+ function readRecord(dir, name) {
420
+ try {
421
+ const record = JSON.parse(readFileSync(join2(dir, "revisions", name), "utf8"));
422
+ return record.version === 1 && Number.isInteger(record.revision) && record.plan?.version === 1 && Array.isArray(record.plan.goals) && Array.isArray(record.ledger) ? record : undefined;
423
+ } catch (error) {
424
+ if (!(error instanceof SyntaxError))
425
+ throw error;
426
+ return;
427
+ }
428
+ }
429
+ function readRecords(dir) {
430
+ const records = [];
431
+ for (const name of logNames(dir)) {
432
+ const record = readRecord(dir, name);
433
+ if (record !== undefined)
434
+ records.push(record);
435
+ }
436
+ return records.sort((a, b) => a.revision - b.revision);
437
+ }
438
+ function readNewestRecord(dir) {
439
+ const names = logNames(dir).sort((a, b) => Number.parseInt(b, 10) - Number.parseInt(a, 10));
440
+ for (const name of names) {
441
+ const record = readRecord(dir, name);
442
+ if (record !== undefined)
443
+ return record;
444
+ }
445
+ return;
446
+ }
447
+ function reconcilePlan(dir) {
448
+ const raw = readOptional(join2(dir, "goals.json"));
449
+ let cached;
450
+ if (raw !== undefined) {
451
+ try {
452
+ cached = JSON.parse(raw);
453
+ } catch (error) {
454
+ if (!(error instanceof SyntaxError))
455
+ throw error;
456
+ }
457
+ }
458
+ const latest = readNewestRecord(dir);
459
+ return latest !== undefined && latest.revision >= (cached?.revision ?? 0) ? latest.plan : cached;
460
+ }
461
+ function planExists(repoRoot, scope) {
462
+ const dir = ulwLoopDir(repoRoot, scope);
463
+ return existsSync3(join2(dir, "goals.json")) || logNames(dir).length > 0;
464
+ }
465
+
466
+ // components/ulw-loop/src/ledger.ts
467
+ function readLedgerAt(dir) {
468
+ const entries = new Map;
469
+ const lines = (readOptional(join3(dir, "ledger.jsonl")) ?? "").split(/\r?\n/);
470
+ for (const [index, line] of lines.entries()) {
471
+ if (line.trim().length === 0)
472
+ continue;
473
+ try {
474
+ const entry = JSON.parse(line);
475
+ entry.revision ??= 0;
476
+ entry.id ??= `legacy-${index + 1}`;
477
+ entries.set(entry.id, entry);
478
+ } catch (error) {
479
+ if (!(error instanceof SyntaxError))
480
+ throw error;
481
+ }
482
+ }
483
+ for (const record of readRecords(dir))
484
+ for (const [seq, entry] of record.ledger.entries()) {
485
+ const id = entry.id ?? `${record.revision}-${seq}`;
486
+ entries.set(id, { ...entry, revision: record.revision, id });
487
+ }
488
+ const reset = reconcilePlan(dir)?.ledgerResetRevision ?? 0;
489
+ const sequence = (entry) => Number(entry.id?.split("-").at(-1) ?? 0);
490
+ return [...entries.values()].filter((entry) => (entry.revision ?? 0) >= reset).sort((a, b) => (a.revision ?? 0) - (b.revision ?? 0) || sequence(a) - sequence(b));
491
+ }
492
+ function readLedger(repoRoot, scope) {
493
+ return readLedgerAt(ulwLoopDir(repoRoot, scope));
494
+ }
495
+
325
496
  // components/ulw-loop/src/plan-io.ts
326
497
  import { AsyncLocalStorage } from "node:async_hooks";
327
- import { createReadStream, readdirSync } from "node:fs";
328
- import { appendFile, mkdir, readFile as readFile2, rename, writeFile } from "node:fs/promises";
329
- import { createInterface } from "node:readline";
498
+ import { readdirSync as readdirSync2 } from "node:fs";
330
499
 
331
500
  // components/ulw-loop/src/plan-missing-recovery.ts
332
501
  var ULW_LOOP_CREATE_GOALS_COMMAND = 'omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json';
333
- function planMissingRecovery(existingSessionIds) {
334
- const lines = [`Recovery: bootstrap the plan with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`];
502
+ function createGoalsAction(surface) {
503
+ return surface === "omo-senpi" ? 'agentToolkit.createGoals({ brief: "<brief>" })' : ULW_LOOP_CREATE_GOALS_COMMAND;
504
+ }
505
+ function planMissingRecovery(existingSessionIds, surface = "lazycodex") {
506
+ const lines = [`Recovery: bootstrap the plan with \`${createGoalsAction(surface)}\`.`];
335
507
  if (existingSessionIds.length === 0)
336
508
  return { message: lines.join(`
337
509
  `) };
338
- lines.push(`Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. Re-run with \`--session-id <id>\` to target one of them.`);
510
+ lines.push(surface === "omo-senpi" ? `Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. The SDK is bound to the current session; resume the owning session to target its plan.` : `Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. Re-run with \`--session-id <id>\` to target one of them.`);
339
511
  return { message: lines.join(`
340
512
  `), details: { existingSessionIds } };
341
513
  }
514
+ function planMissingError(planPath, existingSessionIds, surface = "lazycodex") {
515
+ const recovery = planMissingRecovery(existingSessionIds, surface);
516
+ return new UlwLoopError(`No ulw-loop plan found at ${planPath}.
517
+ ${recovery.message}`, "ULW_LOOP_PLAN_MISSING", {
518
+ ...recovery.details === undefined ? {} : { details: recovery.details }
519
+ });
520
+ }
342
521
  function sessionScopeRequiredMessage(flag, existingSessionIds) {
343
522
  const lines = [
344
523
  "No ulw-loop session scope: neither the session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID) nor the flag names this run, and the shared .omo/ulw-loop root is never used implicitly because every session in this directory would read and overwrite it.",
@@ -361,155 +540,207 @@ function sessionIdRequiredMessage(flag) {
361
540
 
362
541
  // components/ulw-loop/src/state-lock.ts
363
542
  import { randomUUID } from "node:crypto";
364
- import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
543
+ import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync as readFileSync2, statSync, unlinkSync, writeSync } from "node:fs";
365
544
  import { dirname } from "node:path";
366
- import { setTimeout as sleep } from "node:timers/promises";
367
545
  var ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
368
546
  var DEFAULT_TIMEOUT_MS = 1e4;
369
547
  var DEFAULT_STALE_MS = 60000;
370
- var MIN_DELAY_MS = 5;
371
- var MAX_DELAY_MS = 100;
548
+ var DEFAULT_LEASE_MS = 30000;
372
549
  var SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
550
+ var systemClock = {
551
+ now: Date.now,
552
+ schedule(fn, ms) {
553
+ const timer = setTimeout(fn, ms);
554
+ return {
555
+ unref: () => {
556
+ timer.unref();
557
+ },
558
+ cancel: () => clearTimeout(timer)
559
+ };
560
+ }
561
+ };
373
562
  async function withStateLock(lockPath, fn, options = {}) {
374
- const token = await acquireAsync(lockPath, options);
563
+ const holder = await acquireAsync(lockPath, options);
564
+ const clock = options.clock ?? systemClock;
565
+ let timer;
566
+ let heartbeatError;
567
+ const beat = () => {
568
+ try {
569
+ holder.record.leaseUntil = clock.now() + (options.leaseMs ?? DEFAULT_LEASE_MS);
570
+ writeRecord(holder);
571
+ schedule();
572
+ } catch (error) {
573
+ heartbeatError = error;
574
+ }
575
+ };
576
+ const schedule = () => {
577
+ if (options.heartbeatMs === 0)
578
+ return;
579
+ timer = clock.schedule(beat, options.heartbeatMs ?? (options.leaseMs ?? DEFAULT_LEASE_MS) / 3);
580
+ timer.unref();
581
+ };
582
+ schedule();
375
583
  try {
376
- return await fn();
584
+ const result = await fn(holder.record.token);
585
+ if (heartbeatError !== undefined)
586
+ throw heartbeatError;
587
+ return result;
377
588
  } finally {
378
- release(lockPath, token);
589
+ timer?.cancel();
590
+ closeSync(holder.fd);
591
+ release(lockPath, holder.record.token);
379
592
  }
380
593
  }
381
594
  function withStateLockSync(lockPath, fn, options = {}) {
382
- const token = acquireSync(lockPath, options);
595
+ const holder = acquireSync(lockPath, options);
383
596
  try {
384
597
  return fn();
385
598
  } finally {
386
- release(lockPath, token);
599
+ closeSync(holder.fd);
600
+ release(lockPath, holder.record.token);
387
601
  }
388
602
  }
389
603
  function isStateLockTimeout(error) {
390
604
  return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
391
605
  }
392
606
  async function acquireAsync(lockPath, options) {
393
- const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
394
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
607
+ const clock = options.clock ?? systemClock;
608
+ const deadline = clock.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
395
609
  mkdirSync(dirname(lockPath), { recursive: true });
396
610
  for (let attempt = 0;; ) {
397
- const outcome = attemptOnce(lockPath, staleMs);
611
+ const outcome = attemptOnce(lockPath, options, clock.now(), options.leaseMs ?? DEFAULT_LEASE_MS);
398
612
  if (outcome.kind === "acquired")
399
- return outcome.token;
613
+ return outcome.holder;
400
614
  if (outcome.kind === "retry")
401
615
  continue;
402
- if (Date.now() >= deadline)
403
- throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
404
- await sleep(backoffMs(attempt));
616
+ if (clock.now() >= deadline)
617
+ throw lockTimeout(lockPath, options);
618
+ await new Promise((resolve) => clock.schedule(resolve, backoffMs(attempt)));
405
619
  attempt += 1;
406
620
  }
407
621
  }
408
622
  function acquireSync(lockPath, options) {
409
623
  const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
410
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
411
624
  mkdirSync(dirname(lockPath), { recursive: true });
412
625
  for (let attempt = 0;; ) {
413
- const outcome = attemptOnce(lockPath, staleMs);
626
+ const outcome = attemptOnce(lockPath, options, Date.now());
414
627
  if (outcome.kind === "acquired")
415
- return outcome.token;
628
+ return outcome.holder;
416
629
  if (outcome.kind === "retry")
417
630
  continue;
418
631
  if (Date.now() >= deadline)
419
- throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
632
+ throw lockTimeout(lockPath, options);
420
633
  Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
421
634
  attempt += 1;
422
635
  }
423
636
  }
424
- function attemptOnce(lockPath, staleMs) {
637
+ function attemptOnce(lockPath, options, now, leaseMs) {
425
638
  try {
426
- const token = tryCreate(lockPath);
427
- if (token !== null)
428
- return { kind: "acquired", token };
429
- const snapshot = readSnapshot(lockPath);
639
+ const holder = tryCreate(lockPath, now, leaseMs);
640
+ if (holder !== null)
641
+ return { kind: "acquired", holder };
642
+ const snapshot = readSnapshot(lockPath, now);
430
643
  if (snapshot === null)
431
644
  return { kind: "retry" };
432
- if (isStale(snapshot, staleMs) && reclaim(lockPath, snapshot.raw))
645
+ const record = snapshot.record;
646
+ const stale = record === null ? snapshot.ageMs > (options.staleMs ?? DEFAULT_STALE_MS) : !isProcessAlive(record.pid) || record.leaseUntil !== undefined && now > record.leaseUntil;
647
+ if (stale && reclaim(lockPath, snapshot.raw))
433
648
  return { kind: "retry" };
434
649
  return { kind: "wait" };
435
650
  } catch (error) {
436
- if (hasCode(error, "EINTR"))
651
+ if (hasCode2(error, "EINTR"))
437
652
  return { kind: "wait" };
438
653
  throw error;
439
654
  }
440
655
  }
441
- function tryCreate(lockPath) {
656
+ function writeRecord(holder) {
657
+ const buf = Buffer.from(JSON.stringify(holder.record));
658
+ ftruncateSync(holder.fd, 0);
659
+ let offset = 0;
660
+ while (offset < buf.length) {
661
+ const written = writeSync(holder.fd, buf, offset, buf.length - offset, offset);
662
+ if (written === 0)
663
+ throw new Error("State lock write made no progress.");
664
+ offset += written;
665
+ }
666
+ }
667
+ function tryCreate(lockPath, now, leaseMs) {
442
668
  let fd;
443
669
  try {
444
670
  fd = openSync(lockPath, "wx");
445
671
  } catch (error) {
446
- if (hasCode(error, "EEXIST"))
672
+ if (hasCode2(error, "EEXIST"))
447
673
  return null;
448
674
  throw error;
449
675
  }
450
- const record = { pid: process.pid, createdAt: new Date().toISOString(), token: randomUUID() };
676
+ const record = {
677
+ pid: process.pid,
678
+ createdAt: new Date(now).toISOString(),
679
+ token: randomUUID(),
680
+ ...leaseMs === undefined ? {} : { leaseUntil: now + leaseMs }
681
+ };
682
+ const holder = { fd, record };
451
683
  try {
452
- writeSync(fd, JSON.stringify(record));
684
+ writeRecord(holder);
453
685
  } catch (error) {
454
686
  closeSync(fd);
455
687
  try {
456
688
  unlinkSync(lockPath);
457
- } catch {}
689
+ } catch (cleanup) {
690
+ if (!hasCode2(cleanup, "ENOENT"))
691
+ throw cleanup;
692
+ }
458
693
  throw error;
459
694
  }
460
- closeSync(fd);
461
- return record.token;
695
+ return holder;
462
696
  }
463
- function readSnapshot(lockPath) {
697
+ function readSnapshot(lockPath, now) {
464
698
  try {
465
- const raw = readFileSync(lockPath, "utf8");
466
- const ageMs = Date.now() - statSync(lockPath).mtimeMs;
467
- return { raw, record: parseRecord(raw), ageMs };
699
+ const raw = readFileSync2(lockPath, "utf8");
700
+ return { raw, record: parseRecord(raw), ageMs: now - statSync(lockPath).mtimeMs };
468
701
  } catch (error) {
469
- if (hasCode(error, "ENOENT"))
702
+ if (hasCode2(error, "ENOENT"))
470
703
  return null;
471
704
  throw error;
472
705
  }
473
706
  }
474
707
  function parseRecord(raw) {
475
708
  try {
476
- const parsed = JSON.parse(raw);
477
- if (typeof parsed !== "object" || parsed === null)
709
+ const record = JSON.parse(raw);
710
+ if (typeof record !== "object" || record === null)
478
711
  return null;
479
- const record = parsed;
480
- const pid = record["pid"];
481
- const createdAt = record["createdAt"];
482
- const token = record["token"];
483
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof createdAt !== "string")
712
+ if (!("pid" in record) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0)
484
713
  return null;
485
- if (typeof token !== "string" || token.length === 0)
714
+ if (!("createdAt" in record) || typeof record.createdAt !== "string")
486
715
  return null;
487
- return { pid, createdAt, token };
716
+ if (!("token" in record) || typeof record.token !== "string" || record.token.length === 0)
717
+ return null;
718
+ return {
719
+ pid: record.pid,
720
+ createdAt: record.createdAt,
721
+ token: record.token,
722
+ ..."leaseUntil" in record && typeof record.leaseUntil === "number" ? { leaseUntil: record.leaseUntil } : {}
723
+ };
488
724
  } catch (error) {
489
725
  if (error instanceof SyntaxError)
490
726
  return null;
491
727
  throw error;
492
728
  }
493
729
  }
494
- function isStale(snapshot, staleMs) {
495
- if (snapshot.record === null)
496
- return snapshot.ageMs > staleMs;
497
- return !isProcessAlive(snapshot.record.pid);
498
- }
499
730
  function isProcessAlive(pid) {
500
731
  try {
501
732
  process.kill(pid, 0);
502
733
  return true;
503
734
  } catch (error) {
504
- if (hasCode(error, "ESRCH"))
735
+ if (hasCode2(error, "ESRCH"))
505
736
  return false;
506
- if (hasCode(error, "EPERM"))
737
+ if (hasCode2(error, "EPERM"))
507
738
  return true;
508
739
  throw error;
509
740
  }
510
741
  }
511
742
  function reclaim(lockPath, expectedRaw) {
512
- const current = readSnapshot(lockPath);
743
+ const current = readSnapshot(lockPath, Date.now());
513
744
  if (current === null)
514
745
  return true;
515
746
  if (current.raw !== expectedRaw)
@@ -517,63 +748,102 @@ function reclaim(lockPath, expectedRaw) {
517
748
  try {
518
749
  unlinkSync(lockPath);
519
750
  } catch (error) {
520
- if (!hasCode(error, "ENOENT"))
751
+ if (hasCode2(error, "EPERM") || hasCode2(error, "EACCES"))
752
+ return false;
753
+ if (!hasCode2(error, "ENOENT"))
521
754
  throw error;
522
755
  }
523
756
  return true;
524
757
  }
525
758
  function release(lockPath, token) {
526
- const current = readSnapshot(lockPath);
527
- if (current === null || current.record?.token !== token)
759
+ if (readSnapshot(lockPath, Date.now())?.record?.token !== token)
528
760
  return;
529
761
  try {
530
762
  unlinkSync(lockPath);
531
763
  } catch (error) {
532
- if (!hasCode(error, "ENOENT"))
764
+ if (!hasCode2(error, "ENOENT"))
533
765
  throw error;
534
766
  }
535
767
  }
536
768
  function backoffMs(attempt) {
537
- const exponential = Math.min(MAX_DELAY_MS, MIN_DELAY_MS * 2 ** attempt);
538
- return exponential + Math.random() * MIN_DELAY_MS;
769
+ return Math.min(100, 5 * 2 ** attempt) + Math.random() * 5;
539
770
  }
540
- function lockTimeout(lockPath, timeoutMs) {
541
- const holder = readSnapshot(lockPath)?.record;
542
- const owner = holder === undefined || holder === null ? "another process" : `pid ${holder.pid}`;
543
- return new UlwLoopError(`ulw-loop state lock ${lockPath} is held by ${owner} for more than ${timeoutMs}ms; retry once that process finishes, or delete the lock file if that process is gone.`, ULW_LOOP_LOCK_TIMEOUT_CODE, {
544
- details: {
545
- lockPath,
546
- timeoutMs,
547
- ...holder === undefined || holder === null ? {} : { holderPid: holder.pid }
548
- }
549
- });
771
+ function lockTimeout(lockPath, options) {
772
+ const holder = readSnapshot(lockPath, Date.now())?.record;
773
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
774
+ const owner = holder == null ? "another process" : `pid ${holder.pid}`;
775
+ return new UlwLoopError(`ulw-loop state lock ${lockPath} is held by ${owner} for more than ${timeoutMs}ms. The lock owner is still alive. If a JS eval kernel was interrupted while writing, wait for its lease to expire (${options.leaseMs ?? DEFAULT_LEASE_MS} ms) or restart the owning senpi process; never delete a lock owned by a live process.`, ULW_LOOP_LOCK_TIMEOUT_CODE, { details: { lockPath, timeoutMs, ...holder == null ? {} : { holderPid: holder.pid } } });
550
776
  }
551
- function hasCode(error, code) {
777
+ function hasCode2(error, code) {
552
778
  return error instanceof Error && "code" in error && error.code === code;
553
779
  }
554
780
 
555
781
  // components/ulw-loop/src/plan-io.ts
782
+ function readLedger2(repoRoot, scope) {
783
+ const lockPath = ulwLoopStateLockPath(repoRoot, scope);
784
+ if (heldLocks.getStore()?.has(lockPath)) {
785
+ assertStateLockOwned(lockPath);
786
+ materializeSync(ulwLoopDir(repoRoot, scope));
787
+ }
788
+ return readLedger(repoRoot, scope);
789
+ }
556
790
  var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
557
791
  var LEGACY_OBJECTIVE = `Complete all ulw-loop stories listed in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}. Use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the durable audit trail.`;
558
792
  var locks = new Map;
559
793
  var heldLocks = new AsyncLocalStorage;
560
- function hasCode2(error, code) {
561
- return error instanceof Error && "code" in error && error.code === code;
562
- }
563
- function isLegacyEnumeratedAggregateObjective(objective) {
564
- return objective === LEGACY_OBJECTIVE || Boolean(objective?.startsWith(LEGACY_OBJECTIVE_PREFIX));
794
+ var lockOptions = new AsyncLocalStorage;
795
+ function tokenAt(lockPath) {
796
+ const raw = readOptional(lockPath);
797
+ if (raw === undefined)
798
+ return;
799
+ try {
800
+ const record = JSON.parse(raw);
801
+ return typeof record === "object" && record !== null && "token" in record && typeof record.token === "string" ? record.token : undefined;
802
+ } catch (error) {
803
+ if (error instanceof SyntaxError)
804
+ return;
805
+ throw error;
806
+ }
565
807
  }
566
- function isSteeringKind(value) {
567
- return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised" || value === "batch_updated";
808
+ function assertStateLockOwned(lockPath) {
809
+ const context = heldLocks.getStore()?.get(lockPath);
810
+ if (context === undefined)
811
+ return;
812
+ if (tokenAt(lockPath) !== context.token)
813
+ throw new UlwLoopError("The ulw-loop mutation lock changed owners.", "ULW_LOOP_LOCK_LOST");
814
+ }
815
+ function migrationEntries(plan) {
816
+ for (const context of heldLocks.getStore()?.values() ?? []) {
817
+ const entries = context.migrations.get(plan.goalsPath);
818
+ if (entries !== undefined) {
819
+ context.migrations.delete(plan.goalsPath);
820
+ return entries;
821
+ }
822
+ }
823
+ return [];
568
824
  }
569
- async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
825
+ async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn, options = {}) {
570
826
  const scope = typeof scopeOrFn === "function" ? undefined : scopeOrFn;
571
827
  const fn = typeof scopeOrFn === "function" ? scopeOrFn : maybeFn;
572
828
  if (fn === undefined)
573
829
  throw new UlwLoopError("Missing ulw-loop mutation body.", "ULW_LOOP_LOCK_BODY_MISSING");
574
830
  const lockKey = `${repoRoot}\x00${ulwLoopRelativeDir(scope)}`;
575
831
  const lockPath = ulwLoopStateLockPath(repoRoot, scope);
576
- const locked = () => withStateLock(lockPath, () => heldLocks.run(new Set([...heldLocks.getStore() ?? [], lockKey]), fn));
832
+ const locked = () => withStateLock(lockPath, async (token) => {
833
+ return heldLocks.run(new Map([...heldLocks.getStore() ?? [], [lockPath, { token, migrations: new Map }]]), async () => {
834
+ for (let attempt = 0;; attempt += 1) {
835
+ try {
836
+ return await fn();
837
+ } catch (error) {
838
+ if (!(error instanceof UlwLoopError) || error.code !== "ULW_LOOP_PUBLISH_CONFLICT")
839
+ throw error;
840
+ assertStateLockOwned(lockPath);
841
+ if (attempt !== 0)
842
+ throw error;
843
+ }
844
+ }
845
+ });
846
+ }, { ...lockOptions.getStore(), ...options });
577
847
  const prior = locks.get(lockKey) ?? Promise.resolve(undefined);
578
848
  const run = prior.then(locked, locked);
579
849
  const gate = run.then(() => {
@@ -588,98 +858,140 @@ async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
588
858
  });
589
859
  return run;
590
860
  }
591
- async function readUlwLoopPlan(repoRoot, scope) {
861
+ function readUlwLoopPlanSync(repoRoot, scope) {
592
862
  const path = ulwLoopGoalsPath(repoRoot, scope);
593
- let raw;
594
- try {
595
- raw = await readFile2(path, "utf8");
596
- } catch (error) {
597
- if (!hasCode2(error, "ENOENT"))
598
- throw error;
599
- const recovery = planMissingRecovery(listUlwLoopSessionIds(repoRoot));
600
- throw new UlwLoopError(`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.
601
- ${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.details === undefined ? {} : { details: recovery.details } });
602
- }
603
- const parsed = JSON.parse(raw);
604
- if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
863
+ const parsed = reconcilePlan(ulwLoopDir(repoRoot, scope));
864
+ if (parsed === undefined)
865
+ throw planMissingError(repoRelative(path, repoRoot), listUlwLoopSessionIds(repoRoot));
866
+ if (parsed.version !== 1 || !Array.isArray(parsed.goals))
605
867
  throw new UlwLoopError(`Invalid ulw-loop plan at ${repoRelative(path, repoRoot)}.`, "ULW_LOOP_PLAN_INVALID");
606
- }
607
868
  const previousObjective = parsed.codexObjective;
608
- if ((parsed.codexGoalMode ?? "per_story") === "aggregate" && isLegacyEnumeratedAggregateObjective(previousObjective)) {
609
- if (!(heldLocks.getStore()?.has(`${repoRoot}\x00${ulwLoopRelativeDir(scope)}`) ?? false)) {
610
- throw new UlwLoopError(`The ulw-loop plan at ${repoRelative(path, repoRoot)} carries a legacy enumerated aggregate objective that must be migrated before reads continue. Run any state-mutating ulw-loop command once (e.g. \`record-evidence\`, \`steer\`, \`checkpoint\`) to migrate it under the state lock, then retry.`, "ULW_LOOP_MIGRATION_REQUIRED");
611
- }
612
- const now = iso();
869
+ if ((parsed.codexGoalMode ?? "per_story") === "aggregate" && previousObjective !== undefined && (previousObjective === LEGACY_OBJECTIVE || previousObjective.startsWith(LEGACY_OBJECTIVE_PREFIX))) {
613
870
  parsed.codexObjective = aggregateCodexObjectiveForScope(scope);
614
871
  parsed.codexObjectiveAliases = [...new Set([...parsed.codexObjectiveAliases ?? [], previousObjective])];
615
- parsed.updatedAt = now;
616
- await writePlan(repoRoot, parsed, scope);
617
- await appendLedger(repoRoot, {
618
- at: now,
619
- kind: "aggregate_objective_migrated",
620
- message: "Migrated legacy enumerated aggregate Codex objective to the stable pointer objective.",
621
- before: { codexObjective: previousObjective },
622
- after: { codexObjective: parsed.codexObjective }
623
- }, scope);
872
+ heldLocks.getStore()?.get(ulwLoopStateLockPath(repoRoot, scope))?.migrations.set(parsed.goalsPath, [
873
+ {
874
+ at: iso(),
875
+ kind: "aggregate_objective_migrated",
876
+ before: { codexObjective: previousObjective },
877
+ after: { codexObjective: parsed.codexObjective }
878
+ }
879
+ ]);
624
880
  }
625
881
  return parsed;
626
882
  }
883
+ async function readUlwLoopPlan(repoRoot, scope) {
884
+ if (heldLocks.getStore()?.has(ulwLoopStateLockPath(repoRoot, scope))) {
885
+ assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
886
+ await materialize(ulwLoopDir(repoRoot, scope));
887
+ }
888
+ return readUlwLoopPlanSync(repoRoot, scope);
889
+ }
627
890
  function listUlwLoopSessionIds(repoRoot) {
628
891
  try {
629
- return readdirSync(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
892
+ return readdirSync2(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
630
893
  } catch {
631
894
  return [];
632
895
  }
633
896
  }
634
- async function writePlan(repoRoot, plan, scope) {
635
- await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
636
- const path = ulwLoopGoalsPath(repoRoot, scope);
637
- const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
638
- await writeFile(tmpPath, `${JSON.stringify(plan, null, 2)}
639
- `, "utf8");
640
- await rename(tmpPath, path);
641
- }
642
- async function appendLedger(repoRoot, entry, scope) {
643
- await appendLedgerEntries(repoRoot, [entry], scope);
897
+ function isSteeringKind(value) {
898
+ return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised" || value === "batch_updated";
644
899
  }
645
- async function appendLedgerEntries(repoRoot, entries, scope) {
646
- if (entries.length === 0)
647
- return;
648
- await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
649
- await appendFile(ulwLoopLedgerPath(repoRoot, scope), `${entries.map((entry) => JSON.stringify(entry)).join(`
650
- `)}
651
- `, "utf8");
900
+ async function findAcceptedSteeringLedgerEntry(repoRoot, key, scope) {
901
+ return readLedger2(repoRoot, scope).find((entry) => isSteeringKind(entry.kind) && entry.steering?.invariant.accepted === true && (entry.idempotencyKey === key || entry.steering.idempotencyKey === key || entry.steering.promptSignature === key));
652
902
  }
653
- async function* ledgerLines(repoRoot, scope) {
654
- const stream = createReadStream(ulwLoopLedgerPath(repoRoot, scope), { encoding: "utf8" });
655
- const lines = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
903
+
904
+ // components/ulw-loop/src/plan-commit.ts
905
+ var hooks = new AsyncLocalStorage2;
906
+ async function beforePlanMutation() {
907
+ await hooks.getStore()?.beforeMutation?.();
908
+ }
909
+ async function writeViewFile(path, content) {
910
+ await (hooks.getStore()?.writeView ?? writeFile)(path, content);
911
+ }
912
+ async function commit(repoRoot, scope, mutation) {
913
+ const { plan } = mutation;
914
+ const dir = ulwLoopDir(repoRoot, scope);
915
+ assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
916
+ await hooks.getStore()?.beforeCommit?.();
917
+ const next = (plan.revision ?? 0) + 1;
918
+ const brief = plan.brief ?? readOptional(join4(dir, "brief.md")) ?? "";
919
+ const entries = [...migrationEntries(plan), ...mutation.entries].map((entry, seq) => ({
920
+ ...entry,
921
+ revision: next,
922
+ id: `${next}-${seq}`
923
+ }));
924
+ const record = {
925
+ version: 1,
926
+ revision: next,
927
+ plan: { ...plan, revision: next, brief },
928
+ ledger: entries
929
+ };
930
+ mkdirSync2(join4(dir, "revisions"), { recursive: true });
931
+ mkdirSync2(join4(dir, "tmp"), { recursive: true });
932
+ const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}.json`);
656
933
  try {
657
- for await (const line of lines) {
658
- if (line.trim().length > 0)
659
- yield line;
660
- }
661
- } catch (error) {
662
- if (!hasCode2(error, "ENOENT"))
934
+ writeFileSync(temp, `${JSON.stringify(record)}
935
+ `, { flag: "wx" });
936
+ try {
937
+ (hooks.getStore()?.link ?? linkSync)(temp, join4(dir, "revisions", `${String(next).padStart(8, "0")}.json`));
938
+ } catch (error) {
939
+ if (hasCode(error, "EEXIST"))
940
+ throw new UlwLoopError("Another writer published the read revision.", "ULW_LOOP_PUBLISH_CONFLICT", {
941
+ cause: error
942
+ });
943
+ if (["EPERM", "ENOTSUP", "EXDEV"].some((code) => hasCode(error, code)))
944
+ throw new UlwLoopError("Immutable ulw-loop publication requires atomic hard links.", "ULW_LOOP_PUBLISH_UNSUPPORTED_FS", { cause: error });
663
945
  throw error;
946
+ }
664
947
  } finally {
665
- lines.close();
666
- stream.destroy();
948
+ rmSync(temp, { force: true });
667
949
  }
950
+ plan.revision = next;
951
+ plan.brief = brief;
952
+ for (const [index, entry] of mutation.entries.entries())
953
+ Object.assign(entry, entries[index + entries.length - mutation.entries.length]);
954
+ await hooks.getStore()?.afterCommit?.();
955
+ await materialize(dir);
956
+ }
957
+ function viewContents(dir) {
958
+ const plan = reconcilePlan(dir);
959
+ if (plan === undefined || readRecords(dir).length === 0)
960
+ return [];
961
+ const ledger = readLedgerAt(dir);
962
+ const views = [
963
+ ["goals.json", `${JSON.stringify(plan, null, 2)}
964
+ `],
965
+ ["ledger.jsonl", ledger.length === 0 ? "" : `${ledger.map((entry) => JSON.stringify(entry)).join(`
966
+ `)}
967
+ `],
968
+ ["brief.md", plan.brief ?? readOptional(join4(dir, "brief.md")) ?? ""]
969
+ ];
970
+ return views.filter(([name, content]) => readOptional(join4(dir, name)) !== content);
668
971
  }
669
- async function findAcceptedSteeringLedgerEntry(repoRoot, key, scope) {
670
- const probe = JSON.stringify(key);
671
- for await (const line of ledgerLines(repoRoot, scope)) {
672
- if (!line.includes(probe))
673
- continue;
674
- const entry = JSON.parse(line);
675
- if (!isSteeringKind(entry.kind))
676
- continue;
677
- if (entry.steering?.invariant.accepted !== true)
678
- continue;
679
- if (entry.idempotencyKey === key || entry.steering.idempotencyKey === key || entry.steering.promptSignature === key)
680
- return entry;
972
+ function materializeSync(dir) {
973
+ for (const [name, content] of viewContents(dir)) {
974
+ mkdirSync2(join4(dir, "tmp"), { recursive: true });
975
+ const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}-${name}`);
976
+ try {
977
+ writeFileSync(temp, content, { flag: "wx" });
978
+ renameSync(temp, join4(dir, name));
979
+ } finally {
980
+ rmSync(temp, { force: true });
981
+ }
982
+ }
983
+ }
984
+ async function materialize(dir) {
985
+ for (const [name, content] of viewContents(dir)) {
986
+ mkdirSync2(join4(dir, "tmp"), { recursive: true });
987
+ const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}-${name}`);
988
+ try {
989
+ await writeViewFile(temp, content);
990
+ await rename(temp, join4(dir, name));
991
+ } finally {
992
+ rmSync(temp, { force: true });
993
+ }
681
994
  }
682
- return;
683
995
  }
684
996
 
685
997
  // components/ulw-loop/src/evidence.ts
@@ -719,6 +1031,7 @@ async function recordEvidence(repoRoot, args, scope) {
719
1031
  const goal = findGoal(plan, args.goalId);
720
1032
  const criterion = findCriterion(goal, args.criterionId);
721
1033
  const evidence = nonEmptyEvidence(args.evidence);
1034
+ const artifacts = resolveEvidenceArtifacts(repoRoot, args.artifacts);
722
1035
  const kind = ledgerKind(args.status);
723
1036
  const prevStatus = criterion.status;
724
1037
  const capturedAt = iso();
@@ -727,9 +1040,12 @@ async function recordEvidence(repoRoot, args, scope) {
727
1040
  criterion.capturedAt = capturedAt;
728
1041
  if (args.notes !== undefined)
729
1042
  criterion.notes = args.notes;
1043
+ if (artifacts !== undefined)
1044
+ criterion.artifacts = artifacts;
1045
+ else
1046
+ delete criterion.artifacts;
730
1047
  goal.updatedAt = capturedAt;
731
1048
  plan.updatedAt = capturedAt;
732
- await writePlan(repoRoot, plan, scope);
733
1049
  const ledgerEntry = {
734
1050
  at: capturedAt,
735
1051
  kind,
@@ -738,10 +1054,11 @@ async function recordEvidence(repoRoot, args, scope) {
738
1054
  criterionStatus: args.status,
739
1055
  evidence,
740
1056
  capturedEvidence: evidence,
1057
+ ...artifacts === undefined ? {} : { artifacts },
741
1058
  before: { status: prevStatus },
742
1059
  after: { goalId: goal.id, criterionId: criterion.id, status: args.status, evidence, capturedAt, prevStatus }
743
1060
  };
744
- await appendLedger(repoRoot, ledgerEntry, scope);
1061
+ await commit(repoRoot, scope, { plan, entries: [ledgerEntry] });
745
1062
  return { plan, goal, criterion, ledgerEntry };
746
1063
  });
747
1064
  }
@@ -789,7 +1106,7 @@ function requireEssentialCriteriaPass(goal) {
789
1106
  }
790
1107
 
791
1108
  // components/ulw-loop/src/quality-gate-artifacts.ts
792
- import { resolve as resolve2 } from "node:path";
1109
+ import { resolve as resolve3 } from "node:path";
793
1110
 
794
1111
  // components/ulw-loop/src/quality-gate-fields.ts
795
1112
  var PLACEHOLDER_PATTERN = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
@@ -928,14 +1245,14 @@ function artifactCompatible(surface, kind) {
928
1245
  function checkFile(path, field, opts) {
929
1246
  if (opts?.repoRoot === undefined || opts.fs === undefined || isPoisoned(field))
930
1247
  return;
931
- const absolute = resolve2(opts.repoRoot, path);
1248
+ const absolute = resolve3(opts.repoRoot, path);
932
1249
  if (!opts.fs.existsSync(absolute)) {
933
1250
  invalid(`${field} must point to an existing artifact.`, field);
934
1251
  return;
935
1252
  }
936
1253
  if (opts.fs.statSync(absolute).size <= 0)
937
1254
  invalid(`${field} must point to a non-empty artifact.`, field);
938
- if (opts.currentAttemptDir !== undefined && !isWithinAttemptDir(absolute, resolve2(opts.repoRoot, opts.currentAttemptDir)))
1255
+ if (opts.currentAttemptDir !== undefined && !isWithinAttemptDir(absolute, resolve3(opts.repoRoot, opts.currentAttemptDir)))
939
1256
  invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
940
1257
  }
941
1258
  function artifactMap(refs) {
@@ -1011,8 +1328,8 @@ function adversarialVerdict(row, field) {
1011
1328
  }
1012
1329
 
1013
1330
  // components/ulw-loop/src/surface.ts
1014
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
1015
- import { dirname as dirname2, join as join2 } from "node:path";
1331
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
1332
+ import { dirname as dirname2, join as join5 } from "node:path";
1016
1333
  import { fileURLToPath } from "node:url";
1017
1334
  var REVIEWER_ROLES_BY_SURFACE = {
1018
1335
  lazycodex: {
@@ -1066,14 +1383,14 @@ function resolveToolkitSurface(options) {
1066
1383
  if (fromEnv !== null)
1067
1384
  return fromEnv;
1068
1385
  const entryDir = options?.entryDir ?? dirname2(fileURLToPath(import.meta.url));
1069
- const markerPath = join2(entryDir, SURFACE_MARKER_FILENAME);
1386
+ const markerPath = join5(entryDir, SURFACE_MARKER_FILENAME);
1070
1387
  return readMarkerSurface(markerPath) ?? "lazycodex";
1071
1388
  }
1072
1389
  function readMarkerSurface(markerPath) {
1073
1390
  try {
1074
- if (!existsSync2(markerPath))
1391
+ if (!existsSync4(markerPath))
1075
1392
  return null;
1076
- const parsed = JSON.parse(readFileSync2(markerPath, "utf8"));
1393
+ const parsed = JSON.parse(readFileSync3(markerPath, "utf8"));
1077
1394
  return parseSurface(parsed["surface"]);
1078
1395
  } catch (error) {
1079
1396
  if (error instanceof Error)
@@ -1274,7 +1591,7 @@ function parseAdversarialCases(value, byId) {
1274
1591
  }
1275
1592
 
1276
1593
  // components/ulw-loop/src/cli-arg-parser.ts
1277
- import { readFile as readFile3 } from "node:fs/promises";
1594
+ import { readFile as readFile2 } from "node:fs/promises";
1278
1595
  var VALUE_FLAGS = new Set("--brief --brief-file --session-id --codex-goal-mode --validation-batch-json --goal --goal-id --criterion-id --status --evidence --notes --codex-goal-json --quality-gate-json --kind --rationale --title --objective --target-goal-id --source --after-json --directive-json --directive-file --idempotency-key --proposals-json".split(" "));
1279
1596
  var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
1280
1597
  function hasFlag(argv, flag) {
@@ -1322,7 +1639,7 @@ async function readJsonInput(value) {
1322
1639
  if (value === undefined)
1323
1640
  return;
1324
1641
  try {
1325
- return JSON.parse(looksLikeJson(value) ? value : await readFile3(value, "utf8"));
1642
+ return JSON.parse(looksLikeJson(value) ? value : await readFile2(value, "utf8"));
1326
1643
  } catch (error) {
1327
1644
  const message = error instanceof Error ? error.message : "unknown error";
1328
1645
  throw new UlwLoopError(`Invalid JSON input: ${message}`, "ULW_LOOP_JSON_INPUT_INVALID", { cause: error });
@@ -1332,7 +1649,7 @@ async function parseCodexGoalJson(value) {
1332
1649
  if (value === undefined)
1333
1650
  return;
1334
1651
  try {
1335
- const raw = looksLikeJson(value) ? value : await readFile3(value, "utf8");
1652
+ const raw = looksLikeJson(value) ? value : await readFile2(value, "utf8");
1336
1653
  JSON.parse(raw);
1337
1654
  return raw;
1338
1655
  } catch (error) {
@@ -1384,23 +1701,23 @@ async function parseValidationBatches(input, goals) {
1384
1701
  if (raw === undefined)
1385
1702
  return;
1386
1703
  if (!Array.isArray(raw))
1387
- fail("--validation-batch-json must be a JSON array.");
1704
+ fail2("--validation-batch-json must be a JSON array.");
1388
1705
  const batches = raw.map(batchFromObject);
1389
1706
  validateBatches(batches, goals);
1390
1707
  return batches;
1391
1708
  }
1392
1709
  function batchFromObject(value) {
1393
1710
  if (!isObject(value))
1394
- fail("validation batch entries must be objects.");
1711
+ fail2("validation batch entries must be objects.");
1395
1712
  const batchId = text(value, "batchId");
1396
1713
  const memberIds = strings(value, "memberIds");
1397
1714
  const finalGoalId = text(value, "finalGoalId");
1398
1715
  if (batchId === undefined)
1399
- fail("validation batch requires batchId.");
1716
+ fail2("validation batch requires batchId.");
1400
1717
  if (memberIds === undefined || memberIds.length < 2)
1401
- fail("validation batch requires at least two memberIds.");
1718
+ fail2("validation batch requires at least two memberIds.");
1402
1719
  if (finalGoalId === undefined)
1403
- fail("validation batch requires finalGoalId.");
1720
+ fail2("validation batch requires finalGoalId.");
1404
1721
  return { batchId, memberIds, finalGoalId };
1405
1722
  }
1406
1723
  function validateBatches(batches, goals) {
@@ -1409,17 +1726,17 @@ function validateBatches(batches, goals) {
1409
1726
  const members = new Set;
1410
1727
  for (const batch of batches) {
1411
1728
  if (batchIds.has(batch.batchId))
1412
- fail(`duplicate validation batch id: ${batch.batchId}.`);
1729
+ fail2(`duplicate validation batch id: ${batch.batchId}.`);
1413
1730
  batchIds.add(batch.batchId);
1414
1731
  if (new Set(batch.memberIds).size !== batch.memberIds.length)
1415
- fail(`validation batch ${batch.batchId} has duplicate memberIds.`);
1732
+ fail2(`validation batch ${batch.batchId} has duplicate memberIds.`);
1416
1733
  if (!batch.memberIds.includes(batch.finalGoalId))
1417
- fail(`validation batch ${batch.batchId} finalGoalId must be a member.`, "ULW_LOOP_VALIDATION_BATCH_FINAL_NOT_MEMBER");
1734
+ fail2(`validation batch ${batch.batchId} finalGoalId must be a member.`, "ULW_LOOP_VALIDATION_BATCH_FINAL_NOT_MEMBER");
1418
1735
  for (const memberId of batch.memberIds) {
1419
1736
  if (!goalIds.has(memberId))
1420
- fail(`validation batch ${batch.batchId} references unknown goal: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_MEMBER_UNKNOWN");
1737
+ fail2(`validation batch ${batch.batchId} references unknown goal: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_MEMBER_UNKNOWN");
1421
1738
  if (members.has(memberId))
1422
- fail(`goal appears in multiple validation batches: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_OVERLAP");
1739
+ fail2(`goal appears in multiple validation batches: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_OVERLAP");
1423
1740
  members.add(memberId);
1424
1741
  }
1425
1742
  }
@@ -1478,12 +1795,12 @@ function memberResolved(plan, goalId) {
1478
1795
  const goal = plan.goals.find((candidate) => candidate.id === goalId);
1479
1796
  return goal !== undefined && isMemberResolved(goal, plan);
1480
1797
  }
1481
- function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
1798
+ function fail2(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
1482
1799
  throw new UlwLoopError(message, code);
1483
1800
  }
1484
1801
 
1485
1802
  // components/ulw-loop/src/checkpoint.ts
1486
- var QUALITY_GATE_FS = { existsSync: existsSync3, statSync: statSync2 };
1803
+ var QUALITY_GATE_FS = { existsSync: existsSync5, statSync: statSync2 };
1487
1804
  function ulwLoopFail2(message, code) {
1488
1805
  throw new UlwLoopError(message, code);
1489
1806
  }
@@ -1505,11 +1822,11 @@ async function readJsonInput2(raw, repoRoot) {
1505
1822
  if (!(error instanceof SyntaxError))
1506
1823
  throw error;
1507
1824
  }
1508
- const path = resolve3(repoRoot, trimmed);
1509
- if (!existsSync3(path))
1825
+ const path = resolve4(repoRoot, trimmed);
1826
+ if (!existsSync5(path))
1510
1827
  return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
1511
1828
  try {
1512
- return JSON.parse(await readFile4(path, "utf8"));
1829
+ return JSON.parse(await readFile3(path, "utf8"));
1513
1830
  } catch (error) {
1514
1831
  return ulwLoopFail2(`Quality gate path does not contain valid JSON${error instanceof Error ? `: ${error.message}` : "."}`, "ulw_loop_json_input_invalid");
1515
1832
  }
@@ -1605,6 +1922,7 @@ async function checkpointUlwLoop(repoRoot, args, scope, dependencies) {
1605
1922
  codexGoal = validation.raw;
1606
1923
  nextActions = validation.nextActions;
1607
1924
  warnings = validation.warnings;
1925
+ acknowledgeDriverObjective(plan, validation.unacknowledgedObjective);
1608
1926
  } catch (error) {
1609
1927
  if (!(error instanceof UlwLoopError))
1610
1928
  throw error;
@@ -1649,12 +1967,12 @@ async function checkpointUlwLoop(repoRoot, args, scope, dependencies) {
1649
1967
  if (aggregateCompletion !== undefined)
1650
1968
  nextActions = [...nextActions, 'aggregate complete — now update_goal({status:"complete"})'];
1651
1969
  plan.updatedAt = now;
1652
- await writePlan(repoRoot, plan, scope);
1653
1970
  const ledgerEntry = buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion);
1654
- await appendLedger(repoRoot, ledgerEntry, scope);
1971
+ const entries = [ledgerEntry];
1655
1972
  const closedBatch = args.status === "complete" ? batchClosedBy(plan, goal.id) : undefined;
1656
1973
  if (closedBatch !== undefined)
1657
- await appendLedger(repoRoot, { at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId }, scope);
1974
+ entries.push({ at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId });
1975
+ await commit(repoRoot, scope, { plan, entries });
1658
1976
  return aggregateCompletion === undefined ? { plan, goal, ledgerEntry, nextActions, warnings } : { plan, goal, ledgerEntry, aggregateCompletion, nextActions, warnings };
1659
1977
  });
1660
1978
  }
@@ -1990,9 +2308,47 @@ function joinLines(lines) {
1990
2308
  `);
1991
2309
  }
1992
2310
 
1993
- // components/ulw-loop/src/plan-crud.ts
1994
- import { existsSync as existsSync4 } from "node:fs";
1995
- import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
2311
+ // components/ulw-loop/src/success-criteria-input.ts
2312
+ function invalid2(message, details) {
2313
+ throw new UlwLoopError(`Invalid successCriteria: ${message}`, "ULW_LOOP_ARGUMENT_INVALID", { details });
2314
+ }
2315
+ function isUserModel(value) {
2316
+ return typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
2317
+ }
2318
+ function requireText(value, field, index) {
2319
+ const trimmed = typeof value === "string" ? value.trim() : "";
2320
+ return trimmed || invalid2(`entry ${index + 1} needs a non-empty ${field}.`, { index, field });
2321
+ }
2322
+ function criterionId(index) {
2323
+ return `C${String(index + 1).padStart(3, "0")}`;
2324
+ }
2325
+ function criteriaFromInput(input) {
2326
+ if (input.length === 0)
2327
+ invalid2("provide at least one criterion or omit the field for placeholders.", { count: 0 });
2328
+ return input.map((entry, index) => {
2329
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
2330
+ return invalid2(`entry ${index + 1} must be an object.`, { index });
2331
+ const record = { ...entry };
2332
+ const userModel = record["userModel"] ?? "happy";
2333
+ if (!isUserModel(userModel))
2334
+ return invalid2(`entry ${index + 1} userModel must be one of ${ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.join(", ")}.`, {
2335
+ index,
2336
+ userModel: String(userModel)
2337
+ });
2338
+ const essential = record["essential"];
2339
+ if (essential !== undefined && typeof essential !== "boolean")
2340
+ return invalid2(`entry ${index + 1} essential must be a boolean.`, { index });
2341
+ return {
2342
+ id: criterionId(index),
2343
+ scenario: requireText(record["scenario"], "scenario", index),
2344
+ userModel,
2345
+ expectedEvidence: requireText(record["expectedEvidence"], "expectedEvidence", index),
2346
+ essential: essential ?? true,
2347
+ capturedEvidence: null,
2348
+ status: "pending"
2349
+ };
2350
+ });
2351
+ }
1996
2352
 
1997
2353
  // components/ulw-loop/src/plan-goal-factory.ts
1998
2354
  function cleanLine(line) {
@@ -2018,40 +2374,35 @@ function assertNonEmpty(value, label) {
2018
2374
  function truncateObjective(objective) {
2019
2375
  return objective.length > 80 ? `${objective.slice(0, 77).trimEnd()}...` : objective;
2020
2376
  }
2021
- function seedDefaultSuccessCriteria(goalIndex, objective) {
2377
+ function replaceVia(goalId, id, surface) {
2378
+ return surface === "omo-senpi" ? `Replace via agentToolkit.steer({ kind: "revise_criterion", source: "finding", goalId: "${goalId}", criterionId: "${id}", scenario, expectedEvidence, evidence, rationale }) (or pass successCriteria to addGoal)` : `Replace via omo-agent-toolkit ulw-loop steer --kind revise_criterion --goal-id ${goalId} --criterion-id ${id} --scenario "<scenario>" --expected-evidence "<proof>" --evidence "<why>" --rationale "<why>"`;
2379
+ }
2380
+ function seedDefaultSuccessCriteria(goalIndex, objective, options = {}) {
2022
2381
  const subject = truncateObjective(normalizeObjective2(objective) || `Goal ${goalIndex + 1}`);
2382
+ const goalId = options.goalId ?? `G${String(goalIndex + 1).padStart(3, "0")}`;
2383
+ const surface = options.surface ?? "lazycodex";
2023
2384
  const rows = [
2385
+ ["happy", `happy path for: ${subject}`, `observable happy-path proof for goal ${goalIndex + 1}`, true],
2386
+ ["edge", "edge case (boundary/empty/malformed)", `boundary or malformed-input proof for: ${subject}`, true],
2024
2387
  [
2025
- "C001",
2026
- "happy",
2027
- `happy path for: ${subject}`,
2028
- `Replace via revise_criterion with observable happy-path proof for goal ${goalIndex + 1}.`,
2029
- true
2030
- ],
2031
- [
2032
- "C002",
2033
- "edge",
2034
- "edge case (boundary/empty/malformed)",
2035
- `Replace via revise_criterion with boundary or malformed-input proof for: ${subject}.`,
2036
- true
2037
- ],
2038
- [
2039
- "C003",
2040
2388
  "regression",
2041
2389
  "regression: adjacent surface still works",
2042
- `Replace via revise_criterion with regression proof for neighboring behavior after: ${subject}.`,
2390
+ `regression proof for neighboring behavior after: ${subject}`,
2043
2391
  false
2044
2392
  ]
2045
2393
  ];
2046
- return rows.map(([id, userModel, scenario, expectedEvidence, essential]) => ({
2047
- id,
2048
- scenario,
2049
- userModel,
2050
- expectedEvidence,
2051
- essential,
2052
- capturedEvidence: null,
2053
- status: "pending"
2054
- }));
2394
+ return rows.map(([userModel, scenario, proof, essential], index) => {
2395
+ const id = criterionId(index);
2396
+ return {
2397
+ id,
2398
+ scenario,
2399
+ userModel,
2400
+ expectedEvidence: `${replaceVia(goalId, id, surface)} with ${proof}.`,
2401
+ essential,
2402
+ capturedEvidence: null,
2403
+ status: "pending"
2404
+ };
2405
+ });
2055
2406
  }
2056
2407
  function deriveGoalCandidates(brief) {
2057
2408
  const bulletGoals = brief.split(/\r?\n/).map((line) => ({ original: line, cleaned: normalizeObjective2(cleanLine(line)) })).filter(({ cleaned }) => cleaned.length > 0 && cleaned.length <= 1200).filter(({ original, cleaned }, index, all) => /^\s*(?:[-*+]\s+|\d+[.)]\s+)/.test(original) && all.findIndex((candidate) => candidate.cleaned === cleaned) === index).map(({ cleaned }) => cleaned);
@@ -2062,22 +2413,27 @@ function deriveGoalCandidates(brief) {
2062
2413
  objective
2063
2414
  }));
2064
2415
  }
2065
- function makeGoal(title, objective, index, now) {
2416
+ function makeGoal(title, objective, index, now, options = {}) {
2066
2417
  const cleanTitle = assertNonEmpty(title, "title");
2067
2418
  const cleanObjective = assertNonEmpty(objective, "objective");
2419
+ const id = normalizeGoalId(cleanTitle, index);
2420
+ const successCriteria = options.successCriteria === undefined ? seedDefaultSuccessCriteria(index, cleanObjective, {
2421
+ goalId: id,
2422
+ ...options.surface === undefined ? {} : { surface: options.surface }
2423
+ }) : criteriaFromInput(options.successCriteria);
2068
2424
  return {
2069
- id: normalizeGoalId(cleanTitle, index),
2425
+ id,
2070
2426
  title: cleanTitle,
2071
2427
  objective: cleanObjective,
2072
2428
  status: "pending",
2073
- successCriteria: seedDefaultSuccessCriteria(index, cleanObjective),
2429
+ successCriteria,
2074
2430
  attempt: 0,
2075
2431
  createdAt: now,
2076
2432
  updatedAt: now
2077
2433
  };
2078
2434
  }
2079
- function appendGoalToPlan(plan, title, objective, now) {
2080
- const goal = makeGoal(title, objective, plan.goals.length, now);
2435
+ function appendGoalToPlan(plan, title, objective, now, options = {}) {
2436
+ const goal = makeGoal(title, objective, plan.goals.length, now, options);
2081
2437
  plan.goals.push(goal);
2082
2438
  plan.updatedAt = now;
2083
2439
  return goal;
@@ -2099,18 +2455,31 @@ function clearGoalBlockerFields2(goal) {
2099
2455
  ])
2100
2456
  delete goal[key];
2101
2457
  }
2102
- async function createUlwLoopPlan(repoRoot, args, scope) {
2458
+ async function createUlwLoopPlan(repoRoot, args, scope, surface = "lazycodex") {
2103
2459
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
2104
- if (!args.force && existsSync4(ulwLoopGoalsPath(repoRoot, scope))) {
2105
- const existing = await readUlwLoopPlan(repoRoot, scope);
2460
+ let existing;
2461
+ if (planExists(repoRoot, scope)) {
2462
+ try {
2463
+ existing = await readUlwLoopPlan(repoRoot, scope);
2464
+ } catch (error) {
2465
+ if (!args.force)
2466
+ throw error;
2467
+ }
2468
+ }
2469
+ if (!args.force && existing !== undefined) {
2106
2470
  if (isUlwLoopDone(existing))
2107
- throw completedPlanExistsError(scope);
2471
+ throw completedPlanExistsError(scope, surface);
2108
2472
  throw new UlwLoopError(`Refusing to overwrite existing ${ulwLoopGoalsRelativePath(scope)}; pass --force to recreate it.`, "ULW_LOOP_PLAN_EXISTS");
2109
2473
  }
2110
2474
  const now = iso();
2111
- const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now));
2475
+ const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now, { surface }));
2112
2476
  const plan = {
2113
2477
  version: 1,
2478
+ revision: existing?.revision ?? 0,
2479
+ ledgerResetRevision: (existing?.revision ?? 0) + 1,
2480
+ brief: args.brief.endsWith(`
2481
+ `) ? args.brief : `${args.brief}
2482
+ `,
2114
2483
  evidenceLayoutVersion: 2,
2115
2484
  createdAt: now,
2116
2485
  updatedAt: now,
@@ -2125,30 +2494,37 @@ async function createUlwLoopPlan(repoRoot, args, scope) {
2125
2494
  plan.validationBatches = validationBatches;
2126
2495
  if (plan.codexGoalMode === "aggregate")
2127
2496
  plan.codexObjective = aggregateCodexObjectiveForScope(scope);
2128
- await mkdir2(ulwLoopDir(repoRoot, scope), { recursive: true });
2129
- await writeFile2(ulwLoopBriefPath(repoRoot, scope), args.brief.endsWith(`
2130
- `) ? args.brief : `${args.brief}
2131
- `, "utf8");
2132
- await writePlan(repoRoot, plan, scope);
2133
- await writeFile2(ulwLoopLedgerPath(repoRoot, scope), "", "utf8");
2134
- await appendLedger(repoRoot, { at: now, kind: "plan_created", message: `${goals.length} goal(s) created` }, scope);
2497
+ await beforePlanMutation();
2498
+ await commit(repoRoot, scope, {
2499
+ plan,
2500
+ entries: [{ at: now, kind: "plan_created", message: `${goals.length} goal(s) created` }]
2501
+ });
2135
2502
  return plan;
2136
2503
  });
2137
2504
  }
2138
- function completedPlanExistsError(scope) {
2505
+ function completedPlanExistsError(scope, surface) {
2139
2506
  return new UlwLoopError([
2140
2507
  `Existing ulw-loop aggregate is already complete at ${ulwLoopGoalsRelativePath(scope)}.`,
2141
- "Start a new run with `omo-agent-toolkit ulw-loop create-goals --session-id <new-id> ...` to isolate fresh state.",
2142
- "Use --force only when you intentionally want to overwrite the completed evidence."
2508
+ ...surface === "omo-senpi" ? [
2509
+ "Start a new run under a fresh session id (a new senpi session) or call agentToolkit.createGoals({ brief, force: true }) to recreate."
2510
+ ] : [
2511
+ "Start a new run with `omo-agent-toolkit ulw-loop create-goals --session-id <new-id> ...` to isolate fresh state.",
2512
+ "Use --force only when you intentionally want to overwrite the completed evidence."
2513
+ ]
2143
2514
  ].join(" "), "ULW_LOOP_PLAN_EXISTS_COMPLETE");
2144
2515
  }
2145
- async function addUlwLoopGoal(repoRoot, args, scope) {
2516
+ async function addUlwLoopGoal(repoRoot, args, scope, surface = "lazycodex") {
2146
2517
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
2147
2518
  const plan = await readUlwLoopPlan(repoRoot, scope);
2148
2519
  const now = iso();
2149
- const goal = appendGoalToPlan(plan, args.title, args.objective, now);
2150
- await writePlan(repoRoot, plan, scope);
2151
- await appendLedger(repoRoot, { at: now, kind: "goal_added", goalId: goal.id, status: goal.status, message: goal.title }, scope);
2520
+ const goal = appendGoalToPlan(plan, args.title, args.objective, now, {
2521
+ surface,
2522
+ ...args.successCriteria === undefined ? {} : { successCriteria: args.successCriteria }
2523
+ });
2524
+ await commit(repoRoot, scope, {
2525
+ plan,
2526
+ entries: [{ at: now, kind: "goal_added", goalId: goal.id, status: goal.status, message: goal.title }]
2527
+ });
2152
2528
  return { plan, goal };
2153
2529
  });
2154
2530
  }
@@ -2161,17 +2537,18 @@ async function startNextUlwLoop(repoRoot, args = {}, scope) {
2161
2537
  const existing = plan.goals.find((goal) => goal.status === "in_progress" && isScheduleEligible(goal));
2162
2538
  if (existing)
2163
2539
  return { plan, goal: existing, resumed: true };
2540
+ const entries = [];
2164
2541
  let next = plan.goals.find((goal) => goal.status === "pending" && isScheduleEligible(goal));
2165
2542
  if (!next && args.retryFailed) {
2166
2543
  next = plan.goals.find((goal) => goal.status === "failed" && !goal.nonRetriable && isScheduleEligible(goal));
2167
2544
  if (next)
2168
- await appendLedger(repoRoot, {
2545
+ entries.push({
2169
2546
  at: now,
2170
2547
  kind: "goal_retried",
2171
2548
  goalId: next.id,
2172
2549
  status: "pending",
2173
2550
  ...next.failureReason ? { message: next.failureReason } : {}
2174
- }, scope);
2551
+ });
2175
2552
  }
2176
2553
  if (!next)
2177
2554
  return { done: true, plan };
@@ -2182,8 +2559,14 @@ async function startNextUlwLoop(repoRoot, args = {}, scope) {
2182
2559
  next.updatedAt = now;
2183
2560
  plan.activeGoalId = next.id;
2184
2561
  plan.updatedAt = now;
2185
- await writePlan(repoRoot, plan, scope);
2186
- await appendLedger(repoRoot, { at: now, kind: "goal_started", goalId: next.id, status: next.status, message: `Attempt ${next.attempt}` }, scope);
2562
+ entries.push({
2563
+ at: now,
2564
+ kind: "goal_started",
2565
+ goalId: next.id,
2566
+ status: next.status,
2567
+ message: `Attempt ${next.attempt}`
2568
+ });
2569
+ await commit(repoRoot, scope, { plan, entries });
2187
2570
  return { plan, goal: next, resumed: false };
2188
2571
  });
2189
2572
  }
@@ -2279,7 +2662,7 @@ function checkpointStatus(value) {
2279
2662
  }
2280
2663
 
2281
2664
  // components/ulw-loop/src/cli-subcommands.ts
2282
- import { readFile as readFile5 } from "node:fs/promises";
2665
+ import { readFile as readFile4 } from "node:fs/promises";
2283
2666
 
2284
2667
  // components/ulw-loop/src/cli-steering.ts
2285
2668
  var SOURCES = ["user_prompt_submit", "finding", "cli"];
@@ -2305,7 +2688,7 @@ function isSource(value) {
2305
2688
  function isModel(value) {
2306
2689
  return ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
2307
2690
  }
2308
- function fail2(message, code, details) {
2691
+ function fail3(message, code, details) {
2309
2692
  throw new UlwLoopError(message, code, { details });
2310
2693
  }
2311
2694
  function kindMessage(prefix) {
@@ -2319,15 +2702,15 @@ function text2(value, field) {
2319
2702
  const trimmed = value.trim();
2320
2703
  if (trimmed.length > 0)
2321
2704
  return trimmed;
2322
- return fail2(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
2705
+ return fail3(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
2323
2706
  }
2324
2707
  function required3(argv, flag) {
2325
2708
  const value = text2(readValue(argv, flag), flag);
2326
- return value ?? fail2(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
2709
+ return value ?? fail3(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
2327
2710
  }
2328
2711
  function requiredGoal(argv) {
2329
2712
  const value = text2(parseGoalArg(argv), "--goal-id");
2330
- return value ?? fail2("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
2713
+ return value ?? fail3("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
2331
2714
  }
2332
2715
  function readObject(value, key) {
2333
2716
  return Object.entries(value).find(([name]) => name === key)?.[1];
@@ -2351,7 +2734,7 @@ function objectChildren(value, key) {
2351
2734
  for (const item of candidate) {
2352
2735
  const next = child(item);
2353
2736
  if (next === null)
2354
- return fail2(`${key} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { key });
2737
+ return fail3(`${key} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { key });
2355
2738
  parsed.push(next);
2356
2739
  }
2357
2740
  return parsed;
@@ -2366,13 +2749,13 @@ function parseSteeringKind(argv) {
2366
2749
  const value = readValue(argv, "--kind");
2367
2750
  if (isKind(value))
2368
2751
  return value;
2369
- return value === undefined ? fail2(kindMessage("Missing --kind."), "ULW_LOOP_STEERING_KIND_REQUIRED", { flag: "--kind", expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP }) : fail2(kindMessage(`Invalid --kind: ${value}.`), "ULW_LOOP_STEERING_KIND_INVALID", { value, expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP });
2752
+ return value === undefined ? fail3(kindMessage("Missing --kind."), "ULW_LOOP_STEERING_KIND_REQUIRED", { flag: "--kind", expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP }) : fail3(kindMessage(`Invalid --kind: ${value}.`), "ULW_LOOP_STEERING_KIND_INVALID", { value, expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP });
2370
2753
  }
2371
2754
  function parseSteeringSource(argv) {
2372
2755
  const value = readValue(argv, "--source");
2373
2756
  if (value === undefined)
2374
2757
  return "cli";
2375
- return isSource(value) ? value : fail2(`Invalid --source: ${value}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value, expected: SOURCES });
2758
+ return isSource(value) ? value : fail3(`Invalid --source: ${value}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value, expected: SOURCES });
2376
2759
  }
2377
2760
  function child(value) {
2378
2761
  if (!isPlain(value))
@@ -2389,12 +2772,12 @@ async function children(argv, flag, needed) {
2389
2772
  return [];
2390
2773
  const raw = await readJsonInput(input);
2391
2774
  if (!Array.isArray(raw))
2392
- return fail2(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
2775
+ return fail3(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
2393
2776
  const parsed = [];
2394
2777
  for (const item of raw) {
2395
2778
  const next = child(item);
2396
2779
  if (next === null)
2397
- return fail2(`${flag} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { flag });
2780
+ return fail3(`${flag} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { flag });
2398
2781
  parsed.push(next);
2399
2782
  }
2400
2783
  return parsed;
@@ -2402,11 +2785,11 @@ async function children(argv, flag, needed) {
2402
2785
  async function stringArray2(argv, flag) {
2403
2786
  const raw = await readJsonInput(required3(argv, flag));
2404
2787
  if (!Array.isArray(raw))
2405
- return fail2(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
2788
+ return fail3(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
2406
2789
  const values = [];
2407
2790
  for (const item of raw) {
2408
2791
  if (typeof item !== "string")
2409
- return fail2(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
2792
+ return fail3(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
2410
2793
  values.push(text2(item, flag) ?? "");
2411
2794
  }
2412
2795
  return values;
@@ -2415,10 +2798,10 @@ function model(value) {
2415
2798
  const trimmed = text2(value, "--user-model");
2416
2799
  if (trimmed === undefined)
2417
2800
  return;
2418
- return isModel(trimmed) ? trimmed : fail2(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
2801
+ return isModel(trimmed) ? trimmed : fail3(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
2419
2802
  }
2420
2803
  function neverKind(kind) {
2421
- return fail2(`Unsupported steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind });
2804
+ return fail3(`Unsupported steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind });
2422
2805
  }
2423
2806
  async function parseSteeringProposal(argv) {
2424
2807
  const kind = parseSteeringKind(argv);
@@ -2439,7 +2822,7 @@ async function parseSteeringProposal(argv) {
2439
2822
  const revisedTitle = readValue(argv, "--title");
2440
2823
  const revisedObjective = readValue(argv, "--objective");
2441
2824
  if (revisedTitle === undefined && revisedObjective === undefined)
2442
- return fail2("revise_pending_wording requires --title or --objective.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
2825
+ return fail3("revise_pending_wording requires --title or --objective.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
2443
2826
  return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective } });
2444
2827
  }
2445
2828
  case "revise_criterion": {
@@ -2449,7 +2832,7 @@ async function parseSteeringProposal(argv) {
2449
2832
  const expectedEvidence = readValue(argv, "--expected-evidence");
2450
2833
  const userModel = model(readValue(argv, "--user-model"));
2451
2834
  if (scenario === undefined && expectedEvidence === undefined && userModel === undefined)
2452
- return fail2("revise_criterion requires scenario, expected-evidence, or user-model.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
2835
+ return fail3("revise_criterion requires scenario, expected-evidence, or user-model.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
2453
2836
  return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, criterionId, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...userModel === undefined ? {} : { userModel } });
2454
2837
  }
2455
2838
  case "annotate_ledger":
@@ -2499,10 +2882,10 @@ async function parseSteeringProposals(argv) {
2499
2882
  if (input === undefined)
2500
2883
  return [await parseSteeringProposal(argv)];
2501
2884
  if (readValue(argv, "--kind") !== undefined)
2502
- return fail2("--kind and --proposals-json are mutually exclusive.", "ULW_LOOP_STEERING_BATCH_CONFLICT", { flags: ["--kind", "--proposals-json"] });
2885
+ return fail3("--kind and --proposals-json are mutually exclusive.", "ULW_LOOP_STEERING_BATCH_CONFLICT", { flags: ["--kind", "--proposals-json"] });
2503
2886
  const raw = await readJsonInput(input);
2504
2887
  if (!Array.isArray(raw) || raw.length === 0)
2505
- return fail2("--proposals-json must be a non-empty JSON array.", "ULW_LOOP_STEERING_BATCH_ARRAY_REQUIRED", { flag: "--proposals-json" });
2888
+ return fail3("--proposals-json must be a non-empty JSON array.", "ULW_LOOP_STEERING_BATCH_ARRAY_REQUIRED", { flag: "--proposals-json" });
2506
2889
  const proposals = [];
2507
2890
  for (const item of raw)
2508
2891
  proposals.push(normalizeSteeringProposal(proposalFromObject(item)));
@@ -2510,13 +2893,13 @@ async function parseSteeringProposals(argv) {
2510
2893
  }
2511
2894
  function proposalFromObject(value) {
2512
2895
  if (!isPlain(value))
2513
- return fail2("--proposals-json entries must be objects.", "ULW_LOOP_STEERING_BATCH_ITEM_INVALID", { flag: "--proposals-json" });
2896
+ return fail3("--proposals-json entries must be objects.", "ULW_LOOP_STEERING_BATCH_ITEM_INVALID", { flag: "--proposals-json" });
2514
2897
  const kind = readObject(value, "kind");
2515
2898
  const source = readObject(value, "source") ?? "cli";
2516
2899
  if (!isProposalKind(kind))
2517
- return fail2(`Invalid batch steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_INVALID", { value: kind });
2900
+ return fail3(`Invalid batch steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_INVALID", { value: kind });
2518
2901
  if (!isProposalSource(source))
2519
- return fail2(`Invalid batch steering source: ${String(source)}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value: source });
2902
+ return fail3(`Invalid batch steering source: ${String(source)}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value: source });
2520
2903
  let proposal = { kind, source, evidence: objectText(value, "evidence") ?? "", rationale: objectText(value, "rationale") ?? "" };
2521
2904
  const goalId = objectText(value, "goalId");
2522
2905
  const targetGoalId = objectText(value, "targetGoalId");
@@ -2602,12 +2985,13 @@ function nextGoalId(plan) {
2602
2985
  }
2603
2986
  function appendBlockerGoal(plan, args, now) {
2604
2987
  const index = plan.goals.length;
2988
+ const id = nextGoalId(plan);
2605
2989
  const goal = {
2606
- id: nextGoalId(plan),
2990
+ id,
2607
2991
  title: args.title,
2608
2992
  objective: args.objective,
2609
2993
  status: "pending",
2610
- successCriteria: seedDefaultSuccessCriteria(index, args.objective),
2994
+ successCriteria: seedDefaultSuccessCriteria(index, args.objective, { goalId: id }),
2611
2995
  attempt: 0,
2612
2996
  createdAt: now,
2613
2997
  updatedAt: now
@@ -2628,10 +3012,12 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
2628
3012
  const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
2629
3013
  const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
2630
3014
  expectedObjective: expectedCodexObjective(plan, goal),
2631
- acceptedObjectives: compatibleCodexObjectives(plan)
3015
+ acceptedObjectives: compatibleCodexObjectives(plan),
3016
+ acknowledgedObjectives: acknowledgedDriverObjectives(plan)
2632
3017
  });
2633
3018
  if (!reconciliation.ok)
2634
3019
  throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
3020
+ acknowledgeDriverObjective(plan, reconciliation.unacknowledgedObjective);
2635
3021
  const now = iso();
2636
3022
  for (const field of BLOCKER_FIELDS)
2637
3023
  Reflect.deleteProperty(goal, field);
@@ -2649,46 +3035,44 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
2649
3035
  const summaryEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal, message: `Review blockers recorded; appended ${newGoal.id}.` };
2650
3036
  Reflect.set(summaryEntry, "kind", "blocker_recorded");
2651
3037
  const ledgerEntries = [blockedEntry, addedEntry, summaryEntry];
2652
- await writePlan(repoRoot, plan, scope);
2653
- for (const entry of ledgerEntries)
2654
- await appendLedger(repoRoot, entry, scope);
3038
+ await commit(repoRoot, scope, { plan, entries: ledgerEntries });
2655
3039
  return {
2656
3040
  plan,
2657
3041
  blockedGoal: goal,
2658
3042
  newGoal,
2659
3043
  ledgerEntries,
2660
- nextActions: reconciliation.warnings,
2661
- warnings: reconciliation.warnings.filter((warning) => warning.startsWith("driver_objective_differs"))
3044
+ nextActions: reconciliation.nextActions,
3045
+ warnings: reconciliation.warnings
2662
3046
  };
2663
3047
  });
2664
3048
  }
2665
3049
 
2666
3050
  // components/ulw-loop/src/status-next-actions.ts
2667
- function statusNextActions(plan) {
3051
+ function statusNextActions(plan, surface = "lazycodex") {
2668
3052
  const actions = [];
2669
3053
  const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
2670
3054
  if (plan.goals.length === 0)
2671
- actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
3055
+ actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
2672
3056
  else if (active !== undefined)
2673
- actions.push(...activeGoalActions(plan, active));
3057
+ actions.push(...activeGoalActions(plan, active, surface));
2674
3058
  for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
2675
- actions.push(`${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>'\`.`);
3059
+ actions.push(surface === "omo-senpi" ? `${goal.id} is review_blocked: capture the reviewer verdict with \`agentToolkit.recordReviewBlockers({ goalId: "${goal.id}", title: "<title>", objective: "<objective>", evidence: "<verdict>" })\`.` : `${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>'\`.`);
2676
3060
  if (plan.evidenceLayoutVersion !== 2 || active === undefined)
2677
3061
  actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
2678
3062
  return actions;
2679
3063
  }
2680
- function activeGoalActions(plan, active) {
3064
+ function activeGoalActions(plan, active, surface) {
2681
3065
  const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
2682
3066
  if (unresolved.length > 0)
2683
3067
  return [
2684
- `${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>"\`.`
3068
+ surface === "omo-senpi" ? `${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>" })\`.` : `${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>"\`.`
2685
3069
  ];
2686
3070
  if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
2687
3071
  return [
2688
- `${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.`
3072
+ surface === "omo-senpi" ? `${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.` : `${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.`
2689
3073
  ];
2690
3074
  return [
2691
- `${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.`
3075
+ surface === "omo-senpi" ? `${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.` : `${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.`
2692
3076
  ];
2693
3077
  }
2694
3078
 
@@ -2729,11 +3113,11 @@ function nextId(plan, offset) {
2729
3113
  }, 0);
2730
3114
  return `G${String(max + offset).padStart(3, "0")}`;
2731
3115
  }
2732
- function makeGoal2(plan, childGoal, evidence, now, offset) {
3116
+ function makeGoal2(plan, childGoal, evidence, now, offset, surface = "lazycodex") {
2733
3117
  const id = nextId(plan, offset);
2734
3118
  const digits = /^G(\d+)/u.exec(id)?.[1];
2735
3119
  const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
2736
- return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
3120
+ return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective, { goalId: id, surface }), attempt: 0, createdAt: now, updatedAt: now, evidence };
2737
3121
  }
2738
3122
  function reviseWording(plan, proposal, now) {
2739
3123
  const target = goal(plan, targets(proposal)[0]);
@@ -2745,11 +3129,11 @@ function reviseWording(plan, proposal, now) {
2745
3129
  target.steeringRationale = proposal.rationale;
2746
3130
  target.updatedAt = now;
2747
3131
  }
2748
- function splitOrBlock(plan, proposal, now) {
3132
+ function splitOrBlock(plan, proposal, now, surface = "lazycodex") {
2749
3133
  const target = goal(plan, targets(proposal)[0]);
2750
3134
  if (target === undefined)
2751
3135
  return;
2752
- const replacements = children2(proposal).map((item, index) => makeGoal2(plan, item, proposal.evidence, now, index + 1));
3136
+ const replacements = children2(proposal).map((item, index) => makeGoal2(plan, item, proposal.evidence, now, index + 1, surface));
2753
3137
  target.steeringEvidence = proposal.evidence;
2754
3138
  target.steeringRationale = proposal.rationale;
2755
3139
  target.updatedAt = now;
@@ -2805,7 +3189,7 @@ function changedGoalIdsBetween(before, after) {
2805
3189
 
2806
3190
  // components/ulw-loop/src/steering.ts
2807
3191
  var SOURCES2 = ["user_prompt_submit", "finding", "cli"];
2808
- var PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
3192
+ var PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "acknowledgedDriverObjectives", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
2809
3193
  var isObject2 = (value) => typeof value === "object" && value !== null;
2810
3194
  var isPlain2 = (value) => isObject2(value) && !Array.isArray(value);
2811
3195
  var read3 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
@@ -2963,13 +3347,13 @@ function validateCriterion(plan, proposal, reasons) {
2963
3347
  if (model !== undefined && !isModel3(model))
2964
3348
  reasons.push("invalid userModel");
2965
3349
  }
2966
- function applySteeringMutation(plan, proposal, audit) {
3350
+ function applySteeringMutation(plan, proposal, audit, surface = "lazycodex") {
2967
3351
  const next = structuredClone(plan);
2968
3352
  if (!audit.invariant.accepted)
2969
3353
  return next;
2970
3354
  const now = proposal.now?.toISOString() ?? iso();
2971
3355
  if (proposal.kind === "add_subgoal")
2972
- next.goals.push(makeGoal2(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1));
3356
+ next.goals.push(makeGoal2(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1, surface));
2973
3357
  if (proposal.kind === "reorder_pending") {
2974
3358
  const order = pendingOrder(proposal);
2975
3359
  next.goals = [...order.map((id) => goal2(next, id)).filter((item) => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
@@ -2977,7 +3361,7 @@ function applySteeringMutation(plan, proposal, audit) {
2977
3361
  if (proposal.kind === "revise_pending_wording")
2978
3362
  reviseWording(next, proposal, now);
2979
3363
  if (proposal.kind === "split_subgoal" || proposal.kind === "mark_blocked_superseded")
2980
- splitOrBlock(next, proposal, now);
3364
+ splitOrBlock(next, proposal, now, surface);
2981
3365
  if (proposal.kind === "revise_criterion")
2982
3366
  reviseCriterion(next, proposal, now);
2983
3367
  if (proposal.kind !== "annotate_ledger")
@@ -3000,7 +3384,7 @@ function parseUlwLoopSteeringDirective(text) {
3000
3384
  throw error;
3001
3385
  }
3002
3386
  }
3003
- async function steerUlwLoop(repoRoot, proposal, scope) {
3387
+ async function steerUlwLoop(repoRoot, proposal, scope, surface = "lazycodex") {
3004
3388
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
3005
3389
  const plan = await readUlwLoopPlan(repoRoot, scope);
3006
3390
  const key = proposal.idempotencyKey ?? proposal.promptSignature;
@@ -3011,7 +3395,7 @@ async function steerUlwLoop(repoRoot, proposal, scope) {
3011
3395
  }
3012
3396
  const audit = validateUlwLoopSteeringProposal(plan, proposal);
3013
3397
  const accepted = audit.invariant.accepted;
3014
- const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
3398
+ const next = accepted ? applySteeringMutation(plan, proposal, audit, surface) : plan;
3015
3399
  const finalAudit = { ...audit };
3016
3400
  if (accepted) {
3017
3401
  const changed = changedGoalIdsBetween(plan, next);
@@ -3020,11 +3404,10 @@ async function steerUlwLoop(repoRoot, proposal, scope) {
3020
3404
  }
3021
3405
  const at = proposal.now?.toISOString() ?? iso();
3022
3406
  const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
3023
- if (accepted)
3024
- await writePlan(repoRoot, next, scope);
3025
- await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
3407
+ const entries = [ledgerEntry(proposal, finalAudit, at)];
3026
3408
  if (batchEntry !== null)
3027
- await appendLedger(repoRoot, batchEntry, scope);
3409
+ entries.push(batchEntry);
3410
+ await commit(repoRoot, scope, { plan: next, entries });
3028
3411
  return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
3029
3412
  });
3030
3413
  }
@@ -3041,20 +3424,119 @@ function ledgerEntry(proposal, audit, at) {
3041
3424
  }
3042
3425
 
3043
3426
  // components/ulw-loop/src/sdk/manifest.ts
3427
+ var CRITERION_INPUT = '{ scenario: string, expectedEvidence: string, userModel?: "happy" | "edge" | "regression" | "adversarial", essential?: boolean }';
3428
+ var STEER_KINDS = '"add_subgoal" | "split_subgoal" | "reorder_pending" | "revise_pending_wording" | "revise_criterion" | "annotate_ledger" | "mark_blocked_superseded"';
3044
3429
  var ULW_LOOP_MANIFEST = {
3045
3430
  version: 1,
3046
3431
  name: "ulw-loop",
3047
3432
  operations: [
3048
- { name: "help", mutating: false },
3049
- { name: "create-goals", mutating: true },
3050
- { name: "status", mutating: false },
3051
- { name: "complete-goals", mutating: true },
3052
- { name: "checkpoint", mutating: true },
3053
- { name: "steer", mutating: true },
3054
- { name: "add-goal", mutating: true },
3055
- { name: "criteria", mutating: false },
3056
- { name: "record-evidence", mutating: true },
3057
- { name: "record-review-blockers", mutating: true }
3433
+ {
3434
+ name: "help",
3435
+ method: "help",
3436
+ mutating: false,
3437
+ description: "Return this manifest: every operation with its method name, argument fields, and what it does.",
3438
+ args: {}
3439
+ },
3440
+ {
3441
+ name: "create-goals",
3442
+ method: "createGoals",
3443
+ mutating: true,
3444
+ description: "Create the session plan from a brief; one goal per bullet, each seeded with placeholder criteria until revised.",
3445
+ args: {
3446
+ brief: "string",
3447
+ codexGoalMode: '"aggregate" | "per_story"?',
3448
+ force: "boolean?",
3449
+ validationBatchesJson: "string?"
3450
+ }
3451
+ },
3452
+ {
3453
+ name: "status",
3454
+ method: "status",
3455
+ mutating: false,
3456
+ description: "Read the plan, its summary, structured nextActions, the stable evidenceRoot, and the active goal's currentAttemptDir.",
3457
+ args: {}
3458
+ },
3459
+ {
3460
+ name: "complete-goals",
3461
+ method: "completeGoals",
3462
+ mutating: true,
3463
+ description: "Acquire the next eligible goal or resume the in-progress one; returns { done: true } once the aggregate is complete.",
3464
+ args: { retryFailed: "boolean?" }
3465
+ },
3466
+ {
3467
+ name: "checkpoint",
3468
+ method: "checkpoint",
3469
+ mutating: true,
3470
+ description: "Close the goal as complete, failed, or blocked with evidence; the final goal also needs the quality gate. printTemplate returns that gate's template instead.",
3471
+ args: {
3472
+ goalId: "string",
3473
+ status: '"complete" | "failed" | "blocked"',
3474
+ evidence: "string",
3475
+ codexGoalJson: "string?",
3476
+ qualityGateJson: "string?",
3477
+ printTemplate: "true? (with goalId? only)"
3478
+ }
3479
+ },
3480
+ {
3481
+ name: "steer",
3482
+ method: "steer",
3483
+ mutating: true,
3484
+ description: "Propose an evidence-backed plan mutation; revise_criterion replaces a criterion's scenario, expectedEvidence, or userModel.",
3485
+ args: {
3486
+ kind: STEER_KINDS,
3487
+ source: '"finding" | "user_prompt_submit" | "cli"',
3488
+ evidence: "string",
3489
+ rationale: "string",
3490
+ goalId: "string?",
3491
+ criterionId: "string? (revise_criterion)",
3492
+ scenario: "string? (revise_criterion)",
3493
+ expectedEvidence: "string? (revise_criterion)",
3494
+ userModel: '"happy" | "edge" | "regression" | "adversarial"? (revise_criterion)',
3495
+ title: "string? (add_subgoal)",
3496
+ objective: "string? (add_subgoal)",
3497
+ childGoals: "{ title: string, objective: string }[]? (split_subgoal)",
3498
+ revisedTitle: "string? (revise_pending_wording)",
3499
+ revisedObjective: "string? (revise_pending_wording)",
3500
+ pendingOrder: "string[]? (reorder_pending)",
3501
+ blockedReason: "string? (mark_blocked_superseded)",
3502
+ idempotencyKey: "string?"
3503
+ }
3504
+ },
3505
+ {
3506
+ name: "add-goal",
3507
+ method: "addGoal",
3508
+ mutating: true,
3509
+ description: "Append a goal; pass successCriteria to define its criteria in the same call, otherwise placeholders name the revise call.",
3510
+ args: { title: "string", objective: "string", successCriteria: `${CRITERION_INPUT}[]?` }
3511
+ },
3512
+ {
3513
+ name: "criteria",
3514
+ method: "criteria",
3515
+ mutating: false,
3516
+ description: "List one goal's success criteria with their status and captured evidence.",
3517
+ args: { goalId: "string" }
3518
+ },
3519
+ {
3520
+ name: "record-evidence",
3521
+ method: "recordEvidence",
3522
+ mutating: true,
3523
+ description: "Record a criterion's pass, fail, or blocked evidence; artifacts must exist (resolved against the session cwd) and are stored with the criterion and the ledger entry.",
3524
+ args: {
3525
+ goalId: "string",
3526
+ criterionId: "string",
3527
+ status: '"pass" | "fail" | "blocked"',
3528
+ evidence: "string",
3529
+ notes: "string?",
3530
+ artifacts: "string[]?"
3531
+ }
3532
+ },
3533
+ {
3534
+ name: "record-review-blockers",
3535
+ method: "recordReviewBlockers",
3536
+ mutating: true,
3537
+ description: "Mark the final goal review_blocked and append the blocker goal the reviewer verdict names.",
3538
+ args: { goalId: "string", title: "string", objective: "string", evidence: "string", codexGoalJson: "string?" }
3539
+ }
3058
3540
  ]
3059
3541
  };
3060
3542
 
@@ -3071,8 +3553,12 @@ function validateCodexGoalJson(raw) {
3071
3553
  function validateContext(context) {
3072
3554
  if (!context.cwd.trim())
3073
3555
  throw new UlwLoopError("cwd is required.", "ULW_LOOP_CWD_REQUIRED");
3074
- if (!context.sessionId.trim())
3556
+ const sessionId = context.sessionId.trim();
3557
+ if (!sessionId)
3075
3558
  throw new UlwLoopError("ULW_LOOP_SESSION_ID_REQUIRED: sessionId is required.", "ULW_LOOP_SESSION_ID_REQUIRED");
3559
+ const normalizedSessionId = normalizeUlwLoopSessionId(sessionId);
3560
+ if (normalizedSessionId === null || /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(sessionId))
3561
+ throw new UlwLoopError("ULW_LOOP_SESSION_ID_INVALID: sessionId normalizes to null.", "ULW_LOOP_SESSION_ID_INVALID");
3076
3562
  if (context.surface !== "omo-senpi" && context.surface !== "lazycodex")
3077
3563
  throw new UlwLoopError("surface must be omo-senpi or lazycodex.", "ULW_LOOP_SURFACE_INVALID");
3078
3564
  }
@@ -3092,10 +3578,11 @@ function isKnownRequest(request) {
3092
3578
  function unreachable(value) {
3093
3579
  throw new UlwLoopError(`Unhandled operation: ${String(value)}`, "ULW_LOOP_OPERATION_UNHANDLED");
3094
3580
  }
3095
- function nextActionsFrom(result) {
3096
- if (!("nextActions" in result) || !Array.isArray(result.nextActions))
3581
+ function stringsFrom(result, key) {
3582
+ if (!(key in result))
3097
3583
  return [];
3098
- return result.nextActions.filter((action) => typeof action === "string").slice(0, 8);
3584
+ const value = Object.entries(result).find(([name]) => name === key)?.[1];
3585
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string").slice(0, 8) : [];
3099
3586
  }
3100
3587
  function checkpointWithValidatedSnapshot(context, scope, args) {
3101
3588
  validateCodexGoalJson(args.codexGoalJson);
@@ -3112,9 +3599,18 @@ function createAgentToolkit(context, deps = {}) {
3112
3599
  const invoke = async (operation, fn) => {
3113
3600
  try {
3114
3601
  const result = await fn();
3115
- const nextActions = typeof result === "object" && result !== null ? nextActionsFrom(result) : [];
3116
- return await notify(operation, { ok: true, operation, result, nextActions });
3602
+ const nextActions = typeof result === "object" && result !== null ? stringsFrom(result, "nextActions") : [];
3603
+ const warnings = typeof result === "object" && result !== null ? stringsFrom(result, "warnings") : [];
3604
+ return await notify(operation, {
3605
+ ok: true,
3606
+ operation,
3607
+ result,
3608
+ nextActions,
3609
+ ...warnings.length === 0 ? {} : { warnings }
3610
+ });
3117
3611
  } catch (error) {
3612
+ if (error instanceof UlwLoopError && error.code === "ULW_LOOP_PLAN_MISSING" && context.surface === "omo-senpi")
3613
+ return notify(operation, failure(operation, planMissingError(ulwLoopGoalsRelativePath(scope), listUlwLoopSessionIds(context.cwd), context.surface)));
3118
3614
  const response = caught(operation, error instanceof Error ? error : new Error("ULW_LOOP_ERROR"));
3119
3615
  return notify(operation, response);
3120
3616
  }
@@ -3149,21 +3645,22 @@ function createAgentToolkit(context, deps = {}) {
3149
3645
  }
3150
3646
  },
3151
3647
  help: () => invoke("help", async () => ULW_LOOP_MANIFEST),
3152
- createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope)),
3648
+ createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope, context.surface)),
3153
3649
  status: () => invoke("status", async () => {
3154
3650
  const plan = await readUlwLoopPlan(context.cwd, scope);
3155
3651
  const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
3156
3652
  return {
3157
3653
  plan,
3158
3654
  summary: summarizeUlwLoopPlan(plan),
3159
- nextActions: statusNextActions(plan),
3655
+ nextActions: statusNextActions(plan, context.surface),
3656
+ evidenceRoot: ulwLoopEvidenceRoot(scope),
3160
3657
  ...active === undefined || plan.evidenceLayoutVersion !== 2 ? {} : { currentAttemptDir: ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) }
3161
3658
  };
3162
3659
  }),
3163
3660
  completeGoals: (args = {}) => invoke("complete-goals", () => startNextUlwLoop(context.cwd, args, scope)),
3164
3661
  checkpoint: (args) => invoke("checkpoint", () => args.printTemplate === true ? checkpointTemplate(context.cwd, scope, args.goalId, { surface: context.surface }) : checkpointWithValidatedSnapshot(context, scope, args)),
3165
- steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope)),
3166
- addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope)),
3662
+ steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope, context.surface)),
3663
+ addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope, context.surface)),
3167
3664
  criteria: (args) => invoke("criteria", async () => {
3168
3665
  const plan = await readUlwLoopPlan(context.cwd, scope);
3169
3666
  const goal = plan.goals.find((candidate) => candidate.id === args.goalId);
@@ -3184,7 +3681,7 @@ async function steerUlwLoopBatch(repoRoot, proposals, scope) {
3184
3681
  const failed = prepared.results.find((item) => !item.accepted);
3185
3682
  if (failed !== undefined) {
3186
3683
  const entry = rejectedLedgerEntry(prepared.results);
3187
- await appendLedger(repoRoot, entry, scope);
3684
+ await commit(repoRoot, scope, { plan, entries: [entry] });
3188
3685
  return rejected(plan, prepared.results, failed.rejectedReasons);
3189
3686
  }
3190
3687
  let next = plan;
@@ -3193,10 +3690,9 @@ async function steerUlwLoopBatch(repoRoot, proposals, scope) {
3193
3690
  next = item.prepared.next;
3194
3691
  const fresh = prepared.items.filter((item) => item.kind === "fresh");
3195
3692
  if (fresh.length > 0) {
3196
- await writePlan(repoRoot, next, scope);
3197
3693
  const entries = fresh.map((item) => ledgerEntry2(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
3198
3694
  const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
3199
- await appendLedgerEntries(repoRoot, batchEntry === null ? entries : [...entries, batchEntry], scope);
3695
+ await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
3200
3696
  }
3201
3697
  return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
3202
3698
  });
@@ -3260,7 +3756,7 @@ function ledgerEntry2(proposal, audit, at) {
3260
3756
  // components/ulw-loop/src/cli-subcommands.ts
3261
3757
  async function createGoals(repoRoot, argv, json, scope) {
3262
3758
  const briefFile = readValue(argv, "--brief-file");
3263
- const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile5(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
3759
+ const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile4(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
3264
3760
  if (!brief.trim()) {
3265
3761
  throw new UlwLoopError("Missing brief text. Pass --brief, --brief-file, --from-stdin, or positional text.", "ULW_LOOP_BRIEF_REQUIRED");
3266
3762
  }
@@ -3528,10 +4024,10 @@ function commandScope(repoRoot, argv) {
3528
4024
  }
3529
4025
 
3530
4026
  // components/ulw-loop/src/ultrawork-directive.ts
3531
- import { readFileSync as readFileSync4 } from "node:fs";
4027
+ import { readFileSync as readFileSync5 } from "node:fs";
3532
4028
 
3533
4029
  // components/ulw-loop/src/ultrawork-skill-pointer.ts
3534
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
4030
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
3535
4031
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3536
4032
  var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
3537
4033
  ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
@@ -3563,7 +4059,7 @@ Do not start the requested work until all three steps are complete.
3563
4059
  `;
3564
4060
  var ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
3565
4061
  var ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
3566
- var ULTRAWORK_DIRECTIVE = readFileSync3(new URL("../directive.md", import.meta.url), "utf8");
4062
+ var ULTRAWORK_DIRECTIVE = readFileSync4(new URL("../directive.md", import.meta.url), "utf8");
3567
4063
  function resolveUltraworkSkillFilePath() {
3568
4064
  return fileURLToPath2(ULTRAWORK_SKILL_FILE_URL);
3569
4065
  }
@@ -3572,7 +4068,7 @@ function buildUltraworkSkillPointer(skillFilePath) {
3572
4068
  }
3573
4069
  function buildUltraworkAdditionalContext(options = {}) {
3574
4070
  const skillFilePath = options.skillFilePath === undefined ? resolveUltraworkSkillFilePath() : options.skillFilePath;
3575
- if (skillFilePath !== null && existsSync5(skillFilePath)) {
4071
+ if (skillFilePath !== null && existsSync6(skillFilePath)) {
3576
4072
  return buildUltraworkSkillPointer(skillFilePath);
3577
4073
  }
3578
4074
  return ULTRAWORK_DIRECTIVE;
@@ -3626,7 +4122,7 @@ function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath) {
3626
4122
  return false;
3627
4123
  }
3628
4124
  function readTranscriptTail(transcriptPath) {
3629
- const rawTranscript = readFileSync4(transcriptPath);
4125
+ const rawTranscript = readFileSync5(transcriptPath);
3630
4126
  return rawTranscript.subarray(Math.max(0, rawTranscript.byteLength - TRANSCRIPT_SEARCH_BYTES)).toString("utf8");
3631
4127
  }
3632
4128
  function isUltraworkPrompt(prompt) {
@@ -3819,9 +4315,64 @@ function readAll(stdin) {
3819
4315
  }
3820
4316
 
3821
4317
  // components/ulw-loop/src/spawn-guard.ts
4318
+ import { mkdirSync as mkdirSync3 } from "node:fs";
4319
+ import { join as join7 } from "node:path";
4320
+
4321
+ // components/ulw-loop/src/spawn-budget-io.ts
3822
4322
  import { randomBytes } from "node:crypto";
3823
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync, statSync as statSync3, writeFileSync } from "node:fs";
3824
- import { dirname as dirname3, join as join3 } from "node:path";
4323
+ import { existsSync as existsSync7, readFileSync as readFileSync6, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
4324
+ import { dirname as dirname3, join as join6 } from "node:path";
4325
+ function readAdmissionBreaker(sessionId) {
4326
+ const dataDir = process.env["PLUGIN_DATA"];
4327
+ if (typeof dataDir !== "string")
4328
+ return null;
4329
+ try {
4330
+ const value = JSON.parse(readFileSync6(join6(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
4331
+ return typeof value === "object" && value !== null && "reason" in value && typeof value.reason === "string" ? value.reason : "capacity limit";
4332
+ } catch {
4333
+ return null;
4334
+ }
4335
+ }
4336
+ function atomicWriteJson(targetPath, data) {
4337
+ const tmp = join6(dirname3(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
4338
+ writeFileSync2(tmp, JSON.stringify(data));
4339
+ renameSync2(tmp, targetPath);
4340
+ }
4341
+ function isNonEmptyFile(path) {
4342
+ try {
4343
+ return existsSync7(path) && statSync3(path).size > 0;
4344
+ } catch (error) {
4345
+ if (error instanceof Error)
4346
+ return false;
4347
+ throw error;
4348
+ }
4349
+ }
4350
+ function readCount(counterPath) {
4351
+ try {
4352
+ const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
4353
+ return typeof parsed === "object" && parsed !== null && "count" in parsed && typeof parsed.count === "number" && parsed.count >= 0 ? parsed.count : 0;
4354
+ } catch (error) {
4355
+ if (error instanceof Error)
4356
+ return 0;
4357
+ throw error;
4358
+ }
4359
+ }
4360
+ function readCounts(counterPath) {
4361
+ try {
4362
+ const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
4363
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
4364
+ return {};
4365
+ const counts = {};
4366
+ for (const [key, value] of Object.entries(parsed))
4367
+ if (typeof value === "number" && value >= 0)
4368
+ counts[key] = value;
4369
+ return counts;
4370
+ } catch (error) {
4371
+ if (error instanceof Error)
4372
+ return {};
4373
+ throw error;
4374
+ }
4375
+ }
3825
4376
 
3826
4377
  // components/ulw-loop/src/spawn-role-guard.ts
3827
4378
  var LAZYCODEX_SPAWN_ROLES = new Set([
@@ -3879,7 +4430,7 @@ function applySpawnBudgetGuards(payload, options = {}) {
3879
4430
  return deny(`Subagent admission failed earlier in this session (${breaker}). Do not spawn more workers or reviewers; report the capacity block and wait for the user.`);
3880
4431
  const scope = { sessionId: payload.session_id };
3881
4432
  const stateDir = ulwLoopDir(payload.cwd, scope);
3882
- const plan = readPlan(join3(stateDir, "goals.json"));
4433
+ const plan = readPlan(payload.cwd, payload.session_id);
3883
4434
  if (plan === null)
3884
4435
  return "";
3885
4436
  const lockOptions = options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
@@ -3918,10 +4469,10 @@ async function runSpawnAdmissionRecorderCli(stdin, stdout) {
3918
4469
  const dataDir = process.env["PLUGIN_DATA"];
3919
4470
  if (typeof dataDir !== "string" || typeof payload["session_id"] !== "string")
3920
4471
  return;
3921
- const markerDir = join3(dataDir, "spawn-breaker");
4472
+ const markerDir = join7(dataDir, "spawn-breaker");
3922
4473
  try {
3923
- mkdirSync2(markerDir, { recursive: true });
3924
- atomicWriteJson(join3(markerDir, `${payload["session_id"]}.json`), {
4474
+ mkdirSync3(markerDir, { recursive: true });
4475
+ atomicWriteJson(join7(markerDir, `${payload["session_id"]}.json`), {
3925
4476
  reason: response,
3926
4477
  at: new Date().toISOString()
3927
4478
  });
@@ -3950,7 +4501,7 @@ async function runSpawnGuardCli(stdin, stdout) {
3950
4501
  }
3951
4502
  }
3952
4503
  function peekFanOutBudget(stateDir) {
3953
- const counterPath = join3(stateDir, "spawn-count.json");
4504
+ const counterPath = join7(stateDir, "spawn-count.json");
3954
4505
  const count = readCount(counterPath) + 1;
3955
4506
  const limit = fanOutLimit();
3956
4507
  if (count <= limit)
@@ -3958,7 +4509,7 @@ function peekFanOutBudget(stateDir) {
3958
4509
  return `ulw-loop spawn fan-out cap reached (${count}/${limit}). Consolidate work into the agents already running, or raise OMO_SPAWN_FANOUT_LIMIT if this volume is intentional.`;
3959
4510
  }
3960
4511
  function consumeFanOutBudget(stateDir) {
3961
- const counterPath = join3(stateDir, "spawn-count.json");
4512
+ const counterPath = join7(stateDir, "spawn-count.json");
3962
4513
  const count = readCount(counterPath) + 1;
3963
4514
  atomicWriteJson(counterPath, { count });
3964
4515
  const limit = fanOutLimit();
@@ -3973,7 +4524,7 @@ function consumeReviewSpawnBudget(payload, plan, stateDir) {
3973
4524
  const goal = plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ?? plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
3974
4525
  if (goal === undefined)
3975
4526
  return null;
3976
- const counterPath = join3(stateDir, "review-spawn-counts.json");
4527
+ const counterPath = join7(stateDir, "review-spawn-counts.json");
3977
4528
  const limit = reviewSpawnLimit();
3978
4529
  const counts = readCounts(counterPath);
3979
4530
  const key = `${agentType}:${goal.id}:a${goal.attempt}`;
@@ -3998,13 +4549,13 @@ function missingGateArtifact(payload, plan) {
3998
4549
  const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
3999
4550
  for (const name of requiredArtifacts) {
4000
4551
  const relative = `${attemptDir}/${name}`;
4001
- if (!isNonEmptyFile(join3(payload.cwd, relative)))
4552
+ if (!isNonEmptyFile(join7(payload.cwd, relative)))
4002
4553
  return relative;
4003
4554
  }
4004
4555
  return null;
4005
4556
  }
4006
4557
  const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
4007
- return isNonEmptyFile(join3(payload.cwd, manualQa)) ? null : manualQa;
4558
+ return isNonEmptyFile(join7(payload.cwd, manualQa)) ? null : manualQa;
4008
4559
  }
4009
4560
  function isGateReviewerSpawn(toolInput) {
4010
4561
  const agentType = reviewAgentType(toolInput);
@@ -4051,11 +4602,6 @@ function activeSurfaceReviewerAlias(reviewer) {
4051
4602
  }
4052
4603
  return reviewer;
4053
4604
  }
4054
- function atomicWriteJson(targetPath, data) {
4055
- const tmp = join3(dirname3(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
4056
- writeFileSync(tmp, JSON.stringify(data));
4057
- renameSync(tmp, targetPath);
4058
- }
4059
4605
  function deny(reason) {
4060
4606
  return `${JSON.stringify({
4061
4607
  hookSpecificOutput: {
@@ -4067,17 +4613,6 @@ function deny(reason) {
4067
4613
  })}
4068
4614
  `;
4069
4615
  }
4070
- function readAdmissionBreaker(sessionId) {
4071
- const dataDir = process.env["PLUGIN_DATA"];
4072
- if (typeof dataDir !== "string")
4073
- return null;
4074
- try {
4075
- const value = JSON.parse(readFileSync5(join3(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
4076
- return typeof value.reason === "string" ? value.reason : "capacity limit";
4077
- } catch {
4078
- return null;
4079
- }
4080
- }
4081
4616
  function fanOutLimit() {
4082
4617
  const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
4083
4618
  if (raw === undefined)
@@ -4092,45 +4627,9 @@ function reviewSpawnLimit() {
4092
4627
  const parsed = Number.parseInt(raw, 10);
4093
4628
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
4094
4629
  }
4095
- function isNonEmptyFile(path) {
4096
- try {
4097
- return existsSync6(path) && statSync3(path).size > 0;
4098
- } catch (error) {
4099
- if (error instanceof Error)
4100
- return false;
4101
- throw error;
4102
- }
4103
- }
4104
- function readCount(counterPath) {
4105
- try {
4106
- const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
4107
- return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
4108
- } catch (error) {
4109
- if (error instanceof Error)
4110
- return 0;
4111
- throw error;
4112
- }
4113
- }
4114
- function readCounts(counterPath) {
4115
- try {
4116
- const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
4117
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
4118
- return {};
4119
- const counts = {};
4120
- for (const [key, value] of Object.entries(parsed)) {
4121
- if (typeof value === "number" && value >= 0)
4122
- counts[key] = value;
4123
- }
4124
- return counts;
4125
- } catch (error) {
4126
- if (error instanceof Error)
4127
- return {};
4128
- throw error;
4129
- }
4130
- }
4131
- function readPlan(goalsPath) {
4630
+ function readPlan(repoRoot, sessionId) {
4132
4631
  try {
4133
- return JSON.parse(readFileSync5(goalsPath, "utf8"));
4632
+ return readUlwLoopPlanSync(repoRoot, { sessionId });
4134
4633
  } catch (error) {
4135
4634
  if (error instanceof Error)
4136
4635
  return null;
@@ -4139,8 +4638,8 @@ function readPlan(goalsPath) {
4139
4638
  }
4140
4639
 
4141
4640
  // components/ulw-loop/src/stop-resume-hook.ts
4142
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
4143
- import { isAbsolute as isAbsolute2, join as join4, resolve as resolve4, sep as sep2 } from "node:path";
4641
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
4642
+ import { isAbsolute as isAbsolute3, join as join8, resolve as resolve5, sep as sep3 } from "node:path";
4144
4643
  var RESUME_CAP = 2;
4145
4644
  var CONTEXT_PRESSURE_MARKERS2 = [
4146
4645
  "context compacted",
@@ -4161,7 +4660,7 @@ function runStopResumeHook(input) {
4161
4660
  return "";
4162
4661
  const scope = { sessionId: payload.session_id };
4163
4662
  const stateDir = ulwLoopDir(payload.cwd, scope);
4164
- const plan = readPlan2(join4(stateDir, "goals.json"));
4663
+ const plan = readPlan2(payload.cwd, payload.session_id);
4165
4664
  if (plan === null || plan.aggregateCompletion?.status === "complete")
4166
4665
  return "";
4167
4666
  const goal = resumableGoal(plan);
@@ -4207,23 +4706,23 @@ function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
4207
4706
  }
4208
4707
  }
4209
4708
  function consumeResumeBudget(stateDir, goalId) {
4210
- const ledgerLineCount = countLedgerLines(join4(stateDir, "ledger.jsonl"));
4211
- const counterPath = resolve4(stateDir, `auto-resume-${goalId}.json`);
4212
- const stuckPath = resolve4(stateDir, `auto-resume-${goalId}.stuck`);
4709
+ const ledgerLineCount = readLedgerAt(stateDir).length;
4710
+ const counterPath = resolve5(stateDir, `auto-resume-${goalId}.json`);
4711
+ const stuckPath = resolve5(stateDir, `auto-resume-${goalId}.stuck`);
4213
4712
  if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
4214
4713
  return false;
4215
4714
  const previous = readCounter(counterPath);
4216
4715
  const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
4217
4716
  if (count >= RESUME_CAP) {
4218
- writeFileSync2(stuckPath, `no ledger progress after ${count} resumes
4717
+ writeFileSync3(stuckPath, `no ledger progress after ${count} resumes
4219
4718
  `);
4220
4719
  return false;
4221
4720
  }
4222
- writeFileSync2(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
4721
+ writeFileSync3(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
4223
4722
  return true;
4224
4723
  }
4225
4724
  function isInsideDir(dir, candidate) {
4226
- return candidate.startsWith(resolve4(dir) + sep2);
4725
+ return candidate.startsWith(resolve5(dir) + sep3);
4227
4726
  }
4228
4727
  function renderResumeDirective(plan, goal, sessionId) {
4229
4728
  const normalized = normalizeUlwLoopSessionId(sessionId);
@@ -4238,30 +4737,20 @@ function renderResumeDirective(plan, goal, sessionId) {
4238
4737
  ].join(`
4239
4738
  `);
4240
4739
  }
4241
- function readPlan2(goalsPath) {
4740
+ function readPlan2(repoRoot, sessionId) {
4242
4741
  try {
4243
- return JSON.parse(readFileSync6(goalsPath, "utf8"));
4742
+ return readUlwLoopPlanSync(repoRoot, { sessionId });
4244
4743
  } catch (error) {
4245
4744
  if (error instanceof Error)
4246
4745
  return null;
4247
4746
  throw error;
4248
4747
  }
4249
4748
  }
4250
- function countLedgerLines(ledgerPath) {
4251
- try {
4252
- return readFileSync6(ledgerPath, "utf8").split(`
4253
- `).filter(Boolean).length;
4254
- } catch (error) {
4255
- if (error instanceof Error)
4256
- return 0;
4257
- throw error;
4258
- }
4259
- }
4260
4749
  function readCounter(counterPath) {
4261
4750
  try {
4262
- if (!existsSync7(counterPath))
4751
+ if (!existsSync8(counterPath))
4263
4752
  return null;
4264
- const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
4753
+ const parsed = JSON.parse(readFileSync7(counterPath, "utf8"));
4265
4754
  if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
4266
4755
  return null;
4267
4756
  return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
@@ -4273,7 +4762,7 @@ function readCounter(counterPath) {
4273
4762
  }
4274
4763
  function boulderContinuationWillFire(cwd, sessionId) {
4275
4764
  try {
4276
- const raw = JSON.parse(readFileSync6(join4(cwd, ".omo", "boulder.json"), "utf8"));
4765
+ const raw = JSON.parse(readFileSync7(join8(cwd, ".omo", "boulder.json"), "utf8"));
4277
4766
  const works = raw["works"];
4278
4767
  const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
4279
4768
  return entries.some((work) => {
@@ -4292,7 +4781,7 @@ function boulderContinuationWillFire(cwd, sessionId) {
4292
4781
  }
4293
4782
  function transcriptShowsContextPressure(transcriptPath) {
4294
4783
  try {
4295
- const transcript = readFileSync6(transcriptPath, "utf8").toLowerCase();
4784
+ const transcript = readFileSync7(transcriptPath, "utf8").toLowerCase();
4296
4785
  return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
4297
4786
  } catch (error) {
4298
4787
  if (error instanceof Error)
@@ -4304,12 +4793,12 @@ function boulderPlanHasChecklist(cwd, entry) {
4304
4793
  const activePlan = entry["active_plan"];
4305
4794
  if (typeof activePlan !== "string" || activePlan.trim().length === 0)
4306
4795
  return false;
4307
- const planPath = isAbsolute2(activePlan) ? activePlan : join4(cwd, activePlan);
4796
+ const planPath = isAbsolute3(activePlan) ? activePlan : join8(cwd, activePlan);
4308
4797
  const worktree = entry["worktree_path"];
4309
- const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join4(isAbsolute2(worktree) ? worktree : join4(cwd, worktree), activePlan), planPath] : [planPath];
4798
+ const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute3(activePlan) ? [join8(isAbsolute3(worktree) ? worktree : join8(cwd, worktree), activePlan), planPath] : [planPath];
4310
4799
  for (const candidate of candidates) {
4311
4800
  try {
4312
- return readFileSync6(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
4801
+ return readFileSync7(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
4313
4802
  } catch (error) {
4314
4803
  if (!(error instanceof Error))
4315
4804
  throw error;