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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
  2. package/dist/cli/index.js +64 -35
  3. package/dist/cli-node/index.js +64 -35
  4. package/package.json +2 -6
  5. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  6. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -0
  7. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  8. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  9. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  10. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  11. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  12. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  13. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  14. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  15. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  16. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  17. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  19. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
  25. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +5 -4
  26. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  29. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  30. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +13 -0
  33. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +5 -4
  34. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +5 -4
  35. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +582 -322
  36. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +5 -0
  37. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +16 -12
  38. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
  39. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
  40. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +2 -1
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +48 -22
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
  45. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
  46. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
  47. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
  48. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
  49. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
  50. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +3 -4
  51. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +12 -5
  52. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
  53. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +8 -62
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
  56. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
  57. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
  58. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
  59. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
  60. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +5 -5
  61. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
  62. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  63. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  64. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
  65. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +5 -4
  66. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +5 -0
  67. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +16 -16
  68. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
  69. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
  70. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +58 -51
  71. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
  72. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
  73. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
  74. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +3 -3
  75. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +30 -5
  76. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
  77. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +8 -59
  78. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
  79. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
  80. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
  81. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +5 -4
  82. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
  83. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
  84. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
  85. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
  86. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
  88. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
  89. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
  90. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
  93. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
  94. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
  95. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
  96. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
  97. package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
  99. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
  100. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  102. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  103. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  104. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  105. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  106. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  116. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  117. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  118. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  119. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  120. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  121. package/packages/omo-codex/plugin/package-lock.json +12 -12
  122. package/packages/omo-codex/plugin/package.json +1 -1
  123. package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
  124. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +15 -7
  125. package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
  126. package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
  127. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  128. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
  129. package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  130. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  131. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
  132. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  133. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
  134. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  135. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  136. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  137. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  138. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  139. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  140. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  141. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  142. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  143. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  144. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +5 -4
  145. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +1 -1
  146. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  147. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
  148. package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
  149. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
  150. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
  151. package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
  152. package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
  153. package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
  154. package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
  155. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  156. package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
  157. package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
  158. package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  159. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  160. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  161. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
  162. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  163. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
  164. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  165. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  166. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  167. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  168. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  169. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  170. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  171. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  172. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  173. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  174. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
  175. package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
  176. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  177. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
  178. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
  179. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -1,8 +1,8 @@
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";
4
+ import { existsSync as existsSync4, statSync as statSync2 } from "node:fs";
5
+ import { readFile as readFile3 } from "node:fs/promises";
6
6
  import { resolve as resolve3 } from "node:path";
7
7
 
8
8
  // components/ulw-loop/src/codex-goal-snapshot.ts
@@ -178,15 +178,9 @@ function ulwLoopGoalsRelativePath(scope) {
178
178
  function ulwLoopLedgerRelativePath(scope) {
179
179
  return `${ulwLoopRelativeDir(scope)}/${ULW_LOOP_LEDGER}`;
180
180
  }
181
- function ulwLoopBriefPath(repoRoot, scope) {
182
- return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_BRIEF);
183
- }
184
181
  function ulwLoopGoalsPath(repoRoot, scope) {
185
182
  return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_GOALS);
186
183
  }
187
- function ulwLoopLedgerPath(repoRoot, scope) {
188
- return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_LEDGER);
189
- }
190
184
  function ulwLoopStateLockPath(repoRoot, scope) {
191
185
  return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_STATE_LOCK);
192
186
  }
@@ -322,23 +316,142 @@ ${gateError.message}`, "ULW_LOOP_QUALITY_GATE_INVALID", {
322
316
  });
323
317
  }
324
318
 
319
+ // components/ulw-loop/src/plan-commit.ts
320
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
321
+ import { randomUUID as randomUUID2 } from "node:crypto";
322
+ import { linkSync, mkdirSync as mkdirSync2, renameSync, rmSync, writeFileSync } from "node:fs";
323
+ import { rename, writeFile } from "node:fs/promises";
324
+ import { join as join4 } from "node:path";
325
+
326
+ // components/ulw-loop/src/ledger.ts
327
+ import { join as join3 } from "node:path";
328
+
329
+ // components/ulw-loop/src/plan-log.ts
330
+ import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
331
+ import { join as join2 } from "node:path";
332
+ function hasCode(error, code) {
333
+ return error instanceof Error && "code" in error && error.code === code;
334
+ }
335
+ function readOptional(path) {
336
+ try {
337
+ return readFileSync(path, "utf8");
338
+ } catch (error) {
339
+ if (hasCode(error, "ENOENT"))
340
+ return;
341
+ throw error;
342
+ }
343
+ }
344
+ function logNames(dir) {
345
+ try {
346
+ return readdirSync(join2(dir, "revisions")).filter((name) => /^\d{8,}\.json$/.test(name)).sort();
347
+ } catch (error) {
348
+ if (hasCode(error, "ENOENT"))
349
+ return [];
350
+ throw error;
351
+ }
352
+ }
353
+ function readRecord(dir, name) {
354
+ try {
355
+ const record = JSON.parse(readFileSync(join2(dir, "revisions", name), "utf8"));
356
+ return record.version === 1 && Number.isInteger(record.revision) && record.plan?.version === 1 && Array.isArray(record.plan.goals) && Array.isArray(record.ledger) ? record : undefined;
357
+ } catch (error) {
358
+ if (!(error instanceof SyntaxError))
359
+ throw error;
360
+ return;
361
+ }
362
+ }
363
+ function readRecords(dir) {
364
+ const records = [];
365
+ for (const name of logNames(dir)) {
366
+ const record = readRecord(dir, name);
367
+ if (record !== undefined)
368
+ records.push(record);
369
+ }
370
+ return records.sort((a, b) => a.revision - b.revision);
371
+ }
372
+ function readNewestRecord(dir) {
373
+ const names = logNames(dir).sort((a, b) => Number.parseInt(b, 10) - Number.parseInt(a, 10));
374
+ for (const name of names) {
375
+ const record = readRecord(dir, name);
376
+ if (record !== undefined)
377
+ return record;
378
+ }
379
+ return;
380
+ }
381
+ function reconcilePlan(dir) {
382
+ const raw = readOptional(join2(dir, "goals.json"));
383
+ let cached;
384
+ if (raw !== undefined) {
385
+ try {
386
+ cached = JSON.parse(raw);
387
+ } catch (error) {
388
+ if (!(error instanceof SyntaxError))
389
+ throw error;
390
+ }
391
+ }
392
+ const latest = readNewestRecord(dir);
393
+ return latest !== undefined && latest.revision >= (cached?.revision ?? 0) ? latest.plan : cached;
394
+ }
395
+ function planExists(repoRoot, scope) {
396
+ const dir = ulwLoopDir(repoRoot, scope);
397
+ return existsSync2(join2(dir, "goals.json")) || logNames(dir).length > 0;
398
+ }
399
+
400
+ // components/ulw-loop/src/ledger.ts
401
+ function readLedgerAt(dir) {
402
+ const entries = new Map;
403
+ const lines = (readOptional(join3(dir, "ledger.jsonl")) ?? "").split(/\r?\n/);
404
+ for (const [index, line] of lines.entries()) {
405
+ if (line.trim().length === 0)
406
+ continue;
407
+ try {
408
+ const entry = JSON.parse(line);
409
+ entry.revision ??= 0;
410
+ entry.id ??= `legacy-${index + 1}`;
411
+ entries.set(entry.id, entry);
412
+ } catch (error) {
413
+ if (!(error instanceof SyntaxError))
414
+ throw error;
415
+ }
416
+ }
417
+ for (const record of readRecords(dir))
418
+ for (const [seq, entry] of record.ledger.entries()) {
419
+ const id = entry.id ?? `${record.revision}-${seq}`;
420
+ entries.set(id, { ...entry, revision: record.revision, id });
421
+ }
422
+ const reset = reconcilePlan(dir)?.ledgerResetRevision ?? 0;
423
+ const sequence = (entry) => Number(entry.id?.split("-").at(-1) ?? 0);
424
+ return [...entries.values()].filter((entry) => (entry.revision ?? 0) >= reset).sort((a, b) => (a.revision ?? 0) - (b.revision ?? 0) || sequence(a) - sequence(b));
425
+ }
426
+ function readLedger(repoRoot, scope) {
427
+ return readLedgerAt(ulwLoopDir(repoRoot, scope));
428
+ }
429
+
325
430
  // components/ulw-loop/src/plan-io.ts
326
431
  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";
432
+ import { readdirSync as readdirSync2 } from "node:fs";
330
433
 
331
434
  // components/ulw-loop/src/plan-missing-recovery.ts
332
435
  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}\`.`];
436
+ function createGoalsAction(surface) {
437
+ return surface === "omo-senpi" ? 'agentToolkit.createGoals({ brief: "<brief>" })' : ULW_LOOP_CREATE_GOALS_COMMAND;
438
+ }
439
+ function planMissingRecovery(existingSessionIds, surface = "lazycodex") {
440
+ const lines = [`Recovery: bootstrap the plan with \`${createGoalsAction(surface)}\`.`];
335
441
  if (existingSessionIds.length === 0)
336
442
  return { message: lines.join(`
337
443
  `) };
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.`);
444
+ 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
445
  return { message: lines.join(`
340
446
  `), details: { existingSessionIds } };
341
447
  }
448
+ function planMissingError(planPath, existingSessionIds, surface = "lazycodex") {
449
+ const recovery = planMissingRecovery(existingSessionIds, surface);
450
+ return new UlwLoopError(`No ulw-loop plan found at ${planPath}.
451
+ ${recovery.message}`, "ULW_LOOP_PLAN_MISSING", {
452
+ ...recovery.details === undefined ? {} : { details: recovery.details }
453
+ });
454
+ }
342
455
  function sessionScopeRequiredMessage(flag, existingSessionIds) {
343
456
  const lines = [
344
457
  "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 +474,207 @@ function sessionIdRequiredMessage(flag) {
361
474
 
362
475
  // components/ulw-loop/src/state-lock.ts
363
476
  import { randomUUID } from "node:crypto";
364
- import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
477
+ import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync as readFileSync2, statSync, unlinkSync, writeSync } from "node:fs";
365
478
  import { dirname } from "node:path";
366
- import { setTimeout as sleep } from "node:timers/promises";
367
479
  var ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
368
480
  var DEFAULT_TIMEOUT_MS = 1e4;
369
481
  var DEFAULT_STALE_MS = 60000;
370
- var MIN_DELAY_MS = 5;
371
- var MAX_DELAY_MS = 100;
482
+ var DEFAULT_LEASE_MS = 30000;
372
483
  var SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
484
+ var systemClock = {
485
+ now: Date.now,
486
+ schedule(fn, ms) {
487
+ const timer = setTimeout(fn, ms);
488
+ return {
489
+ unref: () => {
490
+ timer.unref();
491
+ },
492
+ cancel: () => clearTimeout(timer)
493
+ };
494
+ }
495
+ };
373
496
  async function withStateLock(lockPath, fn, options = {}) {
374
- const token = await acquireAsync(lockPath, options);
497
+ const holder = await acquireAsync(lockPath, options);
498
+ const clock = options.clock ?? systemClock;
499
+ let timer;
500
+ let heartbeatError;
501
+ const beat = () => {
502
+ try {
503
+ holder.record.leaseUntil = clock.now() + (options.leaseMs ?? DEFAULT_LEASE_MS);
504
+ writeRecord(holder);
505
+ schedule();
506
+ } catch (error) {
507
+ heartbeatError = error;
508
+ }
509
+ };
510
+ const schedule = () => {
511
+ if (options.heartbeatMs === 0)
512
+ return;
513
+ timer = clock.schedule(beat, options.heartbeatMs ?? (options.leaseMs ?? DEFAULT_LEASE_MS) / 3);
514
+ timer.unref();
515
+ };
516
+ schedule();
375
517
  try {
376
- return await fn();
518
+ const result = await fn(holder.record.token);
519
+ if (heartbeatError !== undefined)
520
+ throw heartbeatError;
521
+ return result;
377
522
  } finally {
378
- release(lockPath, token);
523
+ timer?.cancel();
524
+ closeSync(holder.fd);
525
+ release(lockPath, holder.record.token);
379
526
  }
380
527
  }
381
528
  function withStateLockSync(lockPath, fn, options = {}) {
382
- const token = acquireSync(lockPath, options);
529
+ const holder = acquireSync(lockPath, options);
383
530
  try {
384
531
  return fn();
385
532
  } finally {
386
- release(lockPath, token);
533
+ closeSync(holder.fd);
534
+ release(lockPath, holder.record.token);
387
535
  }
388
536
  }
389
537
  function isStateLockTimeout(error) {
390
538
  return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
391
539
  }
392
540
  async function acquireAsync(lockPath, options) {
393
- const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
394
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
541
+ const clock = options.clock ?? systemClock;
542
+ const deadline = clock.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
395
543
  mkdirSync(dirname(lockPath), { recursive: true });
396
544
  for (let attempt = 0;; ) {
397
- const outcome = attemptOnce(lockPath, staleMs);
545
+ const outcome = attemptOnce(lockPath, options, clock.now(), options.leaseMs ?? DEFAULT_LEASE_MS);
398
546
  if (outcome.kind === "acquired")
399
- return outcome.token;
547
+ return outcome.holder;
400
548
  if (outcome.kind === "retry")
401
549
  continue;
402
- if (Date.now() >= deadline)
403
- throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
404
- await sleep(backoffMs(attempt));
550
+ if (clock.now() >= deadline)
551
+ throw lockTimeout(lockPath, options);
552
+ await new Promise((resolve) => clock.schedule(resolve, backoffMs(attempt)));
405
553
  attempt += 1;
406
554
  }
407
555
  }
408
556
  function acquireSync(lockPath, options) {
409
557
  const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
410
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
411
558
  mkdirSync(dirname(lockPath), { recursive: true });
412
559
  for (let attempt = 0;; ) {
413
- const outcome = attemptOnce(lockPath, staleMs);
560
+ const outcome = attemptOnce(lockPath, options, Date.now());
414
561
  if (outcome.kind === "acquired")
415
- return outcome.token;
562
+ return outcome.holder;
416
563
  if (outcome.kind === "retry")
417
564
  continue;
418
565
  if (Date.now() >= deadline)
419
- throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
566
+ throw lockTimeout(lockPath, options);
420
567
  Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
421
568
  attempt += 1;
422
569
  }
423
570
  }
424
- function attemptOnce(lockPath, staleMs) {
571
+ function attemptOnce(lockPath, options, now, leaseMs) {
425
572
  try {
426
- const token = tryCreate(lockPath);
427
- if (token !== null)
428
- return { kind: "acquired", token };
429
- const snapshot = readSnapshot(lockPath);
573
+ const holder = tryCreate(lockPath, now, leaseMs);
574
+ if (holder !== null)
575
+ return { kind: "acquired", holder };
576
+ const snapshot = readSnapshot(lockPath, now);
430
577
  if (snapshot === null)
431
578
  return { kind: "retry" };
432
- if (isStale(snapshot, staleMs) && reclaim(lockPath, snapshot.raw))
579
+ const record = snapshot.record;
580
+ const stale = record === null ? snapshot.ageMs > (options.staleMs ?? DEFAULT_STALE_MS) : !isProcessAlive(record.pid) || record.leaseUntil !== undefined && now > record.leaseUntil;
581
+ if (stale && reclaim(lockPath, snapshot.raw))
433
582
  return { kind: "retry" };
434
583
  return { kind: "wait" };
435
584
  } catch (error) {
436
- if (hasCode(error, "EINTR"))
585
+ if (hasCode2(error, "EINTR"))
437
586
  return { kind: "wait" };
438
587
  throw error;
439
588
  }
440
589
  }
441
- function tryCreate(lockPath) {
590
+ function writeRecord(holder) {
591
+ const buf = Buffer.from(JSON.stringify(holder.record));
592
+ ftruncateSync(holder.fd, 0);
593
+ let offset = 0;
594
+ while (offset < buf.length) {
595
+ const written = writeSync(holder.fd, buf, offset, buf.length - offset, offset);
596
+ if (written === 0)
597
+ throw new Error("State lock write made no progress.");
598
+ offset += written;
599
+ }
600
+ }
601
+ function tryCreate(lockPath, now, leaseMs) {
442
602
  let fd;
443
603
  try {
444
604
  fd = openSync(lockPath, "wx");
445
605
  } catch (error) {
446
- if (hasCode(error, "EEXIST"))
606
+ if (hasCode2(error, "EEXIST"))
447
607
  return null;
448
608
  throw error;
449
609
  }
450
- const record = { pid: process.pid, createdAt: new Date().toISOString(), token: randomUUID() };
610
+ const record = {
611
+ pid: process.pid,
612
+ createdAt: new Date(now).toISOString(),
613
+ token: randomUUID(),
614
+ ...leaseMs === undefined ? {} : { leaseUntil: now + leaseMs }
615
+ };
616
+ const holder = { fd, record };
451
617
  try {
452
- writeSync(fd, JSON.stringify(record));
618
+ writeRecord(holder);
453
619
  } catch (error) {
454
620
  closeSync(fd);
455
621
  try {
456
622
  unlinkSync(lockPath);
457
- } catch {}
623
+ } catch (cleanup) {
624
+ if (!hasCode2(cleanup, "ENOENT"))
625
+ throw cleanup;
626
+ }
458
627
  throw error;
459
628
  }
460
- closeSync(fd);
461
- return record.token;
629
+ return holder;
462
630
  }
463
- function readSnapshot(lockPath) {
631
+ function readSnapshot(lockPath, now) {
464
632
  try {
465
- const raw = readFileSync(lockPath, "utf8");
466
- const ageMs = Date.now() - statSync(lockPath).mtimeMs;
467
- return { raw, record: parseRecord(raw), ageMs };
633
+ const raw = readFileSync2(lockPath, "utf8");
634
+ return { raw, record: parseRecord(raw), ageMs: now - statSync(lockPath).mtimeMs };
468
635
  } catch (error) {
469
- if (hasCode(error, "ENOENT"))
636
+ if (hasCode2(error, "ENOENT"))
470
637
  return null;
471
638
  throw error;
472
639
  }
473
640
  }
474
641
  function parseRecord(raw) {
475
642
  try {
476
- const parsed = JSON.parse(raw);
477
- if (typeof parsed !== "object" || parsed === null)
643
+ const record = JSON.parse(raw);
644
+ if (typeof record !== "object" || record === null)
645
+ return null;
646
+ if (!("pid" in record) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0)
478
647
  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")
648
+ if (!("createdAt" in record) || typeof record.createdAt !== "string")
484
649
  return null;
485
- if (typeof token !== "string" || token.length === 0)
650
+ if (!("token" in record) || typeof record.token !== "string" || record.token.length === 0)
486
651
  return null;
487
- return { pid, createdAt, token };
652
+ return {
653
+ pid: record.pid,
654
+ createdAt: record.createdAt,
655
+ token: record.token,
656
+ ..."leaseUntil" in record && typeof record.leaseUntil === "number" ? { leaseUntil: record.leaseUntil } : {}
657
+ };
488
658
  } catch (error) {
489
659
  if (error instanceof SyntaxError)
490
660
  return null;
491
661
  throw error;
492
662
  }
493
663
  }
494
- function isStale(snapshot, staleMs) {
495
- if (snapshot.record === null)
496
- return snapshot.ageMs > staleMs;
497
- return !isProcessAlive(snapshot.record.pid);
498
- }
499
664
  function isProcessAlive(pid) {
500
665
  try {
501
666
  process.kill(pid, 0);
502
667
  return true;
503
668
  } catch (error) {
504
- if (hasCode(error, "ESRCH"))
669
+ if (hasCode2(error, "ESRCH"))
505
670
  return false;
506
- if (hasCode(error, "EPERM"))
671
+ if (hasCode2(error, "EPERM"))
507
672
  return true;
508
673
  throw error;
509
674
  }
510
675
  }
511
676
  function reclaim(lockPath, expectedRaw) {
512
- const current = readSnapshot(lockPath);
677
+ const current = readSnapshot(lockPath, Date.now());
513
678
  if (current === null)
514
679
  return true;
515
680
  if (current.raw !== expectedRaw)
@@ -517,63 +682,102 @@ function reclaim(lockPath, expectedRaw) {
517
682
  try {
518
683
  unlinkSync(lockPath);
519
684
  } catch (error) {
520
- if (!hasCode(error, "ENOENT"))
685
+ if (hasCode2(error, "EPERM") || hasCode2(error, "EACCES"))
686
+ return false;
687
+ if (!hasCode2(error, "ENOENT"))
521
688
  throw error;
522
689
  }
523
690
  return true;
524
691
  }
525
692
  function release(lockPath, token) {
526
- const current = readSnapshot(lockPath);
527
- if (current === null || current.record?.token !== token)
693
+ if (readSnapshot(lockPath, Date.now())?.record?.token !== token)
528
694
  return;
529
695
  try {
530
696
  unlinkSync(lockPath);
531
697
  } catch (error) {
532
- if (!hasCode(error, "ENOENT"))
698
+ if (!hasCode2(error, "ENOENT"))
533
699
  throw error;
534
700
  }
535
701
  }
536
702
  function backoffMs(attempt) {
537
- const exponential = Math.min(MAX_DELAY_MS, MIN_DELAY_MS * 2 ** attempt);
538
- return exponential + Math.random() * MIN_DELAY_MS;
703
+ return Math.min(100, 5 * 2 ** attempt) + Math.random() * 5;
539
704
  }
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
- });
705
+ function lockTimeout(lockPath, options) {
706
+ const holder = readSnapshot(lockPath, Date.now())?.record;
707
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
708
+ const owner = holder == null ? "another process" : `pid ${holder.pid}`;
709
+ 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
710
  }
551
- function hasCode(error, code) {
711
+ function hasCode2(error, code) {
552
712
  return error instanceof Error && "code" in error && error.code === code;
553
713
  }
554
714
 
555
715
  // components/ulw-loop/src/plan-io.ts
716
+ function readLedger2(repoRoot, scope) {
717
+ const lockPath = ulwLoopStateLockPath(repoRoot, scope);
718
+ if (heldLocks.getStore()?.has(lockPath)) {
719
+ assertStateLockOwned(lockPath);
720
+ materializeSync(ulwLoopDir(repoRoot, scope));
721
+ }
722
+ return readLedger(repoRoot, scope);
723
+ }
556
724
  var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
557
725
  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
726
  var locks = new Map;
559
727
  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));
728
+ var lockOptions = new AsyncLocalStorage;
729
+ function tokenAt(lockPath) {
730
+ const raw = readOptional(lockPath);
731
+ if (raw === undefined)
732
+ return;
733
+ try {
734
+ const record = JSON.parse(raw);
735
+ return typeof record === "object" && record !== null && "token" in record && typeof record.token === "string" ? record.token : undefined;
736
+ } catch (error) {
737
+ if (error instanceof SyntaxError)
738
+ return;
739
+ throw error;
740
+ }
565
741
  }
566
- function isSteeringKind(value) {
567
- return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised" || value === "batch_updated";
742
+ function assertStateLockOwned(lockPath) {
743
+ const context = heldLocks.getStore()?.get(lockPath);
744
+ if (context === undefined)
745
+ return;
746
+ if (tokenAt(lockPath) !== context.token)
747
+ throw new UlwLoopError("The ulw-loop mutation lock changed owners.", "ULW_LOOP_LOCK_LOST");
748
+ }
749
+ function migrationEntries(plan) {
750
+ for (const context of heldLocks.getStore()?.values() ?? []) {
751
+ const entries = context.migrations.get(plan.goalsPath);
752
+ if (entries !== undefined) {
753
+ context.migrations.delete(plan.goalsPath);
754
+ return entries;
755
+ }
756
+ }
757
+ return [];
568
758
  }
569
- async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
759
+ async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn, options = {}) {
570
760
  const scope = typeof scopeOrFn === "function" ? undefined : scopeOrFn;
571
761
  const fn = typeof scopeOrFn === "function" ? scopeOrFn : maybeFn;
572
762
  if (fn === undefined)
573
763
  throw new UlwLoopError("Missing ulw-loop mutation body.", "ULW_LOOP_LOCK_BODY_MISSING");
574
764
  const lockKey = `${repoRoot}\x00${ulwLoopRelativeDir(scope)}`;
575
765
  const lockPath = ulwLoopStateLockPath(repoRoot, scope);
576
- const locked = () => withStateLock(lockPath, () => heldLocks.run(new Set([...heldLocks.getStore() ?? [], lockKey]), fn));
766
+ const locked = () => withStateLock(lockPath, async (token) => {
767
+ return heldLocks.run(new Map([...heldLocks.getStore() ?? [], [lockPath, { token, migrations: new Map }]]), async () => {
768
+ for (let attempt = 0;; attempt += 1) {
769
+ try {
770
+ return await fn();
771
+ } catch (error) {
772
+ if (!(error instanceof UlwLoopError) || error.code !== "ULW_LOOP_PUBLISH_CONFLICT")
773
+ throw error;
774
+ assertStateLockOwned(lockPath);
775
+ if (attempt !== 0)
776
+ throw error;
777
+ }
778
+ }
779
+ });
780
+ }, { ...lockOptions.getStore(), ...options });
577
781
  const prior = locks.get(lockKey) ?? Promise.resolve(undefined);
578
782
  const run = prior.then(locked, locked);
579
783
  const gate = run.then(() => {
@@ -588,98 +792,140 @@ async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
588
792
  });
589
793
  return run;
590
794
  }
591
- async function readUlwLoopPlan(repoRoot, scope) {
795
+ function readUlwLoopPlanSync(repoRoot, scope) {
592
796
  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)) {
797
+ const parsed = reconcilePlan(ulwLoopDir(repoRoot, scope));
798
+ if (parsed === undefined)
799
+ throw planMissingError(repoRelative(path, repoRoot), listUlwLoopSessionIds(repoRoot));
800
+ if (parsed.version !== 1 || !Array.isArray(parsed.goals))
605
801
  throw new UlwLoopError(`Invalid ulw-loop plan at ${repoRelative(path, repoRoot)}.`, "ULW_LOOP_PLAN_INVALID");
606
- }
607
802
  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();
803
+ if ((parsed.codexGoalMode ?? "per_story") === "aggregate" && previousObjective !== undefined && (previousObjective === LEGACY_OBJECTIVE || previousObjective.startsWith(LEGACY_OBJECTIVE_PREFIX))) {
613
804
  parsed.codexObjective = aggregateCodexObjectiveForScope(scope);
614
805
  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);
806
+ heldLocks.getStore()?.get(ulwLoopStateLockPath(repoRoot, scope))?.migrations.set(parsed.goalsPath, [
807
+ {
808
+ at: iso(),
809
+ kind: "aggregate_objective_migrated",
810
+ before: { codexObjective: previousObjective },
811
+ after: { codexObjective: parsed.codexObjective }
812
+ }
813
+ ]);
624
814
  }
625
815
  return parsed;
626
816
  }
817
+ async function readUlwLoopPlan(repoRoot, scope) {
818
+ if (heldLocks.getStore()?.has(ulwLoopStateLockPath(repoRoot, scope))) {
819
+ assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
820
+ await materialize(ulwLoopDir(repoRoot, scope));
821
+ }
822
+ return readUlwLoopPlanSync(repoRoot, scope);
823
+ }
627
824
  function listUlwLoopSessionIds(repoRoot) {
628
825
  try {
629
- return readdirSync(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
826
+ return readdirSync2(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
630
827
  } catch {
631
828
  return [];
632
829
  }
633
830
  }
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);
831
+ function isSteeringKind(value) {
832
+ return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised" || value === "batch_updated";
644
833
  }
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");
834
+ async function findAcceptedSteeringLedgerEntry(repoRoot, key, scope) {
835
+ 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
836
  }
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 });
837
+
838
+ // components/ulw-loop/src/plan-commit.ts
839
+ var hooks = new AsyncLocalStorage2;
840
+ async function beforePlanMutation() {
841
+ await hooks.getStore()?.beforeMutation?.();
842
+ }
843
+ async function writeViewFile(path, content) {
844
+ await (hooks.getStore()?.writeView ?? writeFile)(path, content);
845
+ }
846
+ async function commit(repoRoot, scope, mutation) {
847
+ const { plan } = mutation;
848
+ const dir = ulwLoopDir(repoRoot, scope);
849
+ assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
850
+ await hooks.getStore()?.beforeCommit?.();
851
+ const next = (plan.revision ?? 0) + 1;
852
+ const brief = plan.brief ?? readOptional(join4(dir, "brief.md")) ?? "";
853
+ const entries = [...migrationEntries(plan), ...mutation.entries].map((entry, seq) => ({
854
+ ...entry,
855
+ revision: next,
856
+ id: `${next}-${seq}`
857
+ }));
858
+ const record = {
859
+ version: 1,
860
+ revision: next,
861
+ plan: { ...plan, revision: next, brief },
862
+ ledger: entries
863
+ };
864
+ mkdirSync2(join4(dir, "revisions"), { recursive: true });
865
+ mkdirSync2(join4(dir, "tmp"), { recursive: true });
866
+ const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}.json`);
656
867
  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"))
868
+ writeFileSync(temp, `${JSON.stringify(record)}
869
+ `, { flag: "wx" });
870
+ try {
871
+ (hooks.getStore()?.link ?? linkSync)(temp, join4(dir, "revisions", `${String(next).padStart(8, "0")}.json`));
872
+ } catch (error) {
873
+ if (hasCode(error, "EEXIST"))
874
+ throw new UlwLoopError("Another writer published the read revision.", "ULW_LOOP_PUBLISH_CONFLICT", {
875
+ cause: error
876
+ });
877
+ if (["EPERM", "ENOTSUP", "EXDEV"].some((code) => hasCode(error, code)))
878
+ throw new UlwLoopError("Immutable ulw-loop publication requires atomic hard links.", "ULW_LOOP_PUBLISH_UNSUPPORTED_FS", { cause: error });
663
879
  throw error;
880
+ }
664
881
  } finally {
665
- lines.close();
666
- stream.destroy();
882
+ rmSync(temp, { force: true });
667
883
  }
884
+ plan.revision = next;
885
+ plan.brief = brief;
886
+ for (const [index, entry] of mutation.entries.entries())
887
+ Object.assign(entry, entries[index + entries.length - mutation.entries.length]);
888
+ await hooks.getStore()?.afterCommit?.();
889
+ await materialize(dir);
890
+ }
891
+ function viewContents(dir) {
892
+ const plan = reconcilePlan(dir);
893
+ if (plan === undefined || readRecords(dir).length === 0)
894
+ return [];
895
+ const ledger = readLedgerAt(dir);
896
+ const views = [
897
+ ["goals.json", `${JSON.stringify(plan, null, 2)}
898
+ `],
899
+ ["ledger.jsonl", ledger.length === 0 ? "" : `${ledger.map((entry) => JSON.stringify(entry)).join(`
900
+ `)}
901
+ `],
902
+ ["brief.md", plan.brief ?? readOptional(join4(dir, "brief.md")) ?? ""]
903
+ ];
904
+ return views.filter(([name, content]) => readOptional(join4(dir, name)) !== content);
668
905
  }
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;
906
+ function materializeSync(dir) {
907
+ for (const [name, content] of viewContents(dir)) {
908
+ mkdirSync2(join4(dir, "tmp"), { recursive: true });
909
+ const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}-${name}`);
910
+ try {
911
+ writeFileSync(temp, content, { flag: "wx" });
912
+ renameSync(temp, join4(dir, name));
913
+ } finally {
914
+ rmSync(temp, { force: true });
915
+ }
916
+ }
917
+ }
918
+ async function materialize(dir) {
919
+ for (const [name, content] of viewContents(dir)) {
920
+ mkdirSync2(join4(dir, "tmp"), { recursive: true });
921
+ const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}-${name}`);
922
+ try {
923
+ await writeViewFile(temp, content);
924
+ await rename(temp, join4(dir, name));
925
+ } finally {
926
+ rmSync(temp, { force: true });
927
+ }
681
928
  }
682
- return;
683
929
  }
684
930
 
685
931
  // components/ulw-loop/src/evidence.ts
@@ -729,7 +975,6 @@ async function recordEvidence(repoRoot, args, scope) {
729
975
  criterion.notes = args.notes;
730
976
  goal.updatedAt = capturedAt;
731
977
  plan.updatedAt = capturedAt;
732
- await writePlan(repoRoot, plan, scope);
733
978
  const ledgerEntry = {
734
979
  at: capturedAt,
735
980
  kind,
@@ -741,7 +986,7 @@ async function recordEvidence(repoRoot, args, scope) {
741
986
  before: { status: prevStatus },
742
987
  after: { goalId: goal.id, criterionId: criterion.id, status: args.status, evidence, capturedAt, prevStatus }
743
988
  };
744
- await appendLedger(repoRoot, ledgerEntry, scope);
989
+ await commit(repoRoot, scope, { plan, entries: [ledgerEntry] });
745
990
  return { plan, goal, criterion, ledgerEntry };
746
991
  });
747
992
  }
@@ -1011,8 +1256,8 @@ function adversarialVerdict(row, field) {
1011
1256
  }
1012
1257
 
1013
1258
  // 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";
1259
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1260
+ import { dirname as dirname2, join as join5 } from "node:path";
1016
1261
  import { fileURLToPath } from "node:url";
1017
1262
  var REVIEWER_ROLES_BY_SURFACE = {
1018
1263
  lazycodex: {
@@ -1066,14 +1311,14 @@ function resolveToolkitSurface(options) {
1066
1311
  if (fromEnv !== null)
1067
1312
  return fromEnv;
1068
1313
  const entryDir = options?.entryDir ?? dirname2(fileURLToPath(import.meta.url));
1069
- const markerPath = join2(entryDir, SURFACE_MARKER_FILENAME);
1314
+ const markerPath = join5(entryDir, SURFACE_MARKER_FILENAME);
1070
1315
  return readMarkerSurface(markerPath) ?? "lazycodex";
1071
1316
  }
1072
1317
  function readMarkerSurface(markerPath) {
1073
1318
  try {
1074
- if (!existsSync2(markerPath))
1319
+ if (!existsSync3(markerPath))
1075
1320
  return null;
1076
- const parsed = JSON.parse(readFileSync2(markerPath, "utf8"));
1321
+ const parsed = JSON.parse(readFileSync3(markerPath, "utf8"));
1077
1322
  return parseSurface(parsed["surface"]);
1078
1323
  } catch (error) {
1079
1324
  if (error instanceof Error)
@@ -1274,7 +1519,7 @@ function parseAdversarialCases(value, byId) {
1274
1519
  }
1275
1520
 
1276
1521
  // components/ulw-loop/src/cli-arg-parser.ts
1277
- import { readFile as readFile3 } from "node:fs/promises";
1522
+ import { readFile as readFile2 } from "node:fs/promises";
1278
1523
  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
1524
  var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
1280
1525
  function hasFlag(argv, flag) {
@@ -1322,7 +1567,7 @@ async function readJsonInput(value) {
1322
1567
  if (value === undefined)
1323
1568
  return;
1324
1569
  try {
1325
- return JSON.parse(looksLikeJson(value) ? value : await readFile3(value, "utf8"));
1570
+ return JSON.parse(looksLikeJson(value) ? value : await readFile2(value, "utf8"));
1326
1571
  } catch (error) {
1327
1572
  const message = error instanceof Error ? error.message : "unknown error";
1328
1573
  throw new UlwLoopError(`Invalid JSON input: ${message}`, "ULW_LOOP_JSON_INPUT_INVALID", { cause: error });
@@ -1332,7 +1577,7 @@ async function parseCodexGoalJson(value) {
1332
1577
  if (value === undefined)
1333
1578
  return;
1334
1579
  try {
1335
- const raw = looksLikeJson(value) ? value : await readFile3(value, "utf8");
1580
+ const raw = looksLikeJson(value) ? value : await readFile2(value, "utf8");
1336
1581
  JSON.parse(raw);
1337
1582
  return raw;
1338
1583
  } catch (error) {
@@ -1483,7 +1728,7 @@ function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
1483
1728
  }
1484
1729
 
1485
1730
  // components/ulw-loop/src/checkpoint.ts
1486
- var QUALITY_GATE_FS = { existsSync: existsSync3, statSync: statSync2 };
1731
+ var QUALITY_GATE_FS = { existsSync: existsSync4, statSync: statSync2 };
1487
1732
  function ulwLoopFail2(message, code) {
1488
1733
  throw new UlwLoopError(message, code);
1489
1734
  }
@@ -1506,10 +1751,10 @@ async function readJsonInput2(raw, repoRoot) {
1506
1751
  throw error;
1507
1752
  }
1508
1753
  const path = resolve3(repoRoot, trimmed);
1509
- if (!existsSync3(path))
1754
+ if (!existsSync4(path))
1510
1755
  return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
1511
1756
  try {
1512
- return JSON.parse(await readFile4(path, "utf8"));
1757
+ return JSON.parse(await readFile3(path, "utf8"));
1513
1758
  } catch (error) {
1514
1759
  return ulwLoopFail2(`Quality gate path does not contain valid JSON${error instanceof Error ? `: ${error.message}` : "."}`, "ulw_loop_json_input_invalid");
1515
1760
  }
@@ -1649,12 +1894,12 @@ async function checkpointUlwLoop(repoRoot, args, scope, dependencies) {
1649
1894
  if (aggregateCompletion !== undefined)
1650
1895
  nextActions = [...nextActions, 'aggregate complete — now update_goal({status:"complete"})'];
1651
1896
  plan.updatedAt = now;
1652
- await writePlan(repoRoot, plan, scope);
1653
1897
  const ledgerEntry = buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion);
1654
- await appendLedger(repoRoot, ledgerEntry, scope);
1898
+ const entries = [ledgerEntry];
1655
1899
  const closedBatch = args.status === "complete" ? batchClosedBy(plan, goal.id) : undefined;
1656
1900
  if (closedBatch !== undefined)
1657
- await appendLedger(repoRoot, { at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId }, scope);
1901
+ entries.push({ at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId });
1902
+ await commit(repoRoot, scope, { plan, entries });
1658
1903
  return aggregateCompletion === undefined ? { plan, goal, ledgerEntry, nextActions, warnings } : { plan, goal, ledgerEntry, aggregateCompletion, nextActions, warnings };
1659
1904
  });
1660
1905
  }
@@ -1990,10 +2235,6 @@ function joinLines(lines) {
1990
2235
  `);
1991
2236
  }
1992
2237
 
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";
1996
-
1997
2238
  // components/ulw-loop/src/plan-goal-factory.ts
1998
2239
  function cleanLine(line) {
1999
2240
  return line.replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/, "").trim();
@@ -2099,18 +2340,31 @@ function clearGoalBlockerFields2(goal) {
2099
2340
  ])
2100
2341
  delete goal[key];
2101
2342
  }
2102
- async function createUlwLoopPlan(repoRoot, args, scope) {
2343
+ async function createUlwLoopPlan(repoRoot, args, scope, surface = "lazycodex") {
2103
2344
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
2104
- if (!args.force && existsSync4(ulwLoopGoalsPath(repoRoot, scope))) {
2105
- const existing = await readUlwLoopPlan(repoRoot, scope);
2345
+ let existing;
2346
+ if (planExists(repoRoot, scope)) {
2347
+ try {
2348
+ existing = await readUlwLoopPlan(repoRoot, scope);
2349
+ } catch (error) {
2350
+ if (!args.force)
2351
+ throw error;
2352
+ }
2353
+ }
2354
+ if (!args.force && existing !== undefined) {
2106
2355
  if (isUlwLoopDone(existing))
2107
- throw completedPlanExistsError(scope);
2356
+ throw completedPlanExistsError(scope, surface);
2108
2357
  throw new UlwLoopError(`Refusing to overwrite existing ${ulwLoopGoalsRelativePath(scope)}; pass --force to recreate it.`, "ULW_LOOP_PLAN_EXISTS");
2109
2358
  }
2110
2359
  const now = iso();
2111
2360
  const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now));
2112
2361
  const plan = {
2113
2362
  version: 1,
2363
+ revision: existing?.revision ?? 0,
2364
+ ledgerResetRevision: (existing?.revision ?? 0) + 1,
2365
+ brief: args.brief.endsWith(`
2366
+ `) ? args.brief : `${args.brief}
2367
+ `,
2114
2368
  evidenceLayoutVersion: 2,
2115
2369
  createdAt: now,
2116
2370
  updatedAt: now,
@@ -2125,21 +2379,23 @@ async function createUlwLoopPlan(repoRoot, args, scope) {
2125
2379
  plan.validationBatches = validationBatches;
2126
2380
  if (plan.codexGoalMode === "aggregate")
2127
2381
  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);
2382
+ await beforePlanMutation();
2383
+ await commit(repoRoot, scope, {
2384
+ plan,
2385
+ entries: [{ at: now, kind: "plan_created", message: `${goals.length} goal(s) created` }]
2386
+ });
2135
2387
  return plan;
2136
2388
  });
2137
2389
  }
2138
- function completedPlanExistsError(scope) {
2390
+ function completedPlanExistsError(scope, surface) {
2139
2391
  return new UlwLoopError([
2140
2392
  `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."
2393
+ ...surface === "omo-senpi" ? [
2394
+ "Start a new run under a fresh session id (a new senpi session) or call agentToolkit.createGoals({ brief, force: true }) to recreate."
2395
+ ] : [
2396
+ "Start a new run with `omo-agent-toolkit ulw-loop create-goals --session-id <new-id> ...` to isolate fresh state.",
2397
+ "Use --force only when you intentionally want to overwrite the completed evidence."
2398
+ ]
2143
2399
  ].join(" "), "ULW_LOOP_PLAN_EXISTS_COMPLETE");
2144
2400
  }
2145
2401
  async function addUlwLoopGoal(repoRoot, args, scope) {
@@ -2147,8 +2403,10 @@ async function addUlwLoopGoal(repoRoot, args, scope) {
2147
2403
  const plan = await readUlwLoopPlan(repoRoot, scope);
2148
2404
  const now = iso();
2149
2405
  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);
2406
+ await commit(repoRoot, scope, {
2407
+ plan,
2408
+ entries: [{ at: now, kind: "goal_added", goalId: goal.id, status: goal.status, message: goal.title }]
2409
+ });
2152
2410
  return { plan, goal };
2153
2411
  });
2154
2412
  }
@@ -2161,17 +2419,18 @@ async function startNextUlwLoop(repoRoot, args = {}, scope) {
2161
2419
  const existing = plan.goals.find((goal) => goal.status === "in_progress" && isScheduleEligible(goal));
2162
2420
  if (existing)
2163
2421
  return { plan, goal: existing, resumed: true };
2422
+ const entries = [];
2164
2423
  let next = plan.goals.find((goal) => goal.status === "pending" && isScheduleEligible(goal));
2165
2424
  if (!next && args.retryFailed) {
2166
2425
  next = plan.goals.find((goal) => goal.status === "failed" && !goal.nonRetriable && isScheduleEligible(goal));
2167
2426
  if (next)
2168
- await appendLedger(repoRoot, {
2427
+ entries.push({
2169
2428
  at: now,
2170
2429
  kind: "goal_retried",
2171
2430
  goalId: next.id,
2172
2431
  status: "pending",
2173
2432
  ...next.failureReason ? { message: next.failureReason } : {}
2174
- }, scope);
2433
+ });
2175
2434
  }
2176
2435
  if (!next)
2177
2436
  return { done: true, plan };
@@ -2182,8 +2441,14 @@ async function startNextUlwLoop(repoRoot, args = {}, scope) {
2182
2441
  next.updatedAt = now;
2183
2442
  plan.activeGoalId = next.id;
2184
2443
  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);
2444
+ entries.push({
2445
+ at: now,
2446
+ kind: "goal_started",
2447
+ goalId: next.id,
2448
+ status: next.status,
2449
+ message: `Attempt ${next.attempt}`
2450
+ });
2451
+ await commit(repoRoot, scope, { plan, entries });
2187
2452
  return { plan, goal: next, resumed: false };
2188
2453
  });
2189
2454
  }
@@ -2279,7 +2544,7 @@ function checkpointStatus(value) {
2279
2544
  }
2280
2545
 
2281
2546
  // components/ulw-loop/src/cli-subcommands.ts
2282
- import { readFile as readFile5 } from "node:fs/promises";
2547
+ import { readFile as readFile4 } from "node:fs/promises";
2283
2548
 
2284
2549
  // components/ulw-loop/src/cli-steering.ts
2285
2550
  var SOURCES = ["user_prompt_submit", "finding", "cli"];
@@ -2649,9 +2914,7 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
2649
2914
  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
2915
  Reflect.set(summaryEntry, "kind", "blocker_recorded");
2651
2916
  const ledgerEntries = [blockedEntry, addedEntry, summaryEntry];
2652
- await writePlan(repoRoot, plan, scope);
2653
- for (const entry of ledgerEntries)
2654
- await appendLedger(repoRoot, entry, scope);
2917
+ await commit(repoRoot, scope, { plan, entries: ledgerEntries });
2655
2918
  return {
2656
2919
  plan,
2657
2920
  blockedGoal: goal,
@@ -2664,31 +2927,31 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
2664
2927
  }
2665
2928
 
2666
2929
  // components/ulw-loop/src/status-next-actions.ts
2667
- function statusNextActions(plan) {
2930
+ function statusNextActions(plan, surface = "lazycodex") {
2668
2931
  const actions = [];
2669
2932
  const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
2670
2933
  if (plan.goals.length === 0)
2671
- actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
2934
+ actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
2672
2935
  else if (active !== undefined)
2673
- actions.push(...activeGoalActions(plan, active));
2936
+ actions.push(...activeGoalActions(plan, active, surface));
2674
2937
  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>'\`.`);
2938
+ 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
2939
  if (plan.evidenceLayoutVersion !== 2 || active === undefined)
2677
2940
  actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
2678
2941
  return actions;
2679
2942
  }
2680
- function activeGoalActions(plan, active) {
2943
+ function activeGoalActions(plan, active, surface) {
2681
2944
  const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
2682
2945
  if (unresolved.length > 0)
2683
2946
  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>"\`.`
2947
+ 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
2948
  ];
2686
2949
  if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
2687
2950
  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.`
2951
+ 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
2952
  ];
2690
2953
  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.`
2954
+ 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
2955
  ];
2693
2956
  }
2694
2957
 
@@ -3020,11 +3283,10 @@ async function steerUlwLoop(repoRoot, proposal, scope) {
3020
3283
  }
3021
3284
  const at = proposal.now?.toISOString() ?? iso();
3022
3285
  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);
3286
+ const entries = [ledgerEntry(proposal, finalAudit, at)];
3026
3287
  if (batchEntry !== null)
3027
- await appendLedger(repoRoot, batchEntry, scope);
3288
+ entries.push(batchEntry);
3289
+ await commit(repoRoot, scope, { plan: next, entries });
3028
3290
  return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
3029
3291
  });
3030
3292
  }
@@ -3071,8 +3333,12 @@ function validateCodexGoalJson(raw) {
3071
3333
  function validateContext(context) {
3072
3334
  if (!context.cwd.trim())
3073
3335
  throw new UlwLoopError("cwd is required.", "ULW_LOOP_CWD_REQUIRED");
3074
- if (!context.sessionId.trim())
3336
+ const sessionId = context.sessionId.trim();
3337
+ if (!sessionId)
3075
3338
  throw new UlwLoopError("ULW_LOOP_SESSION_ID_REQUIRED: sessionId is required.", "ULW_LOOP_SESSION_ID_REQUIRED");
3339
+ const normalizedSessionId = normalizeUlwLoopSessionId(sessionId);
3340
+ if (normalizedSessionId === null || /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(sessionId))
3341
+ throw new UlwLoopError("ULW_LOOP_SESSION_ID_INVALID: sessionId normalizes to null.", "ULW_LOOP_SESSION_ID_INVALID");
3076
3342
  if (context.surface !== "omo-senpi" && context.surface !== "lazycodex")
3077
3343
  throw new UlwLoopError("surface must be omo-senpi or lazycodex.", "ULW_LOOP_SURFACE_INVALID");
3078
3344
  }
@@ -3115,6 +3381,8 @@ function createAgentToolkit(context, deps = {}) {
3115
3381
  const nextActions = typeof result === "object" && result !== null ? nextActionsFrom(result) : [];
3116
3382
  return await notify(operation, { ok: true, operation, result, nextActions });
3117
3383
  } catch (error) {
3384
+ if (error instanceof UlwLoopError && error.code === "ULW_LOOP_PLAN_MISSING" && context.surface === "omo-senpi")
3385
+ return notify(operation, failure(operation, planMissingError(ulwLoopGoalsRelativePath(scope), listUlwLoopSessionIds(context.cwd), context.surface)));
3118
3386
  const response = caught(operation, error instanceof Error ? error : new Error("ULW_LOOP_ERROR"));
3119
3387
  return notify(operation, response);
3120
3388
  }
@@ -3149,14 +3417,14 @@ function createAgentToolkit(context, deps = {}) {
3149
3417
  }
3150
3418
  },
3151
3419
  help: () => invoke("help", async () => ULW_LOOP_MANIFEST),
3152
- createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope)),
3420
+ createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope, context.surface)),
3153
3421
  status: () => invoke("status", async () => {
3154
3422
  const plan = await readUlwLoopPlan(context.cwd, scope);
3155
3423
  const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
3156
3424
  return {
3157
3425
  plan,
3158
3426
  summary: summarizeUlwLoopPlan(plan),
3159
- nextActions: statusNextActions(plan),
3427
+ nextActions: statusNextActions(plan, context.surface),
3160
3428
  ...active === undefined || plan.evidenceLayoutVersion !== 2 ? {} : { currentAttemptDir: ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) }
3161
3429
  };
3162
3430
  }),
@@ -3184,7 +3452,7 @@ async function steerUlwLoopBatch(repoRoot, proposals, scope) {
3184
3452
  const failed = prepared.results.find((item) => !item.accepted);
3185
3453
  if (failed !== undefined) {
3186
3454
  const entry = rejectedLedgerEntry(prepared.results);
3187
- await appendLedger(repoRoot, entry, scope);
3455
+ await commit(repoRoot, scope, { plan, entries: [entry] });
3188
3456
  return rejected(plan, prepared.results, failed.rejectedReasons);
3189
3457
  }
3190
3458
  let next = plan;
@@ -3193,10 +3461,9 @@ async function steerUlwLoopBatch(repoRoot, proposals, scope) {
3193
3461
  next = item.prepared.next;
3194
3462
  const fresh = prepared.items.filter((item) => item.kind === "fresh");
3195
3463
  if (fresh.length > 0) {
3196
- await writePlan(repoRoot, next, scope);
3197
3464
  const entries = fresh.map((item) => ledgerEntry2(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
3198
3465
  const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
3199
- await appendLedgerEntries(repoRoot, batchEntry === null ? entries : [...entries, batchEntry], scope);
3466
+ await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
3200
3467
  }
3201
3468
  return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
3202
3469
  });
@@ -3260,7 +3527,7 @@ function ledgerEntry2(proposal, audit, at) {
3260
3527
  // components/ulw-loop/src/cli-subcommands.ts
3261
3528
  async function createGoals(repoRoot, argv, json, scope) {
3262
3529
  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);
3530
+ const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile4(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
3264
3531
  if (!brief.trim()) {
3265
3532
  throw new UlwLoopError("Missing brief text. Pass --brief, --brief-file, --from-stdin, or positional text.", "ULW_LOOP_BRIEF_REQUIRED");
3266
3533
  }
@@ -3528,10 +3795,10 @@ function commandScope(repoRoot, argv) {
3528
3795
  }
3529
3796
 
3530
3797
  // components/ulw-loop/src/ultrawork-directive.ts
3531
- import { readFileSync as readFileSync4 } from "node:fs";
3798
+ import { readFileSync as readFileSync5 } from "node:fs";
3532
3799
 
3533
3800
  // components/ulw-loop/src/ultrawork-skill-pointer.ts
3534
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
3801
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
3535
3802
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3536
3803
  var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
3537
3804
  ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
@@ -3563,7 +3830,7 @@ Do not start the requested work until all three steps are complete.
3563
3830
  `;
3564
3831
  var ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
3565
3832
  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");
3833
+ var ULTRAWORK_DIRECTIVE = readFileSync4(new URL("../directive.md", import.meta.url), "utf8");
3567
3834
  function resolveUltraworkSkillFilePath() {
3568
3835
  return fileURLToPath2(ULTRAWORK_SKILL_FILE_URL);
3569
3836
  }
@@ -3626,7 +3893,7 @@ function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath) {
3626
3893
  return false;
3627
3894
  }
3628
3895
  function readTranscriptTail(transcriptPath) {
3629
- const rawTranscript = readFileSync4(transcriptPath);
3896
+ const rawTranscript = readFileSync5(transcriptPath);
3630
3897
  return rawTranscript.subarray(Math.max(0, rawTranscript.byteLength - TRANSCRIPT_SEARCH_BYTES)).toString("utf8");
3631
3898
  }
3632
3899
  function isUltraworkPrompt(prompt) {
@@ -3819,9 +4086,64 @@ function readAll(stdin) {
3819
4086
  }
3820
4087
 
3821
4088
  // components/ulw-loop/src/spawn-guard.ts
4089
+ import { mkdirSync as mkdirSync3 } from "node:fs";
4090
+ import { join as join7 } from "node:path";
4091
+
4092
+ // components/ulw-loop/src/spawn-budget-io.ts
3822
4093
  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";
4094
+ import { existsSync as existsSync6, readFileSync as readFileSync6, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
4095
+ import { dirname as dirname3, join as join6 } from "node:path";
4096
+ function readAdmissionBreaker(sessionId) {
4097
+ const dataDir = process.env["PLUGIN_DATA"];
4098
+ if (typeof dataDir !== "string")
4099
+ return null;
4100
+ try {
4101
+ const value = JSON.parse(readFileSync6(join6(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
4102
+ return typeof value === "object" && value !== null && "reason" in value && typeof value.reason === "string" ? value.reason : "capacity limit";
4103
+ } catch {
4104
+ return null;
4105
+ }
4106
+ }
4107
+ function atomicWriteJson(targetPath, data) {
4108
+ const tmp = join6(dirname3(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
4109
+ writeFileSync2(tmp, JSON.stringify(data));
4110
+ renameSync2(tmp, targetPath);
4111
+ }
4112
+ function isNonEmptyFile(path) {
4113
+ try {
4114
+ return existsSync6(path) && statSync3(path).size > 0;
4115
+ } catch (error) {
4116
+ if (error instanceof Error)
4117
+ return false;
4118
+ throw error;
4119
+ }
4120
+ }
4121
+ function readCount(counterPath) {
4122
+ try {
4123
+ const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
4124
+ return typeof parsed === "object" && parsed !== null && "count" in parsed && typeof parsed.count === "number" && parsed.count >= 0 ? parsed.count : 0;
4125
+ } catch (error) {
4126
+ if (error instanceof Error)
4127
+ return 0;
4128
+ throw error;
4129
+ }
4130
+ }
4131
+ function readCounts(counterPath) {
4132
+ try {
4133
+ const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
4134
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
4135
+ return {};
4136
+ const counts = {};
4137
+ for (const [key, value] of Object.entries(parsed))
4138
+ if (typeof value === "number" && value >= 0)
4139
+ counts[key] = value;
4140
+ return counts;
4141
+ } catch (error) {
4142
+ if (error instanceof Error)
4143
+ return {};
4144
+ throw error;
4145
+ }
4146
+ }
3825
4147
 
3826
4148
  // components/ulw-loop/src/spawn-role-guard.ts
3827
4149
  var LAZYCODEX_SPAWN_ROLES = new Set([
@@ -3879,7 +4201,7 @@ function applySpawnBudgetGuards(payload, options = {}) {
3879
4201
  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
4202
  const scope = { sessionId: payload.session_id };
3881
4203
  const stateDir = ulwLoopDir(payload.cwd, scope);
3882
- const plan = readPlan(join3(stateDir, "goals.json"));
4204
+ const plan = readPlan(payload.cwd, payload.session_id);
3883
4205
  if (plan === null)
3884
4206
  return "";
3885
4207
  const lockOptions = options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
@@ -3918,10 +4240,10 @@ async function runSpawnAdmissionRecorderCli(stdin, stdout) {
3918
4240
  const dataDir = process.env["PLUGIN_DATA"];
3919
4241
  if (typeof dataDir !== "string" || typeof payload["session_id"] !== "string")
3920
4242
  return;
3921
- const markerDir = join3(dataDir, "spawn-breaker");
4243
+ const markerDir = join7(dataDir, "spawn-breaker");
3922
4244
  try {
3923
- mkdirSync2(markerDir, { recursive: true });
3924
- atomicWriteJson(join3(markerDir, `${payload["session_id"]}.json`), {
4245
+ mkdirSync3(markerDir, { recursive: true });
4246
+ atomicWriteJson(join7(markerDir, `${payload["session_id"]}.json`), {
3925
4247
  reason: response,
3926
4248
  at: new Date().toISOString()
3927
4249
  });
@@ -3950,7 +4272,7 @@ async function runSpawnGuardCli(stdin, stdout) {
3950
4272
  }
3951
4273
  }
3952
4274
  function peekFanOutBudget(stateDir) {
3953
- const counterPath = join3(stateDir, "spawn-count.json");
4275
+ const counterPath = join7(stateDir, "spawn-count.json");
3954
4276
  const count = readCount(counterPath) + 1;
3955
4277
  const limit = fanOutLimit();
3956
4278
  if (count <= limit)
@@ -3958,7 +4280,7 @@ function peekFanOutBudget(stateDir) {
3958
4280
  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
4281
  }
3960
4282
  function consumeFanOutBudget(stateDir) {
3961
- const counterPath = join3(stateDir, "spawn-count.json");
4283
+ const counterPath = join7(stateDir, "spawn-count.json");
3962
4284
  const count = readCount(counterPath) + 1;
3963
4285
  atomicWriteJson(counterPath, { count });
3964
4286
  const limit = fanOutLimit();
@@ -3973,7 +4295,7 @@ function consumeReviewSpawnBudget(payload, plan, stateDir) {
3973
4295
  const goal = plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ?? plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
3974
4296
  if (goal === undefined)
3975
4297
  return null;
3976
- const counterPath = join3(stateDir, "review-spawn-counts.json");
4298
+ const counterPath = join7(stateDir, "review-spawn-counts.json");
3977
4299
  const limit = reviewSpawnLimit();
3978
4300
  const counts = readCounts(counterPath);
3979
4301
  const key = `${agentType}:${goal.id}:a${goal.attempt}`;
@@ -3998,13 +4320,13 @@ function missingGateArtifact(payload, plan) {
3998
4320
  const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
3999
4321
  for (const name of requiredArtifacts) {
4000
4322
  const relative = `${attemptDir}/${name}`;
4001
- if (!isNonEmptyFile(join3(payload.cwd, relative)))
4323
+ if (!isNonEmptyFile(join7(payload.cwd, relative)))
4002
4324
  return relative;
4003
4325
  }
4004
4326
  return null;
4005
4327
  }
4006
4328
  const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
4007
- return isNonEmptyFile(join3(payload.cwd, manualQa)) ? null : manualQa;
4329
+ return isNonEmptyFile(join7(payload.cwd, manualQa)) ? null : manualQa;
4008
4330
  }
4009
4331
  function isGateReviewerSpawn(toolInput) {
4010
4332
  const agentType = reviewAgentType(toolInput);
@@ -4051,11 +4373,6 @@ function activeSurfaceReviewerAlias(reviewer) {
4051
4373
  }
4052
4374
  return reviewer;
4053
4375
  }
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
4376
  function deny(reason) {
4060
4377
  return `${JSON.stringify({
4061
4378
  hookSpecificOutput: {
@@ -4067,17 +4384,6 @@ function deny(reason) {
4067
4384
  })}
4068
4385
  `;
4069
4386
  }
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
4387
  function fanOutLimit() {
4082
4388
  const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
4083
4389
  if (raw === undefined)
@@ -4092,45 +4398,9 @@ function reviewSpawnLimit() {
4092
4398
  const parsed = Number.parseInt(raw, 10);
4093
4399
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
4094
4400
  }
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) {
4401
+ function readPlan(repoRoot, sessionId) {
4132
4402
  try {
4133
- return JSON.parse(readFileSync5(goalsPath, "utf8"));
4403
+ return readUlwLoopPlanSync(repoRoot, { sessionId });
4134
4404
  } catch (error) {
4135
4405
  if (error instanceof Error)
4136
4406
  return null;
@@ -4139,8 +4409,8 @@ function readPlan(goalsPath) {
4139
4409
  }
4140
4410
 
4141
4411
  // 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";
4412
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
4413
+ import { isAbsolute as isAbsolute2, join as join8, resolve as resolve4, sep as sep2 } from "node:path";
4144
4414
  var RESUME_CAP = 2;
4145
4415
  var CONTEXT_PRESSURE_MARKERS2 = [
4146
4416
  "context compacted",
@@ -4161,7 +4431,7 @@ function runStopResumeHook(input) {
4161
4431
  return "";
4162
4432
  const scope = { sessionId: payload.session_id };
4163
4433
  const stateDir = ulwLoopDir(payload.cwd, scope);
4164
- const plan = readPlan2(join4(stateDir, "goals.json"));
4434
+ const plan = readPlan2(payload.cwd, payload.session_id);
4165
4435
  if (plan === null || plan.aggregateCompletion?.status === "complete")
4166
4436
  return "";
4167
4437
  const goal = resumableGoal(plan);
@@ -4207,7 +4477,7 @@ function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
4207
4477
  }
4208
4478
  }
4209
4479
  function consumeResumeBudget(stateDir, goalId) {
4210
- const ledgerLineCount = countLedgerLines(join4(stateDir, "ledger.jsonl"));
4480
+ const ledgerLineCount = readLedgerAt(stateDir).length;
4211
4481
  const counterPath = resolve4(stateDir, `auto-resume-${goalId}.json`);
4212
4482
  const stuckPath = resolve4(stateDir, `auto-resume-${goalId}.stuck`);
4213
4483
  if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
@@ -4215,11 +4485,11 @@ function consumeResumeBudget(stateDir, goalId) {
4215
4485
  const previous = readCounter(counterPath);
4216
4486
  const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
4217
4487
  if (count >= RESUME_CAP) {
4218
- writeFileSync2(stuckPath, `no ledger progress after ${count} resumes
4488
+ writeFileSync3(stuckPath, `no ledger progress after ${count} resumes
4219
4489
  `);
4220
4490
  return false;
4221
4491
  }
4222
- writeFileSync2(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
4492
+ writeFileSync3(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
4223
4493
  return true;
4224
4494
  }
4225
4495
  function isInsideDir(dir, candidate) {
@@ -4238,30 +4508,20 @@ function renderResumeDirective(plan, goal, sessionId) {
4238
4508
  ].join(`
4239
4509
  `);
4240
4510
  }
4241
- function readPlan2(goalsPath) {
4511
+ function readPlan2(repoRoot, sessionId) {
4242
4512
  try {
4243
- return JSON.parse(readFileSync6(goalsPath, "utf8"));
4513
+ return readUlwLoopPlanSync(repoRoot, { sessionId });
4244
4514
  } catch (error) {
4245
4515
  if (error instanceof Error)
4246
4516
  return null;
4247
4517
  throw error;
4248
4518
  }
4249
4519
  }
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
4520
  function readCounter(counterPath) {
4261
4521
  try {
4262
4522
  if (!existsSync7(counterPath))
4263
4523
  return null;
4264
- const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
4524
+ const parsed = JSON.parse(readFileSync7(counterPath, "utf8"));
4265
4525
  if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
4266
4526
  return null;
4267
4527
  return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
@@ -4273,7 +4533,7 @@ function readCounter(counterPath) {
4273
4533
  }
4274
4534
  function boulderContinuationWillFire(cwd, sessionId) {
4275
4535
  try {
4276
- const raw = JSON.parse(readFileSync6(join4(cwd, ".omo", "boulder.json"), "utf8"));
4536
+ const raw = JSON.parse(readFileSync7(join8(cwd, ".omo", "boulder.json"), "utf8"));
4277
4537
  const works = raw["works"];
4278
4538
  const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
4279
4539
  return entries.some((work) => {
@@ -4292,7 +4552,7 @@ function boulderContinuationWillFire(cwd, sessionId) {
4292
4552
  }
4293
4553
  function transcriptShowsContextPressure(transcriptPath) {
4294
4554
  try {
4295
- const transcript = readFileSync6(transcriptPath, "utf8").toLowerCase();
4555
+ const transcript = readFileSync7(transcriptPath, "utf8").toLowerCase();
4296
4556
  return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
4297
4557
  } catch (error) {
4298
4558
  if (error instanceof Error)
@@ -4304,12 +4564,12 @@ function boulderPlanHasChecklist(cwd, entry) {
4304
4564
  const activePlan = entry["active_plan"];
4305
4565
  if (typeof activePlan !== "string" || activePlan.trim().length === 0)
4306
4566
  return false;
4307
- const planPath = isAbsolute2(activePlan) ? activePlan : join4(cwd, activePlan);
4567
+ const planPath = isAbsolute2(activePlan) ? activePlan : join8(cwd, activePlan);
4308
4568
  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];
4569
+ const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join8(isAbsolute2(worktree) ? worktree : join8(cwd, worktree), activePlan), planPath] : [planPath];
4310
4570
  for (const candidate of candidates) {
4311
4571
  try {
4312
- return readFileSync6(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
4572
+ return readFileSync7(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
4313
4573
  } catch (error) {
4314
4574
  if (!(error instanceof Error))
4315
4575
  throw error;