supipowers 1.5.2 → 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 +149 -45
  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 +19 -9
  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 +298 -127
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +115 -14
  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 +11 -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 +1401 -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,368 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { PlatformPaths } from "../../platform/types.js";
4
+ import type {
5
+ UltraPlanHookObservation,
6
+ UltraPlanPendingMutation,
7
+ UltraPlanRuntimeTracker,
8
+ UltraPlanSessionMigrationRecord,
9
+ UltraPlanStorageError,
10
+ UltraPlanStorageResult,
11
+ } from "../../types.js";
12
+ import {
13
+ validateUltraPlanRuntimeTracker,
14
+ validateUltraPlanSessionMigrationRecord,
15
+ } from "../contracts.js";
16
+ import {
17
+ getUltraplanExecutionLogPath,
18
+ getUltraplanHooksLogPath,
19
+ getUltraplanMigrationRecordPath,
20
+ getUltraplanRuntimeTrackerPath,
21
+ } from "../project-paths.js";
22
+
23
+ /**
24
+ * Slice-2 runtime storage seam.
25
+ *
26
+ * This module owns the durable read/write path for `runtime-tracker.json` and `migration.json`.
27
+ * Task 2.3 will grow this module to additionally own `hooks-log.jsonl`, pendingMutation staging,
28
+ * and reconciliation of partial writes against the manifest. For Slice 2/1.4 it provides the
29
+ * round-trip primitives that the storage wrappers and the migration engine depend on.
30
+ */
31
+
32
+ function success<T>(value: T): UltraPlanStorageResult<T> {
33
+ return { ok: true, value };
34
+ }
35
+
36
+ function failure(
37
+ pathname: string,
38
+ kind: UltraPlanStorageError["kind"],
39
+ message: string,
40
+ details?: string[],
41
+ ): UltraPlanStorageResult<never> {
42
+ return {
43
+ ok: false,
44
+ error: {
45
+ kind,
46
+ path: pathname,
47
+ message,
48
+ ...(details ? { details } : {}),
49
+ },
50
+ };
51
+ }
52
+
53
+ function ensureDir(filePath: string): void {
54
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
55
+ }
56
+
57
+ function readJsonFile(filePath: string): UltraPlanStorageResult<unknown> {
58
+ if (!fs.existsSync(filePath)) {
59
+ return failure(filePath, "missing", `Artifact not found: ${filePath}`);
60
+ }
61
+ try {
62
+ return success(JSON.parse(fs.readFileSync(filePath, "utf8")));
63
+ } catch (error) {
64
+ return failure(
65
+ filePath,
66
+ "invalid-json",
67
+ error instanceof Error ? error.message : `Invalid JSON in ${filePath}`,
68
+ );
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Atomic write: write to a sibling `*.tmp` file, then rename onto the destination. Prevents a
74
+ * half-written tracker from surviving a crash and being observed by the loader.
75
+ */
76
+ function writeJsonAtomic(filePath: string, payload: unknown): UltraPlanStorageResult<string> {
77
+ try {
78
+ ensureDir(filePath);
79
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
80
+ fs.writeFileSync(tmpPath, `${JSON.stringify(payload, null, 2)}\n`);
81
+ fs.renameSync(tmpPath, filePath);
82
+ return success(filePath);
83
+ } catch (error) {
84
+ return failure(
85
+ filePath,
86
+ "io",
87
+ error instanceof Error ? error.message : `Unable to write ${filePath}`,
88
+ );
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Load the runtime tracker for a given session. Missing tracker is a first-class, non-error
94
+ * state — callers treat it as "no prior runtime state."
95
+ */
96
+ export function loadTracker(
97
+ paths: PlatformPaths,
98
+ cwd: string,
99
+ sessionId: string,
100
+ ): UltraPlanStorageResult<UltraPlanRuntimeTracker> {
101
+ const filePath = getUltraplanRuntimeTrackerPath(paths, cwd, sessionId);
102
+ const parsed = readJsonFile(filePath);
103
+ if (!parsed.ok) return parsed;
104
+
105
+ const validation = validateUltraPlanRuntimeTracker(parsed.value);
106
+ if (!validation.ok) {
107
+ return failure(
108
+ filePath,
109
+ "validation-error",
110
+ `Runtime tracker failed schema validation: ${filePath}`,
111
+ validation.errors,
112
+ );
113
+ }
114
+ return success(validation.value);
115
+ }
116
+
117
+ /**
118
+ * Save the runtime tracker atomically after semantic and schema validation. Writes never observe
119
+ * a half-finalized tracker because the destination rename happens in one filesystem step.
120
+ */
121
+ export function saveTrackerAtomic(
122
+ paths: PlatformPaths,
123
+ cwd: string,
124
+ sessionId: string,
125
+ tracker: UltraPlanRuntimeTracker,
126
+ ): UltraPlanStorageResult<string> {
127
+ const filePath = getUltraplanRuntimeTrackerPath(paths, cwd, sessionId);
128
+ // Normalize the applied-fingerprint ledger before validation: the invariant enforced on
129
+ // disk is that it contains no duplicates. Callers may hand in repeated fingerprints from
130
+ // replay flows; we dedupe once here so the persisted tracker stays coherent.
131
+ const normalized: UltraPlanRuntimeTracker = {
132
+ ...tracker,
133
+ appliedFingerprints: dedupeInOrder(tracker.appliedFingerprints),
134
+ };
135
+ const validation = validateUltraPlanRuntimeTracker(normalized);
136
+ if (!validation.ok) {
137
+ return failure(
138
+ filePath,
139
+ "validation-error",
140
+ `Runtime tracker failed schema validation: ${filePath}`,
141
+ validation.errors,
142
+ );
143
+ }
144
+ return writeJsonAtomic(filePath, validation.value);
145
+ }
146
+
147
+ function dedupeInOrder(values: readonly string[]): string[] {
148
+ const seen = new Set<string>();
149
+ const out: string[] = [];
150
+ for (const v of values) {
151
+ if (!seen.has(v)) {
152
+ seen.add(v);
153
+ out.push(v);
154
+ }
155
+ }
156
+ return out;
157
+ }
158
+
159
+ export function loadMigrationRecord(
160
+ paths: PlatformPaths,
161
+ cwd: string,
162
+ sessionId: string,
163
+ ): UltraPlanStorageResult<UltraPlanSessionMigrationRecord> {
164
+ const filePath = getUltraplanMigrationRecordPath(paths, cwd, sessionId);
165
+ const parsed = readJsonFile(filePath);
166
+ if (!parsed.ok) return parsed;
167
+
168
+ const validation = validateUltraPlanSessionMigrationRecord(parsed.value);
169
+ if (!validation.ok) {
170
+ return failure(
171
+ filePath,
172
+ "validation-error",
173
+ `Migration record failed schema validation: ${filePath}`,
174
+ validation.errors,
175
+ );
176
+ }
177
+ return success(validation.value);
178
+ }
179
+
180
+ export function saveMigrationRecord(
181
+ paths: PlatformPaths,
182
+ cwd: string,
183
+ sessionId: string,
184
+ record: UltraPlanSessionMigrationRecord,
185
+ ): UltraPlanStorageResult<string> {
186
+ const filePath = getUltraplanMigrationRecordPath(paths, cwd, sessionId);
187
+ const validation = validateUltraPlanSessionMigrationRecord(record);
188
+ if (!validation.ok) {
189
+ return failure(
190
+ filePath,
191
+ "validation-error",
192
+ `Migration record failed schema validation: ${filePath}`,
193
+ validation.errors,
194
+ );
195
+ }
196
+ return writeJsonAtomic(filePath, validation.value);
197
+ }
198
+
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Hooks log (append-only JSONL)
202
+ // ---------------------------------------------------------------------------
203
+
204
+ /**
205
+ * Append a normalized hook observation to `hooks-log.jsonl`. This is the audit trail the
206
+ * reducer reads on replay. The append is suppressed for observations whose `fingerprint` has
207
+ * already been persisted into the tracker's `appliedFingerprints` set — that is what makes
208
+ * replay a persisted no-op on both the tracker AND the hooks log.
209
+ */
210
+ export function appendHookLog(
211
+ paths: PlatformPaths,
212
+ cwd: string,
213
+ sessionId: string,
214
+ observation: UltraPlanHookObservation,
215
+ ): UltraPlanStorageResult<string> {
216
+ const trackerResult = loadTracker(paths, cwd, sessionId);
217
+ if (trackerResult.ok) {
218
+ if (trackerResult.value.appliedFingerprints.includes(observation.fingerprint)) {
219
+ // Observation already applied; replay is a persisted no-op.
220
+ return success(getUltraplanHooksLogPath(paths, cwd, sessionId));
221
+ }
222
+ } else if (trackerResult.error.kind !== "missing") {
223
+ // Tracker exists but is unreadable; fail closed.
224
+ return trackerResult;
225
+ }
226
+
227
+ const logPath = getUltraplanHooksLogPath(paths, cwd, sessionId);
228
+ try {
229
+ ensureDir(logPath);
230
+ fs.appendFileSync(logPath, `${JSON.stringify(observation)}\n`);
231
+ return success(logPath);
232
+ } catch (error) {
233
+ return failure(
234
+ logPath,
235
+ "io",
236
+ error instanceof Error ? error.message : `Unable to append ${logPath}`,
237
+ );
238
+ }
239
+ }
240
+
241
+ export function appendExecutionLog(
242
+ paths: PlatformPaths,
243
+ cwd: string,
244
+ sessionId: string,
245
+ entry: Record<string, unknown>,
246
+ ): UltraPlanStorageResult<string> {
247
+ const logPath = getUltraplanExecutionLogPath(paths, cwd, sessionId);
248
+ const observationFingerprint = typeof entry.observationFingerprint === "string"
249
+ ? entry.observationFingerprint
250
+ : null;
251
+ try {
252
+ ensureDir(logPath);
253
+ if (observationFingerprint && executionLogContainsObservation(logPath, observationFingerprint)) {
254
+ return success(logPath);
255
+ }
256
+ fs.appendFileSync(logPath, `${JSON.stringify(entry)}\n`);
257
+ return success(logPath);
258
+ } catch (error) {
259
+ return failure(
260
+ logPath,
261
+ "io",
262
+ error instanceof Error ? error.message : `Unable to append ${logPath}`,
263
+ );
264
+ }
265
+ }
266
+
267
+ function executionLogContainsObservation(logPath: string, observationFingerprint: string): boolean {
268
+ if (!fs.existsSync(logPath)) {
269
+ return false;
270
+ }
271
+
272
+ for (const line of fs.readFileSync(logPath, "utf8").split("\n")) {
273
+ if (!line) {
274
+ continue;
275
+ }
276
+
277
+ try {
278
+ const parsed = JSON.parse(line) as Record<string, unknown>;
279
+ if (parsed.observationFingerprint === observationFingerprint) {
280
+ return true;
281
+ }
282
+ } catch {
283
+ // Ignore malformed historical lines and keep scanning for a matching durable entry.
284
+ }
285
+ }
286
+
287
+ return false;
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // Pending-mutation staging and reconciliation
292
+ // ---------------------------------------------------------------------------
293
+
294
+ /**
295
+ * Stage a pending mutation atomically: load the tracker, set `pendingMutation`, and rewrite.
296
+ * Caller provides the exact `UltraPlanPendingMutation` record (attemptId, plan, expected
297
+ * manifest fingerprint, stagedAt) per spec §durability order item 3.
298
+ */
299
+ export function stagePendingMutation(
300
+ paths: PlatformPaths,
301
+ cwd: string,
302
+ sessionId: string,
303
+ pending: UltraPlanPendingMutation,
304
+ ): UltraPlanStorageResult<string> {
305
+ const loaded = loadTracker(paths, cwd, sessionId);
306
+ if (!loaded.ok) return loaded;
307
+ const next: UltraPlanRuntimeTracker = {
308
+ ...loaded.value,
309
+ pendingMutation: pending,
310
+ updatedAt: pending.stagedAt,
311
+ };
312
+ return saveTrackerAtomic(paths, cwd, sessionId, next);
313
+ }
314
+
315
+ /**
316
+ * Clear `pendingMutation` atomically. Used at the end of the durability order (item 5) when
317
+ * the manifest write has already landed and the attempt is committed to the finalized ledger.
318
+ */
319
+ export function clearPendingMutation(
320
+ paths: PlatformPaths,
321
+ cwd: string,
322
+ sessionId: string,
323
+ ): UltraPlanStorageResult<string> {
324
+ const loaded = loadTracker(paths, cwd, sessionId);
325
+ if (!loaded.ok) return loaded;
326
+ if (loaded.value.pendingMutation === null) {
327
+ return success(getUltraplanRuntimeTrackerPath(paths, cwd, sessionId));
328
+ }
329
+ const next: UltraPlanRuntimeTracker = {
330
+ ...loaded.value,
331
+ pendingMutation: null,
332
+ };
333
+ return saveTrackerAtomic(paths, cwd, sessionId, next);
334
+ }
335
+
336
+ export type UltraPlanReconciliationOutcome =
337
+ | { kind: "no-pending" }
338
+ | { kind: "committed" }
339
+ | { kind: "replay-needed"; pending: UltraPlanPendingMutation };
340
+
341
+ /**
342
+ * Reconcile a staged `pendingMutation` against the actual manifest contents on resume.
343
+ *
344
+ * - When no pending mutation is present, returns `no-pending`.
345
+ * - When the manifest already matches the staged `expectedManifestFingerprint`, the pending
346
+ * mutation is considered committed: clear it and return `committed`.
347
+ * - Otherwise return `replay-needed` and leave the pending mutation in place so the caller can
348
+ * replay the mutation plan idempotently.
349
+ */
350
+ export function reconcilePendingMutationAgainstManifest(
351
+ paths: PlatformPaths,
352
+ cwd: string,
353
+ sessionId: string,
354
+ actualManifestFingerprint: string,
355
+ ): UltraPlanStorageResult<UltraPlanReconciliationOutcome> {
356
+ const loaded = loadTracker(paths, cwd, sessionId);
357
+ if (!loaded.ok) return loaded;
358
+ const pending = loaded.value.pendingMutation;
359
+ if (pending === null) {
360
+ return success({ kind: "no-pending" });
361
+ }
362
+ if (pending.expectedManifestFingerprint === actualManifestFingerprint) {
363
+ const cleared = clearPendingMutation(paths, cwd, sessionId);
364
+ if (!cleared.ok) return cleared;
365
+ return success({ kind: "committed" });
366
+ }
367
+ return success({ kind: "replay-needed", pending });
368
+ }
@@ -0,0 +1,291 @@
1
+ import type {
2
+ UltraPlanAuthoredArtifact,
3
+ UltraPlanCursor,
4
+ UltraPlanManifest,
5
+ UltraPlanReviewStatus,
6
+ UltraPlanScenario,
7
+ UltraPlanScenarioStatus,
8
+ UltraPlanSessionBucket,
9
+ UltraPlanSessionState,
10
+ UltraPlanSessionSummary,
11
+ UltraPlanStack,
12
+ } from "../types.js";
13
+ import { hasRequiredUltraPlanScenarioProof } from "./contracts.js";
14
+
15
+
16
+ export interface UltraPlanVisibleSession extends UltraPlanSessionSummary {
17
+ bucket: UltraPlanSessionBucket;
18
+ idleReasonLabel: string | null;
19
+ }
20
+
21
+ export interface UltraPlanResolvedCursor {
22
+ cursor: UltraPlanCursor;
23
+ source: "persisted" | "recomputed";
24
+ }
25
+
26
+ const TERMINAL_SCENARIO_STATUSES = new Set<UltraPlanScenarioStatus>([
27
+ "green-proved",
28
+ "review-passed",
29
+ "done",
30
+ ]);
31
+ const ONGOING_CURSOR_STATUSES = new Set([
32
+ "red-running",
33
+ "green-running",
34
+ "in-review",
35
+ ]);
36
+
37
+
38
+ export function mapUltraPlanStateToBucket(state: UltraPlanSessionState): UltraPlanSessionBucket {
39
+ switch (state) {
40
+ case "ready":
41
+ return "pending";
42
+ case "running":
43
+ return "ongoing";
44
+ case "blocked":
45
+ case "awaiting-user":
46
+ return "idle";
47
+ case "complete":
48
+ case "discarded":
49
+ return "done";
50
+ }
51
+ }
52
+
53
+ export function getUltraPlanIdleReasonLabel(session: UltraPlanSessionSummary): string | null {
54
+ if (session.state === "awaiting-user") {
55
+ return session.blocker?.message ? `Awaiting user: ${session.blocker.message}` : "Awaiting user input";
56
+ }
57
+
58
+ if (session.state === "blocked") {
59
+ return session.blocker?.message ?? "Blocked";
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ export function resolveUltraPlanSessionBucket(
66
+ session: UltraPlanSessionSummary,
67
+ resolved?: UltraPlanResolvedCursor,
68
+ ): UltraPlanSessionBucket {
69
+ const cursor = resolved?.cursor ?? session.cursor;
70
+
71
+ if (session.state === "discarded") {
72
+ return "done";
73
+ }
74
+
75
+ if (!resolved && session.state === "complete") {
76
+ return "done";
77
+ }
78
+
79
+ if (cursor?.targetType === "session" && cursor.status === "complete") {
80
+ return "done";
81
+ }
82
+
83
+ if (session.state === "awaiting-user") {
84
+ return "idle";
85
+ }
86
+
87
+ if (session.state === "blocked" || cursor?.status === "blocked") {
88
+ return "idle";
89
+ }
90
+
91
+ if (session.state === "running" || (cursor && ONGOING_CURSOR_STATUSES.has(cursor.status))) {
92
+ return "ongoing";
93
+ }
94
+
95
+ return "pending";
96
+ }
97
+
98
+ export function getVisibleUltraPlanSessions(
99
+ sessions: UltraPlanSessionSummary[],
100
+ options?: { includeDone?: boolean },
101
+ ): UltraPlanVisibleSession[] {
102
+ const includeDone = options?.includeDone ?? false;
103
+
104
+ return sessions
105
+ .map((session) => ({
106
+ ...session,
107
+ bucket: resolveUltraPlanSessionBucket(session),
108
+ idleReasonLabel: getUltraPlanIdleReasonLabel(session),
109
+ }))
110
+ .filter((session) => includeDone || session.bucket !== "done");
111
+ }
112
+
113
+ export function isUltraPlanCursorSummaryValid(
114
+ manifest: UltraPlanManifest,
115
+ authored: UltraPlanAuthoredArtifact,
116
+ ): boolean {
117
+ if (!manifest.cursor) {
118
+ return false;
119
+ }
120
+
121
+ return sameCursor(manifest.cursor, recomputeUltraPlanCursor(manifest, authored));
122
+ }
123
+
124
+ export function resolveUltraPlanCurrentCursor(
125
+ manifest: UltraPlanManifest,
126
+ authored: UltraPlanAuthoredArtifact,
127
+ ): UltraPlanResolvedCursor {
128
+ const recomputed = recomputeUltraPlanCursor(manifest, authored);
129
+ if (manifest.cursor && sameCursor(manifest.cursor, recomputed)) {
130
+ return { cursor: recomputed, source: "persisted" };
131
+ }
132
+
133
+ return { cursor: recomputed, source: "recomputed" };
134
+ }
135
+
136
+ function recomputeUltraPlanCursor(manifest: UltraPlanManifest, authored: UltraPlanAuthoredArtifact): UltraPlanCursor {
137
+ for (const stack of authored.stacks) {
138
+ if (stack.applicability === "not-applicable") {
139
+ continue;
140
+ }
141
+
142
+ const stackCursor = recomputeStackCursor(manifest, stack);
143
+ if (stackCursor) {
144
+ return stackCursor;
145
+ }
146
+ }
147
+
148
+ return {
149
+ targetType: "session",
150
+ stack: null,
151
+ domainId: null,
152
+ level: null,
153
+ scenarioId: null,
154
+ phase: "complete",
155
+ status: "complete",
156
+ summary: "Session complete",
157
+ };
158
+ }
159
+
160
+ function recomputeStackCursor(manifest: UltraPlanManifest, stack: UltraPlanStack): UltraPlanCursor | null {
161
+ for (const domain of stack.domains) {
162
+ const scenarioCursor = findFirstScenarioCursor(domain.unit)
163
+ ?? findFirstScenarioCursor(domain.integration)
164
+ ?? findFirstScenarioCursor(domain.e2e);
165
+
166
+ if (scenarioCursor) {
167
+ return scenarioCursor;
168
+ }
169
+
170
+ if (stack.agentSlots.domainReviewEnabled && domain.review.enabled) {
171
+ if (!stack.agentSlots.domainReviewer) {
172
+ return {
173
+ targetType: "domain-review",
174
+ stack: stack.stack,
175
+ domainId: domain.id,
176
+ level: null,
177
+ scenarioId: null,
178
+ phase: "waiting",
179
+ status: "blocked",
180
+ summary: `${stack.stack} / ${domain.id} / domain review blocked — missing reviewer slot`,
181
+ };
182
+ }
183
+
184
+ const reviewStatus = getDomainReviewStatus(manifest, stack.stack, domain.id) ?? "pending";
185
+ if (reviewStatus !== "passed") {
186
+ return {
187
+ targetType: "domain-review",
188
+ stack: stack.stack,
189
+ domainId: domain.id,
190
+ level: null,
191
+ scenarioId: null,
192
+ phase: reviewStatus === "blocked" ? "waiting" : "review",
193
+ status: reviewStatus,
194
+ summary: `${stack.stack} / ${domain.id} / domain review`,
195
+ };
196
+ }
197
+ }
198
+ }
199
+
200
+ if (stack.agentSlots.stackReviewEnabled) {
201
+ if (!stack.agentSlots.stackReviewer) {
202
+ return {
203
+ targetType: "stack-review",
204
+ stack: stack.stack,
205
+ domainId: null,
206
+ level: null,
207
+ scenarioId: null,
208
+ phase: "waiting",
209
+ status: "blocked",
210
+ summary: `${stack.stack} / stack review blocked — missing reviewer slot`,
211
+ };
212
+ }
213
+
214
+ const reviewStatus = getStackReviewStatus(manifest, stack.stack) ?? "pending";
215
+ if (reviewStatus !== "passed") {
216
+ return {
217
+ targetType: "stack-review",
218
+ stack: stack.stack,
219
+ domainId: null,
220
+ level: null,
221
+ scenarioId: null,
222
+ phase: reviewStatus === "blocked" ? "waiting" : "review",
223
+ status: reviewStatus,
224
+ summary: `${stack.stack} / stack review`,
225
+ };
226
+ }
227
+ }
228
+
229
+ return null;
230
+ }
231
+
232
+ function findFirstScenarioCursor(scenarios: UltraPlanScenario[]): UltraPlanCursor | null {
233
+ for (const scenario of scenarios) {
234
+ if (!TERMINAL_SCENARIO_STATUSES.has(scenario.status) || !hasRequiredUltraPlanScenarioProof(scenario)) {
235
+ return {
236
+ targetType: "scenario",
237
+ stack: scenario.stack,
238
+ domainId: scenario.domainId,
239
+ level: scenario.level,
240
+ scenarioId: scenario.id,
241
+ phase: getScenarioPhase(scenario.status),
242
+ status: scenario.status,
243
+ summary: `${scenario.stack} / ${scenario.domainId} / ${scenario.level} / ${scenario.title}`,
244
+ };
245
+ }
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ function getDomainReviewStatus(
252
+ manifest: UltraPlanManifest,
253
+ stack: UltraPlanStack["stack"],
254
+ domainId: string,
255
+ ): UltraPlanReviewStatus | null {
256
+ return manifest.reviews.find((review) => review.type === "domain" && review.stack === stack && review.domainId === domainId)?.status ?? null;
257
+ }
258
+
259
+ function getStackReviewStatus(manifest: UltraPlanManifest, stack: UltraPlanStack["stack"]): UltraPlanReviewStatus | null {
260
+ return manifest.reviews.find((review) => review.type === "stack" && review.stack === stack)?.status ?? null;
261
+ }
262
+
263
+ function getScenarioPhase(status: UltraPlanScenarioStatus): UltraPlanCursor["phase"] {
264
+ switch (status) {
265
+ case "planned":
266
+ case "red-running":
267
+ return "red";
268
+ case "red-proved":
269
+ case "green-running":
270
+ return "green";
271
+ case "in-review":
272
+ return "review";
273
+ case "blocked":
274
+ return "waiting";
275
+ case "green-proved":
276
+ case "review-passed":
277
+ case "done":
278
+ return "complete";
279
+ }
280
+ }
281
+
282
+ function sameCursor(left: UltraPlanCursor, right: UltraPlanCursor): boolean {
283
+ return left.targetType === right.targetType
284
+ && left.stack === right.stack
285
+ && left.domainId === right.domainId
286
+ && left.level === right.level
287
+ && left.scenarioId === right.scenarioId
288
+ && left.phase === right.phase
289
+ && left.status === right.status
290
+ && left.summary === right.summary;
291
+ }