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,653 @@
1
+ import type {
2
+ ResolvedUltraPlanCatalog,
3
+ ResolvedUltraPlanSlotBinding,
4
+ UltraPlanAgentBinding,
5
+ UltraPlanAgentSlotName,
6
+ UltraPlanAgentSlots,
7
+ UltraPlanApplicability,
8
+ UltraPlanAuthoredArtifact,
9
+ UltraPlanCursor,
10
+ UltraPlanIndexEntry,
11
+ UltraPlanManifest,
12
+ UltraPlanReviewerSlotName,
13
+ UltraPlanScenarioLevel,
14
+ UltraPlanStack,
15
+ UltraPlanStackId,
16
+ } from "../types.js";
17
+ import { isUltraPlanAuthoredArtifact, ULTRAPLAN_STACKS, UltraPlanAuthoredArtifactSchema, getUltraPlanSchemaErrors } from "./contracts.js";
18
+ import { ULTRAPLAN_AUTHORED_JSON_FILENAME } from "./project-paths.js";
19
+
20
+ /**
21
+ * Authoring-time draft: structurally identical to `UltraPlanAuthoredArtifact` with
22
+ * authoring-time invariants enforced by the operations in this module. The type alias
23
+ * keeps downstream call sites honest — the draft is never a parallel shape.
24
+ */
25
+ export type UltraPlanAuthoredDraft = UltraPlanAuthoredArtifact;
26
+
27
+ export type DraftOpResult =
28
+ | { ok: true; draft: UltraPlanAuthoredDraft }
29
+ | { ok: false; reason: DraftOpError };
30
+
31
+ export type DraftOpError =
32
+ | { code: "duplicate-id"; where: "domain" | "scenario"; id: string }
33
+ | { code: "not-found"; where: "stack" | "domain" | "scenario"; id: string }
34
+ | { code: "invariant-violation"; path: string; message: string }
35
+ | { code: "length-cap"; field: string; max: number; got: number }
36
+ | { code: "bad-applicability-transition"; message: string };
37
+
38
+ export type DraftReadiness =
39
+ | { ok: true }
40
+ | { ok: false; blockers: DraftReadinessBlocker[] };
41
+
42
+ export type DraftReadinessBlocker =
43
+ | { code: "empty-session" }
44
+ | { code: "empty-applicable-stack"; stack: UltraPlanStackId }
45
+ | { code: "empty-domain"; stack: UltraPlanStackId; domainId: string }
46
+ | { code: "missing-required-slot"; stack: UltraPlanStackId; slot: UltraPlanAgentSlotName };
47
+
48
+ const ZERO_PROGRESS = { total: 0, terminal: 0, blocked: 0 } as const;
49
+
50
+ export const SESSION_TITLE_MAX = 80;
51
+ export const SESSION_GOAL_MAX = 280;
52
+ export const DOMAIN_NAME_MAX = 60;
53
+ export const SCENARIO_TITLE_MAX = 120;
54
+
55
+ function codePointLength(str: string): number {
56
+ return [...str].length;
57
+ }
58
+
59
+ function lengthCap(field: string, max: number, value: string): DraftOpError | null {
60
+ const got = codePointLength(value);
61
+ if (got === 0 || got > max) {
62
+ return { code: "length-cap", field, max, got };
63
+ }
64
+ return null;
65
+ }
66
+
67
+ export function slugifyUltraPlanId(raw: string, maxLength = 32): string {
68
+ return raw
69
+ .toLowerCase()
70
+ .normalize("NFKD")
71
+ .replace(/[\u0300-\u036f]/g, "")
72
+ .replace(/[^a-z0-9]+/g, "-")
73
+ .replace(/^-+|-+$/g, "")
74
+ .slice(0, maxLength) || "item";
75
+ }
76
+
77
+ function validateDraft(draft: UltraPlanAuthoredDraft): DraftOpResult {
78
+ if (isUltraPlanAuthoredArtifact(draft)) {
79
+ return { ok: true, draft };
80
+ }
81
+ const errors = getUltraPlanSchemaErrors(UltraPlanAuthoredArtifactSchema, draft);
82
+ const first = errors[0] ?? "/ validation failed";
83
+ const spaceAt = first.indexOf(" ");
84
+ const path = spaceAt > 0 ? first.slice(0, spaceAt) : "/";
85
+ const message = spaceAt > 0 ? first.slice(spaceAt + 1) : first;
86
+ return { ok: false, reason: { code: "invariant-violation", path, message } };
87
+ }
88
+
89
+ function isGateEnabled(
90
+ catalog: ResolvedUltraPlanCatalog,
91
+ reviewer: UltraPlanReviewerSlotName,
92
+ ): boolean {
93
+ return catalog.reviewGates[reviewer]?.enabled ?? true;
94
+ }
95
+
96
+ function projectBinding(source: ResolvedUltraPlanSlotBinding): UltraPlanAgentBinding {
97
+ return {
98
+ slot: source.slot,
99
+ agentType: source.agentType,
100
+ agentName: source.agentName,
101
+ model: source.model,
102
+ thinkingLevel: source.thinkingLevel,
103
+ };
104
+ }
105
+
106
+ function buildStackAgentSlots(
107
+ stack: UltraPlanStackId,
108
+ catalog: ResolvedUltraPlanCatalog,
109
+ ): UltraPlanAgentSlots {
110
+ const executorSlot: UltraPlanAgentSlotName = `${stack}-executor`;
111
+ const testerSlot: UltraPlanAgentSlotName = `${stack}-tester`;
112
+ const domainReviewerSlot = `${stack}-domain-reviewer` as UltraPlanReviewerSlotName;
113
+ const stackReviewerSlot = `${stack}-stack-reviewer` as UltraPlanReviewerSlotName;
114
+
115
+ const executor = catalog.slots[executorSlot];
116
+ const tester = catalog.slots[testerSlot];
117
+ if (!executor) {
118
+ throw new Error(`buildInitialAuthoredDraft: catalog is missing required slot ${executorSlot}`);
119
+ }
120
+ if (!tester) {
121
+ throw new Error(`buildInitialAuthoredDraft: catalog is missing required slot ${testerSlot}`);
122
+ }
123
+
124
+ const domainReviewEnabled = isGateEnabled(catalog, domainReviewerSlot);
125
+ const stackReviewEnabled = isGateEnabled(catalog, stackReviewerSlot);
126
+
127
+ const slots: UltraPlanAgentSlots = {
128
+ executor: projectBinding(executor),
129
+ tester: projectBinding(tester),
130
+ domainReviewEnabled,
131
+ stackReviewEnabled,
132
+ };
133
+
134
+ if (domainReviewEnabled) {
135
+ const reviewer = catalog.slots[domainReviewerSlot];
136
+ if (!reviewer) {
137
+ throw new Error(
138
+ `buildInitialAuthoredDraft: catalog is missing required slot ${domainReviewerSlot} while the gate is enabled`,
139
+ );
140
+ }
141
+ slots.domainReviewer = projectBinding(reviewer);
142
+ }
143
+
144
+ if (stackReviewEnabled) {
145
+ const reviewer = catalog.slots[stackReviewerSlot];
146
+ if (!reviewer) {
147
+ throw new Error(
148
+ `buildInitialAuthoredDraft: catalog is missing required slot ${stackReviewerSlot} while the gate is enabled`,
149
+ );
150
+ }
151
+ slots.stackReviewer = projectBinding(reviewer);
152
+ }
153
+
154
+ return slots;
155
+ }
156
+
157
+ export function buildInitialAuthoredDraft(params: {
158
+ sessionId: string;
159
+ title: string;
160
+ goal: string;
161
+ createdAt: Date;
162
+ catalog: ResolvedUltraPlanCatalog;
163
+ }): UltraPlanAuthoredDraft {
164
+ if (!params.sessionId) {
165
+ throw new Error("buildInitialAuthoredDraft: sessionId must be non-empty");
166
+ }
167
+ if (!params.title) {
168
+ throw new Error("buildInitialAuthoredDraft: title must be non-empty");
169
+ }
170
+ if (!params.goal) {
171
+ throw new Error("buildInitialAuthoredDraft: goal must be non-empty");
172
+ }
173
+
174
+ const createdAtIso = params.createdAt.toISOString();
175
+ const stacks: UltraPlanStack[] = ULTRAPLAN_STACKS.map((stack) => ({
176
+ stack,
177
+ applicability: "applicable" as const,
178
+ domains: [],
179
+ status: "ready" as const,
180
+ agentSlots: buildStackAgentSlots(stack, params.catalog),
181
+ progress: { ...ZERO_PROGRESS },
182
+ }));
183
+
184
+ return {
185
+ sessionId: params.sessionId,
186
+ title: params.title,
187
+ goal: params.goal,
188
+ createdAt: createdAtIso,
189
+ updatedAt: createdAtIso,
190
+ stacks,
191
+ };
192
+ }
193
+
194
+ export function setSessionTitleAndGoal(
195
+ draft: UltraPlanAuthoredDraft,
196
+ patch: { title?: string; goal?: string },
197
+ ): DraftOpResult {
198
+ if (patch.title !== undefined) {
199
+ const err = lengthCap("title", SESSION_TITLE_MAX, patch.title);
200
+ if (err) return { ok: false, reason: err };
201
+ }
202
+ if (patch.goal !== undefined) {
203
+ const err = lengthCap("goal", SESSION_GOAL_MAX, patch.goal);
204
+ if (err) return { ok: false, reason: err };
205
+ }
206
+
207
+ const candidate: UltraPlanAuthoredDraft = {
208
+ ...draft,
209
+ title: patch.title ?? draft.title,
210
+ goal: patch.goal ?? draft.goal,
211
+ };
212
+ return validateDraft(candidate);
213
+ }
214
+
215
+ export function setSessionId(draft: UltraPlanAuthoredDraft, sessionId: string): DraftOpResult {
216
+ if (codePointLength(sessionId) === 0) {
217
+ return { ok: false, reason: { code: "length-cap", field: "sessionId", max: 128, got: 0 } };
218
+ }
219
+ const candidate: UltraPlanAuthoredDraft = { ...draft, sessionId };
220
+ return validateDraft(candidate);
221
+ }
222
+
223
+ export function setStackApplicability(
224
+ draft: UltraPlanAuthoredDraft,
225
+ stack: UltraPlanStackId,
226
+ applicability: UltraPlanApplicability,
227
+ ): DraftOpResult {
228
+ const index = draft.stacks.findIndex((s) => s.stack === stack);
229
+ if (index === -1) {
230
+ return { ok: false, reason: { code: "not-found", where: "stack", id: stack as string } };
231
+ }
232
+
233
+ const current = draft.stacks[index];
234
+ if (current.applicability === applicability) {
235
+ return { ok: true, draft };
236
+ }
237
+
238
+ const nextStack: UltraPlanStack = applicability === "not-applicable"
239
+ ? {
240
+ ...current,
241
+ applicability,
242
+ domains: [],
243
+ progress: { ...ZERO_PROGRESS },
244
+ }
245
+ : {
246
+ ...current,
247
+ applicability,
248
+ domains: [],
249
+ progress: { ...ZERO_PROGRESS },
250
+ };
251
+
252
+ const stacks = draft.stacks.slice();
253
+ stacks[index] = nextStack;
254
+ return validateDraft({ ...draft, stacks });
255
+ }
256
+
257
+ function recomputeStackProgress(stack: UltraPlanStack): UltraPlanStack {
258
+ let total = 0;
259
+ let terminal = 0;
260
+ let blocked = 0;
261
+ for (const domain of stack.domains) {
262
+ total += domain.progress.total;
263
+ terminal += domain.progress.terminal;
264
+ blocked += domain.progress.blocked;
265
+ }
266
+ return { ...stack, progress: { total, terminal, blocked } };
267
+ }
268
+
269
+ function mutateStack(
270
+ draft: UltraPlanAuthoredDraft,
271
+ stackId: UltraPlanStackId,
272
+ update: (stack: UltraPlanStack) => UltraPlanStack | DraftOpError,
273
+ ): DraftOpResult {
274
+ const index = draft.stacks.findIndex((s) => s.stack === stackId);
275
+ if (index === -1) {
276
+ return { ok: false, reason: { code: "not-found", where: "stack", id: stackId as string } };
277
+ }
278
+ const updated = update(draft.stacks[index]);
279
+ if (!("stack" in updated) || typeof (updated as UltraPlanStack).stack !== "string") {
280
+ return { ok: false, reason: updated as DraftOpError };
281
+ }
282
+ const stacks = draft.stacks.slice();
283
+ stacks[index] = updated as UltraPlanStack;
284
+ return validateDraft({ ...draft, stacks });
285
+ }
286
+
287
+ export function addDomain(
288
+ draft: UltraPlanAuthoredDraft,
289
+ stack: UltraPlanStackId,
290
+ domain: { id: string; name: string },
291
+ ): DraftOpResult {
292
+ const idErr = lengthCap("domain.id", 32, domain.id);
293
+ if (idErr) return { ok: false, reason: idErr };
294
+ const nameErr = lengthCap("domain.name", DOMAIN_NAME_MAX, domain.name);
295
+ if (nameErr) return { ok: false, reason: nameErr };
296
+
297
+ return mutateStack(draft, stack, (current) => {
298
+ if (current.domains.some((d) => d.id === domain.id)) {
299
+ return { code: "duplicate-id", where: "domain", id: domain.id };
300
+ }
301
+ const newDomain = {
302
+ id: domain.id,
303
+ name: domain.name,
304
+ unit: [],
305
+ integration: [],
306
+ e2e: [],
307
+ review: {
308
+ enabled: current.agentSlots.domainReviewEnabled,
309
+ status: "pending" as const,
310
+ },
311
+ progress: { ...ZERO_PROGRESS },
312
+ };
313
+ const next: UltraPlanStack = {
314
+ ...current,
315
+ domains: [...current.domains, newDomain],
316
+ };
317
+ return recomputeStackProgress(next);
318
+ });
319
+ }
320
+
321
+ export function renameDomain(
322
+ draft: UltraPlanAuthoredDraft,
323
+ stack: UltraPlanStackId,
324
+ domainId: string,
325
+ patch: { name: string },
326
+ ): DraftOpResult {
327
+ const nameErr = lengthCap("domain.name", DOMAIN_NAME_MAX, patch.name);
328
+ if (nameErr) return { ok: false, reason: nameErr };
329
+
330
+ return mutateStack(draft, stack, (current) => {
331
+ const domainIndex = current.domains.findIndex((d) => d.id === domainId);
332
+ if (domainIndex === -1) {
333
+ return { code: "not-found", where: "domain", id: domainId };
334
+ }
335
+ const domains = current.domains.slice();
336
+ domains[domainIndex] = { ...domains[domainIndex], name: patch.name };
337
+ return { ...current, domains };
338
+ });
339
+ }
340
+
341
+ export function removeDomain(
342
+ draft: UltraPlanAuthoredDraft,
343
+ stack: UltraPlanStackId,
344
+ domainId: string,
345
+ ): DraftOpResult {
346
+ return mutateStack(draft, stack, (current) => {
347
+ const domainIndex = current.domains.findIndex((d) => d.id === domainId);
348
+ if (domainIndex === -1) {
349
+ return { code: "not-found", where: "domain", id: domainId };
350
+ }
351
+ const next: UltraPlanStack = {
352
+ ...current,
353
+ domains: current.domains.filter((_, i) => i !== domainIndex),
354
+ };
355
+ return recomputeStackProgress(next);
356
+ });
357
+ }
358
+
359
+ function recomputeDomainProgress<T extends { unit: unknown[]; integration: unknown[]; e2e: unknown[]; progress: { total: number; terminal: number; blocked: number } }>(domain: T): T {
360
+ const total = domain.unit.length + domain.integration.length + domain.e2e.length;
361
+ return { ...domain, progress: { total, terminal: 0, blocked: 0 } };
362
+ }
363
+
364
+ function mutateDomain(
365
+ draft: UltraPlanAuthoredDraft,
366
+ coord: { stack: UltraPlanStackId; domainId: string },
367
+ update: (domain: UltraPlanStack["domains"][number], stack: UltraPlanStack) => UltraPlanStack["domains"][number] | DraftOpError,
368
+ ): DraftOpResult {
369
+ return mutateStack(draft, coord.stack, (current) => {
370
+ const domainIndex = current.domains.findIndex((d) => d.id === coord.domainId);
371
+ if (domainIndex === -1) {
372
+ return { code: "not-found", where: "domain", id: coord.domainId };
373
+ }
374
+ const updated = update(current.domains[domainIndex], current);
375
+ if (!("id" in updated) || typeof (updated as { id?: unknown }).id !== "string" || "code" in updated) {
376
+ return updated as DraftOpError;
377
+ }
378
+ const domains = current.domains.slice();
379
+ domains[domainIndex] = updated as UltraPlanStack["domains"][number];
380
+ return recomputeStackProgress({ ...current, domains });
381
+ });
382
+ }
383
+
384
+ function scenarioAssignedSlots(stack: UltraPlanStack, level: UltraPlanScenarioLevel): UltraPlanAgentSlotName[] {
385
+ if (level === "unit") {
386
+ return [stack.agentSlots.executor.slot];
387
+ }
388
+ return [stack.agentSlots.tester.slot, stack.agentSlots.executor.slot];
389
+ }
390
+
391
+ export function addScenario(
392
+ draft: UltraPlanAuthoredDraft,
393
+ coord: { stack: UltraPlanStackId; domainId: string; level: UltraPlanScenarioLevel },
394
+ scenario: { id: string; title: string; steps?: string[]; dependencies?: string[] },
395
+ ): DraftOpResult {
396
+ const idErr = lengthCap("scenario.id", 48, scenario.id);
397
+ if (idErr) return { ok: false, reason: idErr };
398
+ const titleErr = lengthCap("scenario.title", SCENARIO_TITLE_MAX, scenario.title);
399
+ if (titleErr) return { ok: false, reason: titleErr };
400
+
401
+ return mutateDomain(draft, coord, (domain, stack) => {
402
+ const bucket = domain[coord.level];
403
+ if (bucket.some((s) => s.id === scenario.id)) {
404
+ return { code: "duplicate-id", where: "scenario", id: scenario.id };
405
+ }
406
+ const newScenario = {
407
+ id: scenario.id,
408
+ title: scenario.title,
409
+ stack: coord.stack,
410
+ domainId: coord.domainId,
411
+ level: coord.level,
412
+ status: "planned" as const,
413
+ steps: scenario.steps ?? [],
414
+ assignedSlots: scenarioAssignedSlots(stack, coord.level),
415
+ ...(scenario.dependencies && scenario.dependencies.length > 0 ? { dependencies: scenario.dependencies } : {}),
416
+ proofs: [],
417
+ };
418
+ const nextDomain = {
419
+ ...domain,
420
+ [coord.level]: [...bucket, newScenario],
421
+ };
422
+ return recomputeDomainProgress(nextDomain);
423
+ });
424
+ }
425
+
426
+ export function renameScenario(
427
+ draft: UltraPlanAuthoredDraft,
428
+ coord: {
429
+ stack: UltraPlanStackId;
430
+ domainId: string;
431
+ level: UltraPlanScenarioLevel;
432
+ scenarioId: string;
433
+ },
434
+ patch: { title: string },
435
+ ): DraftOpResult {
436
+ const titleErr = lengthCap("scenario.title", SCENARIO_TITLE_MAX, patch.title);
437
+ if (titleErr) return { ok: false, reason: titleErr };
438
+
439
+ return mutateDomain(draft, coord, (domain) => {
440
+ const bucket = domain[coord.level];
441
+ const scenarioIndex = bucket.findIndex((s) => s.id === coord.scenarioId);
442
+ if (scenarioIndex === -1) {
443
+ return { code: "not-found", where: "scenario", id: coord.scenarioId };
444
+ }
445
+ const nextBucket = bucket.slice();
446
+ nextBucket[scenarioIndex] = { ...nextBucket[scenarioIndex], title: patch.title };
447
+ return { ...domain, [coord.level]: nextBucket };
448
+ });
449
+ }
450
+
451
+ export function removeScenario(
452
+ draft: UltraPlanAuthoredDraft,
453
+ coord: {
454
+ stack: UltraPlanStackId;
455
+ domainId: string;
456
+ level: UltraPlanScenarioLevel;
457
+ scenarioId: string;
458
+ },
459
+ ): DraftOpResult {
460
+ return mutateDomain(draft, coord, (domain) => {
461
+ const bucket = domain[coord.level];
462
+ const scenarioIndex = bucket.findIndex((s) => s.id === coord.scenarioId);
463
+ if (scenarioIndex === -1) {
464
+ return { code: "not-found", where: "scenario", id: coord.scenarioId };
465
+ }
466
+ const nextBucket = bucket.filter((_, i) => i !== scenarioIndex);
467
+ const nextDomain = { ...domain, [coord.level]: nextBucket };
468
+ return recomputeDomainProgress(nextDomain);
469
+ });
470
+ }
471
+
472
+ const LEVEL_ORDER: readonly UltraPlanScenarioLevel[] = ["unit", "integration", "e2e"];
473
+
474
+ function domainReviewRelativePath(stack: UltraPlanStackId, domainId: string): string {
475
+ return `review/${stack}/domains/${domainId}.json`;
476
+ }
477
+
478
+ function stackReviewRelativePath(stack: UltraPlanStackId): string {
479
+ return `review/${stack}/stack.json`;
480
+ }
481
+
482
+ export function draftToAuthoredArtifact(
483
+ draft: UltraPlanAuthoredDraft,
484
+ now: Date,
485
+ ): UltraPlanAuthoredArtifact {
486
+ return {
487
+ sessionId: draft.sessionId,
488
+ title: draft.title,
489
+ goal: draft.goal,
490
+ createdAt: draft.createdAt,
491
+ updatedAt: now.toISOString(),
492
+ stacks: draft.stacks,
493
+ };
494
+ }
495
+
496
+ export function draftToManifest(
497
+ draft: UltraPlanAuthoredDraft,
498
+ projectName: string,
499
+ now: Date,
500
+ ): UltraPlanManifest {
501
+ const stacks = draft.stacks.map((stack) => ({
502
+ stack: stack.stack,
503
+ applicability: stack.applicability,
504
+ progress: { ...stack.progress },
505
+ domainCount: stack.domains.length,
506
+ terminalDomainCount: 0,
507
+ }));
508
+
509
+ let totalScenarios = 0;
510
+ for (const stack of draft.stacks) {
511
+ totalScenarios += stack.progress.total;
512
+ }
513
+
514
+ const reviews: UltraPlanManifest["reviews"] = [];
515
+ for (const stack of draft.stacks) {
516
+ if (stack.applicability !== "applicable") continue;
517
+ if (stack.agentSlots.domainReviewEnabled) {
518
+ for (const domain of stack.domains) {
519
+ reviews.push({
520
+ type: "domain",
521
+ stack: stack.stack,
522
+ domainId: domain.id,
523
+ path: domainReviewRelativePath(stack.stack, domain.id),
524
+ status: "pending",
525
+ });
526
+ }
527
+ }
528
+ if (stack.agentSlots.stackReviewEnabled) {
529
+ reviews.push({
530
+ type: "stack",
531
+ stack: stack.stack,
532
+ domainId: null,
533
+ path: stackReviewRelativePath(stack.stack),
534
+ status: "pending",
535
+ });
536
+ }
537
+ }
538
+
539
+ return {
540
+ sessionId: draft.sessionId,
541
+ projectName,
542
+ title: draft.title,
543
+ authored: { json: ULTRAPLAN_AUTHORED_JSON_FILENAME },
544
+ state: "ready",
545
+ cursor: initialCursor(draft),
546
+ lastCompleted: null,
547
+ progress: { total: totalScenarios, terminal: 0, blocked: 0 },
548
+ stacks,
549
+ blocker: null,
550
+ reviews,
551
+ createdAt: draft.createdAt,
552
+ updatedAt: now.toISOString(),
553
+ };
554
+ }
555
+
556
+ export function draftToIndexEntry(
557
+ draft: UltraPlanAuthoredDraft,
558
+ now: Date,
559
+ ): UltraPlanIndexEntry {
560
+ return {
561
+ sessionId: draft.sessionId,
562
+ title: draft.title,
563
+ state: "ready",
564
+ bucket: "pending",
565
+ createdAt: draft.createdAt,
566
+ updatedAt: now.toISOString(),
567
+ cursor: initialCursor(draft),
568
+ idleReason: null,
569
+ };
570
+ }
571
+
572
+ export function initialCursor(draft: UltraPlanAuthoredDraft): UltraPlanCursor {
573
+ for (const stack of draft.stacks) {
574
+ if (stack.applicability !== "applicable") continue;
575
+ for (const domain of stack.domains) {
576
+ for (const level of LEVEL_ORDER) {
577
+ const bucket = domain[level];
578
+ if (bucket.length === 0) continue;
579
+ const scenario = bucket[0];
580
+ return {
581
+ targetType: "scenario",
582
+ stack: stack.stack,
583
+ domainId: domain.id,
584
+ level,
585
+ scenarioId: scenario.id,
586
+ phase: "red",
587
+ status: "planned",
588
+ summary: scenario.title,
589
+ };
590
+ }
591
+ }
592
+ }
593
+
594
+ // Fallback — a non-persist-ready draft with no scenarios. Projections still need a valid cursor
595
+ // to satisfy the schema; the readiness gate prevents reaching persist here.
596
+ return {
597
+ targetType: "session",
598
+ stack: null,
599
+ domainId: null,
600
+ level: null,
601
+ scenarioId: null,
602
+ phase: "complete",
603
+ status: "ready",
604
+ summary: "No scenarios authored yet",
605
+ };
606
+ }
607
+
608
+ export function isDraftReadyToPersist(draft: UltraPlanAuthoredDraft): DraftReadiness {
609
+ const blockers: DraftReadinessBlocker[] = [];
610
+ const applicableStacks = draft.stacks.filter((s) => s.applicability === "applicable");
611
+
612
+ if (applicableStacks.length === 0) {
613
+ blockers.push({ code: "empty-session" });
614
+ }
615
+
616
+ for (const stack of applicableStacks) {
617
+ // Defensive: reject bypassed drafts whose required slots have been nulled.
618
+ const slots = stack.agentSlots as {
619
+ executor?: unknown;
620
+ tester?: unknown;
621
+ domainReviewEnabled?: boolean;
622
+ stackReviewEnabled?: boolean;
623
+ domainReviewer?: unknown;
624
+ stackReviewer?: unknown;
625
+ };
626
+ if (!slots.executor) {
627
+ blockers.push({ code: "missing-required-slot", stack: stack.stack, slot: `${stack.stack}-executor` as UltraPlanAgentSlotName });
628
+ }
629
+ if (!slots.tester) {
630
+ blockers.push({ code: "missing-required-slot", stack: stack.stack, slot: `${stack.stack}-tester` as UltraPlanAgentSlotName });
631
+ }
632
+ if (slots.domainReviewEnabled && !slots.domainReviewer) {
633
+ blockers.push({ code: "missing-required-slot", stack: stack.stack, slot: `${stack.stack}-domain-reviewer` as UltraPlanAgentSlotName });
634
+ }
635
+ if (slots.stackReviewEnabled && !slots.stackReviewer) {
636
+ blockers.push({ code: "missing-required-slot", stack: stack.stack, slot: `${stack.stack}-stack-reviewer` as UltraPlanAgentSlotName });
637
+ }
638
+
639
+ if (stack.domains.length === 0) {
640
+ blockers.push({ code: "empty-applicable-stack", stack: stack.stack });
641
+ continue;
642
+ }
643
+
644
+ for (const domain of stack.domains) {
645
+ const total = domain.unit.length + domain.integration.length + domain.e2e.length;
646
+ if (total === 0) {
647
+ blockers.push({ code: "empty-domain", stack: stack.stack, domainId: domain.id });
648
+ }
649
+ }
650
+ }
651
+
652
+ return blockers.length === 0 ? { ok: true } : { ok: false, blockers };
653
+ }