oh-my-opencode 5.0.0-beta.60 → 5.0.0-beta.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/THIRD-PARTY-NOTICES.md +8 -6
  2. package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
  3. package/dist/cli/index.js +465 -348
  4. package/dist/cli-node/index.js +465 -348
  5. package/dist/config/schema/agent-names.d.ts +0 -1
  6. package/dist/config/schema/browser-automation.d.ts +0 -2
  7. package/dist/config/schema/oh-my-opencode-config.d.ts +0 -1
  8. package/dist/hooks/comment-checker/__fixtures__/download-probe.fixture.d.ts +1 -0
  9. package/dist/index.js +61 -535
  10. package/dist/oh-my-opencode.schema.json +0 -1
  11. package/dist/skills/ast-grep/SKILL.md +12 -0
  12. package/dist/skills/debugging/references/tools/playwright-cli.md +79 -161
  13. package/dist/skills/frontend/SKILL.md +1 -1
  14. package/dist/skills/frontend/references/design/aside.md +1 -1
  15. package/dist/skills/frontend/references/design/clone-from-url.md +1 -1
  16. package/dist/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  17. package/dist/skills/review-work/SKILL.md +1 -1
  18. package/dist/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  19. package/dist/skills/ultimate-browsing/SKILL.md +17 -29
  20. package/dist/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  21. package/dist/skills/ultimate-browsing/engine/templates/package.json +2 -2
  22. package/dist/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  23. package/dist/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  24. package/dist/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  25. package/dist/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  26. package/dist/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  27. package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  28. package/dist/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  29. package/dist/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  30. package/dist/skills/ulw-execute/SKILL.md +1 -1
  31. package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  32. package/dist/skills/ulw-research/SKILL.md +3 -3
  33. package/dist/skills/visual-qa/AGENTS.md +1 -1
  34. package/dist/skills/visual-qa/SKILL.md +1 -1
  35. package/dist/skills/visual-qa/references/browser-setup.md +75 -0
  36. package/dist/tui.js +12 -5
  37. package/package.json +16 -23
  38. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  39. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +6887 -249
  40. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  41. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +7 -1
  43. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  46. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  48. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  49. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  50. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  51. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  52. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  53. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  54. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  55. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  56. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  57. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  58. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
  59. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +25 -15
  60. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  61. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  63. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  64. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  65. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  66. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +15 -0
  67. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +25 -15
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +5 -4
  69. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +640 -327
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +5 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +16 -12
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +2 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +48 -22
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +3 -4
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +29 -6
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
  88. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +1 -0
  89. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +29 -66
  90. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.d.ts +2 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.js +22 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
  93. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
  94. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
  95. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
  96. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
  97. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +5 -5
  98. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
  99. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +7 -7
  100. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  101. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
  102. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +5 -4
  103. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +5 -0
  104. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +16 -16
  105. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
  106. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
  107. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +58 -51
  108. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
  109. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
  111. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +3 -3
  112. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +55 -6
  113. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
  114. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +29 -62
  115. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-role-guard.ts +22 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
  117. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
  118. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
  119. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +5 -4
  120. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
  121. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
  127. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
  130. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
  131. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
  133. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +145 -0
  134. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
  135. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +7 -1
  136. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-role-matrix.test.ts +113 -0
  137. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
  138. package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
  139. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
  140. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
  141. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  157. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  158. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  159. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  161. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  162. package/packages/omo-codex/plugin/package-lock.json +12 -12
  163. package/packages/omo-codex/plugin/package.json +1 -1
  164. package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
  165. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +18 -10
  166. package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
  167. package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
  168. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  169. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
  170. package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  171. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  172. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +3 -3
  173. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +3 -3
  174. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +3 -3
  175. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +4 -4
  176. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  177. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
  178. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  179. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  180. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  181. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  182. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  183. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  184. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  185. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  186. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  187. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  188. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +25 -15
  189. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +4 -4
  190. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +3 -3
  191. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  192. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +6 -6
  193. package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
  194. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -4
  195. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
  196. package/packages/omo-codex/plugin/test/bootstrap-default-role.test.mjs +66 -0
  197. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +13 -1
  198. package/packages/omo-codex/scripts/install-dist/install-local.mjs +7060 -417
  199. package/packages/prompts-core/prompts/ultrawork/codex.md +25 -15
  200. package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
  201. package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
  202. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  203. package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
  204. package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
  205. package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  206. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  207. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  208. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
  209. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  210. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
  211. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  212. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  213. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  214. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  215. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  216. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  217. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  218. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  219. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  220. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  221. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
  222. package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
  223. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  224. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
  225. package/dist/skills/visual-qa/references/agent-browser-setup.md +0 -45
  226. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
  227. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -1,15 +1,22 @@
1
- import { randomBytes } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
1
+ import { mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
4
3
  import { parsePreToolUsePayload } from "./codex-hook.js";
5
4
  import { isFinalRunCompletionCandidate } from "./goal-status.js";
6
5
  import { ulwLoopAttemptEvidenceDir, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
6
+ import { readUlwLoopPlanSync } from "./plan-io.js";
7
+ import { atomicWriteJson, isNonEmptyFile, readAdmissionBreaker, readCount, readCounts } from "./spawn-budget-io.js";
8
+ import { spawnRoleDenial } from "./spawn-role-guard.js";
7
9
  import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
8
10
  import { GATE_REVIEWER_AGENT_NAMES, REVIEWER_ROLES_BY_SURFACE, resolveToolkitSurface, reviewerRolesFor, } from "./surface.js";
9
11
  // spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
10
12
  // hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
11
13
  // dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
12
- const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
14
+ const SPAWN_TOOL_TOKENS = new Set([
15
+ "spawn_agent",
16
+ "multi_agent_v1.spawn_agent",
17
+ "collaborationspawn_agent",
18
+ "collaboration.spawn_agent",
19
+ ]);
13
20
  export const DEFAULT_FANOUT_LIMIT = 24;
14
21
  const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
15
22
  const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
@@ -20,6 +27,16 @@ const REVIEW_AGENT_TYPES = [
20
27
  ];
21
28
  const REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
22
29
  export function applySpawnGuards(payload, options = {}) {
30
+ if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
31
+ return "";
32
+ if (resolveToolkitSurface() === "lazycodex") {
33
+ const reason = spawnRoleDenial(payload.tool_input);
34
+ if (reason !== null)
35
+ return deny(reason);
36
+ }
37
+ return applySpawnBudgetGuards(payload, options);
38
+ }
39
+ export function applySpawnBudgetGuards(payload, options = {}) {
23
40
  if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
24
41
  return "";
25
42
  const breaker = readAdmissionBreaker(payload.session_id);
@@ -27,7 +44,7 @@ export function applySpawnGuards(payload, options = {}) {
27
44
  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.`);
28
45
  const scope = { sessionId: payload.session_id };
29
46
  const stateDir = ulwLoopDir(payload.cwd, scope);
30
- const plan = readPlan(join(stateDir, "goals.json"));
47
+ const plan = readPlan(payload.cwd, payload.session_id);
31
48
  if (plan === null)
32
49
  return "";
33
50
  const lockOptions = options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
@@ -93,15 +110,16 @@ export async function runSpawnGuardCli(stdin, stdout) {
93
110
  for await (const chunk of stdin)
94
111
  chunks.push(Buffer.from(chunk));
95
112
  const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
96
- if (payload === null)
113
+ if (payload === null) {
114
+ stdout.write(deny("LazyCodex spawn guard received an invalid hook payload; role routing was not verified."));
97
115
  return;
116
+ }
98
117
  const output = applySpawnGuards(payload);
99
118
  if (output.length > 0)
100
119
  stdout.write(output);
101
120
  }
102
121
  catch (error) {
103
- if (error instanceof Error)
104
- return;
122
+ stdout.write(deny(`LazyCodex spawn guard failed: ${error instanceof Error ? error.message : String(error)}`));
105
123
  }
106
124
  }
107
125
  // Read-only fan-out eligibility check. Returns a denial reason when the next
@@ -116,6 +134,7 @@ function peekFanOutBudget(stateDir) {
116
134
  return null;
117
135
  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.`;
118
136
  }
137
+ // Hook budget counters are exempt from plan/audit commits.
119
138
  // Per-session spawn counter; depth/lineage tracking is descoped — this is a
120
139
  // total-volume backstop against fan-out explosions, not a recursion tracker.
121
140
  function consumeFanOutBudget(stateDir) {
@@ -222,11 +241,6 @@ function activeSurfaceReviewerAlias(reviewer) {
222
241
  }
223
242
  return reviewer;
224
243
  }
225
- function atomicWriteJson(targetPath, data) {
226
- const tmp = join(dirname(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
227
- writeFileSync(tmp, JSON.stringify(data));
228
- renameSync(tmp, targetPath);
229
- }
230
244
  function deny(reason) {
231
245
  return `${JSON.stringify({
232
246
  hookSpecificOutput: {
@@ -237,18 +251,6 @@ function deny(reason) {
237
251
  },
238
252
  })}\n`;
239
253
  }
240
- function readAdmissionBreaker(sessionId) {
241
- const dataDir = process.env["PLUGIN_DATA"];
242
- if (typeof dataDir !== "string")
243
- return null;
244
- try {
245
- const value = JSON.parse(readFileSync(join(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
246
- return typeof value.reason === "string" ? value.reason : "capacity limit";
247
- }
248
- catch {
249
- return null;
250
- }
251
- }
252
254
  function fanOutLimit() {
253
255
  const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
254
256
  if (raw === undefined)
@@ -263,48 +265,9 @@ function reviewSpawnLimit() {
263
265
  const parsed = Number.parseInt(raw, 10);
264
266
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
265
267
  }
266
- function isNonEmptyFile(path) {
267
- try {
268
- return existsSync(path) && statSync(path).size > 0;
269
- }
270
- catch (error) {
271
- if (error instanceof Error)
272
- return false;
273
- throw error;
274
- }
275
- }
276
- function readCount(counterPath) {
277
- try {
278
- const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
279
- return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
280
- }
281
- catch (error) {
282
- if (error instanceof Error)
283
- return 0;
284
- throw error;
285
- }
286
- }
287
- function readCounts(counterPath) {
288
- try {
289
- const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
290
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
291
- return {};
292
- const counts = {};
293
- for (const [key, value] of Object.entries(parsed)) {
294
- if (typeof value === "number" && value >= 0)
295
- counts[key] = value;
296
- }
297
- return counts;
298
- }
299
- catch (error) {
300
- if (error instanceof Error)
301
- return {};
302
- throw error;
303
- }
304
- }
305
- function readPlan(goalsPath) {
268
+ function readPlan(repoRoot, sessionId) {
306
269
  try {
307
- return JSON.parse(readFileSync(goalsPath, "utf8"));
270
+ return readUlwLoopPlanSync(repoRoot, { sessionId });
308
271
  }
309
272
  catch (error) {
310
273
  if (error instanceof Error)
@@ -0,0 +1,2 @@
1
+ export declare const LAZYCODEX_SPAWN_ROLES: ReadonlySet<string>;
2
+ export declare function spawnRoleDenial(input: unknown): string | null;
@@ -0,0 +1,22 @@
1
+ // Explicit selectors, pinned by a test to the bundled role TOMLs. The generated
2
+ // default is a non-fork fallback, not a selector: a user can opt out of owning it.
3
+ export const LAZYCODEX_SPAWN_ROLES = new Set([
4
+ "explorer",
5
+ "lazycodex-clone-fidelity-reviewer",
6
+ "lazycodex-code-reviewer",
7
+ "lazycodex-gate-reviewer",
8
+ "lazycodex-qa-executor",
9
+ "lazycodex-worker-high",
10
+ "lazycodex-worker-low",
11
+ "lazycodex-worker-medium",
12
+ "librarian",
13
+ "metis",
14
+ "momus",
15
+ "plan",
16
+ ]);
17
+ export function spawnRoleDenial(input) {
18
+ const role = typeof input === "object" && input !== null && "agent_type" in input ? input.agent_type : undefined;
19
+ if (typeof role === "string" && LAZYCODEX_SPAWN_ROLES.has(role))
20
+ return null;
21
+ return `LazyCodex requires an explicit registered agent_type: ${[...LAZYCODEX_SPAWN_ROLES].join(", ")}. Received ${JSON.stringify(role) ?? "no agent_type"}. Use the matching role and fork_turns: "none" (V2) or fork_context: false (V1), unless full history is deliberately required. The hook cannot see the tool schema; if agent_type is unavailable, stop and report incompatible role routing rather than spawning a generic agent. Describing a role in message does not select its TOML.`;
22
+ }
@@ -1,9 +1,19 @@
1
1
  import { UlwLoopError } from "./types.js";
2
2
  export declare const ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
3
+ export interface StateLockClock {
4
+ readonly now: () => number;
5
+ readonly schedule: (fn: () => void, ms: number) => {
6
+ unref(): void;
7
+ cancel(): void;
8
+ };
9
+ }
3
10
  export interface StateLockOptions {
4
11
  readonly timeoutMs?: number;
5
12
  readonly staleMs?: number;
13
+ readonly leaseMs?: number;
14
+ readonly heartbeatMs?: number;
15
+ readonly clock?: StateLockClock;
6
16
  }
7
- export declare function withStateLock<T>(lockPath: string, fn: () => Promise<T>, options?: StateLockOptions): Promise<T>;
17
+ export declare function withStateLock<T>(lockPath: string, fn: (token: string) => Promise<T>, options?: StateLockOptions): Promise<T>;
8
18
  export declare function withStateLockSync<T>(lockPath: string, fn: () => T, options?: StateLockOptions): T;
9
19
  export declare function isStateLockTimeout(error: unknown): error is UlwLoopError;
@@ -1,89 +1,121 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
2
+ import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
- import { setTimeout as sleep } from "node:timers/promises";
5
4
  import { UlwLoopError } from "./types.js";
6
- // Cross-process exclusive lock for one ulw-loop state directory. Every CLI
7
- // invocation is its own process, so the in-process promise chain in plan-io
8
- // serializes nothing across them; this file-level lock is what makes the
9
- // read-modify-write of goals.json (and the counters next to it) atomic.
10
- //
11
- // Protocol: the lock is a file created with O_EXCL whose body records the
12
- // owner (pid + a per-acquisition token). A waiter reclaims it only when the
13
- // owner is provably gone (pid dead) or the body never became a record within
14
- // `staleMs` (a creator that died mid-write); a live owner is never reclaimed by
15
- // age alone, because overlapping two bodies is exactly the lost update this lock
16
- // exists to prevent. Waiters back off and fail closed at `timeoutMs`. Release
17
- // unlinks only a lock that still carries the releaser's own token.
5
+ // O_EXCL establishes ownership. Async holders keep the fd open and refresh a
6
+ // lease through that fd, never through the pathname: a reclaimed inode is invisible.
7
+ // Sync/legacy holders are lease-less and only dead pids retire their records.
8
+ // Expiry permits overlapping bodies; immutable revision publication fences them.
9
+ // Reclaim's reread/unlink is not atomic. On Windows an open inode may not unlink:
10
+ // treat that refusal as a live owner and fail closed. Release checks its token.
18
11
  export const ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
19
12
  const DEFAULT_TIMEOUT_MS = 10_000;
20
13
  const DEFAULT_STALE_MS = 60_000;
21
- const MIN_DELAY_MS = 5;
22
- const MAX_DELAY_MS = 100;
14
+ const DEFAULT_LEASE_MS = 30_000;
23
15
  const SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
16
+ const systemClock = {
17
+ now: Date.now,
18
+ schedule(fn, ms) {
19
+ const timer = setTimeout(fn, ms);
20
+ return {
21
+ unref: () => {
22
+ timer.unref();
23
+ },
24
+ cancel: () => clearTimeout(timer),
25
+ };
26
+ },
27
+ };
24
28
  export async function withStateLock(lockPath, fn, options = {}) {
25
- const token = await acquireAsync(lockPath, options);
29
+ const holder = await acquireAsync(lockPath, options);
30
+ const clock = options.clock ?? systemClock;
31
+ let timer;
32
+ let heartbeatError;
33
+ const beat = () => {
34
+ try {
35
+ holder.record.leaseUntil = clock.now() + (options.leaseMs ?? DEFAULT_LEASE_MS);
36
+ writeRecord(holder);
37
+ schedule();
38
+ }
39
+ catch (error) {
40
+ heartbeatError = error;
41
+ }
42
+ };
43
+ const schedule = () => {
44
+ if (options.heartbeatMs === 0)
45
+ return;
46
+ timer = clock.schedule(beat, options.heartbeatMs ?? (options.leaseMs ?? DEFAULT_LEASE_MS) / 3);
47
+ timer.unref();
48
+ };
49
+ schedule();
26
50
  try {
27
- return await fn();
51
+ const result = await fn(holder.record.token);
52
+ if (heartbeatError !== undefined)
53
+ throw heartbeatError;
54
+ return result;
28
55
  }
29
56
  finally {
30
- release(lockPath, token);
57
+ timer?.cancel();
58
+ closeSync(holder.fd);
59
+ release(lockPath, holder.record.token);
31
60
  }
32
61
  }
33
62
  export function withStateLockSync(lockPath, fn, options = {}) {
34
- const token = acquireSync(lockPath, options);
63
+ const holder = acquireSync(lockPath, options);
35
64
  try {
36
65
  return fn();
37
66
  }
38
67
  finally {
39
- release(lockPath, token);
68
+ closeSync(holder.fd);
69
+ release(lockPath, holder.record.token);
40
70
  }
41
71
  }
42
72
  export function isStateLockTimeout(error) {
43
73
  return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
44
74
  }
45
75
  async function acquireAsync(lockPath, options) {
46
- const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
47
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
76
+ const clock = options.clock ?? systemClock;
77
+ const deadline = clock.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
48
78
  mkdirSync(dirname(lockPath), { recursive: true });
49
79
  for (let attempt = 0;;) {
50
- const outcome = attemptOnce(lockPath, staleMs);
80
+ const outcome = attemptOnce(lockPath, options, clock.now(), options.leaseMs ?? DEFAULT_LEASE_MS);
51
81
  if (outcome.kind === "acquired")
52
- return outcome.token;
82
+ return outcome.holder;
53
83
  if (outcome.kind === "retry")
54
84
  continue;
55
- if (Date.now() >= deadline)
56
- throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
57
- await sleep(backoffMs(attempt));
85
+ if (clock.now() >= deadline)
86
+ throw lockTimeout(lockPath, options);
87
+ await new Promise((resolve) => clock.schedule(resolve, backoffMs(attempt)));
58
88
  attempt += 1;
59
89
  }
60
90
  }
61
91
  function acquireSync(lockPath, options) {
62
92
  const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
63
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
64
93
  mkdirSync(dirname(lockPath), { recursive: true });
65
94
  for (let attempt = 0;;) {
66
- const outcome = attemptOnce(lockPath, staleMs);
95
+ const outcome = attemptOnce(lockPath, options, Date.now());
67
96
  if (outcome.kind === "acquired")
68
- return outcome.token;
97
+ return outcome.holder;
69
98
  if (outcome.kind === "retry")
70
99
  continue;
71
100
  if (Date.now() >= deadline)
72
- throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
101
+ throw lockTimeout(lockPath, options);
73
102
  Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
74
103
  attempt += 1;
75
104
  }
76
105
  }
77
- // EINTR surfaces raw from macOS fs syscalls under some runtimes; it is a retry, never a verdict.
78
- function attemptOnce(lockPath, staleMs) {
106
+ function attemptOnce(lockPath, options, now, leaseMs) {
79
107
  try {
80
- const token = tryCreate(lockPath);
81
- if (token !== null)
82
- return { kind: "acquired", token };
83
- const snapshot = readSnapshot(lockPath);
108
+ const holder = tryCreate(lockPath, now, leaseMs);
109
+ if (holder !== null)
110
+ return { kind: "acquired", holder };
111
+ const snapshot = readSnapshot(lockPath, now);
84
112
  if (snapshot === null)
85
113
  return { kind: "retry" };
86
- if (isStale(snapshot, staleMs) && reclaim(lockPath, snapshot.raw))
114
+ const record = snapshot.record;
115
+ const stale = record === null
116
+ ? snapshot.ageMs > (options.staleMs ?? DEFAULT_STALE_MS)
117
+ : !isProcessAlive(record.pid) || (record.leaseUntil !== undefined && now > record.leaseUntil);
118
+ if (stale && reclaim(lockPath, snapshot.raw))
87
119
  return { kind: "retry" };
88
120
  return { kind: "wait" };
89
121
  }
@@ -93,7 +125,18 @@ function attemptOnce(lockPath, staleMs) {
93
125
  throw error;
94
126
  }
95
127
  }
96
- function tryCreate(lockPath) {
128
+ function writeRecord(holder) {
129
+ const buf = Buffer.from(JSON.stringify(holder.record));
130
+ ftruncateSync(holder.fd, 0);
131
+ let offset = 0;
132
+ while (offset < buf.length) {
133
+ const written = writeSync(holder.fd, buf, offset, buf.length - offset, offset);
134
+ if (written === 0)
135
+ throw new Error("State lock write made no progress.");
136
+ offset += written;
137
+ }
138
+ }
139
+ function tryCreate(lockPath, now, leaseMs) {
97
140
  let fd;
98
141
  try {
99
142
  fd = openSync(lockPath, "wx");
@@ -103,30 +146,33 @@ function tryCreate(lockPath) {
103
146
  return null;
104
147
  throw error;
105
148
  }
106
- const record = { pid: process.pid, createdAt: new Date().toISOString(), token: randomUUID() };
149
+ const record = {
150
+ pid: process.pid,
151
+ createdAt: new Date(now).toISOString(),
152
+ token: randomUUID(),
153
+ ...(leaseMs === undefined ? {} : { leaseUntil: now + leaseMs }),
154
+ };
155
+ const holder = { fd, record };
107
156
  try {
108
- writeSync(fd, JSON.stringify(record));
157
+ writeRecord(holder);
109
158
  }
110
159
  catch (error) {
111
160
  closeSync(fd);
112
- // A failed write (e.g. EINTR) must not leave a partial lock the owner never recorded;
113
- // otherwise later readers see a young ownerless lock and only age can retire it.
114
161
  try {
115
162
  unlinkSync(lockPath);
116
163
  }
117
- catch {
118
- // another process already reclaimed the partial file; leave it alone
164
+ catch (cleanup) {
165
+ if (!hasCode(cleanup, "ENOENT"))
166
+ throw cleanup;
119
167
  }
120
168
  throw error;
121
169
  }
122
- closeSync(fd);
123
- return record.token;
170
+ return holder;
124
171
  }
125
- function readSnapshot(lockPath) {
172
+ function readSnapshot(lockPath, now) {
126
173
  try {
127
174
  const raw = readFileSync(lockPath, "utf8");
128
- const ageMs = Date.now() - statSync(lockPath).mtimeMs;
129
- return { raw, record: parseRecord(raw), ageMs };
175
+ return { raw, record: parseRecord(raw), ageMs: now - statSync(lockPath).mtimeMs };
130
176
  }
131
177
  catch (error) {
132
178
  if (hasCode(error, "ENOENT"))
@@ -136,18 +182,21 @@ function readSnapshot(lockPath) {
136
182
  }
137
183
  function parseRecord(raw) {
138
184
  try {
139
- const parsed = JSON.parse(raw);
140
- if (typeof parsed !== "object" || parsed === null)
185
+ const record = JSON.parse(raw);
186
+ if (typeof record !== "object" || record === null)
187
+ return null;
188
+ if (!("pid" in record) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0)
141
189
  return null;
142
- const record = parsed;
143
- const pid = record["pid"];
144
- const createdAt = record["createdAt"];
145
- const token = record["token"];
146
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof createdAt !== "string")
190
+ if (!("createdAt" in record) || typeof record.createdAt !== "string")
147
191
  return null;
148
- if (typeof token !== "string" || token.length === 0)
192
+ if (!("token" in record) || typeof record.token !== "string" || record.token.length === 0)
149
193
  return null;
150
- return { pid, createdAt, token };
194
+ return {
195
+ pid: record.pid,
196
+ createdAt: record.createdAt,
197
+ token: record.token,
198
+ ...("leaseUntil" in record && typeof record.leaseUntil === "number" ? { leaseUntil: record.leaseUntil } : {}),
199
+ };
151
200
  }
152
201
  catch (error) {
153
202
  if (error instanceof SyntaxError)
@@ -155,14 +204,6 @@ function parseRecord(raw) {
155
204
  throw error;
156
205
  }
157
206
  }
158
- // A body that is not a record is a lock mid-write (or a foreign/legacy file); only
159
- // age retires it. A parsable record is stale only when its owner is dead: a live
160
- // owner running past staleMs is slow, not gone, and the waiter fails closed instead.
161
- function isStale(snapshot, staleMs) {
162
- if (snapshot.record === null)
163
- return snapshot.ageMs > staleMs;
164
- return !isProcessAlive(snapshot.record.pid);
165
- }
166
207
  function isProcessAlive(pid) {
167
208
  try {
168
209
  process.kill(pid, 0);
@@ -176,10 +217,8 @@ function isProcessAlive(pid) {
176
217
  throw error;
177
218
  }
178
219
  }
179
- // Re-read right before unlinking so a lock that changed hands since the stale
180
- // verdict (a sibling waiter reclaimed and re-acquired it) is left alone.
181
220
  function reclaim(lockPath, expectedRaw) {
182
- const current = readSnapshot(lockPath);
221
+ const current = readSnapshot(lockPath, Date.now());
183
222
  if (current === null)
184
223
  return true;
185
224
  if (current.raw !== expectedRaw)
@@ -188,16 +227,15 @@ function reclaim(lockPath, expectedRaw) {
188
227
  unlinkSync(lockPath);
189
228
  }
190
229
  catch (error) {
230
+ if (hasCode(error, "EPERM") || hasCode(error, "EACCES"))
231
+ return false;
191
232
  if (!hasCode(error, "ENOENT"))
192
233
  throw error;
193
234
  }
194
235
  return true;
195
236
  }
196
- // Only the acquisition that wrote this token may unlink: if the file now carries
197
- // another token, a waiter has legitimately taken over and its lock must stand.
198
237
  function release(lockPath, token) {
199
- const current = readSnapshot(lockPath);
200
- if (current === null || current.record?.token !== token)
238
+ if (readSnapshot(lockPath, Date.now())?.record?.token !== token)
201
239
  return;
202
240
  try {
203
241
  unlinkSync(lockPath);
@@ -208,19 +246,13 @@ function release(lockPath, token) {
208
246
  }
209
247
  }
210
248
  function backoffMs(attempt) {
211
- const exponential = Math.min(MAX_DELAY_MS, MIN_DELAY_MS * 2 ** attempt);
212
- return exponential + Math.random() * MIN_DELAY_MS;
213
- }
214
- function lockTimeout(lockPath, timeoutMs) {
215
- const holder = readSnapshot(lockPath)?.record;
216
- const owner = holder === undefined || holder === null ? "another process" : `pid ${holder.pid}`;
217
- 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, {
218
- details: {
219
- lockPath,
220
- timeoutMs,
221
- ...(holder === undefined || holder === null ? {} : { holderPid: holder.pid }),
222
- },
223
- });
249
+ return Math.min(100, 5 * 2 ** attempt) + Math.random() * 5;
250
+ }
251
+ function lockTimeout(lockPath, options) {
252
+ const holder = readSnapshot(lockPath, Date.now())?.record;
253
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
254
+ const owner = holder == null ? "another process" : `pid ${holder.pid}`;
255
+ 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 }) } });
224
256
  }
225
257
  function hasCode(error, code) {
226
258
  return error instanceof Error && "code" in error && error.code === code;
@@ -1,6 +1,7 @@
1
+ import type { UlwLoopToolkitSurface } from "./surface.js";
1
2
  import type { UlwLoopPlan } from "./types.js";
2
3
  /**
3
- * `status --json` is the one call every agent already makes between steps, so it is
4
+ * Status is the one call every agent already makes between steps, so it is
4
5
  * also the cheapest place to answer "what now?" without a second round trip.
5
6
  */
6
- export declare function statusNextActions(plan: UlwLoopPlan): readonly string[];
7
+ export declare function statusNextActions(plan: UlwLoopPlan, surface?: UlwLoopToolkitSurface): readonly string[];
@@ -1,33 +1,41 @@
1
1
  import { hasAllCriteriaPass, isFinalRunCompletionCandidate } from "./goal-status.js";
2
- import { ULW_LOOP_CREATE_GOALS_COMMAND } from "./plan-missing-recovery.js";
2
+ import { createGoalsAction } from "./plan-missing-recovery.js";
3
3
  /**
4
- * `status --json` is the one call every agent already makes between steps, so it is
4
+ * Status is the one call every agent already makes between steps, so it is
5
5
  * also the cheapest place to answer "what now?" without a second round trip.
6
6
  */
7
- export function statusNextActions(plan) {
7
+ export function statusNextActions(plan, surface = "lazycodex") {
8
8
  const actions = [];
9
9
  const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
10
10
  if (plan.goals.length === 0)
11
- actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
11
+ actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
12
12
  else if (active !== undefined)
13
- actions.push(...activeGoalActions(plan, active));
13
+ actions.push(...activeGoalActions(plan, active, surface));
14
14
  for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
15
- 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>'\`.`);
15
+ actions.push(surface === "omo-senpi"
16
+ ? `${goal.id} is review_blocked: capture the reviewer verdict with \`agentToolkit.recordReviewBlockers({ goalId: "${goal.id}", title: "<title>", objective: "<objective>", evidence: "<verdict>" })\`.`
17
+ : `${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>'\`.`);
16
18
  if (plan.evidenceLayoutVersion !== 2 || active === undefined)
17
19
  actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
18
20
  return actions;
19
21
  }
20
- function activeGoalActions(plan, active) {
22
+ function activeGoalActions(plan, active, surface) {
21
23
  const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
22
24
  if (unresolved.length > 0)
23
25
  return [
24
- `${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>"\`.`,
26
+ surface === "omo-senpi"
27
+ ? `${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>" })\`.`
28
+ : `${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>"\`.`,
25
29
  ];
26
30
  if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
27
31
  return [
28
- `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
32
+ surface === "omo-senpi"
33
+ ? `${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.`
34
+ : `${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.`,
29
35
  ];
30
36
  return [
31
- `${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.`,
37
+ surface === "omo-senpi"
38
+ ? `${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.`
39
+ : `${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.`,
32
40
  ];
33
41
  }
@@ -1,4 +1,5 @@
1
- import { appendLedger, appendLedgerEntries, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
1
+ import { commit } from "./plan-commit.js";
2
+ import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
2
3
  import { applySteeringMutation, validateUlwLoopSteeringProposal } from "./steering.js";
3
4
  import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
4
5
  import { iso } from "./types.js";
@@ -10,7 +11,7 @@ export async function steerUlwLoopBatch(repoRoot, proposals, scope) {
10
11
  const failed = prepared.results.find((item) => !item.accepted);
11
12
  if (failed !== undefined) {
12
13
  const entry = rejectedLedgerEntry(prepared.results);
13
- await appendLedger(repoRoot, entry, scope);
14
+ await commit(repoRoot, scope, { plan, entries: [entry] });
14
15
  return rejected(plan, prepared.results, failed.rejectedReasons);
15
16
  }
16
17
  let next = plan;
@@ -19,10 +20,9 @@ export async function steerUlwLoopBatch(repoRoot, proposals, scope) {
19
20
  next = item.prepared.next;
20
21
  const fresh = prepared.items.filter((item) => item.kind === "fresh");
21
22
  if (fresh.length > 0) {
22
- await writePlan(repoRoot, next, scope);
23
23
  const entries = fresh.map((item) => ledgerEntry(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
24
24
  const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
25
- await appendLedgerEntries(repoRoot, batchEntry === null ? entries : [...entries, batchEntry], scope);
25
+ await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
26
26
  }
27
27
  return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
28
28
  });