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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
  2. package/dist/cli/index.js +70 -41
  3. package/dist/cli-node/index.js +70 -41
  4. package/package.json +2 -6
  5. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  6. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +4 -2
  7. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  8. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  9. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  10. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  11. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  12. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  13. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  14. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  15. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  16. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  17. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  19. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
  25. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +5 -4
  26. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  29. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  30. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +13 -0
  33. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +5 -4
  34. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +7 -5
  35. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +7 -2
  36. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -4
  37. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +931 -442
  38. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +6 -0
  39. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +20 -12
  40. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +8 -0
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.d.ts +4 -0
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.js +18 -0
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.d.ts +1 -0
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.js +37 -0
  45. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.d.ts +1 -0
  46. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +24 -12
  47. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
  48. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
  49. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +1 -0
  50. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +6 -0
  51. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
  52. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
  53. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +6 -2
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +54 -25
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.d.ts +10 -3
  56. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.js +38 -31
  57. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
  58. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
  59. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
  60. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
  61. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
  62. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
  63. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +11 -8
  64. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +28 -12
  65. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.d.ts +335 -0
  66. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.js +109 -10
  67. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.d.ts +3 -0
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
  69. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +8 -62
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.d.ts +3 -2
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +4 -4
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +9 -1
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.d.ts +3 -2
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +11 -11
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.d.ts +9 -0
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.js +41 -0
  84. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  85. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  86. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
  87. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +15 -3
  88. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +7 -4
  89. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +26 -14
  90. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +8 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/src/driver-objective-ack.ts +20 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence-artifacts.ts +45 -0
  93. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +23 -16
  94. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
  95. package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +7 -0
  96. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
  97. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +67 -54
  98. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts +63 -31
  99. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
  100. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
  101. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
  102. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +11 -7
  103. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +47 -12
  104. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/manifest.ts +124 -10
  105. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/types.ts +3 -0
  106. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
  107. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +8 -59
  108. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
  109. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
  110. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
  111. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-mutations.ts +5 -4
  112. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +13 -1
  113. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +12 -10
  114. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
  115. package/packages/omo-codex/plugin/components/ulw-loop/src/success-criteria-input.ts +56 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +37 -1
  117. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
  118. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
  119. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
  120. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
  121. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
  127. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +33 -4
  130. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-dx.test.ts +219 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
  133. package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
  134. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
  135. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
  136. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  137. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  138. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  139. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  140. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  141. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  157. package/packages/omo-codex/plugin/package-lock.json +12 -12
  158. package/packages/omo-codex/plugin/package.json +1 -1
  159. package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
  160. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +15 -7
  161. package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
  162. package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
  163. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  164. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
  165. package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  166. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  167. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
  168. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  169. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
  170. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  171. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  172. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  173. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  174. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  175. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  176. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  177. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  178. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  179. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  180. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +5 -4
  181. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +1 -1
  182. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  183. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
  184. package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
  185. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
  186. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
  187. package/packages/omo-codex/scripts/install-dist/install-local.mjs +6 -4
  188. package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
  189. package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
  190. package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
  191. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  192. package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
  193. package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
  194. package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  195. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  196. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  197. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
  198. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  199. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
  200. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  201. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  202. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  203. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  204. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  205. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  206. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  207. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  208. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  209. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  210. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
  211. package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
  212. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  213. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
  214. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
  215. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -1,12 +1,13 @@
1
1
  // biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget
2
2
  import { isUlwLoopDone } from "./goal-status.js";
3
- import { appendLedger, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
3
+ import { commit } from "./plan-commit.js";
4
+ import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
4
5
  import { makeGoal, reviseCriterion, reviseWording, splitOrBlock } from "./steering-mutations.js";
5
6
  import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
6
7
  import { iso, ULW_LOOP_STEERING_MUTATION_KINDS, ULW_LOOP_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
7
8
  import { batchUpdateLedgerEntry } from "./validation-batch.js";
8
9
  const SOURCES = ["user_prompt_submit", "finding", "cli"];
9
- const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
10
+ const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "acknowledgedDriverObjectives", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
10
11
  const isObject = (value) => typeof value === "object" && value !== null;
11
12
  const isPlain = (value) => isObject(value) && !Array.isArray(value);
12
13
  const read = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
@@ -163,13 +164,13 @@ function validateCriterion(plan, proposal, reasons) {
163
164
  if (model !== undefined && !isModel(model))
164
165
  reasons.push("invalid userModel");
165
166
  }
166
- export function applySteeringMutation(plan, proposal, audit) {
167
+ export function applySteeringMutation(plan, proposal, audit, surface = "lazycodex") {
167
168
  const next = structuredClone(plan);
168
169
  if (!audit.invariant.accepted)
169
170
  return next;
170
171
  const now = proposal.now?.toISOString() ?? iso();
171
172
  if (proposal.kind === "add_subgoal")
172
- next.goals.push(makeGoal(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1));
173
+ next.goals.push(makeGoal(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1, surface));
173
174
  if (proposal.kind === "reorder_pending") {
174
175
  const order = pendingOrder(proposal);
175
176
  next.goals = [...order.map((id) => goal(next, id)).filter((item) => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
@@ -177,7 +178,7 @@ export function applySteeringMutation(plan, proposal, audit) {
177
178
  if (proposal.kind === "revise_pending_wording")
178
179
  reviseWording(next, proposal, now);
179
180
  if (proposal.kind === "split_subgoal" || proposal.kind === "mark_blocked_superseded")
180
- splitOrBlock(next, proposal, now);
181
+ splitOrBlock(next, proposal, now, surface);
181
182
  if (proposal.kind === "revise_criterion")
182
183
  reviseCriterion(next, proposal, now);
183
184
  if (proposal.kind !== "annotate_ledger")
@@ -201,7 +202,7 @@ export function parseUlwLoopSteeringDirective(text) {
201
202
  throw error;
202
203
  }
203
204
  }
204
- export async function steerUlwLoop(repoRoot, proposal, scope) {
205
+ export async function steerUlwLoop(repoRoot, proposal, scope, surface = "lazycodex") {
205
206
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
206
207
  const plan = await readUlwLoopPlan(repoRoot, scope);
207
208
  const key = proposal.idempotencyKey ?? proposal.promptSignature;
@@ -214,7 +215,7 @@ export async function steerUlwLoop(repoRoot, proposal, scope) {
214
215
  }
215
216
  const audit = validateUlwLoopSteeringProposal(plan, proposal);
216
217
  const accepted = audit.invariant.accepted;
217
- const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
218
+ const next = accepted ? applySteeringMutation(plan, proposal, audit, surface) : plan;
218
219
  const finalAudit = { ...audit };
219
220
  if (accepted) {
220
221
  const changed = changedGoalIdsBetween(plan, next);
@@ -223,11 +224,10 @@ export async function steerUlwLoop(repoRoot, proposal, scope) {
223
224
  }
224
225
  const at = proposal.now?.toISOString() ?? iso();
225
226
  const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
226
- if (accepted)
227
- await writePlan(repoRoot, next, scope);
228
- await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
227
+ const entries = [ledgerEntry(proposal, finalAudit, at)];
229
228
  if (batchEntry !== null)
230
- await appendLedger(repoRoot, batchEntry, scope);
229
+ entries.push(batchEntry);
230
+ await commit(repoRoot, scope, { plan: next, entries });
231
231
  return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
232
232
  });
233
233
  }
@@ -1,6 +1,8 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { isAbsolute, join, resolve, sep } from "node:path";
3
+ import { readLedgerAt } from "./ledger.js";
3
4
  import { normalizeUlwLoopSessionId, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
5
+ import { readUlwLoopPlanSync } from "./plan-io.js";
4
6
  import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
5
7
  // Turn-death recovery only: Codex emits Stop when a turn ends, so a run that
6
8
  // dies mid-turn (crash, kill, context blowup before any Stop) never reaches
@@ -27,7 +29,7 @@ export function runStopResumeHook(input) {
27
29
  return "";
28
30
  const scope = { sessionId: payload.session_id };
29
31
  const stateDir = ulwLoopDir(payload.cwd, scope);
30
- const plan = readPlan(join(stateDir, "goals.json"));
32
+ const plan = readPlan(payload.cwd, payload.session_id);
31
33
  if (plan === null || plan.aggregateCompletion?.status === "complete")
32
34
  return "";
33
35
  const goal = resumableGoal(plan);
@@ -76,11 +78,10 @@ function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
76
78
  throw error;
77
79
  }
78
80
  }
79
- // Two-strike cap keyed on ledger movement: an unchanged ledger.jsonl line
80
- // count across resumes means the loop is not progressing. The stuck marker is
81
- // a separate file — a ledger append would change the count and self-reset.
81
+ // Hook budget counters are exempt from plan/audit commits. Count reconciled
82
+ // entries, not cache lines: a writer dying after link still made progress.
82
83
  function consumeResumeBudget(stateDir, goalId) {
83
- const ledgerLineCount = countLedgerLines(join(stateDir, "ledger.jsonl"));
84
+ const ledgerLineCount = readLedgerAt(stateDir).length;
84
85
  const counterPath = resolve(stateDir, `auto-resume-${goalId}.json`);
85
86
  const stuckPath = resolve(stateDir, `auto-resume-${goalId}.stuck`);
86
87
  // goals.json is untrusted input: a crafted goal id (e.g. `../../x`) must
@@ -111,9 +112,9 @@ function renderResumeDirective(plan, goal, sessionId) {
111
112
  "If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead.",
112
113
  ].join("\n");
113
114
  }
114
- function readPlan(goalsPath) {
115
+ function readPlan(repoRoot, sessionId) {
115
116
  try {
116
- return JSON.parse(readFileSync(goalsPath, "utf8"));
117
+ return readUlwLoopPlanSync(repoRoot, { sessionId });
117
118
  }
118
119
  catch (error) {
119
120
  if (error instanceof Error)
@@ -121,16 +122,6 @@ function readPlan(goalsPath) {
121
122
  throw error;
122
123
  }
123
124
  }
124
- function countLedgerLines(ledgerPath) {
125
- try {
126
- return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
127
- }
128
- catch (error) {
129
- if (error instanceof Error)
130
- return 0;
131
- throw error;
132
- }
133
- }
134
125
  function readCounter(counterPath) {
135
126
  try {
136
127
  if (!existsSync(counterPath))
@@ -0,0 +1,9 @@
1
+ import type { UlwLoopSuccessCriterion, UlwLoopSuccessCriterionUserModel } from "./types.js";
2
+ export interface SuccessCriterionInput {
3
+ readonly scenario: string;
4
+ readonly expectedEvidence: string;
5
+ readonly userModel?: UlwLoopSuccessCriterionUserModel;
6
+ readonly essential?: boolean;
7
+ }
8
+ export declare function criterionId(index: number): string;
9
+ export declare function criteriaFromInput(input: readonly unknown[]): UlwLoopSuccessCriterion[];
@@ -0,0 +1,41 @@
1
+ import { ULW_LOOP_SUCCESS_CRITERION_USER_MODELS, UlwLoopError } from "./types.js";
2
+ function invalid(message, details) {
3
+ throw new UlwLoopError(`Invalid successCriteria: ${message}`, "ULW_LOOP_ARGUMENT_INVALID", { details });
4
+ }
5
+ function isUserModel(value) {
6
+ return typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
7
+ }
8
+ function requireText(value, field, index) {
9
+ const trimmed = typeof value === "string" ? value.trim() : "";
10
+ return trimmed || invalid(`entry ${index + 1} needs a non-empty ${field}.`, { index, field });
11
+ }
12
+ export function criterionId(index) {
13
+ return `C${String(index + 1).padStart(3, "0")}`;
14
+ }
15
+ export function criteriaFromInput(input) {
16
+ if (input.length === 0)
17
+ invalid("provide at least one criterion or omit the field for placeholders.", { count: 0 });
18
+ return input.map((entry, index) => {
19
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
20
+ return invalid(`entry ${index + 1} must be an object.`, { index });
21
+ const record = { ...entry };
22
+ const userModel = record["userModel"] ?? "happy";
23
+ if (!isUserModel(userModel))
24
+ return invalid(`entry ${index + 1} userModel must be one of ${ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.join(", ")}.`, {
25
+ index,
26
+ userModel: String(userModel),
27
+ });
28
+ const essential = record["essential"];
29
+ if (essential !== undefined && typeof essential !== "boolean")
30
+ return invalid(`entry ${index + 1} essential must be a boolean.`, { index });
31
+ return {
32
+ id: criterionId(index),
33
+ scenario: requireText(record["scenario"], "scenario", index),
34
+ userModel,
35
+ expectedEvidence: requireText(record["expectedEvidence"], "expectedEvidence", index),
36
+ essential: essential ?? true,
37
+ capturedEvidence: null,
38
+ status: "pending",
39
+ };
40
+ });
41
+ }
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.62) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 5.0.0-beta.64) Checking Ulw-Loop Steering"
11
11
  }
12
12
  ]
13
13
  }
@@ -20,7 +20,7 @@
20
20
  "type": "command",
21
21
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use-spawn",
22
22
  "timeout": 5,
23
- "statusMessage": "(OmO 5.0.0-beta.62) Recording Spawn Admission"
23
+ "statusMessage": "(OmO 5.0.0-beta.64) Recording Spawn Admission"
24
24
  }
25
25
  ]
26
26
  }
@@ -33,7 +33,7 @@
33
33
  "type": "command",
34
34
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
35
35
  "timeout": 5,
36
- "statusMessage": "(OmO 5.0.0-beta.62) Enforcing Unlimited Ulw-Loop Budget"
36
+ "statusMessage": "(OmO 5.0.0-beta.64) Enforcing Unlimited Ulw-Loop Budget"
37
37
  }
38
38
  ]
39
39
  },
@@ -44,7 +44,7 @@
44
44
  "type": "command",
45
45
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
46
46
  "timeout": 5,
47
- "statusMessage": "(OmO 5.0.0-beta.62) Guarding Ulw-Loop Spawns"
47
+ "statusMessage": "(OmO 5.0.0-beta.64) Guarding Ulw-Loop Spawns"
48
48
  }
49
49
  ]
50
50
  }
@@ -56,7 +56,7 @@
56
56
  "type": "command",
57
57
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
58
58
  "timeout": 10,
59
- "statusMessage": "(OmO 5.0.0-beta.62) Checking Ulw-Loop Resume"
59
+ "statusMessage": "(OmO 5.0.0-beta.64) Checking Ulw-Loop Resume"
60
60
  }
61
61
  ]
62
62
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ulw-loop",
3
- "version": "5.0.0-beta.62",
3
+ "version": "5.0.0-beta.64",
4
4
  "description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -20,7 +20,7 @@ Run each criterion's real-surface proof yourself through the channel that faithf
20
20
 
21
21
  1. **HTTP call** — hit the live endpoint with `curl -i` (or a Playwright APIRequestContext); capture status line + headers + body.
22
22
  2. **Terminal / TUI** - prove it through the xterm.js web terminal; tmux `send-keys` is fine for a boot smoke, but NEVER `tmux capture-pane` for color/layout/CJK evidence (it degrades truecolor).
23
- 3. **Browser use** — in Codex, use `browser:control-in-app-browser` first when available and the scenario does not need an authenticated or persistent user browser profile. Otherwise use Chrome to drive the REAL page; if unavailable, use agent-browser. Capture action log + screenshot path. Never downgrade a browser-facing criterion.
23
+ 3. **Browser use** — in Codex, prefer `browser:control-in-app-browser`. Otherwise, or for Chrome semantics, stealth, trace, or auth, WRITE a `playwright-core` script and run it from js eval against local Chrome (`channel: "chrome"`; persistent context on a CLONED profile, never the live one). Capture action log + screenshot path. Never downgrade a browser-facing criterion.
24
24
  4. **Computer use** — for desktop/GUI apps, drive the running app via OS automation (computer-use, AppleScript, xdotool, etc.); capture action log + screenshot.
25
25
 
26
26
  For TUI visual QA (mandatory when a PR or review must inspect the terminal screen),
@@ -165,7 +165,7 @@ Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures a
165
165
  4. INTEGRATE + CRITICAL SELF-QA + GIT CHECKPOINT (EVERY WORKER RETURN): do NOT trust the worker's report. Read the diff yourself, re-run its tests, and run LSP diagnostics on the changed files. Treat "done" as a claim to disprove. If the diff drifts, the test is hollow, or evidence is missing, RESPAWN the worker with the specific failure context. Once the work unit is verified, use `git-master` before staging: inspect recent repository commits and touched-path history to infer commit language, Conventional Commit scope, message shape, and unit size. Stage only that unit's files and commit in the observed style; do not carry verified work forward into a later omnibus commit. If no git-tracked files changed or committing is unsafe, record the no-commit reason as evidence. Forward every finding/learning to subsequent workers.
166
166
  5. EXECUTE-AS-SCENARIO: ACTUALLY run the Manual-QA scenario the criterion named (channel table above). Run it yourself for the orchestrator check; for heavier flows dispatch a dedicated QA execution worker (`lazycodex-worker-medium` by default; `lazycodex-worker-high` when the QA flow itself is hard) whose ONLY job is to drive the channel and write the artifact to the named evidence path. If the scenario FAILS, respawn the implementing worker with the captured failure — do not hand-patch around it.
167
167
  6. CAPTURE: collect the observable artifact path: transcript, stdout, screenshot, assertion, status+body, diff, or parsed dump. No artifact written at the evidence path — not done; record BLOCKED and respawn QA.
168
- 7. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 5 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars, AND close every finished worker (v1 `close_agent`; on V2 finished workers end on their own — `interrupt_agent` any still running). Register each teardown as its own todo the moment the QA spawns the resource (scripts, tmux assets, browsers / agent-browser sessions, PIDs, ports) so none is forgotten. Embed a one-line cleanup receipt in the evidence string, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo; rm -rf /tmp/ulw.aB12cD; interrupt_agent w-3`. Missing receipt → record BLOCKED, not PASS.
168
+ 7. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 5 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars, AND close every finished worker (v1 `close_agent`; on V2 finished workers end on their own — `interrupt_agent` any still running). Register each teardown as its own todo the moment the QA spawns the resource (scripts, tmux assets, browser contexts, PIDs, ports) so none is forgotten. Embed a one-line cleanup receipt in the evidence string, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo; rm -rf /tmp/ulw.aB12cD; interrupt_agent w-3`. Missing receipt → record BLOCKED, not PASS.
169
169
  8. RECORD one result immediately from the artifact you just wrote — never from memory or a later turn — stamping the capture tree `$(git rev-parse --short "HEAD^{tree}")` into the evidence:
170
170
  - PASS: `omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass --evidence "<observable> @tree:<short-tree> | <cleanup receipt>" --json`
171
171
  - FAIL: `omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status fail --evidence "<observable> @tree:<short-tree> | <cleanup receipt>" --notes "<diagnosis>" --json`
@@ -4,11 +4,19 @@ import {
4
4
  readCodexGoalSnapshotInput,
5
5
  reconcileCodexGoalSnapshot,
6
6
  } from "./codex-goal-snapshot.js";
7
+ import { acknowledgedDriverObjectives } from "./driver-objective-ack.js";
7
8
  import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective } from "./goal-status.js";
8
9
  import type { UlwLoopScope } from "./paths.js";
9
10
  import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
10
11
  import { UlwLoopError } from "./types.js";
11
12
 
13
+ export interface CheckpointCodexGoalValidation {
14
+ readonly raw: unknown;
15
+ readonly nextActions: readonly string[];
16
+ readonly warnings: readonly string[];
17
+ readonly unacknowledgedObjective?: string;
18
+ }
19
+
12
20
  export async function validateCheckpointCodexGoal(input: {
13
21
  readonly repoRoot: string;
14
22
  readonly plan: UlwLoopPlan;
@@ -16,11 +24,12 @@ export async function validateCheckpointCodexGoal(input: {
16
24
  readonly raw: string | undefined;
17
25
  readonly evidence: string;
18
26
  readonly scope?: UlwLoopScope;
19
- }): Promise<{ readonly raw: unknown; readonly nextActions: readonly string[]; readonly warnings: readonly string[] }> {
27
+ }): Promise<CheckpointCodexGoalValidation> {
20
28
  const snapshot = await readCodexGoalSnapshotInput(input.raw, input.repoRoot);
21
29
  const expected = expectedCodexObjective(input.plan, input.goal);
22
30
  const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
23
31
  expectedObjective: expected,
32
+ acknowledgedObjectives: acknowledgedDriverObjectives(input.plan),
24
33
  ...(codexGoalMode(input.plan) === "aggregate"
25
34
  ? { acceptedObjectives: compatibleCodexObjectives(input.plan) }
26
35
  : {}),
@@ -28,8 +37,11 @@ export async function validateCheckpointCodexGoal(input: {
28
37
  if (!reconciliation.ok) throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
29
38
  return {
30
39
  raw: snapshot?.raw,
31
- nextActions: reconciliation.warnings,
32
- warnings: reconciliation.warnings.filter((warning) => warning.startsWith("driver_objective_differs")),
40
+ nextActions: reconciliation.nextActions,
41
+ warnings: reconciliation.warnings,
42
+ ...(reconciliation.unacknowledgedObjective === undefined
43
+ ? {}
44
+ : { unacknowledgedObjective: reconciliation.unacknowledgedObjective }),
33
45
  };
34
46
  }
35
47
 
@@ -6,11 +6,13 @@ import {
6
6
  combineCheckpointValidationErrors,
7
7
  validateCheckpointCodexGoal,
8
8
  } from "./checkpoint-codex-validation.js";
9
+ import { acknowledgeDriverObjective } from "./driver-objective-ack.js";
9
10
  import { requireAllCriteriaPass, requireAllPlanCriteriaPass, requireEssentialCriteriaPass } from "./evidence.js";
10
11
  import { codexGoalMode, isFinalRunCompletionCandidate } from "./goal-status.js";
11
12
  import type { UlwLoopScope } from "./paths.js";
12
13
  import { ulwLoopAttemptEvidenceDir } from "./paths.js";
13
- import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
14
+ import { commit } from "./plan-commit.js";
15
+ import { readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
14
16
  import {
15
17
  classifyExternalAuthorizationBlocker,
16
18
  clearGoalBlockerFields,
@@ -189,6 +191,7 @@ export async function checkpointUlwLoop(
189
191
  codexGoal = validation.raw;
190
192
  nextActions = validation.nextActions;
191
193
  warnings = validation.warnings;
194
+ acknowledgeDriverObjective(plan, validation.unacknowledgedObjective);
192
195
  } catch (error) {
193
196
  if (!(error instanceof UlwLoopError)) throw error;
194
197
  codexValidationError = error;
@@ -226,11 +229,11 @@ export async function checkpointUlwLoop(
226
229
  if (aggregateCompletion !== undefined) plan.aggregateCompletion = aggregateCompletion;
227
230
  if (aggregateCompletion !== undefined) nextActions = [...nextActions, 'aggregate complete — now update_goal({status:"complete"})'];
228
231
  plan.updatedAt = now;
229
- await writePlan(repoRoot, plan, scope);
230
232
  const ledgerEntry = buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion);
231
- await appendLedger(repoRoot, ledgerEntry, scope);
233
+ const entries = [ledgerEntry];
232
234
  const closedBatch = args.status === "complete" ? batchClosedBy(plan, goal.id) : undefined;
233
- if (closedBatch !== undefined) await appendLedger(repoRoot, { at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId }, scope);
235
+ if (closedBatch !== undefined) entries.push({ at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId });
236
+ await commit(repoRoot, scope, { plan, entries });
234
237
  return aggregateCompletion === undefined
235
238
  ? { plan, goal, ledgerEntry, nextActions, warnings }
236
239
  : { plan, goal, ledgerEntry, aggregateCompletion, nextActions, warnings };
@@ -22,13 +22,19 @@ export interface CodexGoalSnapshot {
22
22
  export interface CodexGoalReconciliation {
23
23
  ok: boolean;
24
24
  snapshot: CodexGoalSnapshot;
25
+ /** Facts to know (a differing driver objective); never something to do. */
25
26
  warnings: string[];
27
+ /** Things to do next (create, re-create, or resume the driver goal). */
28
+ nextActions: string[];
26
29
  errors: string[];
30
+ /** The differing objective this reconciliation reported for the first time, for the caller to acknowledge. */
31
+ unacknowledgedObjective?: string;
27
32
  }
28
33
 
29
34
  export interface ReconcileCodexGoalOptions {
30
35
  expectedObjective: string;
31
36
  readonly acceptedObjectives?: readonly string[];
37
+ readonly acknowledgedObjectives?: readonly string[];
32
38
  }
33
39
 
34
40
  export class CodexGoalSnapshotError extends Error {}
@@ -111,35 +117,41 @@ export function reconcileCodexGoalSnapshot(
111
117
  const effectiveSnapshot = snapshot ?? { available: false, raw: null };
112
118
  const errors: string[] = [];
113
119
  const warnings: string[] = [];
120
+ const nextActions: string[] = [];
114
121
 
115
122
  const expected = options.expectedObjective;
116
- const normalizedExpected = normalizeObjective(expected);
117
123
  if (!effectiveSnapshot.available) {
118
- warnings.push(`call get_goal; if none, create_goal with codexObjective "${expected}" verbatim`);
119
- return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
124
+ nextActions.push(`call get_goal; if none, create_goal with codexObjective "${expected}" verbatim`);
125
+ return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, nextActions, errors };
120
126
  }
121
127
 
122
- const accepted = new Set(
123
- [
124
- normalizedExpected,
125
- ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective)),
126
- ].filter(Boolean),
127
- );
128
+ const normalized = (objectives: readonly string[]) => new Set(objectives.map(normalizeObjective).filter(Boolean));
129
+ const accepted = normalized([expected, ...(options.acceptedObjectives ?? [])]);
130
+ const acknowledged = normalized(options.acknowledgedObjectives ?? []);
128
131
  const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
129
- if (actual && !accepted.has(normalizeObjective(actual))) {
132
+ let unacknowledgedObjective: string | undefined;
133
+ if (actual && !accepted.has(actual) && !acknowledged.has(actual)) {
130
134
  warnings.push(`driver_objective_differs: expected "${expected}", got "${actual}".`);
135
+ unacknowledgedObjective = actual;
131
136
  }
132
137
 
133
138
  const actualStatus = effectiveSnapshot.status ?? "unknown";
134
139
  if (actualStatus === "paused" || actualStatus === "usage_limited" || actualStatus === "budget_limited") {
135
- warnings.push("/goal resume or raise the budget");
140
+ nextActions.push("/goal resume or raise the budget");
136
141
  }
137
142
  if (actualStatus === "complete")
138
- warnings.push(`driver closed early: call create_goal with codexObjective "${expected}" verbatim`);
139
- return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
143
+ nextActions.push(`driver closed early: call create_goal with codexObjective "${expected}" verbatim`);
144
+ return {
145
+ ok: errors.length === 0,
146
+ snapshot: effectiveSnapshot,
147
+ warnings,
148
+ nextActions,
149
+ errors,
150
+ ...(unacknowledgedObjective === undefined ? {} : { unacknowledgedObjective }),
151
+ };
140
152
  }
141
153
 
142
154
  export function formatCodexGoalReconciliation(reconciliation: CodexGoalReconciliation): string {
143
- const parts = [...reconciliation.errors, ...reconciliation.warnings];
155
+ const parts = [...reconciliation.errors, ...reconciliation.nextActions, ...reconciliation.warnings];
144
156
  return parts.join(" ");
145
157
  }
@@ -19,6 +19,7 @@ export interface UlwLoopSuccessCriterion {
19
19
  status: UlwLoopCriterionStatus;
20
20
  capturedAt?: string;
21
21
  notes?: string;
22
+ artifacts?: string[];
22
23
  }
23
24
 
24
25
  export interface UlwLoopItem {
@@ -63,6 +64,9 @@ export interface UlwLoopValidationBatch {
63
64
 
64
65
  export interface UlwLoopPlan {
65
66
  version: 1;
67
+ revision?: number;
68
+ brief?: string;
69
+ ledgerResetRevision?: number;
66
70
  evidenceLayoutVersion?: 2;
67
71
  createdAt: string;
68
72
  updatedAt: string;
@@ -72,6 +76,7 @@ export interface UlwLoopPlan {
72
76
  codexGoalMode?: UlwLoopCodexGoalMode;
73
77
  codexObjective?: string;
74
78
  codexObjectiveAliases?: string[];
79
+ acknowledgedDriverObjectives?: string[];
75
80
  aggregateCompletion?: UlwLoopAggregateCompletion;
76
81
  activeGoalId?: string;
77
82
  validationBatches?: readonly UlwLoopValidationBatch[];
@@ -158,6 +163,8 @@ export interface UlwLoopQualityGateSenpi extends UlwLoopQualityGateCommon {
158
163
  export type UlwLoopQualityGate = UlwLoopQualityGateLazycodex | UlwLoopQualityGateSenpi;
159
164
 
160
165
  export interface UlwLoopLedgerEntry {
166
+ revision?: number;
167
+ id?: string;
161
168
  at: string;
162
169
  kind: UlwLoopLedgerEventKind;
163
170
  goalId?: string;
@@ -168,6 +175,7 @@ export interface UlwLoopLedgerEntry {
168
175
  codexGoal?: unknown;
169
176
  evidence?: string;
170
177
  capturedEvidence?: string;
178
+ artifacts?: string[];
171
179
  qualityGate?: unknown;
172
180
  steering?: UlwLoopSteeringAudit;
173
181
  before?: unknown;
@@ -0,0 +1,20 @@
1
+ import type { UlwLoopPlan } from "./types.js";
2
+
3
+ export function normalizeDriverObjective(value: string): string {
4
+ return value.replace(/\s+/g, " ").trim();
5
+ }
6
+
7
+ export function acknowledgedDriverObjectives(plan: UlwLoopPlan): readonly string[] {
8
+ return plan.acknowledgedDriverObjectives ?? [];
9
+ }
10
+
11
+ // A differing driver objective is reported once; the plan remembers the exact text so every later
12
+ // checkpoint under the same driver stays quiet, while a driver rewritten to a third objective is
13
+ // reported again.
14
+ export function acknowledgeDriverObjective(plan: UlwLoopPlan, objective: string | undefined): boolean {
15
+ if (objective === undefined) return false;
16
+ const normalized = normalizeDriverObjective(objective);
17
+ if (!normalized || acknowledgedDriverObjectives(plan).includes(normalized)) return false;
18
+ plan.acknowledgedDriverObjectives = [...acknowledgedDriverObjectives(plan), normalized];
19
+ return true;
20
+ }
@@ -0,0 +1,45 @@
1
+ import { existsSync } from "node:fs";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { UlwLoopError } from "./types.js";
4
+
5
+ function fail(message: string, code: string, details: Record<string, unknown>): never {
6
+ throw new UlwLoopError(message, code, { details });
7
+ }
8
+
9
+ function stored(repoRoot: string, absolute: string): string {
10
+ const rel = relative(repoRoot, absolute);
11
+ const inside = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
12
+ return inside ? rel.split(sep).join("/") : absolute;
13
+ }
14
+
15
+ // Artifacts are bound at record time: a cited path that does not exist is a claim without a file,
16
+ // so the whole record is refused before any state changes. Paths inside the session cwd persist
17
+ // repo-relative (portable across machines); paths outside persist absolute.
18
+ export function resolveEvidenceArtifacts(
19
+ repoRoot: string,
20
+ artifacts: readonly unknown[] | undefined,
21
+ ): string[] | undefined {
22
+ if (artifacts === undefined) return undefined;
23
+ const seen = new Set<string>();
24
+ const resolved: string[] = [];
25
+ for (const [index, candidate] of artifacts.entries()) {
26
+ const path = typeof candidate === "string" ? candidate.trim() : "";
27
+ if (!path) fail(`Artifact ${index + 1} must be a non-empty path.`, "ULW_LOOP_ARGUMENT_INVALID", { index });
28
+ const absolute = resolve(repoRoot, path);
29
+ if (!existsSync(absolute))
30
+ fail(
31
+ `Evidence artifact does not exist: ${path} (resolved to ${absolute}).`,
32
+ "ULW_LOOP_EVIDENCE_ARTIFACT_MISSING",
33
+ {
34
+ path,
35
+ resolved: absolute,
36
+ },
37
+ );
38
+ const value = stored(repoRoot, absolute);
39
+ if (!seen.has(value)) {
40
+ seen.add(value);
41
+ resolved.push(value);
42
+ }
43
+ }
44
+ return resolved;
45
+ }
@@ -1,6 +1,8 @@
1
+ import { resolveEvidenceArtifacts } from "./evidence-artifacts.js";
1
2
  import { essentialCriteriaOf, hasAllCriteriaPass, hasEssentialCriteriaPass } from "./goal-status.js";
2
3
  import type { UlwLoopScope } from "./paths.js";
3
- import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
4
+ import { commit } from "./plan-commit.js";
5
+ import { readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
4
6
  import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js";
5
7
  import { iso, UlwLoopError } from "./types.js";
6
8
 
@@ -11,6 +13,7 @@ type RecordEvidenceArgs = {
11
13
  readonly status: EvidenceStatus;
12
14
  readonly evidence: string;
13
15
  readonly notes?: string;
16
+ readonly artifacts?: readonly string[];
14
17
  };
15
18
 
16
19
  function ulwLoopFail(message: string, code: string, details: Record<string, unknown>): never {
@@ -66,6 +69,7 @@ export async function recordEvidence(
66
69
  const goal = findGoal(plan, args.goalId);
67
70
  const criterion = findCriterion(goal, args.criterionId);
68
71
  const evidence = nonEmptyEvidence(args.evidence);
72
+ const artifacts = resolveEvidenceArtifacts(repoRoot, args.artifacts);
69
73
  const kind = ledgerKind(args.status);
70
74
  const prevStatus = criterion.status;
71
75
  const capturedAt = iso();
@@ -73,9 +77,10 @@ export async function recordEvidence(
73
77
  criterion.capturedEvidence = evidence;
74
78
  criterion.capturedAt = capturedAt;
75
79
  if (args.notes !== undefined) criterion.notes = args.notes;
80
+ if (artifacts !== undefined) criterion.artifacts = artifacts;
81
+ else delete criterion.artifacts;
76
82
  goal.updatedAt = capturedAt;
77
83
  plan.updatedAt = capturedAt;
78
- await writePlan(repoRoot, plan, scope);
79
84
  const ledgerEntry: UlwLoopLedgerEntry = {
80
85
  at: capturedAt,
81
86
  kind,
@@ -84,10 +89,11 @@ export async function recordEvidence(
84
89
  criterionStatus: args.status,
85
90
  evidence,
86
91
  capturedEvidence: evidence,
92
+ ...(artifacts === undefined ? {} : { artifacts }),
87
93
  before: { status: prevStatus },
88
94
  after: { goalId: goal.id, criterionId: criterion.id, status: args.status, evidence, capturedAt, prevStatus },
89
95
  };
90
- await appendLedger(repoRoot, ledgerEntry, scope);
96
+ await commit(repoRoot, scope, { plan, entries: [ledgerEntry] });
91
97
  return { plan, goal, criterion, ledgerEntry };
92
98
  });
93
99
  }
@@ -112,22 +118,23 @@ export async function markCriteriaPendingResetForGoal(
112
118
  criterion.capturedEvidence = null;
113
119
  delete criterion.capturedAt;
114
120
  delete criterion.notes;
121
+ delete criterion.artifacts;
115
122
  }
116
123
  goal.updatedAt = now;
117
124
  plan.updatedAt = now;
118
- await writePlan(repoRoot, plan, scope);
119
- await appendLedger(
120
- repoRoot,
121
- {
122
- at: now,
123
- kind: "criteria_revised",
124
- goalId,
125
- message: `Reset ${goal.successCriteria.length} criteria to pending.`,
126
- before,
127
- after: { resetCount: goal.successCriteria.length },
128
- },
129
- scope,
130
- );
125
+ await commit(repoRoot, scope, {
126
+ plan,
127
+ entries: [
128
+ {
129
+ at: now,
130
+ kind: "criteria_revised",
131
+ goalId,
132
+ message: `Reset ${goal.successCriteria.length} criteria to pending.`,
133
+ before,
134
+ after: { resetCount: goal.successCriteria.length },
135
+ },
136
+ ],
137
+ });
131
138
  return { plan, resetCount: goal.successCriteria.length };
132
139
  });
133
140
  }