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
@@ -1,9 +1,13 @@
1
1
  import type { Platform } from "../platform/types.js";
2
2
  import type { DebugLogger } from "../debug/logger.js";
3
- import type { ResolvedModel } from "../types.js";
3
+ import type { Plan, ResolvedModel } from "../types.js";
4
4
  import type { PlanningSystemPromptOptions } from "./system-prompt.js";
5
5
  import { applyModelOverride } from "../config/model-resolver.js";
6
- import { listPlans, readPlanFile } from "../storage/plans.js";
6
+ import { listPlans, parsePlan, readPlanFile } from "../storage/plans.js";
7
+ import { validatePlanMarkdown } from "./validate.js";
8
+ import { getProjectStatePath } from "../workspace/state-paths.js";
9
+ import * as path from "node:path";
10
+ import { appendReliabilityRecord } from "../storage/reliability-metrics.js";
7
11
 
8
12
  /**
9
13
  * Plan approval flow state.
@@ -79,13 +83,135 @@ export function getPlanningDebugLogger(): DebugLogger | null {
79
83
  return planningDebugLogger;
80
84
  }
81
85
 
86
+ /**
87
+ * Mirrors OMP 14.5.11+'s `todo_write` payload shape just enough to hand the
88
+ * agent a ready-to-execute payload after plan approval.
89
+ *
90
+ * The canonical schema lives in OMP \u2014 keep this type local; we only construct
91
+ * the payload to embed in a prompt string. Task identity is the task content
92
+ * verbatim; later progress updates (`start`/`done`/`note`) refer to that exact
93
+ * string, not synthetic ids.
94
+ */
95
+ type TodoWriteOp =
96
+ | {
97
+ op: "init";
98
+ list: Array<{ phase: string; items: string[] }>;
99
+ }
100
+ | { op: "note"; task: string; text: string };
101
+
102
+ /** Cap individual task labels so the embedded payload stays bounded. */
103
+ const TODO_WRITE_TASK_LABEL_MAX_CHARS = 200;
104
+
105
+ /**
106
+ * Single phase name supipowers' planner emits today. Imported into the
107
+ * execution prompt so the prose stays in lock-step with the JSON payload \u2014
108
+ * change here and `buildExecutionPrompt` follows automatically.
109
+ */
110
+ const PLAN_PHASE_NAME = "Implementation";
111
+
112
+ function truncateTaskLabel(label: string): string {
113
+ if (label.length <= TODO_WRITE_TASK_LABEL_MAX_CHARS) return label;
114
+ return `${label.slice(0, TODO_WRITE_TASK_LABEL_MAX_CHARS - 1).trimEnd()}\u2026`;
115
+ }
116
+
117
+ function appendTaskLabelOrdinal(label: string, ordinal: number): string {
118
+ const suffix = ` (${ordinal})`;
119
+ if (label.length + suffix.length <= TODO_WRITE_TASK_LABEL_MAX_CHARS) {
120
+ return `${label}${suffix}`;
121
+ }
122
+ const prefixMax = TODO_WRITE_TASK_LABEL_MAX_CHARS - suffix.length - 1;
123
+ return `${label.slice(0, prefixMax).trimEnd()}\u2026${suffix}`;
124
+ }
125
+
126
+ function uniqueTaskLabels(names: string[]): string[] {
127
+ const used = new Set<string>();
128
+ const nextOrdinalByBase = new Map<string, number>();
129
+ return names.map((name) => {
130
+ const base = truncateTaskLabel(name);
131
+ let label = base;
132
+ if (used.has(label)) {
133
+ let ordinal = nextOrdinalByBase.get(base) ?? 2;
134
+ do {
135
+ label = appendTaskLabelOrdinal(base, ordinal);
136
+ ordinal += 1;
137
+ } while (used.has(label));
138
+ nextOrdinalByBase.set(base, ordinal);
139
+ } else {
140
+ nextOrdinalByBase.set(base, 2);
141
+ }
142
+ used.add(label);
143
+ return label;
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Build the canonical `todo_write` ops payload from a parsed plan.
149
+ *
150
+ * Empty plans return `{ ops: [] }` so callers can skip emit cleanly.
151
+ * Non-empty plans always start with a single `init` op containing one phase
152
+ * (`Implementation`) whose `items` is one task content string per `plan.tasks`
153
+ * entry. Task names are truncated and de-duplicated before they become todo
154
+ * identities. Tasks that carry acceptance criteria get a follow-up `note` op
155
+ * whose `task` field is the exact item label, keeping note targets in lock-step
156
+ */
157
+ export function buildTodoWriteOpsForPlan(plan: Plan): { ops: TodoWriteOp[] } {
158
+ if (plan.tasks.length === 0) return { ops: [] };
159
+
160
+ const items = uniqueTaskLabels(plan.tasks.map((task) => task.name));
161
+
162
+ const ops: TodoWriteOp[] = [
163
+ {
164
+ op: "init",
165
+ list: [{ phase: PLAN_PHASE_NAME, items }],
166
+ },
167
+ ];
168
+
169
+ for (const [index, task] of plan.tasks.entries()) {
170
+ const trimmed = task.criteria.trim();
171
+ if (!trimmed) continue;
172
+ // Note targets MUST equal the item label verbatim \u2014 task identity is the
173
+ // task content string, never a synthetic id.
174
+ ops.push({ op: "note", task: items[index], text: trimmed });
175
+ }
176
+
177
+ return { ops };
178
+ }
179
+
82
180
  /**
83
181
  * Build the execution handoff prompt from an approved plan.
84
182
  *
85
183
  * Mirrors OMP's `plan-mode-approved.md` template: critical directive
86
184
  * to execute, the full plan content, and step-by-step instructions.
185
+ *
186
+ * When `plan` is provided and has tasks, the prompt also embeds the
187
+ * exact `todo_write` payload the agent must call before doing any work.
87
188
  */
88
- function buildExecutionPrompt(planContent: string, planPath: string): string {
189
+ function buildExecutionPrompt(
190
+ planContent: string,
191
+ planPath: string,
192
+ plan?: Plan,
193
+ ): string {
194
+ const todoBlock: string[] = [];
195
+ if (plan && plan.tasks.length > 0) {
196
+ const payload = buildTodoWriteOpsForPlan(plan);
197
+ const initOp = payload.ops.find(
198
+ (op): op is Extract<TodoWriteOp, { op: "init" }> => op.op === "init",
199
+ );
200
+ const phaseName = initOp?.list[0]?.phase ?? PLAN_PHASE_NAME;
201
+ todoBlock.push(
202
+ "",
203
+ "## Initialize todo tracker",
204
+ "",
205
+ "Before any other work, call `todo_write` with exactly this payload:",
206
+ "",
207
+ "```json",
208
+ JSON.stringify(payload, null, 2),
209
+ "```",
210
+ "",
211
+ `Task identity is the task content verbatim. Later progress updates (\`start\`, \`done\`, \`note\`) MUST pass \`task\` equal to the exact item string above; phase updates MUST pass \`phase: "${phaseName}"\`. Mark the first task \`in_progress\` and proceed.`,
212
+ );
213
+ }
214
+
89
215
  return [
90
216
  "<critical>",
91
217
  "Plan approved. You **MUST** execute it now.",
@@ -100,9 +226,8 @@ function buildExecutionPrompt(planContent: string, planPath: string): string {
100
226
  "<instruction>",
101
227
  `You **MUST** execute this plan step by step from \`${planPath}\`.`,
102
228
  "You **MUST** verify each step before proceeding to the next.",
103
- "Before execution, you **MUST** initialize todo tracking with the task list.",
104
- "After each completed step, immediately update progress so it stays visible.",
105
229
  "</instruction>",
230
+ ...todoBlock,
106
231
  "",
107
232
  "<critical>",
108
233
  "You **MUST** keep going until complete. This matters.",
@@ -128,8 +253,9 @@ async function executeApproveFlow(
128
253
  newSession: ((options?: any) => Promise<{ cancelled: boolean }>) | null,
129
254
  resolvedModel: ResolvedModel | null,
130
255
  debugLogger: DebugLogger | null,
256
+ plan: Plan | null,
131
257
  ): Promise<void> {
132
- const prompt = buildExecutionPrompt(planContent, planPath);
258
+ const prompt = buildExecutionPrompt(planContent, planPath, plan ?? undefined);
133
259
  debugLogger?.log("execution_handoff_started", {
134
260
  planPath,
135
261
  promptLength: prompt.length,
@@ -211,7 +337,76 @@ export function registerPlanApprovalHook(platform: Platform): void {
211
337
  return;
212
338
  }
213
339
 
214
- const planPath = `${platform.paths.dotDirDisplay}/supipowers/plans/${planName}`;
340
+ // Schema-first validation: the plan must parse into a valid PlanSpec.
341
+ // Invalid plans trigger a retry steer — no approval UI until the agent
342
+ // produces an artifact whose task list matches the PlanSpec contract.
343
+ //
344
+ // We validate but do NOT canonicalize the on-disk file. Today's plan
345
+ // writer produces rich markdown (architecture, per-task TDD steps) that
346
+ // the parser intentionally does not capture. Rewriting the file from the
347
+ // parsed PlanSpec would strip that structure. The schema is the
348
+ // validation gate; markdown stays the user-visible form until a future
349
+ // phase lifts the agent to write PlanSpec directly.
350
+ const validated = validatePlanMarkdown(planContent, planName);
351
+ if (!validated.output) {
352
+ debugLogger?.log("approval_flow_plan_invalid", {
353
+ planName,
354
+ error: validated.error,
355
+ errors: validated.errors,
356
+ });
357
+ try {
358
+ appendReliabilityRecord(platform.paths, planCwd, {
359
+ ts: new Date().toISOString(),
360
+ command: "plan",
361
+ operation: "plan-spec",
362
+ outcome: "blocked",
363
+ attempts: 1,
364
+ reason: validated.error ?? "Plan validation failed.",
365
+ cwd: planCwd,
366
+ });
367
+ } catch {}
368
+ plansBefore = plansNow;
369
+ const steer = [
370
+ `The plan you just wrote to \`${path.join(getProjectStatePath(platform.paths, planCwd, "plans"), planName)}\` does not match the required schema.`,
371
+ "",
372
+ "Validation errors:",
373
+ ...validated.errors.map((err) => `- ${err.path}: ${err.message}`),
374
+ "",
375
+ "Fix the plan and rewrite the file so every task includes id, name, files, criteria, and complexity (small|medium|large).",
376
+ ].join("\n");
377
+ platform.sendMessage(
378
+ {
379
+ customType: "supi-plan-invalid",
380
+ content: [{ type: "text", text: steer }],
381
+ display: "none",
382
+ },
383
+ { deliverAs: "steer", triggerTurn: true },
384
+ );
385
+ return;
386
+ }
387
+
388
+ const canonicalContent = planContent;
389
+ const planPath = path.join(getProjectStatePath(platform.paths, planCwd, "plans"), planName);
390
+ let parsedPlan: Plan | null = null;
391
+ try {
392
+ parsedPlan = parsePlan(planContent, planPath);
393
+ } catch (error) {
394
+ debugLogger?.log("approval_flow_plan_parse_failed", {
395
+ planName,
396
+ planPath,
397
+ error: error instanceof Error ? error.message : String(error),
398
+ });
399
+ }
400
+ try {
401
+ appendReliabilityRecord(platform.paths, planCwd, {
402
+ ts: new Date().toISOString(),
403
+ command: "plan",
404
+ operation: "plan-spec",
405
+ outcome: "ok",
406
+ attempts: 1,
407
+ cwd: planCwd,
408
+ });
409
+ } catch {}
215
410
  const approvalOptions = [
216
411
  "Approve and execute",
217
412
  "Refine plan",
@@ -238,11 +433,12 @@ export function registerPlanApprovalHook(platform: Platform): void {
238
433
  await executeApproveFlow(
239
434
  platform,
240
435
  ctx,
241
- planContent,
436
+ canonicalContent,
242
437
  planPath,
243
438
  executionNewSession,
244
439
  executionModel,
245
440
  debugLogger,
441
+ parsedPlan,
246
442
  );
247
443
  } else if (choice === "Refine plan") {
248
444
  // Keep planning active, let user type refinement.
@@ -260,11 +456,12 @@ export function registerPlanApprovalHook(platform: Platform): void {
260
456
  await executeApproveFlow(
261
457
  platform,
262
458
  ctx,
263
- planContent,
459
+ canonicalContent,
264
460
  planPath,
265
461
  executionNewSession,
266
462
  executionModel,
267
463
  debugLogger,
464
+ parsedPlan,
268
465
  );
269
466
  } else {
270
467
  debugLogger?.log("approval_flow_refine_requested", {
@@ -3,7 +3,8 @@ import { PLAN_CODE_CONTENT_REQUIREMENTS, formatPlanReviewCategorySummary } from
3
3
 
4
4
  export interface PlanWriterOptions {
5
5
  specPath: string;
6
- dotDirDisplay: string;
6
+ /** Absolute path to the directory where the plan must be saved. */
7
+ plansDir: string;
7
8
  }
8
9
 
9
10
  /**
@@ -18,7 +19,7 @@ export interface PlanWriterOptions {
18
19
  * - Execution handoff
19
20
  */
20
21
  export function buildPlanWriterPrompt(options: PlanWriterOptions): string {
21
- const { specPath, dotDirDisplay } = options;
22
+ const { specPath, plansDir } = options;
22
23
 
23
24
  const sections: string[] = [
24
25
  "You are writing a comprehensive implementation plan from an approved design spec.",
@@ -134,7 +135,7 @@ export function buildPlanWriterPrompt(options: PlanWriterOptions): string {
134
135
  // ── Save Location ────────────────────────────────────────────
135
136
  "## Step 5: Save Plan",
136
137
  "",
137
- `Save the plan to \`${dotDirDisplay}/supipowers/plans/YYYY-MM-DD-<feature-name>.md\``,
138
+ `Save the plan to \`${plansDir}/YYYY-MM-DD-<feature-name>.md\``,
138
139
  "",
139
140
 
140
141
  // ── Execution Handoff ────────────────────────────────────────
@@ -1,4 +1,6 @@
1
1
  import type { Platform } from "../platform/types.js";
2
+ import { isPlanningActive } from "./approval-flow.js";
3
+ import { isUiDesignActive, recordUiDesignReviewApproval } from "../ui-design/session.js";
2
4
 
3
5
  /**
4
6
  * Register a `planning_ask` tool — identical to the built-in `ask` tool
@@ -66,6 +68,7 @@ export function registerPlanningAskTool(platform: Platform): void {
66
68
  });
67
69
 
68
70
  const selected = choice ?? labels[params.recommended ?? 0] ?? labels[0];
71
+ recordUiDesignReviewApproval(params.question, labels, selected);
69
72
 
70
73
  return {
71
74
  content: [
@@ -79,3 +82,39 @@ export function registerPlanningAskTool(platform: Platform): void {
79
82
  },
80
83
  });
81
84
  }
85
+
86
+ function getAskRedirectReason(): string | null {
87
+ if (isPlanningActive()) {
88
+ return "Planning mode: use the `planning_ask` tool instead of `ask`. `planning_ask` has no timeout so the user can think without pressure.";
89
+ }
90
+
91
+ if (isUiDesignActive()) {
92
+ return "UI-design mode: use the `planning_ask` tool instead of `ask`. The Design Director workflow requires auditable gated responses through `planning_ask`.";
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Register a tool_call guard that blocks the generic `ask` tool during
100
+ * active planning or ui-design sessions and points the model at
101
+ * `planning_ask` instead.
102
+ *
103
+ * This is the runtime complement to the prompt-level directive in the
104
+ * planning and ui-design prompts: even if prompt wording drifts or the
105
+ * model ignores it, invoking `ask` in those modes returns a block result
106
+ * with a truthful redirection.
107
+ */
108
+ export function registerPlanningAskToolGuard(platform: Platform): void {
109
+ platform.on("tool_call", (event) => {
110
+ if (event.toolName !== "ask") return;
111
+
112
+ const reason = getAskRedirectReason();
113
+ if (!reason) return;
114
+
115
+ return {
116
+ block: true,
117
+ reason,
118
+ };
119
+ });
120
+ }
@@ -0,0 +1,74 @@
1
+ // src/planning/render-markdown.ts
2
+ //
3
+ // Deterministic PlanSpec → markdown renderer. Every saved plan comes from
4
+ // this module, so the on-disk representation cannot drift from the
5
+ // canonical PlanSpec. Parsers in src/storage/plans.ts must be able to
6
+ // recover a valid PlanSpec from this output — covered by the round-trip
7
+ // test in tests/planning/render-markdown.test.ts.
8
+
9
+ import type { PlanSpec, PlanSpecTask } from "./spec.js";
10
+
11
+ function renderFrontmatter(spec: PlanSpec): string {
12
+ const lines = ["---"];
13
+ lines.push(`name: ${spec.name}`);
14
+ if (spec.created) lines.push(`created: ${spec.created}`);
15
+ lines.push(`tags: [${spec.tags.join(", ")}]`);
16
+ lines.push("---");
17
+ return lines.join("\n");
18
+ }
19
+
20
+ function renderContextSection(context: string): string {
21
+ const body = context.trim();
22
+ return body.length > 0 ? `## Context\n\n${body}` : "## Context\n";
23
+ }
24
+
25
+ function renderFilesList(files: string[]): string {
26
+ if (files.length === 0) return "**files**: (none)";
27
+ return files.map((file) => `- \`${file}\``).join("\n");
28
+ }
29
+
30
+ function renderTask(task: PlanSpecTask): string {
31
+ const header = `### Task ${task.id}: ${task.name}${task.model ? ` [model: ${task.model}]` : ""}`;
32
+ const parts: string[] = [header, ""];
33
+
34
+ parts.push("**files**:");
35
+ parts.push(renderFilesList(task.files));
36
+ parts.push("");
37
+
38
+ parts.push(`**criteria**: ${task.criteria}`);
39
+ parts.push(`**complexity**: ${task.complexity}`);
40
+
41
+ if (task.description && task.description.trim() !== task.name.trim()) {
42
+ parts.push("");
43
+ parts.push(task.description.trim());
44
+ }
45
+
46
+ return parts.join("\n");
47
+ }
48
+
49
+ /**
50
+ * Render a validated PlanSpec to the canonical markdown representation
51
+ * accepted by src/storage/plans.ts's parser. Output is stable across
52
+ * identical input so diffs stay reviewable.
53
+ */
54
+ export function renderPlanSpec(spec: PlanSpec): string {
55
+ const sections: string[] = [
56
+ renderFrontmatter(spec),
57
+ "",
58
+ `# ${spec.name}`,
59
+ "",
60
+ renderContextSection(spec.context),
61
+ ];
62
+
63
+ if (spec.tasks.length > 0) {
64
+ sections.push("");
65
+ sections.push("## Tasks");
66
+ for (const task of spec.tasks) {
67
+ sections.push("");
68
+ sections.push(renderTask(task));
69
+ }
70
+ }
71
+
72
+ // Trailing newline for POSIX hygiene.
73
+ return sections.join("\n") + "\n";
74
+ }
@@ -0,0 +1,42 @@
1
+ // src/planning/spec.ts
2
+ //
3
+ // Canonical TypeBox contract for a supipowers implementation plan (PlanSpec).
4
+ // The agent produces markdown, src/storage/plans.ts parses it, and this
5
+ // schema is the validator of record. Markdown that doesn't parse into a
6
+ // PlanSpec is rejected — no silent promotion of partial artifacts.
7
+ //
8
+ // Phase 3 exit gate: PlanSpec is the canonical planning artifact; markdown
9
+ // is rendered from it via render-markdown.ts, not the other way around.
10
+
11
+ import { Type, type Static } from "@sinclair/typebox";
12
+
13
+ export const TASK_COMPLEXITY_VALUES = ["small", "medium", "large"] as const;
14
+
15
+ export const PlanSpecTaskSchema = Type.Object(
16
+ {
17
+ id: Type.Integer({ minimum: 1 }),
18
+ name: Type.String({ minLength: 1 }),
19
+ description: Type.String(),
20
+ files: Type.Array(Type.String({ minLength: 1 })),
21
+ criteria: Type.String(),
22
+ complexity: Type.Union(TASK_COMPLEXITY_VALUES.map((v) => Type.Literal(v))),
23
+ model: Type.Optional(Type.String({ minLength: 1 })),
24
+ },
25
+ { additionalProperties: false },
26
+ );
27
+
28
+ export const PlanSpecSchema = Type.Object(
29
+ {
30
+ name: Type.String({ minLength: 1 }),
31
+ /** ISO date string, e.g. "2026-04-17". Empty string is tolerated for
32
+ * legacy plans produced before the field was required. */
33
+ created: Type.String(),
34
+ tags: Type.Array(Type.String()),
35
+ context: Type.String(),
36
+ tasks: Type.Array(PlanSpecTaskSchema),
37
+ },
38
+ { additionalProperties: false },
39
+ );
40
+
41
+ export type PlanSpec = Static<typeof PlanSpecSchema>;
42
+ export type PlanSpecTask = Static<typeof PlanSpecTaskSchema>;
@@ -1,4 +1,5 @@
1
1
  import type { Platform } from "../platform/types.js";
2
+ import { systemPromptText } from "../platform/system-prompt.js";
2
3
  import { createDebugLogger } from "../debug/logger.js";
3
4
  import { getPlanningDebugLogger, getPlanningPromptOptions, isPlanningActive } from "./approval-flow.js";
4
5
  import { buildPlanWriterPrompt } from "./plan-writer-prompt.js";
@@ -8,6 +9,9 @@ import { buildSpecReviewerPrompt } from "./spec-reviewer.js";
8
9
  export interface PlanningSystemPromptOptions {
9
10
  skillContent?: string;
10
11
  dotDirDisplay: string;
12
+ /** Absolute path to the directory where plans must be saved.
13
+ * Owned by approval-flow's listPlans watcher; the agent MUST write here. */
14
+ plansDir: string;
11
15
  topic?: string;
12
16
  isQuick?: boolean;
13
17
  }
@@ -81,7 +85,7 @@ function buildFullPlanningSection(options: PlanningSystemPromptOptions): string
81
85
  const specReviewerPrompt = buildSpecReviewerPrompt("<path-to-spec-file>");
82
86
  const planWriterPrompt = buildPlanWriterPrompt({
83
87
  specPath: "<path-to-approved-spec>",
84
- dotDirDisplay: options.dotDirDisplay,
88
+ plansDir: options.plansDir,
85
89
  });
86
90
 
87
91
  return [
@@ -97,10 +101,9 @@ function buildFullPlanningSection(options: PlanningSystemPromptOptions): string
97
101
  "## HARD-GATE",
98
102
  "",
99
103
  "- Do NOT write production code, apply patches, or claim that you changed files during planning.",
100
- "- The only allowed file writes are the approved design doc under `.omp/supipowers/specs/` and the final implementation plan under `.omp/supipowers/plans/`.",
104
+ `- The only allowed file writes are the approved design doc under \`${options.dotDirDisplay}/supipowers/specs/\` and the final implementation plan under \`${options.plansDir}/\`.`,
101
105
  "- Keep planning artifacts local. Do NOT stage, commit, or push the design doc or implementation plan.",
102
106
  "- If the user asks to jump into coding early, explain that planning mode must finish first.",
103
- "- When you need to ask the user a question with options, use the `planning_ask` tool instead of `ask`. It has no timeout so the user can think without pressure.",
104
107
  "",
105
108
  "## Planning Workflow",
106
109
  "",
@@ -131,7 +134,7 @@ function buildFullPlanningSection(options: PlanningSystemPromptOptions): string
131
134
  "- Apply YAGNI ruthlessly and prefer isolated units with clear boundaries.",
132
135
  "",
133
136
  "### Phase 5: Write the design doc",
134
- "- After the user approves the design, write `.omp/supipowers/specs/YYYY-MM-DD-<topic>-design.md`.",
137
+ `- After the user approves the design, write \`${options.dotDirDisplay}/supipowers/specs/YYYY-MM-DD-<topic>-design.md\`.`,
135
138
  "- Use concise, implementation-ready language.",
136
139
  "- Keep the design doc local; do NOT commit it to git.",
137
140
  "",
@@ -209,7 +212,7 @@ function buildQuickPlanningSection(options: PlanningSystemPromptOptions): string
209
212
  "",
210
213
  "4. Each task must include name, files, criteria, and complexity (small/medium/large).",
211
214
  ` - ${QUICK_PLAN_TASK_CONTENT_REQUIREMENT}`,
212
- "5. Save the plan under `.omp/supipowers/plans/YYYY-MM-DD-<feature-name>.md`.",
215
+ `5. Save the plan under \`${options.plansDir}/YYYY-MM-DD-<feature-name>.md\`.`,
213
216
  "6. After saving, tell the user: `Plan saved to <path>. Review it and approve when ready.`",
214
217
  "7. Then stop and wait. The approval UI handles execution handoff.",
215
218
  ...buildAdditionalGuidelines(options.skillContent),
@@ -232,7 +235,7 @@ function buildPlanningCriticalBlock(options: PlanningSystemPromptOptions): strin
232
235
  "This is NOT native OMP plan mode.",
233
236
  "You **MUST NOT** call `exit_plan_mode` or `ExitPlanMode` — it will fail.",
234
237
  `You **MUST NOT** write plans to \`local://PLAN.md\` — that is OMP's native plan location and will not trigger the approval flow.`,
235
- `You **MUST** save the plan to \`${options.dotDirDisplay}/supipowers/plans/YYYY-MM-DD-<feature-name>.md\` using the Write tool.`,
238
+ `You **MUST** save the plan to \`${options.plansDir}/YYYY-MM-DD-<feature-name>.md\` using the Write tool.`,
236
239
  "After saving, tell the user the plan path, then **stop and yield your turn**.",
237
240
  "The approval UI appears automatically when your turn ends and a new plan file is detected in that directory.",
238
241
  "</critical>",
@@ -270,7 +273,7 @@ export function registerPlanningSystemPromptHook(platform: Platform): void {
270
273
 
271
274
  const debugLogger = getPlanningDebugLogger() ?? createDebugLogger(platform.paths, ctx as any, "plan");
272
275
  const options = getPlanningPromptOptions();
273
- const basePrompt = (event as any).systemPrompt as string | undefined;
276
+ const basePrompt = systemPromptText((event as any).systemPrompt);
274
277
  if (!options || !basePrompt) {
275
278
  debugLogger.log("system_prompt_override_skipped", {
276
279
  hasPlanningOptions: Boolean(options),
@@ -288,6 +291,6 @@ export function registerPlanningSystemPromptHook(platform: Platform): void {
288
291
  systemPrompt,
289
292
  });
290
293
 
291
- return { systemPrompt };
294
+ return { systemPrompt: [systemPrompt] };
292
295
  });
293
296
  }
@@ -0,0 +1,84 @@
1
+ // src/planning/validate.ts
2
+ //
3
+ // Typed validation helpers for PlanSpec. Consumers (plan command, approval
4
+ // flow) call `validatePlanSpec(data)` and branch on `.output` vs `.error`.
5
+ // Everyone converges on the same path so validation errors surface in one
6
+ // consistent shape with field-level paths.
7
+
8
+ import { Value } from "@sinclair/typebox/value";
9
+ import { collectValidationErrors, formatValidationErrors } from "../ai/structured-output.js";
10
+ import type { ValidationError } from "../types.js";
11
+ import { PlanSpecSchema, type PlanSpec } from "./spec.js";
12
+
13
+ export interface PlanSpecValidationResult {
14
+ output: PlanSpec | null;
15
+ error: string | null;
16
+ errors: ValidationError[];
17
+ }
18
+
19
+ /**
20
+ * Validate an arbitrary value against PlanSpecSchema. Returns `{output, null}`
21
+ * on success and `{null, error, errors}` on failure, where `error` is a
22
+ * human-readable summary and `errors` lists every field-level issue.
23
+ */
24
+ export function validatePlanSpec(data: unknown): PlanSpecValidationResult {
25
+ if (Value.Check(PlanSpecSchema, data)) {
26
+ return { output: data as PlanSpec, error: null, errors: [] };
27
+ }
28
+
29
+ const errors = collectValidationErrors(PlanSpecSchema, data);
30
+ const error = errors.length > 0
31
+ ? formatValidationErrors(errors).join("; ")
32
+ : "Plan does not match the PlanSpec schema.";
33
+ return { output: null, error, errors };
34
+ }
35
+
36
+ /**
37
+ * Narrowing predicate for PlanSpec. Use when you do not need error detail.
38
+ */
39
+ export function isPlanSpec(value: unknown): value is PlanSpec {
40
+ return Value.Check(PlanSpecSchema, value);
41
+ }
42
+
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Markdown-level convenience
46
+ // ---------------------------------------------------------------------------
47
+
48
+ import { parsePlan } from "../storage/plans.js";
49
+ import { renderPlanSpec } from "./render-markdown.js";
50
+
51
+ export interface PlanMarkdownValidationResult extends PlanSpecValidationResult {
52
+ /** Canonical markdown rendered from the validated PlanSpec. Null on failure. */
53
+ canonicalMarkdown: string | null;
54
+ }
55
+
56
+ /**
57
+ * Parse a plan markdown document, project it into a PlanSpec candidate,
58
+ * validate against PlanSpecSchema, and render the canonical markdown for a
59
+ * validated spec. On failure, returns `canonicalMarkdown: null` alongside
60
+ * the usual error and errors fields.
61
+ */
62
+ export function validatePlanMarkdown(content: string, planFileName: string = ""): PlanMarkdownValidationResult {
63
+ const parsed = parsePlan(content, planFileName);
64
+ const candidate = {
65
+ name: parsed.name,
66
+ created: parsed.created,
67
+ tags: parsed.tags,
68
+ context: parsed.context,
69
+ tasks: parsed.tasks.map((t) => ({
70
+ id: t.id,
71
+ name: t.name,
72
+ description: t.description,
73
+ files: t.files,
74
+ criteria: t.criteria,
75
+ complexity: t.complexity,
76
+ ...(t.model ? { model: t.model } : {}),
77
+ })),
78
+ };
79
+ const validated = validatePlanSpec(candidate);
80
+ return {
81
+ ...validated,
82
+ canonicalMarkdown: validated.output ? renderPlanSpec(validated.output) : null,
83
+ };
84
+ }