supipowers 1.5.3 → 2.0.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 (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 +135 -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 +268 -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,524 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { PlatformPaths } from "../../platform/types.js";
5
+ import type {
6
+ UltraPlanAuthoredArtifact,
7
+ UltraPlanBlocker,
8
+ UltraPlanManifest,
9
+ UltraPlanSessionMigrationRecord,
10
+ } from "../../types.js";
11
+ import {
12
+ validateUltraPlanAuthoredArtifact,
13
+ validateUltraPlanManifest,
14
+ validateUltraPlanSessionMigrationRecord,
15
+ } from "../contracts.js";
16
+ import {
17
+ getLegacyUltraplanSessionDir,
18
+ getUltraplanAuthoredJsonPath,
19
+ getUltraplanManifestPath,
20
+ getUltraplanMigrationRecordPath,
21
+ getUltraplanSessionDir,
22
+ ULTRAPLAN_AUTHORED_JSON_FILENAME,
23
+ ULTRAPLAN_MANIFEST_FILENAME,
24
+ } from "../project-paths.js";
25
+ import {
26
+ buildMigrationConflictBlocker,
27
+ buildMigrationUnsafeBlocker,
28
+ } from "./blockers.js";
29
+ import { saveMigrationRecord } from "./tracker-storage.js";
30
+
31
+ /**
32
+ * Slice-2 migration engine.
33
+ *
34
+ * Implements the 7-branch decision procedure from the delta spec §per-session decision procedure.
35
+ * The engine runs fail-closed: any branch it cannot complete deterministically surfaces a
36
+ * structured `migration-unsafe` or `migration-conflict` blocker instead of a partially-migrated
37
+ * global directory.
38
+ */
39
+
40
+ export interface ResolveSessionMigrationInput {
41
+ paths: PlatformPaths;
42
+ cwd: string;
43
+ sessionId: string;
44
+ nowIso: string;
45
+ }
46
+
47
+ export type MigrationOutcome =
48
+ | { kind: "skip" }
49
+ | { kind: "native" }
50
+ | { kind: "migrated-copied"; record: UltraPlanSessionMigrationRecord }
51
+ | { kind: "reconciled-no-op"; record: UltraPlanSessionMigrationRecord }
52
+ | { kind: "blocked"; blocker: UltraPlanBlocker };
53
+
54
+ interface GlobalState {
55
+ exists: boolean;
56
+ authored: UltraPlanAuthoredArtifact | null;
57
+ manifest: UltraPlanManifest | null;
58
+ hasMigrationJson: boolean;
59
+ migrationJsonValid: boolean;
60
+ }
61
+
62
+ interface LegacyState {
63
+ exists: boolean;
64
+ authored: UltraPlanAuthoredArtifact | null;
65
+ manifest: UltraPlanManifest | null;
66
+ /** True when authored.json and manifest.json are both present and pass schema validation. */
67
+ canonical: boolean;
68
+ legacyDir: string;
69
+ }
70
+
71
+ export function resolveSessionMigration(input: ResolveSessionMigrationInput): MigrationOutcome {
72
+ const global = inspectGlobal(input);
73
+ const legacy = inspectLegacy(input);
74
+
75
+ // Branch 1: no global, no legacy.
76
+ if (!global.exists && !legacy.exists) {
77
+ return { kind: "skip" };
78
+ }
79
+
80
+ // Branch 6: global absent, legacy present.
81
+ if (!global.exists && legacy.exists) {
82
+ return migrateFromLegacy(input, legacy);
83
+ }
84
+
85
+ // Global exists. Branch 3/4 only apply when the global directory is canonical per the delta
86
+ // spec definition: authored+manifest valid AND (no legacy OR migration.json present+valid).
87
+ // A global directory with matching content but no migration.json is partial/interrupted —
88
+ // the spec explicitly forbids the loader from accepting it as canonical on retry.
89
+ const globalHasValidArtifacts = global.authored !== null && global.manifest !== null;
90
+ const globalIsCanonical = globalHasValidArtifacts
91
+ && (!legacy.exists || (global.hasMigrationJson && global.migrationJsonValid));
92
+
93
+ if (globalIsCanonical && legacy.exists && legacy.canonical) {
94
+ const fingerprintGlobal = fingerprintArtifacts(global.authored!, global.manifest!);
95
+ const fingerprintLegacy = fingerprintArtifacts(legacy.authored!, legacy.manifest!);
96
+ const contentsMatch =
97
+ fingerprintGlobal === fingerprintLegacy
98
+ && global.manifest!.updatedAt === legacy.manifest!.updatedAt
99
+ && sameCursor(global.manifest!.cursor, legacy.manifest!.cursor);
100
+ if (contentsMatch) {
101
+ return reconcileSameContent(input, legacy, fingerprintGlobal);
102
+ }
103
+ // Branch 4: both canonical-shaped but contents conflict.
104
+ return {
105
+ kind: "blocked",
106
+ blocker: buildMigrationConflictBlocker({
107
+ detectedAt: input.nowIso,
108
+ legacyPath: legacy.legacyDir,
109
+ globalPath: getUltraplanSessionDir(input.paths, input.cwd, input.sessionId),
110
+ reason: describeContentMismatch(global, legacy, fingerprintGlobal, fingerprintLegacy),
111
+ }),
112
+ };
113
+ }
114
+
115
+ // Branch 2: canonical global, no legacy.
116
+ if (globalIsCanonical && !legacy.exists) {
117
+ return { kind: "native" };
118
+ }
119
+
120
+ // Branch 5: non-canonical global with a valid legacy copy. Rename the partial global directory,
121
+ // then migrate in from legacy via branch 6.
122
+ if (legacy.exists && legacy.canonical) {
123
+ return recoverFromPartialGlobal(input, legacy);
124
+ }
125
+
126
+ // Branch 7: non-canonical global, no legacy. Rename the partial global directory and emit a
127
+ // migration-unsafe blocker naming the interrupted path.
128
+ return classifyOrphanedGlobal(input);
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Branch 6 — copy legacy into global, then rename legacy
133
+ // ---------------------------------------------------------------------------
134
+
135
+ function migrateFromLegacy(input: ResolveSessionMigrationInput, legacy: LegacyState): MigrationOutcome {
136
+ if (!legacy.canonical || !legacy.authored || !legacy.manifest) {
137
+ return {
138
+ kind: "blocked",
139
+ blocker: buildMigrationUnsafeBlocker({
140
+ detectedAt: input.nowIso,
141
+ legacyPath: legacy.legacyDir,
142
+ reason: "legacy authored.json or manifest.json failed schema validation",
143
+ }),
144
+ };
145
+ }
146
+
147
+ const globalDir = getUltraplanSessionDir(input.paths, input.cwd, input.sessionId);
148
+
149
+ // Durability order: copy tree, then write migration.json, then rename legacy.
150
+ try {
151
+ fs.mkdirSync(globalDir, { recursive: true });
152
+ copyDirectoryTree(legacy.legacyDir, globalDir);
153
+ } catch (error) {
154
+ return {
155
+ kind: "blocked",
156
+ blocker: buildMigrationUnsafeBlocker({
157
+ detectedAt: input.nowIso,
158
+ legacyPath: legacy.legacyDir,
159
+ reason: `copy failed before migration.json could be written: ${formatError(error)}`,
160
+ }),
161
+ };
162
+ }
163
+
164
+ const fingerprintBefore = fingerprintLegacyArtifacts(legacy.authored, legacy.manifest);
165
+ const fingerprintAfter = fingerprintGlobalArtifacts(input);
166
+ if (fingerprintBefore !== fingerprintAfter) {
167
+ // Copy corrupted the canonical content; back out.
168
+ safeRemove(globalDir);
169
+ return {
170
+ kind: "blocked",
171
+ blocker: buildMigrationUnsafeBlocker({
172
+ detectedAt: input.nowIso,
173
+ legacyPath: legacy.legacyDir,
174
+ reason: "copy produced non-matching fingerprint; aborted",
175
+ }),
176
+ };
177
+ }
178
+
179
+ const renamedPath = interruptedOrMigratedPath(legacy.legacyDir, "migrated", input.nowIso);
180
+
181
+ const record: UltraPlanSessionMigrationRecord = {
182
+ migratedAt: input.nowIso,
183
+ legacyPath: legacy.legacyDir,
184
+ fingerprintBefore,
185
+ fingerprintAfter,
186
+ legacyRenamedTo: renamedPath,
187
+ kind: "copied",
188
+ };
189
+
190
+ const saved = saveMigrationRecord(input.paths, input.cwd, input.sessionId, record);
191
+ if (!saved.ok) {
192
+ safeRemove(globalDir);
193
+ return {
194
+ kind: "blocked",
195
+ blocker: buildMigrationUnsafeBlocker({
196
+ detectedAt: input.nowIso,
197
+ legacyPath: legacy.legacyDir,
198
+ reason: `migration.json write failed: ${saved.error.message}`,
199
+ }),
200
+ };
201
+ }
202
+
203
+ try {
204
+ fs.renameSync(legacy.legacyDir, renamedPath);
205
+ } catch (error) {
206
+ return {
207
+ kind: "blocked",
208
+ blocker: buildMigrationUnsafeBlocker({
209
+ detectedAt: input.nowIso,
210
+ legacyPath: legacy.legacyDir,
211
+ reason: `legacy rename failed: ${formatError(error)}`,
212
+ }),
213
+ };
214
+ }
215
+
216
+ return { kind: "migrated-copied", record };
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Inspection helpers
221
+ // ---------------------------------------------------------------------------
222
+
223
+ function inspectGlobal(input: ResolveSessionMigrationInput): GlobalState {
224
+ const globalDir = getUltraplanSessionDir(input.paths, input.cwd, input.sessionId);
225
+ if (!fs.existsSync(globalDir)) {
226
+ return { exists: false, authored: null, manifest: null, hasMigrationJson: false, migrationJsonValid: false };
227
+ }
228
+ const authored = readValidatedAuthored(getUltraplanAuthoredJsonPath(input.paths, input.cwd, input.sessionId));
229
+ const manifest = readValidatedManifest(getUltraplanManifestPath(input.paths, input.cwd, input.sessionId));
230
+ const migrationPath = getUltraplanMigrationRecordPath(input.paths, input.cwd, input.sessionId);
231
+ const hasMigrationJson = fs.existsSync(migrationPath);
232
+ let migrationJsonValid = false;
233
+ if (hasMigrationJson) {
234
+ try {
235
+ const raw = JSON.parse(fs.readFileSync(migrationPath, "utf8"));
236
+ migrationJsonValid = validateUltraPlanSessionMigrationRecord(raw).ok;
237
+ } catch {
238
+ migrationJsonValid = false;
239
+ }
240
+ }
241
+ return {
242
+ exists: true,
243
+ authored,
244
+ manifest,
245
+ hasMigrationJson,
246
+ migrationJsonValid,
247
+ };
248
+ }
249
+
250
+ function inspectLegacy(input: ResolveSessionMigrationInput): LegacyState {
251
+ const legacyDir = getLegacyUltraplanSessionDir(input.cwd, input.sessionId);
252
+ if (!fs.existsSync(legacyDir)) {
253
+ return { exists: false, authored: null, manifest: null, canonical: false, legacyDir };
254
+ }
255
+ const authored = readValidatedAuthored(path.join(legacyDir, ULTRAPLAN_AUTHORED_JSON_FILENAME));
256
+ const manifest = readValidatedManifest(path.join(legacyDir, ULTRAPLAN_MANIFEST_FILENAME));
257
+ const canonical = authored !== null && manifest !== null;
258
+ return { exists: true, authored, manifest, canonical, legacyDir };
259
+ }
260
+
261
+ function isGlobalCanonical(global: GlobalState, legacy: LegacyState): boolean {
262
+ if (!global.exists) return false;
263
+ if (!global.authored || !global.manifest) return false;
264
+ // Either no legacy copy (native), or migration.json present and valid (migrated).
265
+ if (!legacy.exists) return true;
266
+ return global.hasMigrationJson && global.migrationJsonValid;
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Filesystem helpers
271
+ // ---------------------------------------------------------------------------
272
+
273
+ function readValidatedAuthored(filePath: string): UltraPlanAuthoredArtifact | null {
274
+ if (!fs.existsSync(filePath)) return null;
275
+ try {
276
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
277
+ const result = validateUltraPlanAuthoredArtifact(raw);
278
+ return result.ok ? result.value : null;
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+
284
+ function readValidatedManifest(filePath: string): UltraPlanManifest | null {
285
+ if (!fs.existsSync(filePath)) return null;
286
+ try {
287
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
288
+ const result = validateUltraPlanManifest(raw);
289
+ return result.ok ? result.value : null;
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+
295
+ function copyDirectoryTree(src: string, dest: string): void {
296
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
297
+ const from = path.join(src, entry.name);
298
+ const to = path.join(dest, entry.name);
299
+ if (entry.isDirectory()) {
300
+ fs.mkdirSync(to, { recursive: true });
301
+ copyDirectoryTree(from, to);
302
+ } else if (entry.isFile()) {
303
+ fs.copyFileSync(from, to);
304
+ }
305
+ }
306
+ }
307
+
308
+ function safeRemove(target: string): void {
309
+ try {
310
+ fs.rmSync(target, { recursive: true, force: true });
311
+ } catch {
312
+ // best effort cleanup; the caller is already in a blocked state
313
+ }
314
+ }
315
+
316
+ function interruptedOrMigratedPath(source: string, suffix: "migrated" | "interrupted", nowIso: string): string {
317
+ const safeTimestamp = nowIso.replace(/[:.]/g, "-");
318
+ return `${source}.${suffix}-${safeTimestamp}`;
319
+ }
320
+
321
+ function formatError(error: unknown): string {
322
+ return error instanceof Error ? error.message : String(error);
323
+ }
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Fingerprinting
327
+ // ---------------------------------------------------------------------------
328
+
329
+ function fingerprintLegacyArtifacts(authored: UltraPlanAuthoredArtifact, manifest: UltraPlanManifest): string {
330
+ return fingerprintArtifacts(authored, manifest);
331
+ }
332
+
333
+ function fingerprintGlobalArtifacts(input: ResolveSessionMigrationInput): string {
334
+ const authored = readValidatedAuthored(getUltraplanAuthoredJsonPath(input.paths, input.cwd, input.sessionId));
335
+ const manifest = readValidatedManifest(getUltraplanManifestPath(input.paths, input.cwd, input.sessionId));
336
+ if (!authored || !manifest) {
337
+ // Produce a distinct, non-canonical sentinel so the caller can detect the mismatch.
338
+ return `sha256:incomplete-global-${Date.now()}`;
339
+ }
340
+ return fingerprintArtifacts(authored, manifest);
341
+ }
342
+
343
+ function fingerprintArtifacts(authored: UltraPlanAuthoredArtifact, manifest: UltraPlanManifest): string {
344
+ const canonical = JSON.stringify({
345
+ authored: canonicalize(authored),
346
+ manifest: canonicalize(manifest),
347
+ });
348
+ return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
349
+ }
350
+
351
+ function canonicalize(value: unknown): unknown {
352
+ if (Array.isArray(value)) return value.map(canonicalize);
353
+ if (value && typeof value === "object") {
354
+ const entries = Object.entries(value as Record<string, unknown>)
355
+ .filter(([, v]) => v !== undefined)
356
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
357
+ .map(([k, v]) => [k, canonicalize(v)] as const);
358
+ return Object.fromEntries(entries);
359
+ }
360
+ return value;
361
+ }
362
+
363
+
364
+ function reconcileSameContent(
365
+ input: ResolveSessionMigrationInput,
366
+ legacy: LegacyState,
367
+ fingerprint: string,
368
+ ): MigrationOutcome {
369
+ const migrationPath = getUltraplanMigrationRecordPath(input.paths, input.cwd, input.sessionId);
370
+ const renamedPath = interruptedOrMigratedPath(legacy.legacyDir, "migrated", input.nowIso);
371
+
372
+ const record: UltraPlanSessionMigrationRecord = {
373
+ migratedAt: input.nowIso,
374
+ legacyPath: legacy.legacyDir,
375
+ fingerprintBefore: fingerprint,
376
+ fingerprintAfter: fingerprint,
377
+ legacyRenamedTo: renamedPath,
378
+ kind: "reconciled-no-op",
379
+ };
380
+
381
+ // Branch 3 semantics: ensure migration.json exists. If it already does, leave it as-is (it was
382
+ // written by a prior migration). Otherwise write a fresh reconciled-no-op record.
383
+ let persistedRecord: UltraPlanSessionMigrationRecord = record;
384
+ if (fs.existsSync(migrationPath)) {
385
+ try {
386
+ const raw = JSON.parse(fs.readFileSync(migrationPath, "utf8"));
387
+ const validation = validateUltraPlanSessionMigrationRecord(raw);
388
+ if (validation.ok) {
389
+ persistedRecord = validation.value;
390
+ }
391
+ } catch {
392
+ // Fall through and write a fresh record below.
393
+ }
394
+ // If the existing file is invalid JSON or schema-invalid, overwrite with a fresh record.
395
+ if (persistedRecord === record) {
396
+ const saved = saveMigrationRecord(input.paths, input.cwd, input.sessionId, record);
397
+ if (!saved.ok) {
398
+ return {
399
+ kind: "blocked",
400
+ blocker: buildMigrationUnsafeBlocker({
401
+ detectedAt: input.nowIso,
402
+ legacyPath: legacy.legacyDir,
403
+ reason: `reconciled migration.json write failed: ${saved.error.message}`,
404
+ }),
405
+ };
406
+ }
407
+ }
408
+ } else {
409
+ const saved = saveMigrationRecord(input.paths, input.cwd, input.sessionId, record);
410
+ if (!saved.ok) {
411
+ return {
412
+ kind: "blocked",
413
+ blocker: buildMigrationUnsafeBlocker({
414
+ detectedAt: input.nowIso,
415
+ legacyPath: legacy.legacyDir,
416
+ reason: `reconciled migration.json write failed: ${saved.error.message}`,
417
+ }),
418
+ };
419
+ }
420
+ }
421
+
422
+ try {
423
+ fs.renameSync(legacy.legacyDir, renamedPath);
424
+ } catch (error) {
425
+ return {
426
+ kind: "blocked",
427
+ blocker: buildMigrationUnsafeBlocker({
428
+ detectedAt: input.nowIso,
429
+ legacyPath: legacy.legacyDir,
430
+ reason: `legacy rename failed during reconciliation: ${formatError(error)}`,
431
+ }),
432
+ };
433
+ }
434
+
435
+ return { kind: "reconciled-no-op", record: persistedRecord };
436
+ }
437
+
438
+ function sameCursor(a: UltraPlanManifest["cursor"], b: UltraPlanManifest["cursor"]): boolean {
439
+ if (a === null && b === null) return true;
440
+ if (a === null || b === null) return false;
441
+ return a.targetType === b.targetType
442
+ && a.stack === b.stack
443
+ && a.domainId === b.domainId
444
+ && a.level === b.level
445
+ && a.scenarioId === b.scenarioId
446
+ && a.phase === b.phase
447
+ && a.status === b.status;
448
+ }
449
+
450
+ // ---------------------------------------------------------------------------
451
+ // Branch 5 — partial global, valid legacy: rename partial global then migrate from legacy.
452
+ // ---------------------------------------------------------------------------
453
+
454
+ function recoverFromPartialGlobal(
455
+ input: ResolveSessionMigrationInput,
456
+ legacy: LegacyState,
457
+ ): MigrationOutcome {
458
+ const globalDir = getUltraplanSessionDir(input.paths, input.cwd, input.sessionId);
459
+ const interruptedPath = interruptedOrMigratedPath(globalDir, "interrupted", input.nowIso);
460
+ try {
461
+ fs.renameSync(globalDir, interruptedPath);
462
+ } catch (error) {
463
+ return {
464
+ kind: "blocked",
465
+ blocker: buildMigrationUnsafeBlocker({
466
+ detectedAt: input.nowIso,
467
+ legacyPath: legacy.legacyDir,
468
+ reason: `failed to rename partial global directory: ${formatError(error)}`,
469
+ interruptedPath,
470
+ }),
471
+ };
472
+ }
473
+ // Re-run branch 6 against the now-absent global directory.
474
+ return migrateFromLegacy(input, legacy);
475
+ }
476
+
477
+ // ---------------------------------------------------------------------------
478
+ // Branch 7 — non-canonical global, no legacy: rename and emit migration-unsafe blocker.
479
+ // ---------------------------------------------------------------------------
480
+
481
+ function classifyOrphanedGlobal(input: ResolveSessionMigrationInput): MigrationOutcome {
482
+ const globalDir = getUltraplanSessionDir(input.paths, input.cwd, input.sessionId);
483
+ const interruptedPath = interruptedOrMigratedPath(globalDir, "interrupted", input.nowIso);
484
+ try {
485
+ fs.renameSync(globalDir, interruptedPath);
486
+ return {
487
+ kind: "blocked",
488
+ blocker: buildMigrationUnsafeBlocker({
489
+ detectedAt: input.nowIso,
490
+ legacyPath: getLegacyUltraplanSessionDir(input.cwd, input.sessionId),
491
+ reason: "global session directory is not canonical and no legacy copy is available",
492
+ interruptedPath,
493
+ }),
494
+ };
495
+ } catch (error) {
496
+ return {
497
+ kind: "blocked",
498
+ blocker: buildMigrationUnsafeBlocker({
499
+ detectedAt: input.nowIso,
500
+ legacyPath: getLegacyUltraplanSessionDir(input.cwd, input.sessionId),
501
+ reason: `failed to rename non-canonical global directory: ${formatError(error)}`,
502
+ }),
503
+ };
504
+ }
505
+ }
506
+
507
+ function describeContentMismatch(
508
+ global: GlobalState,
509
+ legacy: LegacyState,
510
+ fingerprintGlobal: string,
511
+ fingerprintLegacy: string,
512
+ ): string {
513
+ const reasons: string[] = [];
514
+ if (global.manifest?.updatedAt !== legacy.manifest?.updatedAt) {
515
+ reasons.push(`updatedAt differs (global=${global.manifest?.updatedAt}, legacy=${legacy.manifest?.updatedAt})`);
516
+ }
517
+ if (!sameCursor(global.manifest?.cursor ?? null, legacy.manifest?.cursor ?? null)) {
518
+ reasons.push("cursor differs");
519
+ }
520
+ if (fingerprintGlobal !== fingerprintLegacy) {
521
+ reasons.push("authored/manifest fingerprints differ");
522
+ }
523
+ return reasons.length > 0 ? reasons.join("; ") : "contents differ";
524
+ }