oh-my-opencode 4.15.1 → 4.16.1

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 (234) hide show
  1. package/.agents/skills/codex-qa/SKILL.md +9 -7
  2. package/.agents/skills/codex-qa/references/logging-debug.md +6 -4
  3. package/.agents/skills/opencode-qa/SKILL.md +9 -9
  4. package/.agents/skills/opencode-qa/references/tui-tmux.md +10 -8
  5. package/dist/cli/codex-ulw-loop.d.ts +8 -0
  6. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +4 -4
  7. package/dist/cli/get-local-version/types.d.ts +1 -1
  8. package/dist/cli/index.js +792 -744
  9. package/dist/cli/install-senpi/index.d.ts +1 -0
  10. package/dist/cli/run/poll-for-completion.d.ts +4 -0
  11. package/dist/cli/run/session-resolver.d.ts +1 -0
  12. package/dist/cli/senpi-platform-flag.d.ts +4 -0
  13. package/dist/cli/types.d.ts +2 -1
  14. package/dist/cli-node/index.js +792 -744
  15. package/dist/features/background-agent/manager.d.ts +16 -0
  16. package/dist/features/builtin-commands/templates/start-work.d.ts +1 -1
  17. package/dist/features/monitor/process.d.ts +17 -1
  18. package/dist/hooks/anthropic-context-window-limit-recovery/aggressive-truncation-strategy.d.ts +1 -0
  19. package/dist/index.js +2206 -1971
  20. package/dist/skills/debugging/references/methodology/06-fix.md +6 -0
  21. package/dist/skills/frontend/ATTRIBUTION.md +10 -1
  22. package/dist/skills/frontend/SKILL.md +12 -7
  23. package/dist/skills/frontend/references/design/README.md +8 -4
  24. package/dist/skills/frontend/references/design/_INDEX.md +2 -2
  25. package/dist/skills/frontend/references/design/design-system-architecture.md +24 -2
  26. package/dist/skills/frontend/references/design/lazyweb.md +77 -0
  27. package/dist/skills/frontend/references/design/stitch-design-example.md +121 -0
  28. package/dist/skills/frontend/references/designpowers/README.md +2 -2
  29. package/dist/skills/frontend/references/designpowers/lane-b-execution.md +1 -1
  30. package/dist/skills/programming/SKILL.md +10 -1
  31. package/dist/skills/programming/references/logging.md +91 -0
  32. package/dist/skills/start-work/SKILL.md +2 -2
  33. package/dist/skills/ulw-plan/SKILL.md +5 -5
  34. package/dist/skills/ulw-plan/references/full-workflow.md +7 -7
  35. package/dist/skills/ulw-plan/references/intent-unclear.md +3 -3
  36. package/dist/skills/ulw-research/ATTRIBUTION.md +2 -2
  37. package/dist/skills/ulw-research/SKILL.md +28 -7
  38. package/dist/skills/visual-qa/SKILL.md +13 -17
  39. package/dist/tools/skill/description-formatter.d.ts +1 -0
  40. package/dist/tools/skill/description-formatter.test-support.d.ts +9 -0
  41. package/dist/tools/skill/zauc-mocks-skill-tools/test-support.d.ts +25 -0
  42. package/dist/tui.js +121 -419
  43. package/docs/reference/web-terminal-visual-qa.md +39 -45
  44. package/package.json +48 -25
  45. package/packages/git-bash-mcp/dist/cli.js +2 -2
  46. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  47. package/packages/omo-codex/plugin/components/bootstrap/AGENTS.md +55 -0
  48. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +19 -16
  49. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  50. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/codegraph/AGENTS.md +51 -0
  52. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +936 -248
  53. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +233 -135
  54. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/codegraph/src/cache-gc.ts +29 -0
  56. package/packages/omo-codex/plugin/components/codegraph/src/cli.ts +13 -0
  57. package/packages/omo-codex/plugin/components/codegraph/src/hook-sweep.ts +25 -0
  58. package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +3 -1
  59. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +30 -1
  60. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +16 -2
  61. package/packages/omo-codex/plugin/components/codegraph/src/sweep-cli.ts +80 -0
  62. package/packages/omo-codex/plugin/components/codegraph/test/cache-gc.test.ts +34 -0
  63. package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +107 -0
  64. package/packages/omo-codex/plugin/components/codegraph/test/hook-registration.test.ts +26 -0
  65. package/packages/omo-codex/plugin/components/codegraph/test/hook-sweep.test.ts +29 -0
  66. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +8 -21
  67. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +108 -0
  68. package/packages/omo-codex/plugin/components/codegraph/test/serve-built-wrapper.test.ts +76 -0
  69. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +17 -111
  70. package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +1 -1
  71. package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +45 -67
  72. package/packages/omo-codex/plugin/components/codegraph/test/sweep-cli.test.ts +56 -0
  73. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  74. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  75. package/packages/omo-codex/plugin/components/git-bash/AGENTS.md +31 -0
  76. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  77. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  78. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/AGENTS.md +45 -0
  79. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  80. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  81. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  82. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  83. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +37 -102
  84. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  85. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  86. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +2 -2
  87. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  88. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  89. package/packages/omo-codex/plugin/components/teammode/AGENTS.md +46 -0
  90. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  91. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  92. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +6 -0
  93. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +6 -0
  94. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  95. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  96. package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +5 -2
  97. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +2 -0
  98. package/packages/omo-codex/plugin/components/ultrawork/README.md +3 -3
  99. package/packages/omo-codex/plugin/components/ultrawork/directive.md +18 -10
  100. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +51 -2
  101. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  102. package/packages/omo-codex/plugin/components/ultrawork/package.json +2 -1
  103. package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +14 -1
  104. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +388 -0
  105. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +16 -2
  106. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +4 -4
  107. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-unclear.md +2 -2
  108. package/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts +5 -3
  109. package/packages/omo-codex/plugin/components/ultrawork/src/skill-pointer.ts +56 -0
  110. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +26 -5
  111. package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +12 -0
  112. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +1 -0
  113. package/packages/omo-codex/plugin/components/ultrawork/test/skill-pointer.test.ts +82 -0
  114. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +5 -0
  115. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +18 -10
  116. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +15 -2
  117. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-steering.js +2 -1
  118. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +145 -32
  119. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-hook.d.ts +1 -0
  120. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-hook.js +5 -1
  121. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +6 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +55 -9
  123. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-snapshot.d.ts +15 -0
  124. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-snapshot.js +33 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +10 -3
  126. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +15 -11
  127. package/packages/omo-codex/plugin/components/ulw-loop/dist/ultrawork-directive.d.ts +2 -1
  128. package/packages/omo-codex/plugin/components/ulw-loop/dist/ultrawork-directive.js +5 -3
  129. package/packages/omo-codex/plugin/components/ulw-loop/dist/ultrawork-skill-pointer.d.ts +7 -0
  130. package/packages/omo-codex/plugin/components/ulw-loop/dist/ultrawork-skill-pointer.js +46 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  132. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  133. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  134. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +7 -7
  135. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +17 -2
  136. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts +2 -1
  137. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-hook.ts +5 -1
  138. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +59 -11
  139. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-snapshot.ts +38 -0
  140. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +11 -3
  141. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +15 -7
  142. package/packages/omo-codex/plugin/components/ulw-loop/src/ultrawork-directive.ts +9 -3
  143. package/packages/omo-codex/plugin/components/ulw-loop/src/ultrawork-skill-pointer.ts +55 -0
  144. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +16 -0
  145. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +260 -2
  146. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-snapshot.test.ts +124 -0
  147. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +101 -2
  148. package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +49 -0
  149. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  157. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  158. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  159. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  161. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  162. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  163. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  164. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  165. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  166. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  167. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  168. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  169. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  170. package/packages/omo-codex/plugin/package-lock.json +13 -13
  171. package/packages/omo-codex/plugin/package.json +1 -1
  172. package/packages/omo-codex/plugin/scripts/build-components.mjs +63 -17
  173. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +10 -6
  174. package/packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs +10 -3
  175. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -0
  176. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +18 -0
  177. package/packages/omo-codex/plugin/skills/debugging/references/methodology/06-fix.md +6 -0
  178. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +10 -1
  179. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +12 -7
  180. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +8 -4
  181. package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +2 -2
  182. package/packages/omo-codex/plugin/skills/frontend/references/design/design-system-architecture.md +24 -2
  183. package/packages/omo-codex/plugin/skills/frontend/references/design/lazyweb.md +77 -0
  184. package/packages/omo-codex/plugin/skills/frontend/references/design/stitch-design-example.md +121 -0
  185. package/packages/omo-codex/plugin/skills/frontend/references/designpowers/README.md +2 -2
  186. package/packages/omo-codex/plugin/skills/frontend/references/designpowers/lane-b-execution.md +1 -1
  187. package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -1
  188. package/packages/omo-codex/plugin/skills/programming/references/logging.md +91 -0
  189. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -2
  190. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +388 -0
  191. package/packages/omo-codex/plugin/skills/ultrawork/agents/openai.yaml +2 -0
  192. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  193. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +7 -7
  194. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +16 -2
  195. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +4 -4
  196. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-unclear.md +2 -2
  197. package/packages/omo-codex/plugin/skills/ulw-research/ATTRIBUTION.md +2 -2
  198. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +28 -7
  199. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +13 -17
  200. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +4 -4
  201. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +1 -1
  202. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +22 -9
  203. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +1 -1
  204. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
  205. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -0
  206. package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +56 -0
  207. package/packages/omo-codex/plugin/test/ulw-plan-skill-contract.test.mjs +52 -0
  208. package/packages/omo-codex/plugin/test/ulw-research-epistemic-contract.test.mjs +98 -0
  209. package/packages/omo-codex/plugin/test/ulw-research-skill-contract.test.mjs +6 -6
  210. package/packages/omo-codex/scripts/install-dist/install-local.mjs +310 -3979
  211. package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +2 -2
  212. package/packages/shared-skills/skills/debugging/references/methodology/06-fix.md +6 -0
  213. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +10 -1
  214. package/packages/shared-skills/skills/frontend/SKILL.md +12 -7
  215. package/packages/shared-skills/skills/frontend/references/design/README.md +8 -4
  216. package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +2 -2
  217. package/packages/shared-skills/skills/frontend/references/design/design-system-architecture.md +24 -2
  218. package/packages/shared-skills/skills/frontend/references/design/lazyweb.md +77 -0
  219. package/packages/shared-skills/skills/frontend/references/design/stitch-design-example.md +121 -0
  220. package/packages/shared-skills/skills/frontend/references/designpowers/README.md +2 -2
  221. package/packages/shared-skills/skills/frontend/references/designpowers/lane-b-execution.md +1 -1
  222. package/packages/shared-skills/skills/programming/SKILL.md +10 -1
  223. package/packages/shared-skills/skills/programming/references/logging.md +91 -0
  224. package/packages/shared-skills/skills/start-work/SKILL.md +2 -2
  225. package/packages/shared-skills/skills/ulw-plan/SKILL.md +5 -5
  226. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +7 -7
  227. package/packages/shared-skills/skills/ulw-plan/references/intent-unclear.md +3 -3
  228. package/packages/shared-skills/skills/ulw-research/ATTRIBUTION.md +2 -2
  229. package/packages/shared-skills/skills/ulw-research/SKILL.md +28 -7
  230. package/packages/shared-skills/skills/visual-qa/SKILL.md +13 -17
  231. package/script/qa/strip-ansi.mjs +10 -0
  232. package/script/qa/web-terminal-visual-qa.mjs +112 -195
  233. package/script/qa/xterm-live-terminal.mjs +180 -0
  234. package/script/qa/web-terminal-renderer.mjs +0 -218
@@ -2,7 +2,8 @@
2
2
  import { isUlwLoopDone } from "./goal-status.js";
3
3
  import type { UlwLoopScope } from "./paths.js";
4
4
  import { seedDefaultSuccessCriteria } from "./plan-crud.js";
5
- import { appendLedger, readSteeringLedgerEntries, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
5
+ import { appendLedger, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
6
+ import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
6
7
  import type {
7
8
  SteerUlwLoopResult,
8
9
  UlwLoopItem,
@@ -245,13 +246,22 @@ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringPr
245
246
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
246
247
  const plan = await readUlwLoopPlan(repoRoot, scope);
247
248
  const key = proposal.idempotencyKey ?? proposal.promptSignature;
248
- const prior = key === undefined ? undefined : (await readSteeringLedgerEntries(repoRoot, scope)).find((entry) => entry.steering?.invariant.accepted === true && (entry.idempotencyKey === key || entry.steering.idempotencyKey === key || entry.steering.promptSignature === key));
249
- if (prior?.steering !== undefined) return { plan, accepted: true, audit: { ...prior.steering, deduped: true }, rejectedReasons: [], deduped: true };
249
+ const prior = key === undefined ? undefined : await findAcceptedSteeringLedgerEntry(repoRoot, key, scope);
250
+ if (prior?.steering !== undefined) {
251
+ // Legacy entries embedded full-plan before/after snapshots; never
252
+ // re-surface those multi-MB payloads on the dedup path.
253
+ const { before: _before, after: _after, ...compactPrior } = prior.steering;
254
+ return { plan, accepted: true, audit: { ...compactPrior, deduped: true }, rejectedReasons: [], deduped: true };
255
+ }
250
256
  const audit = validateUlwLoopSteeringProposal(plan, proposal);
251
257
  const accepted = audit.invariant.accepted;
252
258
  const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
253
- const finalAudit: UlwLoopSteeringAudit = { ...audit, before: plan };
254
- if (accepted) finalAudit.after = next;
259
+ const finalAudit: UlwLoopSteeringAudit = { ...audit };
260
+ if (accepted) {
261
+ const changed = changedGoalIdsBetween(plan, next);
262
+ finalAudit.before = buildSteeringPlanSnapshot(plan, changed);
263
+ finalAudit.after = buildSteeringPlanSnapshot(next, changed);
264
+ }
255
265
  if (accepted) await writePlan(repoRoot, next, scope);
256
266
  await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, proposal.now?.toISOString() ?? iso()), scope);
257
267
  return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
@@ -264,7 +274,5 @@ function ledgerEntry(proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAu
264
274
  if (goalId !== undefined) entry.goalId = goalId;
265
275
  if (proposal.criterionId !== undefined) entry.criterionId = proposal.criterionId;
266
276
  if (proposal.idempotencyKey !== undefined) entry.idempotencyKey = proposal.idempotencyKey;
267
- if (audit.before !== undefined) entry.before = audit.before;
268
- if (audit.after !== undefined) entry.after = audit.after;
269
277
  return entry;
270
278
  }
@@ -1,5 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
+ import { buildUltraworkAdditionalContext, type UltraworkAdditionalContextOptions } from "./ultrawork-skill-pointer.js";
4
+
3
5
  export interface UltraworkDirectiveInput {
4
6
  readonly prompt: string;
5
7
  readonly transcript_path?: string | null;
@@ -12,7 +14,6 @@ interface UserPromptSubmitHookOutput {
12
14
  };
13
15
  }
14
16
 
15
- const ULTRAWORK_DIRECTIVE = readFileSync(new URL("../directive.md", import.meta.url), "utf8");
16
17
  const ULTRAWORK_CURRENT_PROMPT_PATTERN = /(?:ultrawork|ulw)/i;
17
18
  const ULTRAWORK_DIRECTIVE_MARKER = "<ultrawork-mode>";
18
19
  const TRANSCRIPT_SEARCH_BYTES = 512_000;
@@ -26,11 +27,16 @@ const CONTEXT_PRESSURE_MARKERS = [
26
27
  "long threads and multiple compactions",
27
28
  ] as const;
28
29
 
29
- export function buildUltraworkDirectiveOutput(input: UltraworkDirectiveInput): string {
30
+ export function buildUltraworkDirectiveOutput(
31
+ input: UltraworkDirectiveInput,
32
+ options: UltraworkAdditionalContextOptions = {},
33
+ ): string {
30
34
  if (isContextPressureRecoveryPrompt(input.prompt)) return "";
31
35
  if (hasUltraworkDirectiveAlreadyInTranscript(input.transcript_path)) return "";
32
36
  if (isContextPressureTranscript(input.transcript_path)) return "";
33
- return isUltraworkPrompt(input.prompt) ? formatAdditionalContextOutput(ULTRAWORK_DIRECTIVE) : "";
37
+ return isUltraworkPrompt(input.prompt)
38
+ ? formatAdditionalContextOutput(buildUltraworkAdditionalContext(options))
39
+ : "";
34
40
  }
35
41
 
36
42
  function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath: string | null | undefined): boolean {
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export const ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
5
+ ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
6
+
7
+ MANDATORY BOOTSTRAP: do all three steps, in order, before anything else.
8
+
9
+ 1. First user-visible line this turn MUST be exactly:
10
+ \`ULTRAWORK MODE ENABLED!\`
11
+
12
+ 2. Call \`create_goal\` NOW with \`objective\` set to the user's request.
13
+ Send \`objective\` only: no \`status\`, no budget fields. If the
14
+ \`create_goal\` tool is unavailable, open your reply with a binding
15
+ \`# Goal\` block instead. Never skip this step.
16
+
17
+ 3. Read the FULL ultrawork directive NOW, before any other tool call,
18
+ plan, or edit. It is the \`ultrawork\` skill, stored at:
19
+
20
+ {{ULTRAWORK_SKILL_PATH}}
21
+
22
+ Read the whole file. If a read result comes back truncated, keep
23
+ reading the remaining line ranges until you have seen every line.
24
+ Every rule in that file is binding for this entire task: no
25
+ compromise, no summarizing from memory, no skipping. If the file does
26
+ not exist, tell the user the omo ultrawork skill is missing and
27
+ continue with steps 1 and 2 plus evidence-bound execution.
28
+
29
+ Do not start the requested work until all three steps are complete.
30
+ </ultrawork-mode>
31
+ `;
32
+
33
+ const ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
34
+ const ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
35
+ const ULTRAWORK_DIRECTIVE = readFileSync(new URL("../directive.md", import.meta.url), "utf8");
36
+
37
+ export interface UltraworkAdditionalContextOptions {
38
+ readonly skillFilePath?: string | null;
39
+ }
40
+
41
+ export function resolveUltraworkSkillFilePath(): string {
42
+ return fileURLToPath(ULTRAWORK_SKILL_FILE_URL);
43
+ }
44
+
45
+ export function buildUltraworkSkillPointer(skillFilePath: string): string {
46
+ return ULTRAWORK_SKILL_POINTER_TEMPLATE.replace(ULTRAWORK_SKILL_PATH_PLACEHOLDER, skillFilePath);
47
+ }
48
+
49
+ export function buildUltraworkAdditionalContext(options: UltraworkAdditionalContextOptions = {}): string {
50
+ const skillFilePath = options.skillFilePath === undefined ? resolveUltraworkSkillFilePath() : options.skillFilePath;
51
+ if (skillFilePath !== null && existsSync(skillFilePath)) {
52
+ return buildUltraworkSkillPointer(skillFilePath);
53
+ }
54
+ return ULTRAWORK_DIRECTIVE;
55
+ }
@@ -169,4 +169,20 @@ describe("ulwLoopCommand create-goals", () => {
169
169
 
170
170
  expect(await readFile(join(testDir, ".omo/ulw-loop/manual-456/goals.json"), "utf8")).toContain("Manual scoped");
171
171
  });
172
+
173
+ it("#given a valueless --session-id #when creating goals #then it fails and writes no plan", async () => {
174
+ const code = await ulwLoopCommand(["create-goals", "--session-id", "--brief", "- Alpha"]);
175
+
176
+ expect(code).toBe(1);
177
+ expect(err.join("")).toContain("--session-id requires a non-empty value");
178
+ await expect(readFile(join(testDir, ".omo/ulw-loop/goals.json"), "utf8")).rejects.toThrow();
179
+ });
180
+
181
+ it("#given an empty --session-id= #when creating goals #then it fails and writes no plan", async () => {
182
+ const code = await ulwLoopCommand(["create-goals", "--session-id=", "--brief", "- Alpha"]);
183
+
184
+ expect(code).toBe(1);
185
+ expect(err.join("")).toContain("--session-id requires a non-empty value");
186
+ await expect(readFile(join(testDir, ".omo/ulw-loop/goals.json"), "utf8")).rejects.toThrow();
187
+ });
172
188
  });
@@ -1,16 +1,18 @@
1
- import { copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { appendFile, copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { setImmediate as tick } from "node:timers/promises";
4
5
  import { beforeEach, describe, expect, it } from "vitest";
5
6
  import { ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js";
6
7
  import {
7
8
  appendLedger,
9
+ findAcceptedSteeringLedgerEntry,
8
10
  readSteeringLedgerEntries,
9
11
  readUlwLoopPlan,
10
12
  withUlwLoopMutationLock,
11
13
  writePlan,
12
14
  } from "../src/plan-io.js";
13
- import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan } from "../src/types.js";
15
+ import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit } from "../src/types.js";
14
16
  import { UlwLoopError } from "../src/types.js";
15
17
 
16
18
  const NOW = "2026-05-23T00:00:00.000Z";
@@ -51,6 +53,45 @@ function entry(kind: UlwLoopLedgerEntry["kind"], goalId = "G001"): UlwLoopLedger
51
53
  return { at: NOW, kind, goalId };
52
54
  }
53
55
 
56
+ function steeringAudit(overrides: Partial<UlwLoopSteeringAudit> = {}): UlwLoopSteeringAudit {
57
+ return {
58
+ kind: "revise_pending_wording",
59
+ source: "cli",
60
+ targetGoalIds: ["G001"],
61
+ evidence: "observed evidence",
62
+ rationale: "needed change",
63
+ invariant: {
64
+ accepted: true,
65
+ structuralInvariantAccepted: true,
66
+ evidenceBackedNecessity: true,
67
+ noEasierCompletion: true,
68
+ rejectedReasons: [],
69
+ },
70
+ ...overrides,
71
+ };
72
+ }
73
+
74
+ function rejectedSteeringAudit(overrides: Partial<UlwLoopSteeringAudit> = {}): UlwLoopSteeringAudit {
75
+ return steeringAudit({
76
+ invariant: {
77
+ accepted: false,
78
+ structuralInvariantAccepted: false,
79
+ evidenceBackedNecessity: false,
80
+ noEasierCompletion: true,
81
+ rejectedReasons: ["missing evidence"],
82
+ },
83
+ ...overrides,
84
+ });
85
+ }
86
+
87
+ function steeringEntry(
88
+ kind: UlwLoopLedgerEntry["kind"],
89
+ audit: UlwLoopSteeringAudit,
90
+ overrides: Partial<UlwLoopLedgerEntry> = {},
91
+ ): UlwLoopLedgerEntry {
92
+ return { at: NOW, kind, goalId: "G001", steering: audit, ...overrides };
93
+ }
94
+
54
95
  async function makeRepo(): Promise<string> {
55
96
  return mkdtemp(join(tmpdir(), "ug-io-"));
56
97
  }
@@ -209,6 +250,112 @@ describe("readSteeringLedgerEntries", () => {
209
250
  });
210
251
  });
211
252
 
253
+ describe("findAcceptedSteeringLedgerEntry", () => {
254
+ it.each([
255
+ [
256
+ "top-level idempotencyKey",
257
+ steeringEntry("steering_accepted", steeringAudit(), { idempotencyKey: "needle-key" }),
258
+ ],
259
+ ["steering.idempotencyKey", steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "needle-key" }))],
260
+ [
261
+ "steering.promptSignature",
262
+ steeringEntry("steering_accepted", steeringAudit({ promptSignature: "needle-key" })),
263
+ ],
264
+ ] as const)("finds an accepted entry by %s", async (_name, target) => {
265
+ // given
266
+ const repoRoot = await makeRepo();
267
+ await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "other-key" })));
268
+ await appendLedger(repoRoot, target);
269
+
270
+ // when
271
+ const found = await findAcceptedSteeringLedgerEntry(repoRoot, "needle-key");
272
+
273
+ // then
274
+ expect(found).toEqual(target);
275
+ });
276
+
277
+ it("skips a rejected entry carrying the same key", async () => {
278
+ // given
279
+ const repoRoot = await makeRepo();
280
+ await appendLedger(
281
+ repoRoot,
282
+ steeringEntry("steering_rejected", rejectedSteeringAudit({ idempotencyKey: "needle-key" })),
283
+ );
284
+
285
+ // when/then
286
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
287
+ });
288
+
289
+ it("returns the accepted entry even when a rejected one with the same key precedes it", async () => {
290
+ // given
291
+ const repoRoot = await makeRepo();
292
+ const accepted = steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "needle-key" }));
293
+ await appendLedger(
294
+ repoRoot,
295
+ steeringEntry("steering_rejected", rejectedSteeringAudit({ idempotencyKey: "needle-key" })),
296
+ );
297
+ await appendLedger(repoRoot, accepted);
298
+
299
+ // when/then
300
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toEqual(accepted);
301
+ });
302
+
303
+ it("returns undefined when the ledger file is missing", async () => {
304
+ // given
305
+ const repoRoot = await makeRepo();
306
+
307
+ // when/then
308
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
309
+ });
310
+
311
+ it("ignores the key when it only appears in non-steering entries", async () => {
312
+ // given
313
+ const repoRoot = await makeRepo();
314
+ await appendLedger(repoRoot, { at: NOW, kind: "goal_completed", goalId: "G001", idempotencyKey: "needle-key" });
315
+ await appendLedger(repoRoot, { at: NOW, kind: "evidence_captured", goalId: "G001", evidence: "needle-key" });
316
+
317
+ // when/then
318
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
319
+ });
320
+
321
+ it("ignores an accepted entry whose key only appears in evidence text", async () => {
322
+ // given
323
+ const repoRoot = await makeRepo();
324
+ await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ evidence: "needle-key" })));
325
+
326
+ // when/then
327
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
328
+ });
329
+
330
+ it("finds the key inside a legacy entry embedding full before/after plans", async () => {
331
+ // given
332
+ const repoRoot = await makeRepo();
333
+ const bulkyGoals = Array.from({ length: 20 }, (_, index) =>
334
+ makeGoal({ id: `G${String(index + 1).padStart(3, "0")}`, objective: `evidence detail ${index} `.repeat(200) }),
335
+ );
336
+ const bulkyPlan = makePlan({ goals: bulkyGoals });
337
+ const legacyLine = JSON.stringify({
338
+ at: NOW,
339
+ kind: "steering_accepted",
340
+ goalId: "G001",
341
+ idempotencyKey: "needle-key",
342
+ before: bulkyPlan,
343
+ after: bulkyPlan,
344
+ steering: { ...steeringAudit({ idempotencyKey: "needle-key" }), before: bulkyPlan, after: bulkyPlan },
345
+ });
346
+ await mkdir(ulwLoopDir(repoRoot), { recursive: true });
347
+ await appendFile(ulwLoopLedgerPath(repoRoot), `${legacyLine}\n`, "utf8");
348
+
349
+ // when
350
+ const found = await findAcceptedSteeringLedgerEntry(repoRoot, "needle-key");
351
+
352
+ // then
353
+ expect(legacyLine.length).toBeGreaterThan(100_000);
354
+ expect(found?.kind).toBe("steering_accepted");
355
+ expect(found?.idempotencyKey).toBe("needle-key");
356
+ });
357
+ });
358
+
212
359
  describe("withUlwLoopMutationLock", () => {
213
360
  it("serializes concurrent invocations", async () => {
214
361
  // given
@@ -236,4 +383,115 @@ describe("withUlwLoopMutationLock", () => {
236
383
  expect(maxActive).toBe(1);
237
384
  expect(await readFile(counterPath, "utf8")).toBe("3");
238
385
  });
386
+
387
+ it("propagates the body rejection and keeps later calls working", async () => {
388
+ // given
389
+ const repoRoot = await makeRepo();
390
+
391
+ // when
392
+ const failure = withUlwLoopMutationLock(repoRoot, async () => {
393
+ throw new Error("body exploded");
394
+ });
395
+
396
+ // then
397
+ await expect(failure).rejects.toThrow("body exploded");
398
+ await expect(withUlwLoopMutationLock(repoRoot, async () => "recovered")).resolves.toBe("recovered");
399
+ });
400
+
401
+ it("still serializes a second batch after the first batch's gate settles", async () => {
402
+ // given
403
+ const repoRoot = await makeRepo();
404
+ let active = 0;
405
+ let maxActive = 0;
406
+ const body = async (): Promise<void> => {
407
+ active += 1;
408
+ maxActive = Math.max(maxActive, active);
409
+ await tick();
410
+ active -= 1;
411
+ };
412
+
413
+ // when
414
+ await Promise.all([1, 2, 3].map(() => withUlwLoopMutationLock(repoRoot, body)));
415
+ await tick();
416
+ await Promise.all([1, 2].map(() => withUlwLoopMutationLock(repoRoot, body)));
417
+
418
+ // then
419
+ expect(maxActive).toBe(1);
420
+ });
421
+
422
+ it("keeps an in-flight successor locked when an earlier gate settles", async () => {
423
+ // given
424
+ const repoRoot = await makeRepo();
425
+ const events: string[] = [];
426
+ let releaseSecond = false;
427
+
428
+ // when
429
+ const first = withUlwLoopMutationLock(repoRoot, async () => {
430
+ events.push("first");
431
+ });
432
+ const second = withUlwLoopMutationLock(repoRoot, async () => {
433
+ events.push("second:start");
434
+ while (!releaseSecond) await tick();
435
+ events.push("second:end");
436
+ });
437
+ await first;
438
+ await tick();
439
+ const third = withUlwLoopMutationLock(repoRoot, async () => {
440
+ events.push("third");
441
+ });
442
+ releaseSecond = true;
443
+ await Promise.all([second, third]);
444
+
445
+ // then
446
+ expect(events).toEqual(["first", "second:start", "second:end", "third"]);
447
+ });
448
+
449
+ it("does not serialize distinct repo roots against each other", async () => {
450
+ // given
451
+ const rootA = await makeRepo();
452
+ const rootB = await makeRepo();
453
+ const events: string[] = [];
454
+ let releaseA = false;
455
+
456
+ // when
457
+ const held = withUlwLoopMutationLock(rootA, async () => {
458
+ events.push("a:start");
459
+ while (!releaseA) await tick();
460
+ events.push("a:end");
461
+ });
462
+ await withUlwLoopMutationLock(rootB, async () => {
463
+ events.push("b");
464
+ });
465
+ releaseA = true;
466
+ await held;
467
+
468
+ // then
469
+ expect(events).toEqual(["a:start", "b", "a:end"]);
470
+ });
471
+
472
+ it("serializes per scope while distinct scopes stay independent", async () => {
473
+ // given
474
+ const repoRoot = await makeRepo();
475
+ const scoped = { sessionId: "session-a" };
476
+ const events: string[] = [];
477
+ let releaseScoped = false;
478
+
479
+ // when
480
+ const heldScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
481
+ events.push("scoped-1:start");
482
+ while (!releaseScoped) await tick();
483
+ events.push("scoped-1:end");
484
+ });
485
+ const queuedScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
486
+ events.push("scoped-2");
487
+ });
488
+ await withUlwLoopMutationLock(repoRoot, async () => {
489
+ events.push("root-scope");
490
+ });
491
+ releaseScoped = true;
492
+ await Promise.all([heldScoped, queuedScoped]);
493
+
494
+ // then
495
+ expect(events).toEqual(["scoped-1:start", "root-scope", "scoped-1:end", "scoped-2"]);
496
+ });
239
497
  });
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "../src/steering-snapshot.js";
3
+ import type { UlwLoopItem, UlwLoopPlan } from "../src/types.js";
4
+
5
+ const NOW = "2026-05-23T00:00:00.000Z";
6
+
7
+ function goal(id: string, overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
8
+ return {
9
+ id,
10
+ title: `Goal ${id}`,
11
+ objective: `Objective for ${id}`,
12
+ status: "pending",
13
+ successCriteria: [],
14
+ attempt: 0,
15
+ createdAt: NOW,
16
+ updatedAt: NOW,
17
+ ...overrides,
18
+ };
19
+ }
20
+
21
+ function plan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
22
+ return {
23
+ version: 1,
24
+ createdAt: NOW,
25
+ updatedAt: NOW,
26
+ briefPath: ".omo/ulw-loop/brief.md",
27
+ goalsPath: ".omo/ulw-loop/goals.json",
28
+ ledgerPath: ".omo/ulw-loop/ledger.jsonl",
29
+ goals,
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ describe("changedGoalIdsBetween", () => {
35
+ it.each([
36
+ {
37
+ name: "added goal",
38
+ before: [goal("G001"), goal("G002")],
39
+ after: [goal("G001"), goal("G002"), goal("G003")],
40
+ expected: ["G003"],
41
+ },
42
+ {
43
+ name: "removed goal",
44
+ before: [goal("G001"), goal("G002")],
45
+ after: [goal("G001")],
46
+ expected: ["G002"],
47
+ },
48
+ {
49
+ name: "mutated goal",
50
+ before: [goal("G001"), goal("G002")],
51
+ after: [goal("G001"), goal("G002", { title: "Rewritten" })],
52
+ expected: ["G002"],
53
+ },
54
+ {
55
+ name: "reordered-only goals",
56
+ before: [goal("G001"), goal("G002"), goal("G003")],
57
+ after: [goal("G003"), goal("G001"), goal("G002")],
58
+ expected: [],
59
+ },
60
+ {
61
+ name: "identical plans",
62
+ before: [goal("G001"), goal("G002")],
63
+ after: [goal("G001"), goal("G002")],
64
+ expected: [],
65
+ },
66
+ {
67
+ name: "add + remove + mutate at once",
68
+ before: [goal("G001"), goal("G002"), goal("G003")],
69
+ after: [goal("G001", { status: "blocked" }), goal("G003"), goal("G004")],
70
+ expected: ["G001", "G002", "G004"],
71
+ },
72
+ ])("detects $name", ({ before, after, expected }) => {
73
+ // when
74
+ const changed = changedGoalIdsBetween(plan(before), plan(after));
75
+
76
+ // then
77
+ expect([...changed].sort()).toEqual(expected);
78
+ });
79
+ });
80
+
81
+ describe("buildSteeringPlanSnapshot", () => {
82
+ it("includes only changed goals while keeping full goalIds order and goalCount", () => {
83
+ // given
84
+ const goals = [goal("G001"), goal("G002"), goal("G003"), goal("G004")];
85
+ const source = plan(goals);
86
+
87
+ // when
88
+ const snapshot = buildSteeringPlanSnapshot(source, new Set(["G003"]));
89
+
90
+ // then
91
+ expect(snapshot.goals.map((item) => item.id)).toEqual(["G003"]);
92
+ expect(snapshot.goals[0]).toEqual(goals[2]);
93
+ expect(snapshot.goalIds).toEqual(["G001", "G002", "G003", "G004"]);
94
+ expect(snapshot.goalCount).toBe(4);
95
+ expect(snapshot.updatedAt).toBe(NOW);
96
+ });
97
+
98
+ it("omits the activeGoalId key entirely when the plan has none", () => {
99
+ // when
100
+ const snapshot = buildSteeringPlanSnapshot(plan([goal("G001")]), new Set(["G001"]));
101
+
102
+ // then
103
+ expect("activeGoalId" in snapshot).toBe(false);
104
+ });
105
+
106
+ it("carries activeGoalId when the plan has one", () => {
107
+ // when
108
+ const snapshot = buildSteeringPlanSnapshot(plan([goal("G001")], { activeGoalId: "G001" }), new Set());
109
+
110
+ // then
111
+ expect(snapshot.activeGoalId).toBe("G001");
112
+ });
113
+
114
+ it("keeps plan order for changed goals regardless of set insertion order", () => {
115
+ // given
116
+ const source = plan([goal("G001"), goal("G002"), goal("G003")]);
117
+
118
+ // when
119
+ const snapshot = buildSteeringPlanSnapshot(source, new Set(["G003", "G001"]));
120
+
121
+ // then
122
+ expect(snapshot.goals.map((item) => item.id)).toEqual(["G001", "G003"]);
123
+ });
124
+ });