supipowers 1.5.3 → 2.0.0

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 (340) hide show
  1. package/README.md +14 -8
  2. package/bin/install.mjs +20 -5
  3. package/bin/install.ts +95 -0
  4. package/package.json +8 -4
  5. package/skills/context-mode/SKILL.md +17 -10
  6. package/skills/harness/SKILL.md +94 -0
  7. package/skills/ui-design/SKILL.md +63 -0
  8. package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
  9. package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
  10. package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
  11. package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
  12. package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
  13. package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
  14. package/skills/ultraplan-discover/SKILL.md +96 -0
  15. package/skills/ultraplan-intake/SKILL.md +89 -0
  16. package/skills/ultraplan-research/SKILL.md +129 -0
  17. package/skills/ultraplan-review/SKILL.md +86 -0
  18. package/skills/ultraplan-review-scope/SKILL.md +111 -0
  19. package/skills/ultraplan-review-structure/SKILL.md +120 -0
  20. package/skills/ultraplan-review-tdd/SKILL.md +142 -0
  21. package/skills/ultraplan-scout/SKILL.md +110 -0
  22. package/skills/ultraplan-synthesize/SKILL.md +124 -0
  23. package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
  24. package/src/ai/schema-text.ts +129 -0
  25. package/src/ai/structured-output.ts +274 -0
  26. package/src/ai/template.ts +27 -0
  27. package/src/bootstrap.ts +63 -28
  28. package/src/commands/agents.ts +131 -42
  29. package/src/commands/ai-review.ts +251 -30
  30. package/src/commands/clear.ts +434 -0
  31. package/src/commands/commit.ts +1 -0
  32. package/src/commands/config.ts +242 -44
  33. package/src/commands/context.ts +55 -28
  34. package/src/commands/doctor.ts +234 -6
  35. package/src/commands/fix-pr.ts +306 -131
  36. package/src/commands/generate.ts +111 -21
  37. package/src/commands/memory.ts +192 -0
  38. package/src/commands/model-picker.ts +28 -21
  39. package/src/commands/model.ts +18 -8
  40. package/src/commands/optimize-context.ts +408 -29
  41. package/src/commands/plan.ts +2 -0
  42. package/src/commands/qa.ts +312 -137
  43. package/src/commands/release.ts +259 -76
  44. package/src/commands/review.ts +293 -59
  45. package/src/commands/status.ts +200 -13
  46. package/src/commands/supi.ts +3 -35
  47. package/src/commands/ui-design.ts +394 -0
  48. package/src/commands/ultraplan.ts +1518 -0
  49. package/src/commands/update.ts +86 -0
  50. package/src/config/defaults.ts +62 -0
  51. package/src/config/loader.ts +448 -60
  52. package/src/config/schema.ts +108 -2
  53. package/src/context/optimizer.ts +25 -33
  54. package/src/context/rule-renderer.ts +223 -0
  55. package/src/context/savings.ts +258 -0
  56. package/src/context/startup-check.ts +380 -0
  57. package/src/context/startup-optimizer.ts +355 -0
  58. package/src/context/tokenignore.ts +146 -0
  59. package/src/context-mode/cache-handle.ts +49 -0
  60. package/src/context-mode/cache-preview.ts +71 -0
  61. package/src/context-mode/cache-store.ts +738 -0
  62. package/src/context-mode/compressor.ts +131 -26
  63. package/src/context-mode/dedup.ts +108 -0
  64. package/src/context-mode/detector.ts +35 -4
  65. package/src/context-mode/event-extractor.ts +14 -12
  66. package/src/context-mode/event-store.ts +91 -36
  67. package/src/context-mode/hooks.ts +798 -56
  68. package/src/context-mode/knowledge/store.ts +255 -11
  69. package/src/context-mode/memory-store.ts +325 -0
  70. package/src/context-mode/metrics-recorder.ts +158 -0
  71. package/src/context-mode/metrics-store.ts +765 -0
  72. package/src/context-mode/model.ts +24 -0
  73. package/src/context-mode/processor-keys.ts +29 -0
  74. package/src/context-mode/processors/build.ts +66 -0
  75. package/src/context-mode/processors/docker.ts +57 -0
  76. package/src/context-mode/processors/git.ts +111 -0
  77. package/src/context-mode/processors/json.ts +112 -0
  78. package/src/context-mode/processors/k8s.ts +67 -0
  79. package/src/context-mode/processors/lint.ts +67 -0
  80. package/src/context-mode/processors/log.ts +86 -0
  81. package/src/context-mode/processors/registry.ts +116 -0
  82. package/src/context-mode/processors/test-runner.ts +102 -0
  83. package/src/context-mode/processors/types.ts +20 -0
  84. package/src/context-mode/repomap.ts +400 -0
  85. package/src/context-mode/routing.ts +97 -24
  86. package/src/context-mode/sandbox/runners.ts +5 -1
  87. package/src/context-mode/snapshot-builder.ts +106 -11
  88. package/src/context-mode/source-hash.ts +173 -0
  89. package/src/context-mode/tool-name.ts +11 -0
  90. package/src/context-mode/tools.ts +654 -22
  91. package/src/context-mode/web/fetcher.ts +31 -12
  92. package/src/debug/logger.ts +2 -1
  93. package/src/deps/registry.ts +1 -1
  94. package/src/discipline/failure-summarizer.ts +170 -0
  95. package/src/discipline/failure-taxonomy.ts +131 -0
  96. package/src/discipline/workflow-invariants.ts +125 -0
  97. package/src/discovery/index.ts +31 -0
  98. package/src/discovery/lsp.ts +87 -0
  99. package/src/discovery/rank.ts +144 -0
  100. package/src/discovery/sources.ts +89 -0
  101. package/src/discovery/workflow.ts +87 -0
  102. package/src/docs/contracts.ts +39 -0
  103. package/src/docs/drift.ts +117 -87
  104. package/src/fix-pr/assessment.ts +200 -0
  105. package/src/fix-pr/contracts.ts +47 -0
  106. package/src/fix-pr/fetch-comments.ts +80 -0
  107. package/src/fix-pr/prompt-builder.ts +58 -40
  108. package/src/fix-pr/scripts/exec.ts +34 -0
  109. package/src/fix-pr/scripts/trigger-review.ts +106 -0
  110. package/src/fix-pr/scripts/wait-and-check.ts +108 -0
  111. package/src/fix-pr/types.ts +4 -0
  112. package/src/git/branch-finish.ts +5 -0
  113. package/src/git/commit-contract.ts +83 -0
  114. package/src/git/commit.ts +121 -184
  115. package/src/git/status.ts +62 -8
  116. package/src/harness/anti_slop/architecture-parser.ts +210 -0
  117. package/src/harness/anti_slop/backend-factory.ts +30 -0
  118. package/src/harness/anti_slop/backend.ts +140 -0
  119. package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
  120. package/src/harness/anti_slop/fallow-adapter.ts +305 -0
  121. package/src/harness/anti_slop/installer.ts +227 -0
  122. package/src/harness/anti_slop/queue.ts +216 -0
  123. package/src/harness/anti_slop/recommend.ts +84 -0
  124. package/src/harness/anti_slop/score.ts +180 -0
  125. package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
  126. package/src/harness/artifacts/agents-md.ts +88 -0
  127. package/src/harness/artifacts/checks-wiring.ts +57 -0
  128. package/src/harness/artifacts/docs-tree.ts +79 -0
  129. package/src/harness/artifacts/lint-configs.ts +136 -0
  130. package/src/harness/artifacts/review-agents.ts +67 -0
  131. package/src/harness/bare-entry.ts +108 -0
  132. package/src/harness/command.ts +1010 -0
  133. package/src/harness/default-agents/design.md +23 -0
  134. package/src/harness/default-agents/discover.md +18 -0
  135. package/src/harness/default-agents/implement.md +24 -0
  136. package/src/harness/default-agents/plan.md +19 -0
  137. package/src/harness/default-agents/research.md +21 -0
  138. package/src/harness/default-agents/validate.md +22 -0
  139. package/src/harness/gc/reporter.ts +28 -0
  140. package/src/harness/gc/runner.ts +136 -0
  141. package/src/harness/hooks/layer-context-inject.ts +155 -0
  142. package/src/harness/hooks/post-session-sweep.ts +130 -0
  143. package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
  144. package/src/harness/hooks/register.ts +118 -0
  145. package/src/harness/model.ts +117 -0
  146. package/src/harness/pipeline.ts +348 -0
  147. package/src/harness/project-paths.ts +235 -0
  148. package/src/harness/stage-runner.ts +107 -0
  149. package/src/harness/stages/design.ts +386 -0
  150. package/src/harness/stages/discover.ts +454 -0
  151. package/src/harness/stages/implement.ts +162 -0
  152. package/src/harness/stages/plan.ts +335 -0
  153. package/src/harness/stages/research.ts +263 -0
  154. package/src/harness/stages/validate.ts +684 -0
  155. package/src/harness/storage.ts +467 -0
  156. package/src/harness/tools.ts +426 -0
  157. package/src/lsp/bridge.ts +56 -95
  158. package/src/lsp/capabilities.ts +108 -0
  159. package/src/lsp/contracts.ts +35 -0
  160. package/src/lsp/detector.ts +8 -12
  161. package/src/markdown-frontmatter.ts +68 -0
  162. package/src/mempalace/bridge.ts +129 -0
  163. package/src/mempalace/config.ts +75 -0
  164. package/src/mempalace/format.ts +163 -0
  165. package/src/mempalace/hooks.ts +370 -0
  166. package/src/mempalace/installer-helper.ts +194 -0
  167. package/src/mempalace/python/mempalace_bridge.py +440 -0
  168. package/src/mempalace/runtime.ts +565 -0
  169. package/src/mempalace/schema.ts +264 -0
  170. package/src/mempalace/session-summary.ts +198 -0
  171. package/src/mempalace/tool.ts +186 -0
  172. package/src/mempalace/uv.ts +256 -0
  173. package/src/migrate/runner.ts +354 -0
  174. package/src/planning/approval-flow.ts +206 -9
  175. package/src/planning/plan-writer-prompt.ts +4 -3
  176. package/src/planning/planning-ask-tool.ts +39 -0
  177. package/src/planning/render-markdown.ts +74 -0
  178. package/src/planning/spec.ts +42 -0
  179. package/src/planning/system-prompt.ts +11 -8
  180. package/src/planning/validate.ts +84 -0
  181. package/src/platform/omp.ts +15 -2
  182. package/src/platform/system-prompt.ts +37 -0
  183. package/src/platform/test-utils.ts +3 -0
  184. package/src/platform/types.ts +6 -1
  185. package/src/qa/config.ts +12 -6
  186. package/src/qa/detect-app-type.ts +13 -6
  187. package/src/qa/matrix.ts +12 -6
  188. package/src/qa/prompt-builder.ts +28 -30
  189. package/src/qa/scripts/dev-server-utils.ts +72 -0
  190. package/src/qa/scripts/run-e2e-tests.ts +226 -0
  191. package/src/qa/scripts/start-dev-server.ts +138 -0
  192. package/src/qa/scripts/stop-dev-server.ts +77 -0
  193. package/src/qa/session.ts +13 -7
  194. package/src/quality/ai-setup.ts +27 -25
  195. package/src/quality/contracts.ts +34 -0
  196. package/src/quality/gates/ai-review.ts +20 -58
  197. package/src/quality/gates/command.ts +249 -46
  198. package/src/quality/review-gates.ts +18 -2
  199. package/src/quality/runner.ts +63 -22
  200. package/src/quality/schemas.ts +37 -2
  201. package/src/quality/setup.ts +96 -16
  202. package/src/release/changelog.ts +1 -1
  203. package/src/release/channels/custom.ts +13 -3
  204. package/src/release/channels/types.ts +5 -0
  205. package/src/release/contracts.ts +90 -0
  206. package/src/release/executor.ts +122 -45
  207. package/src/release/prompt.ts +18 -2
  208. package/src/release/targets.ts +86 -0
  209. package/src/release/version.ts +96 -71
  210. package/src/review/agent-loader.ts +221 -109
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +114 -13
  213. package/src/review/output.ts +12 -139
  214. package/src/review/runner.ts +12 -6
  215. package/src/review/scope.ts +144 -24
  216. package/src/review/types.ts +1 -20
  217. package/src/review/validator.ts +12 -6
  218. package/src/storage/fix-pr-sessions.ts +21 -14
  219. package/src/storage/plans.ts +14 -5
  220. package/src/storage/qa-sessions.ts +25 -19
  221. package/src/storage/reliability-metrics.ts +180 -0
  222. package/src/storage/reports.ts +8 -7
  223. package/src/storage/review-sessions.ts +55 -20
  224. package/src/tool-catalog/active-tool-controller.ts +164 -0
  225. package/src/tool-catalog/active-tool-planner.ts +212 -0
  226. package/src/tool-catalog/tool-groups.ts +102 -0
  227. package/src/types.ts +1399 -5
  228. package/src/ui-design/backend-adapter.ts +78 -0
  229. package/src/ui-design/backends/local-html.ts +82 -0
  230. package/src/ui-design/backends/pencil-mcp.ts +111 -0
  231. package/src/ui-design/components-scanner.ts +124 -0
  232. package/src/ui-design/config.ts +55 -0
  233. package/src/ui-design/pen-scanner.ts +95 -0
  234. package/src/ui-design/pen-selector.ts +72 -0
  235. package/src/ui-design/prompt-builder.ts +73 -0
  236. package/src/ui-design/scanner.ts +136 -0
  237. package/src/ui-design/session.ts +974 -0
  238. package/src/ui-design/system-prompt.ts +312 -0
  239. package/src/ui-design/tokens-scanner.ts +181 -0
  240. package/src/ui-design/types.ts +96 -0
  241. package/src/ultraplan/agent-catalog.ts +522 -0
  242. package/src/ultraplan/authoring/agent-catalog.ts +310 -0
  243. package/src/ultraplan/authoring/authoring-tools.ts +552 -0
  244. package/src/ultraplan/authoring/command-handlers.ts +339 -0
  245. package/src/ultraplan/authoring/markdown.ts +510 -0
  246. package/src/ultraplan/authoring/model.ts +162 -0
  247. package/src/ultraplan/authoring/pipeline.ts +319 -0
  248. package/src/ultraplan/authoring/stage-runner.ts +141 -0
  249. package/src/ultraplan/authoring/stages/approve.ts +249 -0
  250. package/src/ultraplan/authoring/stages/discover.ts +289 -0
  251. package/src/ultraplan/authoring/stages/intake.ts +203 -0
  252. package/src/ultraplan/authoring/stages/research.ts +399 -0
  253. package/src/ultraplan/authoring/stages/review.ts +333 -0
  254. package/src/ultraplan/authoring/stages/scout.ts +188 -0
  255. package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
  256. package/src/ultraplan/authoring/storage.ts +594 -0
  257. package/src/ultraplan/authoring/synth-gate.ts +165 -0
  258. package/src/ultraplan/authoring-draft.ts +653 -0
  259. package/src/ultraplan/authoring-persist.ts +180 -0
  260. package/src/ultraplan/authoring-tool.ts +608 -0
  261. package/src/ultraplan/authoring-wizard.ts +587 -0
  262. package/src/ultraplan/batch/merge.ts +98 -0
  263. package/src/ultraplan/batch/planner.ts +150 -0
  264. package/src/ultraplan/batch/presenter.ts +97 -0
  265. package/src/ultraplan/batch/storage.ts +420 -0
  266. package/src/ultraplan/batch/supervisor.ts +317 -0
  267. package/src/ultraplan/batch/worker.ts +26 -0
  268. package/src/ultraplan/batch/worktree.ts +110 -0
  269. package/src/ultraplan/contracts.ts +1593 -0
  270. package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
  271. package/src/ultraplan/default-agents/authoring/intake.md +12 -0
  272. package/src/ultraplan/default-agents/authoring/planner.md +12 -0
  273. package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
  274. package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
  275. package/src/ultraplan/default-agents/authoring/scout.md +12 -0
  276. package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
  277. package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
  278. package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
  279. package/src/ultraplan/default-agents/backend-executor.md +10 -0
  280. package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
  281. package/src/ultraplan/default-agents/backend-tester.md +10 -0
  282. package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
  283. package/src/ultraplan/default-agents/frontend-executor.md +10 -0
  284. package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
  285. package/src/ultraplan/default-agents/frontend-tester.md +10 -0
  286. package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
  287. package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
  288. package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
  289. package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
  290. package/src/ultraplan/execution/contract.ts +71 -0
  291. package/src/ultraplan/execution/policy.ts +217 -0
  292. package/src/ultraplan/execution/runtime-tools.ts +107 -0
  293. package/src/ultraplan/execution/session-runner.ts +281 -0
  294. package/src/ultraplan/next-router.ts +85 -0
  295. package/src/ultraplan/presenter.ts +359 -0
  296. package/src/ultraplan/project-paths.ts +342 -0
  297. package/src/ultraplan/runtime/active-execution.ts +72 -0
  298. package/src/ultraplan/runtime/apply-mutation.ts +416 -0
  299. package/src/ultraplan/runtime/blockers.ts +243 -0
  300. package/src/ultraplan/runtime/hook-bridge.ts +486 -0
  301. package/src/ultraplan/runtime/launch-context.ts +207 -0
  302. package/src/ultraplan/runtime/migration.ts +524 -0
  303. package/src/ultraplan/runtime/normalize.ts +281 -0
  304. package/src/ultraplan/runtime/proof.ts +260 -0
  305. package/src/ultraplan/runtime/reducer.ts +416 -0
  306. package/src/ultraplan/runtime/repair.ts +251 -0
  307. package/src/ultraplan/runtime/tracker-storage.ts +368 -0
  308. package/src/ultraplan/session-selection.ts +291 -0
  309. package/src/ultraplan/storage.ts +374 -0
  310. package/src/utils/editor.ts +38 -0
  311. package/src/utils/executable.ts +80 -0
  312. package/src/utils/paths.ts +1 -20
  313. package/src/utils/shell.ts +31 -0
  314. package/src/visual/companion.ts +2 -1
  315. package/src/visual/scripts/frame-template.html +60 -0
  316. package/src/visual/scripts/index.js +59 -13
  317. package/src/visual/scripts/package.json +3 -0
  318. package/src/visual/start-server.ts +2 -1
  319. package/src/workspace/git-scope.ts +64 -0
  320. package/src/workspace/locks.ts +23 -0
  321. package/src/workspace/package-manager.ts +117 -0
  322. package/src/workspace/path-mapping.ts +75 -0
  323. package/src/workspace/project-slug.ts +92 -0
  324. package/src/workspace/repo-root.ts +137 -0
  325. package/src/workspace/selector.ts +115 -0
  326. package/src/workspace/state-paths.ts +118 -0
  327. package/src/workspace/targets.ts +313 -0
  328. package/src/fix-pr/scripts/diff-comments.sh +0 -33
  329. package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
  330. package/src/fix-pr/scripts/trigger-review.sh +0 -36
  331. package/src/fix-pr/scripts/wait-and-check.sh +0 -37
  332. package/src/qa/scripts/detect-app-type.sh +0 -68
  333. package/src/qa/scripts/discover-routes.sh +0 -143
  334. package/src/qa/scripts/run-e2e-tests.sh +0 -131
  335. package/src/qa/scripts/start-dev-server.sh +0 -46
  336. package/src/qa/scripts/stop-dev-server.sh +0 -36
  337. package/src/review/prompts/fix-output-schema.md +0 -18
  338. package/src/review/prompts/review-output-schema.md +0 -38
  339. package/src/review/template.ts +0 -15
  340. /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
@@ -0,0 +1,416 @@
1
+ import type {
2
+ UltraPlanAttemptRecord,
3
+ UltraPlanBlockerCandidate,
4
+ UltraPlanCursor,
5
+ UltraPlanHookObservation,
6
+ UltraPlanMutationPlan,
7
+ UltraPlanProofCandidate,
8
+ UltraPlanReducerAction,
9
+ UltraPlanRuntimeTracker,
10
+ UltraPlanScenarioStatus,
11
+ } from "../../types.js";
12
+ import {
13
+ buildConflictingEvidenceBlocker,
14
+ buildInterruptedAttemptBlocker,
15
+ buildProofMissingBlocker,
16
+ buildUnsafeRepairRequiredBlocker,
17
+ } from "./blockers.js";
18
+
19
+ /**
20
+ * Slice-2 pure reducer.
21
+ *
22
+ * Decides, given the current state and a validated action, what the persisted mutation should
23
+ * be. The reducer performs no I/O; its output is consumed by the hook bridge's
24
+ * `applyMutationPlan` seam which runs the durability order. See approved spec §reducer outcome
25
+ * precedence (lines 562–572) and §transition rules.
26
+ */
27
+
28
+ export interface ReducerState {
29
+ tracker: UltraPlanRuntimeTracker;
30
+ cursor: UltraPlanCursor | null;
31
+ }
32
+
33
+ export function reduce(state: ReducerState, action: UltraPlanReducerAction): UltraPlanMutationPlan {
34
+ if (state.cursor?.targetType === "session" && state.cursor.status === "complete") {
35
+ return buildPlan({
36
+ kind: "complete",
37
+ rationale: "cursor already resolved to session complete",
38
+ cursorUpdate: state.cursor,
39
+ sessionStateUpdate: "complete",
40
+ });
41
+ }
42
+
43
+ switch (action.kind) {
44
+ case "session_started":
45
+ return buildPlan({ kind: "noop", rationale: "session_started observed; repair runs separately" });
46
+ case "attempt_started":
47
+ return reduceAttemptStarted(state, action);
48
+ case "observation_staged":
49
+ return reduceObservationStaged(state, action);
50
+ case "attempt_finalized":
51
+ return reduceAttemptFinalized(state, action);
52
+ case "session_shutdown":
53
+ return reduceSessionShutdown(state, action);
54
+ case "repair_applied":
55
+ return buildPlan({
56
+ kind: "repair",
57
+ rationale: action.details.reason,
58
+ repairActions: action.details.actions,
59
+ });
60
+ }
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // attempt_started
65
+ // ---------------------------------------------------------------------------
66
+
67
+ function reduceAttemptStarted(
68
+ state: ReducerState,
69
+ action: Extract<UltraPlanReducerAction, { kind: "attempt_started" }>,
70
+ ): UltraPlanMutationPlan {
71
+ const { observation } = action;
72
+
73
+ if (alreadyApplied(state.tracker, observation.fingerprint)) {
74
+ return buildPlan({
75
+ kind: "noop",
76
+ rationale: `attempt_started observation ${observation.fingerprint} already applied`,
77
+ });
78
+ }
79
+
80
+ if (state.tracker.activeAttempt) {
81
+ return buildPlan({
82
+ kind: "block",
83
+ rationale: `nested before_agent_start observed while attempt ${state.tracker.activeAttempt.attemptId} is still active`,
84
+ blockerUpdate: {
85
+ scope: "session",
86
+ nextValue: buildUnsafeRepairRequiredBlocker({
87
+ detectedAt: observation.occurredAt,
88
+ scope: "session",
89
+ reason: `active attempt ${state.tracker.activeAttempt.attemptId} must finalize before a new attempt starts`,
90
+ }),
91
+ clearedByObservationFingerprint: null,
92
+ },
93
+ sessionStateUpdate: "blocked",
94
+ appendObservationFingerprint: observation.fingerprint,
95
+ });
96
+ }
97
+
98
+ const cursor = state.cursor;
99
+ if (!cursor || !isLegalStartFromCursor(cursor)) {
100
+ return buildPlan({
101
+ kind: "block",
102
+ rationale: "attempt_started from a cursor that has no legal-start transition",
103
+ blockerUpdate: {
104
+ scope: "session",
105
+ nextValue: buildUnsafeRepairRequiredBlocker({
106
+ detectedAt: observation.occurredAt,
107
+ scope: "session",
108
+ reason: `cursor status ${cursor?.status ?? "null"} has no legal-start transition`,
109
+ }),
110
+ clearedByObservationFingerprint: null,
111
+ },
112
+ sessionStateUpdate: "blocked",
113
+ appendObservationFingerprint: observation.fingerprint,
114
+ });
115
+ }
116
+
117
+ const nextStatus = legalStartNextStatus(cursor);
118
+ return buildPlan({
119
+ kind: "start-attempt",
120
+ rationale: `legal start: ${cursor.status} -> ${nextStatus}`,
121
+ cursorUpdate: { ...cursor, status: nextStatus, phase: phaseForStatus(nextStatus, cursor.phase) },
122
+ sessionStateUpdate: "running",
123
+ appendObservationFingerprint: observation.fingerprint,
124
+ });
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // observation_staged
129
+ // ---------------------------------------------------------------------------
130
+
131
+ function reduceObservationStaged(
132
+ state: ReducerState,
133
+ action: Extract<UltraPlanReducerAction, { kind: "observation_staged" }>,
134
+ ): UltraPlanMutationPlan {
135
+ const { observation } = action;
136
+ if (alreadyApplied(state.tracker, observation.fingerprint)) {
137
+ return buildPlan({ kind: "noop", rationale: "observation already applied" });
138
+ }
139
+ return buildPlan({
140
+ kind: "stage-observation",
141
+ rationale: `stage observation ${observation.fingerprint}`,
142
+ appendObservationFingerprint: observation.fingerprint,
143
+ });
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // attempt_finalized (precedence: conflicting -> proof -> blocker -> interrupted -> noop)
148
+ // ---------------------------------------------------------------------------
149
+
150
+ function reduceAttemptFinalized(
151
+ state: ReducerState,
152
+ action: Extract<UltraPlanReducerAction, { kind: "attempt_finalized" }>,
153
+ ): UltraPlanMutationPlan {
154
+ const { observation, nowIso } = action;
155
+ if (alreadyApplied(state.tracker, observation.fingerprint)) {
156
+ return buildPlan({ kind: "noop", rationale: "attempt_finalized already applied" });
157
+ }
158
+
159
+ const active = state.tracker.activeAttempt;
160
+ if (!active) {
161
+ return buildPlan({
162
+ kind: "noop",
163
+ rationale: "attempt_finalized observed without an active attempt; treated as replay",
164
+ });
165
+ }
166
+
167
+ const proofs = active.proofCandidates;
168
+ const blockers = active.blockerCandidates;
169
+ const cursor = state.cursor;
170
+
171
+ // Rule 1 — conflicting evidence: fail closed.
172
+ if (proofs.length > 0 && blockers.length > 0) {
173
+ return buildPlan({
174
+ kind: "block",
175
+ rationale: "conflicting evidence: proof and blocker in the same attempt finalization",
176
+ blockerUpdate: {
177
+ scope: "scenario",
178
+ nextValue: buildConflictingEvidenceBlocker({
179
+ detectedAt: nowIso,
180
+ scope: "scenario",
181
+ affected: cursor ? toAffected(cursor) : undefined,
182
+ reason: "valid proof and blocker candidate observed in same attempt",
183
+ }),
184
+ clearedByObservationFingerprint: null,
185
+ },
186
+ trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "blocked", finalizedAt: nowIso },
187
+ sessionStateUpdate: "blocked",
188
+ appendObservationFingerprint: observation.fingerprint,
189
+ });
190
+ }
191
+
192
+ // Rule 2 — valid proof for the current target/phase advances scenario status.
193
+ if (proofs.length > 0 && cursor) {
194
+ const proof = pickProofForCursor(proofs, cursor);
195
+ if (proof) {
196
+ const nextStatus = provedStatusForPhase(proof.phase);
197
+ if (nextStatus) {
198
+ return buildPlan({
199
+ kind: "advance",
200
+ rationale: `${proof.phase}-phase proof matched cursor; advancing to ${nextStatus}`,
201
+ scenarioStatusUpdate: {
202
+ stack: cursor.stack!,
203
+ domainId: cursor.domainId!,
204
+ level: cursor.level!,
205
+ scenarioId: cursor.scenarioId!,
206
+ nextStatus,
207
+ appendProof: {
208
+ type: proof.type,
209
+ phase: proof.phase,
210
+ recordedAt: observation.occurredAt,
211
+ actor: observation.target?.resolvedSlot ?? "frontend-executor",
212
+ evidence: proof.evidence,
213
+ artifactRef: proof.artifactRef ?? `artifact://${proof.phase}-${active.attemptId}`,
214
+ },
215
+ },
216
+ cursorUpdate: advancedCursor(cursor, nextStatus),
217
+ trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "advanced", finalizedAt: nowIso },
218
+ recomputeProgress: true,
219
+ appendObservationFingerprint: observation.fingerprint,
220
+ });
221
+ }
222
+ }
223
+ }
224
+
225
+ // Rule 3 — explicit blocker candidate.
226
+ if (blockers.length > 0) {
227
+ const candidate = blockers[0];
228
+ return buildPlan({
229
+ kind: "block",
230
+ rationale: `explicit blocker candidate ${candidate.blocker.code}`,
231
+ blockerUpdate: {
232
+ scope: candidate.blocker.scope,
233
+ nextValue: candidate.blocker,
234
+ clearedByObservationFingerprint: null,
235
+ },
236
+ trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "blocked", finalizedAt: nowIso },
237
+ sessionStateUpdate: candidate.blocker.recoveryMode === "await-user" ? "awaiting-user" : "blocked",
238
+ appendObservationFingerprint: observation.fingerprint,
239
+ });
240
+ }
241
+
242
+ // Rule 4 — no proof, no blocker: interrupted outcome. Do NOT silently advance.
243
+ const interruptedBlocker = buildInterruptedAttemptBlocker({
244
+ detectedAt: nowIso,
245
+ scope: "scenario",
246
+ affected: cursor ? toAffected(cursor) : undefined,
247
+ attemptId: active.attemptId,
248
+ });
249
+ return buildPlan({
250
+ kind: "interrupt",
251
+ rationale: "no proof and no blocker observed; attempt interrupted",
252
+ blockerUpdate: {
253
+ scope: "scenario",
254
+ nextValue: interruptedBlocker,
255
+ clearedByObservationFingerprint: null,
256
+ },
257
+ trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "interrupted", finalizedAt: nowIso },
258
+ sessionStateUpdate: "blocked",
259
+ appendObservationFingerprint: observation.fingerprint,
260
+ notes: [`missing-proof: ${buildProofMissingBlocker({ detectedAt: nowIso, expectedPhase: inferPhase(cursor) }).message}`],
261
+ });
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // session_shutdown
266
+ // ---------------------------------------------------------------------------
267
+
268
+ function reduceSessionShutdown(
269
+ state: ReducerState,
270
+ action: Extract<UltraPlanReducerAction, { kind: "session_shutdown" }>,
271
+ ): UltraPlanMutationPlan {
272
+ const active = state.tracker.activeAttempt;
273
+ if (!active) {
274
+ return buildPlan({ kind: "noop", rationale: "session_shutdown with no active attempt" });
275
+ }
276
+ const interruptedBlocker = buildInterruptedAttemptBlocker({
277
+ detectedAt: action.nowIso,
278
+ scope: "scenario",
279
+ affected: state.cursor ? toAffected(state.cursor) : undefined,
280
+ attemptId: active.attemptId,
281
+ reason: "session shut down with in-flight active attempt",
282
+ });
283
+ return buildPlan({
284
+ kind: "interrupt",
285
+ rationale: `session_shutdown interrupted attempt ${active.attemptId}`,
286
+ blockerUpdate: {
287
+ scope: "scenario",
288
+ nextValue: interruptedBlocker,
289
+ clearedByObservationFingerprint: null,
290
+ },
291
+ trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "interrupted", finalizedAt: action.nowIso },
292
+ sessionStateUpdate: "blocked",
293
+ appendObservationFingerprint: action.observation.fingerprint,
294
+ });
295
+ }
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // helpers
299
+ // ---------------------------------------------------------------------------
300
+
301
+ interface PlanBuilderInput {
302
+ kind: UltraPlanMutationPlan["kind"];
303
+ rationale: string;
304
+ appendObservationFingerprint?: string | null;
305
+ scenarioStatusUpdate?: UltraPlanMutationPlan["scenarioStatusUpdate"];
306
+ reviewStatusUpdate?: UltraPlanMutationPlan["reviewStatusUpdate"];
307
+ blockerUpdate?: UltraPlanMutationPlan["blockerUpdate"];
308
+ cursorUpdate?: UltraPlanMutationPlan["cursorUpdate"];
309
+ sessionStateUpdate?: UltraPlanMutationPlan["sessionStateUpdate"];
310
+ trackerAttemptFinalization?: UltraPlanMutationPlan["trackerAttemptFinalization"];
311
+ recomputeProgress?: boolean;
312
+ repairActions?: UltraPlanMutationPlan["repairActions"];
313
+ notes?: string[];
314
+ }
315
+
316
+ function buildPlan(input: PlanBuilderInput): UltraPlanMutationPlan {
317
+ return {
318
+ kind: input.kind,
319
+ rationale: input.rationale,
320
+ appendObservationFingerprint: input.appendObservationFingerprint ?? null,
321
+ scenarioStatusUpdate: input.scenarioStatusUpdate ?? null,
322
+ reviewStatusUpdate: input.reviewStatusUpdate ?? null,
323
+ blockerUpdate: input.blockerUpdate ?? null,
324
+ cursorUpdate: input.cursorUpdate ?? null,
325
+ sessionStateUpdate: input.sessionStateUpdate ?? null,
326
+ trackerAttemptFinalization: input.trackerAttemptFinalization ?? null,
327
+ recomputeProgress: input.recomputeProgress ?? false,
328
+ repairActions: input.repairActions ?? [],
329
+ notes: input.notes ?? [],
330
+ };
331
+ }
332
+
333
+ function alreadyApplied(tracker: UltraPlanRuntimeTracker, fingerprint: string): boolean {
334
+ return tracker.appliedFingerprints.includes(fingerprint);
335
+ }
336
+
337
+ function isLegalStartFromCursor(cursor: UltraPlanCursor): boolean {
338
+ // Legal-start transitions per spec §transition classes:
339
+ // planned -> red-running
340
+ // red-proved -> green-running
341
+ // review pending -> running
342
+ if (cursor.targetType === "scenario") {
343
+ return cursor.status === "planned" || cursor.status === "red-proved";
344
+ }
345
+ if (cursor.targetType === "domain-review" || cursor.targetType === "stack-review") {
346
+ return cursor.status === "pending";
347
+ }
348
+ return false;
349
+ }
350
+
351
+ function legalStartNextStatus(cursor: UltraPlanCursor): UltraPlanCursor["status"] {
352
+ if (cursor.targetType === "scenario") {
353
+ if (cursor.status === "planned") return "red-running";
354
+ if (cursor.status === "red-proved") return "green-running";
355
+ }
356
+ if (cursor.targetType === "domain-review" || cursor.targetType === "stack-review") {
357
+ return "running";
358
+ }
359
+ throw new Error(`legalStartNextStatus called with unsupported cursor status ${cursor.status}`);
360
+ }
361
+
362
+ function phaseForStatus(status: UltraPlanCursor["status"], fallback: UltraPlanCursor["phase"]): UltraPlanCursor["phase"] {
363
+ switch (status) {
364
+ case "red-running":
365
+ return "red";
366
+ case "green-running":
367
+ return "green";
368
+ case "red-proved":
369
+ return "green";
370
+ case "green-proved":
371
+ return "complete";
372
+ case "running":
373
+ return "review";
374
+ default:
375
+ return fallback;
376
+ }
377
+ }
378
+
379
+ function provedStatusForPhase(phase: UltraPlanProofCandidate["phase"]): UltraPlanScenarioStatus | null {
380
+ if (phase === "red") return "red-proved";
381
+ if (phase === "green") return "green-proved";
382
+ return null;
383
+ }
384
+
385
+ function advancedCursor(cursor: UltraPlanCursor, nextStatus: UltraPlanScenarioStatus): UltraPlanCursor {
386
+ return { ...cursor, status: nextStatus, phase: phaseForStatus(nextStatus, cursor.phase) };
387
+ }
388
+
389
+ function pickProofForCursor(proofs: UltraPlanProofCandidate[], cursor: UltraPlanCursor): UltraPlanProofCandidate | null {
390
+ const expectedPhase = inferPhase(cursor);
391
+ for (const proof of proofs) {
392
+ if (proof.phase !== expectedPhase) continue;
393
+ if (proof.target.stack !== cursor.stack) continue;
394
+ if (proof.target.domainId !== cursor.domainId) continue;
395
+ if (proof.target.level !== cursor.level) continue;
396
+ if (proof.target.scenarioId !== cursor.scenarioId) continue;
397
+ return proof;
398
+ }
399
+ return null;
400
+ }
401
+
402
+ function inferPhase(cursor: UltraPlanCursor | null): UltraPlanCursor["phase"] {
403
+ if (!cursor) return "red";
404
+ if (cursor.status === "red-running") return "red";
405
+ if (cursor.status === "green-running") return "green";
406
+ return cursor.phase;
407
+ }
408
+
409
+ function toAffected(cursor: UltraPlanCursor) {
410
+ return {
411
+ stack: cursor.stack,
412
+ domainId: cursor.domainId,
413
+ level: cursor.level,
414
+ scenarioId: cursor.scenarioId,
415
+ };
416
+ }
@@ -0,0 +1,251 @@
1
+ import type {
2
+ UltraPlanAttemptRecord,
3
+ UltraPlanBlocker,
4
+ UltraPlanManifest,
5
+ UltraPlanPendingMutation,
6
+ UltraPlanRepairAction,
7
+ UltraPlanRuntimeTracker,
8
+ } from "../../types.js";
9
+ import { buildUnsafeRepairRequiredBlocker } from "./blockers.js";
10
+
11
+ /**
12
+ * Slice-2 deterministic repair engine. Pure.
13
+ *
14
+ * Safe auto-repair categories (spec §safe auto-repair boundaries, lines 707–723):
15
+ * - recompute cursor
16
+ * - recompute progress summaries
17
+ * - clear impossible active-attempt references
18
+ * - convert orphaned in-flight attempts into interrupted state
19
+ * - clear blockers directly invalidated by later proof on the same target
20
+ *
21
+ * Forbidden: inventing proof, promoting a target to terminal without evidence, discarding
22
+ * conflicting evidence, skipping ahead, or rewriting authored intent. When repair would require
23
+ * any of those, the engine emits an `unsafe-repair-required` blocker instead of a mutation.
24
+ */
25
+
26
+ export interface RepairState {
27
+ tracker: UltraPlanRuntimeTracker;
28
+ manifest: UltraPlanManifest | null;
29
+ }
30
+
31
+ export interface RepairPlan {
32
+ /** Deterministic actions the caller applies to the tracker / derived state. */
33
+ actions: UltraPlanRepairAction[];
34
+ /** Blockers emitted by the repair engine when deterministic recovery is unsafe. */
35
+ emittedBlockers: UltraPlanBlocker[];
36
+ /** What the caller should do with the tracker's `activeAttempt`. */
37
+ activeAttemptAction: "leave" | "clear" | "finalize-as-interrupted";
38
+ /** Resume-time handling for a staged pending mutation. */
39
+ pendingMutationAction: "leave" | "clear" | "apply-and-clear";
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // session_start
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export function repairOnSessionStart(state: RepairState, nowIso: string): RepairPlan {
47
+ const actions: UltraPlanRepairAction[] = [];
48
+ const emittedBlockers: UltraPlanBlocker[] = [];
49
+ let activeAttemptAction: RepairPlan["activeAttemptAction"] = "leave";
50
+ let pendingMutationAction: RepairPlan["pendingMutationAction"] = "leave";
51
+
52
+ const active = state.tracker.activeAttempt;
53
+ if (active) {
54
+ const hasProof = active.proofCandidates.length > 0;
55
+ const hasBlocker = active.blockerCandidates.length > 0;
56
+
57
+ if (hasProof && hasBlocker) {
58
+ emittedBlockers.push(buildUnsafeRepairRequiredBlocker({
59
+ detectedAt: nowIso,
60
+ scope: "scenario",
61
+ affected: {
62
+ stack: active.cursorSnapshot?.stack ?? null,
63
+ domainId: active.cursorSnapshot?.domainId ?? null,
64
+ level: active.cursorSnapshot?.level ?? null,
65
+ scenarioId: active.cursorSnapshot?.scenarioId ?? null,
66
+ },
67
+ reason: `attempt ${active.attemptId} carries both proof and blocker candidates; cannot auto-finalize`,
68
+ }));
69
+ actions.push({
70
+ op: "convert-active-to-interrupted",
71
+ attemptId: active.attemptId,
72
+ reason: "conflicting proof+blocker on resume",
73
+ });
74
+ activeAttemptAction = "finalize-as-interrupted";
75
+ } else {
76
+ actions.push({
77
+ op: "convert-active-to-interrupted",
78
+ attemptId: active.attemptId,
79
+ reason: "orphaned in-flight attempt recovered on session_start",
80
+ });
81
+ activeAttemptAction = "finalize-as-interrupted";
82
+ }
83
+ }
84
+
85
+ const pending = reconcilePendingMutation(state, nowIso);
86
+ pendingMutationAction = pending.pendingMutationAction;
87
+ emittedBlockers.push(...pending.emittedBlockers);
88
+
89
+ const manifestBlocker = state.manifest?.blocker ?? null;
90
+ if (manifestBlocker) {
91
+ const clearedBy = findLaterProofForBlocker(state.tracker, manifestBlocker);
92
+ if (clearedBy) {
93
+ actions.push({
94
+ op: "clear-blocker",
95
+ scope: manifestBlocker.scope,
96
+ clearedByObservationFingerprint: clearedBy,
97
+ });
98
+ }
99
+ }
100
+
101
+ return { actions, emittedBlockers, activeAttemptAction, pendingMutationAction };
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // session_shutdown
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export function repairOnSessionShutdown(state: RepairState, nowIso: string): RepairPlan {
109
+ const actions: UltraPlanRepairAction[] = [];
110
+ let activeAttemptAction: RepairPlan["activeAttemptAction"] = "leave";
111
+
112
+ const active = state.tracker.activeAttempt;
113
+ if (active) {
114
+ actions.push({
115
+ op: "convert-active-to-interrupted",
116
+ attemptId: active.attemptId,
117
+ reason: `session shutdown with in-flight attempt (recorded at ${nowIso})`,
118
+ });
119
+ activeAttemptAction = "finalize-as-interrupted";
120
+ }
121
+
122
+ return { actions, emittedBlockers: [], activeAttemptAction, pendingMutationAction: "leave" };
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // helpers
127
+ // ---------------------------------------------------------------------------
128
+
129
+ function findLaterProofForBlocker(tracker: UltraPlanRuntimeTracker, blocker: UltraPlanBlocker): string | null {
130
+ const affected = blocker.affected;
131
+ for (const attempt of tracker.finalizedAttempts) {
132
+ if (attempt.outcome !== "advanced") continue;
133
+ const proof = attempt.proofCandidates.find((p) =>
134
+ p.target.stack === affected.stack
135
+ && p.target.domainId === affected.domainId
136
+ && p.target.level === affected.level
137
+ && p.target.scenarioId === affected.scenarioId,
138
+ );
139
+ if (proof && isLaterThan(attempt.finalizedAt, blocker.detectedAt)) {
140
+ return proof.observationFingerprint;
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+
146
+ function isLaterThan(candidate: string | null, reference: string): boolean {
147
+ if (!candidate) return false;
148
+ const a = Date.parse(candidate);
149
+ const b = Date.parse(reference);
150
+ if (Number.isNaN(a) || Number.isNaN(b)) return false;
151
+ return a > b;
152
+ }
153
+
154
+ function reconcilePendingMutation(
155
+ state: RepairState,
156
+ nowIso: string,
157
+ ): Pick<RepairPlan, "pendingMutationAction" | "emittedBlockers"> {
158
+ const pending = state.tracker.pendingMutation;
159
+ if (!pending) {
160
+ return { pendingMutationAction: "leave", emittedBlockers: [] };
161
+ }
162
+
163
+ if (!state.manifest) {
164
+ return {
165
+ pendingMutationAction: "leave",
166
+ emittedBlockers: [buildUnsafeRepairRequiredBlocker({
167
+ detectedAt: nowIso,
168
+ scope: "session",
169
+ reason: `pending mutation ${pending.attemptId} cannot be reconciled without a manifest snapshot`,
170
+ })],
171
+ };
172
+ }
173
+
174
+ if (isPendingMutationReflected(state.manifest, pending)) {
175
+ return { pendingMutationAction: "clear", emittedBlockers: [] };
176
+ }
177
+
178
+ if (canSafelyApplyPendingMutation(pending)) {
179
+ return { pendingMutationAction: "apply-and-clear", emittedBlockers: [] };
180
+ }
181
+
182
+ return {
183
+ pendingMutationAction: "leave",
184
+ emittedBlockers: [buildUnsafeRepairRequiredBlocker({
185
+ detectedAt: nowIso,
186
+ scope: "session",
187
+ reason: `pending mutation ${pending.attemptId} still requires canonical authored/review reconciliation`,
188
+ })],
189
+ };
190
+ }
191
+
192
+ function isPendingMutationReflected(manifest: UltraPlanManifest, pending: UltraPlanPendingMutation): boolean {
193
+ const mutationPlan = pending.mutationPlan;
194
+
195
+ if (mutationPlan.scenarioStatusUpdate || mutationPlan.recomputeProgress) {
196
+ return false;
197
+ }
198
+
199
+ const checks: boolean[] = [];
200
+
201
+ if (mutationPlan.blockerUpdate) {
202
+ const nextValue = mutationPlan.blockerUpdate.nextValue;
203
+ checks.push(nextValue === null
204
+ ? manifest.blocker === null
205
+ : JSON.stringify(manifest.blocker) === JSON.stringify(nextValue));
206
+ }
207
+
208
+ if (mutationPlan.cursorUpdate) {
209
+ checks.push(sameCursor(manifest.cursor, mutationPlan.cursorUpdate));
210
+ }
211
+
212
+ if (mutationPlan.sessionStateUpdate) {
213
+ checks.push(manifest.state === mutationPlan.sessionStateUpdate);
214
+ }
215
+
216
+ if (mutationPlan.reviewStatusUpdate) {
217
+ const review = manifest.reviews.find((candidate) =>
218
+ candidate.type === mutationPlan.reviewStatusUpdate?.type
219
+ && candidate.stack === mutationPlan.reviewStatusUpdate.stack
220
+ && candidate.domainId === mutationPlan.reviewStatusUpdate.domainId,
221
+ );
222
+ checks.push(
223
+ review?.status === mutationPlan.reviewStatusUpdate.nextStatus
224
+ && review.path === mutationPlan.reviewStatusUpdate.artifactRef,
225
+ );
226
+ }
227
+
228
+ return checks.length > 0 && checks.every(Boolean);
229
+ }
230
+
231
+ function canSafelyApplyPendingMutation(pending: UltraPlanPendingMutation): boolean {
232
+ const mutationPlan = pending.mutationPlan;
233
+ return mutationPlan.scenarioStatusUpdate === null
234
+ && mutationPlan.reviewStatusUpdate === null
235
+ && mutationPlan.recomputeProgress === false;
236
+ }
237
+
238
+ function sameCursor(left: UltraPlanManifest["cursor"], right: UltraPlanManifest["cursor"]): boolean {
239
+ if (!left || !right) {
240
+ return left === right;
241
+ }
242
+
243
+ return left.targetType === right.targetType
244
+ && left.stack === right.stack
245
+ && left.domainId === right.domainId
246
+ && left.level === right.level
247
+ && left.scenarioId === right.scenarioId
248
+ && left.phase === right.phase
249
+ && left.status === right.status
250
+ && left.summary === right.summary;
251
+ }