@planu/cli 4.14.1 → 5.1.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 (452) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/LICENSE +55 -12
  3. package/README.md +10 -2
  4. package/dist/cli/commands/install.d.ts +1 -12
  5. package/dist/cli/commands/install.js +9 -65
  6. package/dist/cli/commands/job.js +4 -3
  7. package/dist/cli/commands/spec.js +12 -3
  8. package/dist/cli/commands/validate.js +2 -2
  9. package/dist/cli/router.js +1 -3
  10. package/dist/config/environment-schema.json +1 -3
  11. package/dist/config/official-sdd-tools.d.ts +4 -0
  12. package/dist/config/official-sdd-tools.js +35 -0
  13. package/dist/config/product-public.d.ts +139 -3
  14. package/dist/config/product-public.js +8 -1
  15. package/dist/config/product-public.json +7 -36
  16. package/dist/config/registries/hosts/codex.json +20 -363
  17. package/dist/config/release-policy.json +84 -2
  18. package/dist/config/runtime-policy.json +12 -1
  19. package/dist/config/skill-templates/planu-resume-work.md +2 -2
  20. package/dist/core/index.d.ts +1 -1
  21. package/dist/core/spec-validator.d.ts +4 -4
  22. package/dist/core/spec-validator.js +33 -19
  23. package/dist/engine/abortable-process-runner.d.ts +20 -2
  24. package/dist/engine/abortable-process-runner.js +243 -74
  25. package/dist/engine/ambiguity-scorer.js +4 -26
  26. package/dist/engine/auth/scope-mapper.js +1 -7
  27. package/dist/engine/autopilot/state-updater.js +4 -4
  28. package/dist/engine/cascade-hooks/hooks/review-enricher.hook.js +9 -6
  29. package/dist/engine/cascade-hooks/state-drift-detector.d.ts +3 -3
  30. package/dist/engine/cascade-hooks/state-drift-detector.js +1 -1
  31. package/dist/engine/ci-generator/planu-config.js +1 -7
  32. package/dist/engine/clarification-gate/gate.d.ts +3 -3
  33. package/dist/engine/code-scanner/layer-scanner.js +6 -22
  34. package/dist/engine/core-bridge.d.ts +3 -3
  35. package/dist/engine/cost-guardrails.d.ts +1 -1
  36. package/dist/engine/cost-guardrails.js +1 -1
  37. package/dist/engine/criterion-identity.d.ts +1 -0
  38. package/dist/engine/criterion-identity.js +7 -0
  39. package/dist/engine/dashboard/data-loader.js +2 -7
  40. package/dist/engine/dashboard/templates-layout.js +0 -3
  41. package/dist/engine/dashboard/templates-project.js +1 -13
  42. package/dist/engine/docs-site-generator/data-collector.d.ts +1 -1
  43. package/dist/engine/docs-site-generator/data-collector.js +7 -28
  44. package/dist/engine/docs-site-generator/tools-renderer.js +3 -10
  45. package/dist/engine/evidence-gates/artifact-reader.d.ts +12 -1
  46. package/dist/engine/evidence-gates/artifact-reader.js +174 -4
  47. package/dist/engine/evidence-gates/evidence-autofill.d.ts +10 -0
  48. package/dist/engine/evidence-gates/evidence-autofill.js +148 -0
  49. package/dist/engine/evidence-gates/evidence-skeletons.d.ts +19 -0
  50. package/dist/engine/evidence-gates/evidence-skeletons.js +69 -0
  51. package/dist/engine/evidence-gates/lifecycle-gate.js +4 -3
  52. package/dist/engine/evidence-gates/reconciliation-freshness.d.ts +11 -0
  53. package/dist/engine/evidence-gates/reconciliation-freshness.js +72 -0
  54. package/dist/engine/evidence-index/done-drift.d.ts +1 -1
  55. package/dist/engine/evidence-index/done-drift.js +40 -18
  56. package/dist/engine/execution/context.js +3 -0
  57. package/dist/engine/execution/durable-job-heartbeat-runtime.d.ts +4 -0
  58. package/dist/engine/execution/durable-job-heartbeat-runtime.js +400 -0
  59. package/dist/engine/execution/job-runtime.d.ts +30 -0
  60. package/dist/engine/execution/job-runtime.js +114 -6
  61. package/dist/engine/execution/operation-journal.js +10 -4
  62. package/dist/engine/execution/public-job-sanitizer.d.ts +4 -0
  63. package/dist/engine/execution/public-job-sanitizer.js +31 -0
  64. package/dist/engine/execution/validate-job-executor.d.ts +12 -1
  65. package/dist/engine/execution/validate-job-executor.js +549 -86
  66. package/dist/engine/execution/validate-job-payload.js +107 -50
  67. package/dist/engine/execution/validate-job-recovery.js +1 -1
  68. package/dist/engine/execution/validate-worker-runtime.d.ts +5 -0
  69. package/dist/engine/execution/validate-worker-runtime.js +90 -2
  70. package/dist/engine/execution/validate-worker-thread.js +17 -24
  71. package/dist/engine/handoff-artifacts/schemas.d.ts +2 -1
  72. package/dist/engine/handoff-artifacts/schemas.js +33 -0
  73. package/dist/engine/handoff-packager.d.ts +19 -0
  74. package/dist/engine/handoff-packager.js +415 -13
  75. package/dist/engine/hooks/effort-configurator.js +1 -1
  76. package/dist/engine/impact-detector/test-break-predictor.js +3 -3
  77. package/dist/engine/impact-detector/tool-registration.js +1 -1
  78. package/dist/engine/implementation-contract/renderer.js +23 -8
  79. package/dist/engine/lifecycle-reconciliation-io.d.ts +11 -0
  80. package/dist/engine/lifecycle-reconciliation-io.js +180 -0
  81. package/dist/engine/lifecycle-reconciliation.d.ts +16 -0
  82. package/dist/engine/lifecycle-reconciliation.js +406 -0
  83. package/dist/engine/local-first/tool-classification.js +2 -0
  84. package/dist/engine/macos-keychain-native-port.d.ts +5 -0
  85. package/dist/engine/macos-keychain-native-port.js +19 -7
  86. package/dist/engine/migrations/remove-commercial-state.d.ts +3 -0
  87. package/dist/engine/migrations/remove-commercial-state.js +343 -0
  88. package/dist/engine/minimality/policy-loader.js +247 -6
  89. package/dist/engine/network-policy.js +1 -0
  90. package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
  91. package/dist/engine/planu-core.darwin-arm64.node.sbom.json +20 -20
  92. package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
  93. package/dist/engine/planu-core.darwin-x64.node.sbom.json +20 -20
  94. package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
  95. package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +20 -20
  96. package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
  97. package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +20 -20
  98. package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
  99. package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +20 -20
  100. package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
  101. package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +20 -20
  102. package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
  103. package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +20 -20
  104. package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
  105. package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +20 -20
  106. package/dist/engine/readiness-checker.d.ts +1 -16
  107. package/dist/engine/readiness-checker.js +27 -88
  108. package/dist/engine/reverse-engineer/api-detector.js +2 -13
  109. package/dist/engine/reverse-engineer/complexity-analyzer.js +2 -13
  110. package/dist/engine/reverse-engineer/config-analyzer.js +2 -13
  111. package/dist/engine/reverse-engineer/dependency-graph.js +2 -13
  112. package/dist/engine/reverse-engineer/test-analyzer.js +2 -13
  113. package/dist/engine/reverse-engineer/walk-ignore.d.ts +3 -0
  114. package/dist/engine/reverse-engineer/walk-ignore.js +26 -0
  115. package/dist/engine/runtime-policy.d.ts +2 -0
  116. package/dist/engine/runtime-policy.js +42 -5
  117. package/dist/engine/runtime-security/rate-limiter.js +0 -1
  118. package/dist/engine/safety/contained-project-file.d.ts +3 -0
  119. package/dist/engine/safety/contained-project-file.js +23 -0
  120. package/dist/engine/scope-boundaries/contradiction-checker.js +63 -1
  121. package/dist/engine/scope-boundaries/scope-validator.js +45 -7
  122. package/dist/engine/session/checkpoint-writer.js +0 -9
  123. package/dist/engine/spec-format/acceptance-criteria.d.ts +6 -2
  124. package/dist/engine/spec-format/acceptance-criteria.js +82 -22
  125. package/dist/engine/spec-format/markdown-sections.d.ts +14 -0
  126. package/dist/engine/spec-format/markdown-sections.js +105 -0
  127. package/dist/engine/spec-format/read-technical-section.js +33 -5
  128. package/dist/engine/spec-format/technical-md-populator.js +3 -2
  129. package/dist/engine/spec-format/text-fences.js +20 -2
  130. package/dist/engine/spec-format/unified-spec-builder.d.ts +7 -0
  131. package/dist/engine/spec-format/unified-spec-builder.js +202 -0
  132. package/dist/engine/spec-migrator/canonical-deduplicator.d.ts +18 -0
  133. package/dist/engine/spec-migrator/canonical-deduplicator.js +479 -0
  134. package/dist/engine/spec-migrator/filesystem-import.d.ts +4 -8
  135. package/dist/engine/spec-migrator/filesystem-import.js +121 -14
  136. package/dist/engine/spec-migrator/frontmatter-parser.js +32 -15
  137. package/dist/engine/spec-migrator/index.d.ts +1 -0
  138. package/dist/engine/spec-migrator/index.js +1 -0
  139. package/dist/engine/spec-state-machine/transition-spec.d.ts +6 -1
  140. package/dist/engine/spec-state-machine/transition-spec.js +10 -1
  141. package/dist/engine/spec-state-syncer.js +1 -1
  142. package/dist/engine/technical-enricher/index.js +16 -8
  143. package/dist/engine/technical-enricher/render-enriched.d.ts +1 -0
  144. package/dist/engine/technical-enricher/render-enriched.js +42 -14
  145. package/dist/engine/telemetry/health-checker.js +53 -17
  146. package/dist/engine/telemetry/telemetry-client.js +26 -8
  147. package/dist/engine/text-signal-boundaries.d.ts +2 -0
  148. package/dist/engine/text-signal-boundaries.js +25 -2
  149. package/dist/engine/time/relative-date.d.ts +3 -0
  150. package/dist/engine/time/relative-date.js +16 -0
  151. package/dist/engine/timing/budget.d.ts +11 -0
  152. package/dist/engine/timing/budget.js +31 -9
  153. package/dist/engine/token-optimizer/analytics.d.ts +1 -1
  154. package/dist/engine/token-optimizer/analytics.js +5 -6
  155. package/dist/engine/usage-tracker/core.d.ts +2 -2
  156. package/dist/engine/usage-tracker/core.js +1 -2
  157. package/dist/engine/usage-tracker/index.d.ts +0 -3
  158. package/dist/engine/usage-tracker/index.js +0 -3
  159. package/dist/engine/usage-tracker/stats.d.ts +2 -2
  160. package/dist/engine/usage-tracker/stats.js +1 -28
  161. package/dist/engine/validation/durable-validation.d.ts +1 -1
  162. package/dist/engine/validation/durable-validation.js +3 -82
  163. package/dist/engine/validation/validation-freshness.d.ts +11 -1
  164. package/dist/engine/validation/validation-freshness.js +231 -91
  165. package/dist/engine/validation/validation-receipt.d.ts +9 -6
  166. package/dist/engine/validation/validation-receipt.js +84 -24
  167. package/dist/engine/validation/validation-source-digest.d.ts +2 -0
  168. package/dist/engine/validation/validation-source-digest.js +86 -0
  169. package/dist/engine/validation/validation-submission.d.ts +5 -4
  170. package/dist/engine/validation/validation-submission.js +25 -11
  171. package/dist/engine/validation/validation-worktree.d.ts +21 -0
  172. package/dist/engine/validation/validation-worktree.js +260 -0
  173. package/dist/engine/validation-impact-planner.d.ts +14 -2
  174. package/dist/engine/validation-impact-planner.js +0 -3
  175. package/dist/engine/validator/dor-dod.d.ts +1 -0
  176. package/dist/engine/validator/dor-dod.js +5 -3
  177. package/dist/engine/validator/extractors.d.ts +1 -5
  178. package/dist/engine/validator/extractors.js +24 -69
  179. package/dist/engine/validator/spec-compliance-runner.d.ts +12 -24
  180. package/dist/engine/validator/spec-compliance-runner.js +429 -211
  181. package/dist/engine/validator.js +35 -22
  182. package/dist/index.js +4 -16
  183. package/dist/resources/usage-stats-resource.js +2 -18
  184. package/dist/security/adapters/linux-secret-service.js +7 -3
  185. package/dist/security/adapters/windows-credential-manager.js +6 -4
  186. package/dist/server/routes/specs.js +13 -1
  187. package/dist/storage/base-store.d.ts +1 -1
  188. package/dist/storage/base-store.js +1 -1
  189. package/dist/storage/feedback-remote.d.ts +2 -3
  190. package/dist/storage/feedback-remote.js +45 -31
  191. package/dist/storage/global-store.d.ts +1 -17
  192. package/dist/storage/global-store.js +1 -42
  193. package/dist/storage/index.d.ts +0 -2
  194. package/dist/storage/index.js +0 -2
  195. package/dist/storage/migrations/canonical-storage.js +124 -11
  196. package/dist/storage/runtime-db.d.ts +9 -0
  197. package/dist/storage/runtime-db.js +64 -8
  198. package/dist/storage/spec-store.d.ts +14 -2
  199. package/dist/storage/spec-store.js +104 -14
  200. package/dist/storage/status-store/atomic-status-write.d.ts +5 -0
  201. package/dist/storage/status-store/atomic-status-write.js +57 -0
  202. package/dist/storage/status-store/file-lock.js +30 -11
  203. package/dist/storage/status-store/self-healing.js +3 -3
  204. package/dist/storage/status-store/version-sync.d.ts +2 -1
  205. package/dist/storage/status-store/version-sync.js +35 -40
  206. package/dist/storage/usage-store.d.ts +1 -5
  207. package/dist/storage/usage-store.js +0 -35
  208. package/dist/tools/auto-update-handler.d.ts +3 -3
  209. package/dist/tools/auto-update-handler.js +3 -3
  210. package/dist/tools/challenge-spec/resilience-challenge-scenarios.d.ts +2 -1
  211. package/dist/tools/challenge-spec/resilience-challenge-scenarios.js +10 -7
  212. package/dist/tools/challenge-spec/scenario-collector.js +1 -1
  213. package/dist/tools/challenge-spec/scenarios-data.js +1 -1
  214. package/dist/tools/challenge-spec/scenarios-utils.d.ts +1 -1
  215. package/dist/tools/challenge-spec/scenarios-utils.js +35 -11
  216. package/dist/tools/challenge-spec-helpers.d.ts +10 -1
  217. package/dist/tools/challenge-spec-helpers.js +63 -22
  218. package/dist/tools/challenge-spec.js +23 -8
  219. package/dist/tools/check-readiness.js +38 -14
  220. package/dist/tools/configure-telemetry.js +30 -2
  221. package/dist/tools/create-spec/spec-builder.d.ts +7 -0
  222. package/dist/tools/create-spec/spec-builder.js +19 -15
  223. package/dist/tools/create-spec.d.ts +1 -0
  224. package/dist/tools/create-spec.js +584 -309
  225. package/dist/tools/git/branch-ops.d.ts +1 -1
  226. package/dist/tools/git/branch-ops.js +3 -2
  227. package/dist/tools/init-project/handler.js +10 -12
  228. package/dist/tools/init-project/host-assets-writer.js +26 -1
  229. package/dist/tools/init-project/lint-scaffolder.d.ts +3 -3
  230. package/dist/tools/init-project/migration-runner.d.ts +2 -4
  231. package/dist/tools/init-project/migration-runner.js +3 -2
  232. package/dist/tools/init-project/portable-index-reconciler.d.ts +4 -0
  233. package/dist/tools/init-project/portable-index-reconciler.js +14 -0
  234. package/dist/tools/init-project/result-builder.js +1 -3
  235. package/dist/tools/jobs/handlers.js +38 -6
  236. package/dist/tools/list-specs.js +5 -26
  237. package/dist/tools/merge-risk-handler.js +2 -2
  238. package/dist/tools/package-handoff.js +2 -0
  239. package/dist/tools/reconcile-spec.js +0 -27
  240. package/dist/tools/register-agent-squad-tools.js +2 -2
  241. package/dist/tools/register-platform-tools/design-stack-tools.js +11 -11
  242. package/dist/tools/register-platform-tools/lifecycle-infra-tools.js +7 -7
  243. package/dist/tools/register-sdd-tools.d.ts +1 -1
  244. package/dist/tools/register-sdd-tools.js +2 -29
  245. package/dist/tools/register-spec-tools/analysis-tools.js +8 -8
  246. package/dist/tools/register-spec-tools/core-spec-tools.js +29 -19
  247. package/dist/tools/safe-handler.d.ts +5 -13
  248. package/dist/tools/safe-handler.js +7 -23
  249. package/dist/tools/schemas/index.d.ts +1 -1
  250. package/dist/tools/schemas/index.js +1 -1
  251. package/dist/tools/schemas/output-schemas.d.ts +0 -23
  252. package/dist/tools/schemas/output-schemas.js +0 -28
  253. package/dist/tools/schemas/spec.d.ts +4 -0
  254. package/dist/tools/schemas/spec.js +3 -0
  255. package/dist/tools/schemas/validate-output-schema.d.ts +11 -0
  256. package/dist/tools/schemas/validate-output-schema.js +7 -0
  257. package/dist/tools/spec-prompt-handler.js +3 -3
  258. package/dist/tools/sync-spec-state-handler.d.ts +1 -1
  259. package/dist/tools/sync-spec-state-handler.js +50 -2
  260. package/dist/tools/tool-entry.d.ts +4 -4
  261. package/dist/tools/tool-entry.js +3 -3
  262. package/dist/tools/tool-registry/core-tools.js +31 -15
  263. package/dist/tools/tool-registry/group-infra.js +27 -110
  264. package/dist/tools/tool-registry/group-quality-compliance.js +26 -26
  265. package/dist/tools/tool-registry/inventory.d.ts +2 -0
  266. package/dist/tools/tool-registry/inventory.js +2 -0
  267. package/dist/tools/tool-registry-helpers.d.ts +3 -3
  268. package/dist/tools/tool-registry-helpers.js +3 -3
  269. package/dist/tools/tool-usage-report/registered-tools.d.ts +1 -1
  270. package/dist/tools/tool-usage-report/registered-tools.js +3 -15
  271. package/dist/tools/update-status/approval-gates.js +3 -0
  272. package/dist/tools/update-status/batch.d.ts +6 -2
  273. package/dist/tools/update-status/batch.js +58 -1
  274. package/dist/tools/update-status/dod-gates.d.ts +16 -1
  275. package/dist/tools/update-status/dod-gates.js +191 -1
  276. package/dist/tools/update-status/done-receipt-verifier.d.ts +8 -0
  277. package/dist/tools/update-status/done-receipt-verifier.js +75 -27
  278. package/dist/tools/update-status/evidence-gate.d.ts +4 -0
  279. package/dist/tools/update-status/evidence-gate.js +103 -3
  280. package/dist/tools/update-status/file-sync.d.ts +4 -3
  281. package/dist/tools/update-status/file-sync.js +57 -6
  282. package/dist/tools/update-status/index.d.ts +25 -1
  283. package/dist/tools/update-status/index.js +292 -78
  284. package/dist/tools/update-status/response-builder.js +3 -3
  285. package/dist/tools/update-status/side-effects.d.ts +2 -0
  286. package/dist/tools/update-status/side-effects.js +2 -2
  287. package/dist/tools/update-status/transition-guard.js +45 -16
  288. package/dist/tools/update-status-actions.d.ts +4 -2
  289. package/dist/tools/update-status-actions.js +16 -14
  290. package/dist/tools/usage-report.js +1 -16
  291. package/dist/tools/usage-stats.js +1 -18
  292. package/dist/tools/usage-tracking.d.ts +4 -0
  293. package/dist/tools/usage-tracking.js +20 -0
  294. package/dist/tools/validate-lint.d.ts +1 -0
  295. package/dist/tools/validate-lint.js +1 -1
  296. package/dist/tools/validate.d.ts +2 -1
  297. package/dist/tools/validate.js +114 -34
  298. package/dist/tools/workspace-dashboard-handler.js +38 -0
  299. package/dist/transports/http-transport.js +6 -8
  300. package/dist/transports/mcp-types.d.ts +0 -2
  301. package/dist/transports/session-context.js +1 -1
  302. package/dist/types/cascade-hooks.d.ts +1 -1
  303. package/dist/types/ci.d.ts +0 -4
  304. package/dist/types/clarification-token.d.ts +1 -1
  305. package/dist/types/claude-code-runtime.d.ts +0 -27
  306. package/dist/types/commercial-migration.d.ts +17 -0
  307. package/dist/types/commercial-migration.js +2 -0
  308. package/dist/types/common/primitives.d.ts +0 -2
  309. package/dist/types/core-bridge.d.ts +4 -10
  310. package/dist/types/dashboard.d.ts +3 -7
  311. package/dist/types/docs.d.ts +0 -1
  312. package/dist/types/durable-job.d.ts +4 -4
  313. package/dist/types/durable-validation.d.ts +100 -2
  314. package/dist/types/durable-validation.js +165 -1
  315. package/dist/types/errors/structured-error.d.ts +3 -3
  316. package/dist/types/estimation.d.ts +2 -0
  317. package/dist/types/evidence-autofill.d.ts +34 -0
  318. package/dist/types/evidence-autofill.js +2 -0
  319. package/dist/types/evidence-gates.d.ts +9 -1
  320. package/dist/types/execution.d.ts +2 -0
  321. package/dist/types/gemini-integration.d.ts +0 -11
  322. package/dist/types/handoff-artifacts.d.ts +41 -0
  323. package/dist/types/hooks-advanced.d.ts +0 -13
  324. package/dist/types/impact-detection.d.ts +2 -2
  325. package/dist/types/index.d.ts +3 -6
  326. package/dist/types/index.js +2 -5
  327. package/dist/types/kiro-integration.d.ts +0 -6
  328. package/dist/types/network-policy.d.ts +1 -1
  329. package/dist/types/project/lint-scaffolder.d.ts +1 -1
  330. package/dist/types/runtime-policy.d.ts +11 -0
  331. package/dist/types/session-safeguard.d.ts +0 -31
  332. package/dist/types/spec/canonical-repair.d.ts +81 -0
  333. package/dist/types/spec/canonical-repair.js +2 -0
  334. package/dist/types/spec/core.d.ts +20 -0
  335. package/dist/types/spec/index.d.ts +1 -0
  336. package/dist/types/spec/index.js +1 -0
  337. package/dist/types/spec/inputs.d.ts +35 -1
  338. package/dist/types/spec-format.d.ts +25 -1
  339. package/dist/types/telemetry.d.ts +4 -1
  340. package/dist/types/transition-log.d.ts +1 -1
  341. package/dist/types/transport.d.ts +0 -2
  342. package/dist/types/usage.d.ts +1 -36
  343. package/dist/types/usage.js +1 -1
  344. package/dist/types/validate-worker.d.ts +6 -0
  345. package/dist/types/validation-receipt.d.ts +1 -1
  346. package/dist/types/validation.d.ts +10 -4
  347. package/package.json +17 -12
  348. package/planu-native.json +1 -1
  349. package/planu-plugin.json +23 -40
  350. package/scripts/lib/pending-release-file.mjs +93 -0
  351. package/src/i18n/messages/en.json +0 -41
  352. package/src/i18n/messages/es.json +0 -41
  353. package/src/i18n/messages/pt.json +0 -41
  354. package/dist/cli/commands/activate.d.ts +0 -14
  355. package/dist/cli/commands/activate.js +0 -174
  356. package/dist/config/license-plans.json +0 -251
  357. package/dist/config/product-public.ts +0 -148
  358. package/dist/config/server-instructions.ts +0 -121
  359. package/dist/config/version-defaults.ts +0 -76
  360. package/dist/config/version.ts +0 -21
  361. package/dist/engine/ai-integration/gemini/settings-generator.d.ts +0 -4
  362. package/dist/engine/ai-integration/gemini/settings-generator.js +0 -195
  363. package/dist/engine/ai-integration/kiro/hooks-generator.d.ts +0 -6
  364. package/dist/engine/ai-integration/kiro/hooks-generator.js +0 -75
  365. package/dist/engine/hooks/full-spectrum-generator.d.ts +0 -32
  366. package/dist/engine/hooks/full-spectrum-generator.js +0 -117
  367. package/dist/engine/license-validator/config-loader.d.ts +0 -5
  368. package/dist/engine/license-validator/config-loader.js +0 -16
  369. package/dist/engine/license-validator/core.d.ts +0 -15
  370. package/dist/engine/license-validator/core.js +0 -151
  371. package/dist/engine/license-validator/fingerprint.d.ts +0 -3
  372. package/dist/engine/license-validator/fingerprint.js +0 -17
  373. package/dist/engine/license-validator/lemon-squeezy.d.ts +0 -5
  374. package/dist/engine/license-validator/lemon-squeezy.js +0 -55
  375. package/dist/engine/license-validator.d.ts +0 -5
  376. package/dist/engine/license-validator.js +0 -6
  377. package/dist/engine/product-intelligence/index.d.ts +0 -7
  378. package/dist/engine/product-intelligence/index.js +0 -211
  379. package/dist/engine/session/auto-save.d.ts +0 -18
  380. package/dist/engine/session/auto-save.js +0 -98
  381. package/dist/engine/session/context-snapshot.d.ts +0 -8
  382. package/dist/engine/session/context-snapshot.js +0 -49
  383. package/dist/engine/session/memory-sync.d.ts +0 -12
  384. package/dist/engine/session/memory-sync.js +0 -34
  385. package/dist/engine/session/session-diff.d.ts +0 -11
  386. package/dist/engine/session/session-diff.js +0 -42
  387. package/dist/engine/session/session-merge.d.ts +0 -12
  388. package/dist/engine/session/session-merge.js +0 -55
  389. package/dist/engine/session-safeguard/checkpoint-runner.d.ts +0 -4
  390. package/dist/engine/session-safeguard/checkpoint-runner.js +0 -70
  391. package/dist/engine/session-safeguard/learnings-buffer.d.ts +0 -15
  392. package/dist/engine/session-safeguard/learnings-buffer.js +0 -91
  393. package/dist/engine/session-safeguard/precompaction-drain.d.ts +0 -10
  394. package/dist/engine/session-safeguard/precompaction-drain.js +0 -82
  395. package/dist/engine/session-safeguard/unpushed-detector.d.ts +0 -7
  396. package/dist/engine/session-safeguard/unpushed-detector.js +0 -59
  397. package/dist/engine/spec-changelog/core.d.ts +0 -16
  398. package/dist/engine/spec-changelog/core.js +0 -176
  399. package/dist/engine/spec-changelog/diff.d.ts +0 -18
  400. package/dist/engine/spec-changelog/diff.js +0 -121
  401. package/dist/engine/spec-changelog/index.d.ts +0 -3
  402. package/dist/engine/spec-changelog/index.js +0 -4
  403. package/dist/engine/trial-engine.d.ts +0 -5
  404. package/dist/engine/trial-engine.js +0 -14
  405. package/dist/engine/usage-tracker/rate-check.d.ts +0 -7
  406. package/dist/engine/usage-tracker/rate-check.js +0 -18
  407. package/dist/engine/usage-tracker/rate-limiter.d.ts +0 -14
  408. package/dist/engine/usage-tracker/rate-limiter.js +0 -73
  409. package/dist/engine/usage-tracker/trial.d.ts +0 -27
  410. package/dist/engine/usage-tracker/trial.js +0 -49
  411. package/dist/engine/webhook/event-handlers.d.ts +0 -25
  412. package/dist/engine/webhook/event-handlers.js +0 -89
  413. package/dist/engine/webhook/index.d.ts +0 -4
  414. package/dist/engine/webhook/index.js +0 -5
  415. package/dist/engine/webhook/server.d.ts +0 -12
  416. package/dist/engine/webhook/server.js +0 -249
  417. package/dist/engine/webhook/signature.d.ts +0 -16
  418. package/dist/engine/webhook/signature.js +0 -49
  419. package/dist/storage/license-store.d.ts +0 -6
  420. package/dist/storage/license-store.js +0 -63
  421. package/dist/storage/session-store.d.ts +0 -20
  422. package/dist/storage/session-store.js +0 -136
  423. package/dist/tools/activate-license.d.ts +0 -6
  424. package/dist/tools/activate-license.js +0 -132
  425. package/dist/tools/checkpoint-handler.d.ts +0 -3
  426. package/dist/tools/checkpoint-handler.js +0 -130
  427. package/dist/tools/export-session.d.ts +0 -3
  428. package/dist/tools/export-session.js +0 -90
  429. package/dist/tools/license-gate.d.ts +0 -18
  430. package/dist/tools/license-gate.js +0 -222
  431. package/dist/tools/license-status.d.ts +0 -3
  432. package/dist/tools/license-status.js +0 -93
  433. package/dist/tools/list-sessions.d.ts +0 -3
  434. package/dist/tools/list-sessions.js +0 -46
  435. package/dist/tools/planu-session-checkpoint.d.ts +0 -6
  436. package/dist/tools/planu-session-checkpoint.js +0 -42
  437. package/dist/tools/restore-session.d.ts +0 -3
  438. package/dist/tools/restore-session.js +0 -71
  439. package/dist/tools/spec-history.d.ts +0 -3
  440. package/dist/tools/spec-history.js +0 -59
  441. package/dist/tools/webhook.d.ts +0 -6
  442. package/dist/tools/webhook.js +0 -150
  443. package/dist/types/changelog.d.ts +0 -49
  444. package/dist/types/changelog.js +0 -3
  445. package/dist/types/licensing.d.ts +0 -109
  446. package/dist/types/licensing.js +0 -4
  447. package/dist/types/product-intelligence.d.ts +0 -60
  448. package/dist/types/product-intelligence.js +0 -3
  449. package/dist/types/session.d.ts +0 -84
  450. package/dist/types/session.js +0 -3
  451. package/dist/types/webhook.d.ts +0 -62
  452. package/dist/types/webhook.js +0 -4
@@ -6,15 +6,17 @@ import { readTechnologySelectionContract } from '../storage/technology-selection
6
6
  import { toolResult, interactiveResult } from './response-helpers.js';
7
7
  import { readFile, stat as fsStat } from 'node:fs/promises';
8
8
  import { createHash, randomUUID } from 'node:crypto';
9
- import { dirname as pathDirname, isAbsolute as pathIsAbsolute, join as pathJoin, relative as pathRelative, sep as pathSeparator, } from 'node:path';
10
- import { buildSpecContext, buildSplitResult } from './create-spec/spec-builder.js';
9
+ import { basename as pathBasename, dirname as pathDirname, isAbsolute as pathIsAbsolute, join as pathJoin, relative as pathRelative, sep as pathSeparator, } from 'node:path';
10
+ import { withFileLock } from '../storage/file-mutex.js';
11
+ import { generateBranchName } from './create-spec-helpers.js';
12
+ import { buildSpecContext, buildSplitResult, computeNextSpecId, } from './create-spec/spec-builder.js';
11
13
  import { validateConstitution } from './create-spec/constitution-validator.js';
12
14
  import { getAsyncAnalysisPath } from './create-spec/post-creation.js';
13
15
  import { extractCriteria, generateLeanSpecContent, } from '../engine/spec-format/lean-spec-generator.js';
14
16
  import { generateLeanTechnicalContent, } from '../engine/spec-format/lean-technical-generator.js';
15
17
  import { extractFilesFromSpecBody } from '../engine/spec-format/technical-md-populator.js';
16
- import { buildUnifiedSpecContent } from '../engine/spec-format/unified-spec-builder.js';
17
- import { appendImplementationContractIfMissing } from '../engine/implementation-contract/index.js';
18
+ import { buildCanonicalUnifiedSpecContent, validateUnifiedSpecCandidate, } from '../engine/spec-format/unified-spec-builder.js';
19
+ import { buildImplementationContractSection } from '../engine/implementation-contract/index.js';
18
20
  import { validateEnglishOnlySpecText } from '../engine/spec-language/english-only.js';
19
21
  import { FallbackGenerator } from '../engine/spec-generator/index.js';
20
22
  import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/autopilot-analyzer.js';
@@ -31,6 +33,8 @@ import { buildCriterionGroundingRecords, filterGroundedCriteria, getAdvisoryCrit
31
33
  import { checkGenericSpecOutput } from '../engine/spec-quality/generic-output-gate.js';
32
34
  import { calculateActionableSpecMetrics, shouldExposeMetric, } from '../engine/spec-metrics/actionable-metrics.js';
33
35
  import { RuntimeDatabase } from '../storage/runtime-db.js';
36
+ import { BoundaryFailure } from '../errors/error-taxonomy.js';
37
+ import { SpecAcFormatEnum, SpecScopeEnum, SpecTargetEnum, SpecTypeEnum } from './schemas/spec.js';
34
38
  import { SPEC_CREATED_POST_COMMIT_TASKS } from '../types/outbox-worker.js';
35
39
  import { resolveStorageLayout } from '../storage/storage-layout.js';
36
40
  import { OperationJournal, recoverOperationJournal, } from '../engine/execution/operation-journal.js';
@@ -185,8 +189,15 @@ function buildAdvisoryCompat() {
185
189
  };
186
190
  }
187
191
  /** Persist only out-of-scope items supplied explicitly by the caller. */
188
- function resolveOutOfScope(provided) {
189
- return provided?.filter((item) => item.trim().length > 0) ?? [];
192
+ function resolveOutOfScope(provided, source = '') {
193
+ const explicit = provided?.filter((item) => item.trim().length > 0) ?? [];
194
+ if (explicit.length > 0) {
195
+ return explicit;
196
+ }
197
+ const section = /(?:^|\n)##\s+Out of scope\s*\n([\s\S]*?)(?=\n##\s|$)/i.exec(source)?.[1];
198
+ return section
199
+ ? Array.from(section.matchAll(/^[-*]\s+(.+)$/gm), (match) => match[1]?.trim() ?? '').filter(Boolean)
200
+ : [];
190
201
  }
191
202
  /** SPEC-783: Synthesize agent team findings into unified spec.md ## Technical section. */
192
203
  async function handleAgentTeamSynthesis(specId, projectPath, findings) {
@@ -275,6 +286,19 @@ function finishRecoveredOperation(journal, key, result) {
275
286
  }
276
287
  return entry?.result ?? result;
277
288
  }
289
+ /**
290
+ * SPEC-1348: release the journal claim of an attempt that failed before commit.
291
+ * Rolling back marks the key re-claimable; committed entries are left untouched
292
+ * because recovery resolution only acts on active (intent/prepared) states.
293
+ */
294
+ function rollbackUncommittedOperation(journal, key, reason) {
295
+ try {
296
+ journal.resolveRecovery('create_spec', key, { action: 'rollback', reason });
297
+ }
298
+ catch {
299
+ // Best-effort: the original failure remains the primary error.
300
+ }
301
+ }
278
302
  function buildCommittedCreateResult(input) {
279
303
  const advisorySignals = [];
280
304
  if (input.advisoryCriteria.length > 0) {
@@ -330,9 +354,12 @@ function buildCommittedCreateResult(input) {
330
354
  deprecatedAlias: 'riskWarning',
331
355
  }));
332
356
  }
333
- return toolResult(`Spec ${spec.id} created and committed.`, {
357
+ return toolResult(`Spec ${spec.id} created and persisted.`, {
334
358
  operationId: input.operationId,
359
+ /** @deprecated Operation-journal durability; this is not Git evidence. */
335
360
  committed: true,
361
+ persisted: true,
362
+ gitCommitPerformed: false,
336
363
  pendingAnalysis: true,
337
364
  specId: spec.id,
338
365
  title: spec.title,
@@ -711,28 +738,27 @@ async function buildIdempotencyMatchResult(projectPath, existing) {
711
738
  };
712
739
  }
713
740
  const FRONTMATTER_ID_RE = /^id:\s*(SPEC-\d+)/m;
714
- const FRONTMATTER_TITLE_RE = /^title:\s*"?([^"\n]+?)"?\s*$/m;
715
741
  const FRONTMATTER_STATUS_RE = /^status:\s*(\S+)/m;
716
- /** SPEC-770: Scan store and filesystem for a spec with matching idempotency key within windowMs. */
717
- async function findByIdempotencyKey(projectPath, key, projectId, windowMs = IDEMPOTENCY_MATCH_WINDOW_MS) {
718
- const cutoff = Date.now() - windowMs;
719
- // Fast path: check store (always authoritative for successfully created specs)
742
+ async function findStoreByIdempotencyKey(projectId, key, cutoff) {
720
743
  try {
721
744
  const existing = await specStore.listSpecs(projectId);
722
- const match = existing.find((s) => s.idempotencyKey === key && new Date(s.createdAt).getTime() > cutoff);
723
- if (match !== undefined) {
724
- return match;
725
- }
745
+ return existing.find((s) => s.idempotencyKey === key && new Date(s.createdAt).getTime() > cutoff);
726
746
  }
727
747
  catch {
728
- /* best-effort */
748
+ return undefined;
749
+ }
750
+ }
751
+ /** SPEC-770: Scan store and filesystem for a spec with matching idempotency key within windowMs. */
752
+ async function findByIdempotencyKey(projectPath, key, projectId, windowMs = IDEMPOTENCY_MATCH_WINDOW_MS) {
753
+ const cutoff = Date.now() - windowMs;
754
+ const storeMatch = await findStoreByIdempotencyKey(projectId, key, cutoff);
755
+ if (storeMatch) {
756
+ return storeMatch;
729
757
  }
730
- // Transactional fallback: the request key stays in external project data, not spec.md.
731
758
  const evidenceMatch = await findByIdempotencyEvidence(projectPath, key, cutoff);
732
759
  if (evidenceMatch !== undefined) {
733
760
  return evidenceMatch;
734
761
  }
735
- // Compatibility fallback for specs created before external evidence was introduced.
736
762
  try {
737
763
  const { glob } = await import('glob');
738
764
  const { join: joinPath } = await import('node:path');
@@ -762,7 +788,7 @@ async function findByIdempotencyKey(projectPath, key, projectId, windowMs = IDEM
762
788
  return undefined;
763
789
  }
764
790
  /** SPEC-770: Scan for recently written spec.md files (for possibleDuplicates in timeout response). */
765
- async function findRecentlyWrittenSpecs(projectPath, windowMs) {
791
+ async function findRecentlyWrittenSpecs(projectPath, windowMs, requestedTitle) {
766
792
  try {
767
793
  const { glob } = await import('glob');
768
794
  const { join: joinPath } = await import('node:path');
@@ -776,9 +802,11 @@ async function findRecentlyWrittenSpecs(projectPath, windowMs) {
776
802
  }
777
803
  const content = await readFile(specFile, 'utf-8').catch(() => '');
778
804
  const idMatch = FRONTMATTER_ID_RE.exec(content);
779
- const titleMatch = FRONTMATTER_TITLE_RE.exec(content);
780
- if (idMatch?.[1] !== undefined) {
781
- results.push({ id: idMatch[1], title: titleMatch?.[1] ?? '' });
805
+ const title = extractFrontmatterTitle(content);
806
+ if (idMatch?.[1] !== undefined &&
807
+ normalizeDuplicateTitle(title) !== '' &&
808
+ normalizeDuplicateTitle(title) === normalizeDuplicateTitle(requestedTitle)) {
809
+ results.push({ id: idMatch[1], title });
782
810
  }
783
811
  }
784
812
  return results;
@@ -787,6 +815,69 @@ async function findRecentlyWrittenSpecs(projectPath, windowMs) {
787
815
  return [];
788
816
  }
789
817
  }
818
+ function extractFrontmatterTitle(content) {
819
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1];
820
+ const rawTitle = frontmatter ? /^title:\s*(.*?)\s*$/m.exec(frontmatter)?.[1] : undefined;
821
+ if (!rawTitle) {
822
+ return '';
823
+ }
824
+ if (rawTitle.startsWith('"') && rawTitle.endsWith('"')) {
825
+ try {
826
+ const parsed = JSON.parse(rawTitle);
827
+ return typeof parsed === 'string' ? parsed : '';
828
+ }
829
+ catch {
830
+ return '';
831
+ }
832
+ }
833
+ if (rawTitle.startsWith("'") && rawTitle.endsWith("'")) {
834
+ return rawTitle.slice(1, -1).replace(/''/g, "'");
835
+ }
836
+ return rawTitle;
837
+ }
838
+ function normalizeDuplicateTitle(value) {
839
+ return value
840
+ .normalize('NFKC')
841
+ .toLocaleLowerCase('en-US')
842
+ .replace(/[\p{P}]+/gu, ' ')
843
+ .replace(/\s+/g, ' ')
844
+ .trim();
845
+ }
846
+ const CREATE_SPEC_ENUMS = {
847
+ type: SpecTypeEnum,
848
+ scope: SpecScopeEnum,
849
+ target: SpecTargetEnum,
850
+ acFormat: SpecAcFormatEnum,
851
+ };
852
+ function validateCreateSpecEnums(input) {
853
+ for (const [field, schema] of Object.entries(CREATE_SPEC_ENUMS)) {
854
+ const received = input[field];
855
+ if (received === undefined || schema.safeParse(received).success) {
856
+ continue;
857
+ }
858
+ const boundedReceived = received === null || ['string', 'number', 'boolean'].includes(typeof received)
859
+ ? received
860
+ : typeof received;
861
+ return {
862
+ content: [
863
+ {
864
+ type: 'text',
865
+ text: `${field} must be one of: ${schema.options.join(', ')}.`,
866
+ },
867
+ ],
868
+ isError: true,
869
+ structuredContent: {
870
+ error: 'INVALID_INPUT',
871
+ code: 'INVALID_INPUT',
872
+ status: 422,
873
+ field,
874
+ received: boundedReceived,
875
+ allowed: schema.options,
876
+ },
877
+ };
878
+ }
879
+ return null;
880
+ }
790
881
  const DESCRIPTION_MAX_CHARS = 10_000;
791
882
  const DESCRIPTION_EXCEED_MSG = 'Description exceeds 10000 chars. Create with a summary (<3k) and use reconcile_spec to add the rest.';
792
883
  function checkEnglishOnlyInput(params) {
@@ -811,8 +902,260 @@ function checkEnglishOnlyInput(params) {
811
902
  },
812
903
  };
813
904
  }
905
+ function quotePosixShellArgument(value) {
906
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
907
+ }
908
+ function earlyPreparation(result, clarificationProjectId) {
909
+ return {
910
+ kind: 'early',
911
+ result,
912
+ ...(clarificationProjectId ? { clarificationProjectId } : {}),
913
+ };
914
+ }
915
+ export async function resolveGroundedVerificationCommands(projectPath, testPaths) {
916
+ if (testPaths.length === 0) {
917
+ return [];
918
+ }
919
+ const packageJsonRaw = await readFile(pathJoin(projectPath, 'package.json'), 'utf8').catch(() => '');
920
+ if (!packageJsonRaw) {
921
+ return [];
922
+ }
923
+ try {
924
+ const manifest = JSON.parse(packageJsonRaw);
925
+ if (typeof manifest.packageManager !== 'string') {
926
+ return [];
927
+ }
928
+ const packageManager = /^(pnpm|npm|yarn|bun)@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.exec(manifest.packageManager)?.[1];
929
+ if (!packageManager) {
930
+ return [];
931
+ }
932
+ if (typeof manifest.scripts?.test !== 'string' || manifest.scripts.test.trim() === '') {
933
+ return [];
934
+ }
935
+ return [`${packageManager} run test -- ${testPaths.map(quotePosixShellArgument).join(' ')}`];
936
+ }
937
+ catch {
938
+ return [];
939
+ }
940
+ }
941
+ async function prepareCreateSpecCandidate(initialParams, server) {
942
+ let params = initialParams;
943
+ const buildResult = await measureStep('buildSpecContext', () => buildSpecContext(params));
944
+ if (!buildResult.ok) {
945
+ return earlyPreparation({
946
+ content: [{ type: 'text', text: buildResult.errorMessage }],
947
+ isError: true,
948
+ });
949
+ }
950
+ const context = buildResult.context;
951
+ const { spec, estimation, projectId } = context;
952
+ const knowledge = await measureStep('loadKnowledge', () => knowledgeStore.getKnowledge(projectId));
953
+ const clarificationSession = params.clarificationSessionId
954
+ ? await knowledgeStore.getClarification(projectId, params.clarificationSessionId)
955
+ : null;
956
+ const constitutionCheck = await measureStep('validateConstitution', () => validateConstitution(projectId, spec.title, spec.tags));
957
+ if (constitutionCheck.blocked && constitutionCheck.errorResult) {
958
+ return earlyPreparation(constitutionCheck.errorResult);
959
+ }
960
+ const inlineAnalysis = await withBudget('create-spec-inline-analysis', getRuntimePolicy().createSpec.inlineAnalysisBudgetMs, (signal) => analyzeProjectForSpec(params.projectPath ?? '', params.description, spec.title, knowledge, signal));
961
+ const autopilot = unwrapBudget(inlineAnalysis, getEmptyAutopilotResult());
962
+ if (autopilot.isIdea) {
963
+ return earlyPreparation({
964
+ content: [
965
+ {
966
+ type: 'text',
967
+ text: `💡 **Idea captured**: "${params.description}"\n\nThe description is too brief for a full spec. Use \`capture_idea\` to save it to the backlog, or provide more detail (>10 words) to create a spec.`,
968
+ },
969
+ ],
970
+ });
971
+ }
972
+ const clarificationResult = handleClarification(server, params.description, knowledge, autopilot, params);
973
+ if (clarificationResult !== null) {
974
+ if ('earlyReturn' in clarificationResult) {
975
+ return earlyPreparation(clarificationResult.earlyReturn, projectId);
976
+ }
977
+ params = clarificationResult.params;
978
+ }
979
+ const filteredCriteria = filterGroundedCriteria(autopilot.suggestedCriteria);
980
+ const technologyContract = await readTechnologySelectionContract(params.projectPath ?? '');
981
+ const contractNote = technologyContract
982
+ ? [
983
+ '',
984
+ 'Technology Contract:',
985
+ `- Mode: ${technologyContract.mode}`,
986
+ technologyContract.language ? `- Language: ${technologyContract.language}` : '',
987
+ technologyContract.framework
988
+ ? `- Framework: ${technologyContract.framework}`
989
+ : '- Framework: none or not selected',
990
+ technologyContract.platform ? `- Platform: ${technologyContract.platform}` : '',
991
+ technologyContract.projectType ? `- Project type: ${technologyContract.projectType}` : '',
992
+ '- Agents must not choose a different stack unless the user approves a new contract.',
993
+ ]
994
+ .filter(Boolean)
995
+ .join('\n')
996
+ : '';
997
+ const generatedSpec = await measureStep('generateSpecBody', () => new FallbackGenerator().generate({
998
+ title: spec.title,
999
+ description: `${params.description}${contractNote}`,
1000
+ type: spec.type,
1001
+ scope: spec.scope,
1002
+ target: spec.target,
1003
+ acFormat: params.acFormat,
1004
+ projectContext: {
1005
+ language: technologyContract?.language ?? knowledge?.language ?? undefined,
1006
+ framework: technologyContract?.framework ?? knowledge?.framework ?? undefined,
1007
+ architecture: knowledge?.architecture.primary ?? undefined,
1008
+ },
1009
+ }));
1010
+ spec.generation = generatedSpec.generation;
1011
+ spec.qualityWarnings =
1012
+ generatedSpec.qualityWarnings.length > 0 ? generatedSpec.qualityWarnings : undefined;
1013
+ const baseCriteria = extractCriteria(generatedSpec.specBody).map((criterion) => criterion.text);
1014
+ const groundingCriteria = buildCriterionGroundingRecords({
1015
+ criteria: [...baseCriteria, ...filteredCriteria],
1016
+ userInput: params.description,
1017
+ generatedEvidence: [generatedSpec.generation.modelId ?? generatedSpec.generation.method],
1018
+ });
1019
+ const contractCriteria = getContractCriteria(groundingCriteria);
1020
+ const advisoryCriteria = getAdvisoryCriteria(groundingCriteria).map((record) => record.text);
1021
+ if (contractCriteria.length === 0) {
1022
+ return earlyPreparation({
1023
+ content: [
1024
+ {
1025
+ type: 'text',
1026
+ text: 'create_spec did not persist a spec because no acceptance criterion was grounded in the user request. ' +
1027
+ 'Needs decision: provide at least one explicit, verifiable criterion as a checkbox or Given/When/Then outcome.',
1028
+ },
1029
+ ],
1030
+ isError: true,
1031
+ structuredContent: {
1032
+ error: 'MISSING_GROUNDED_ACCEPTANCE_CRITERIA',
1033
+ missingDecision: 'Provide at least one explicit, verifiable acceptance criterion.',
1034
+ persisted: false,
1035
+ },
1036
+ });
1037
+ }
1038
+ const actionableMetrics = calculateActionableSpecMetrics({
1039
+ criteria: [...baseCriteria, ...filteredCriteria],
1040
+ groundingRecords: groundingCriteria,
1041
+ }).filter(shouldExposeMetric);
1042
+ const technicalFiles = await measureStep('resolveTechnicalFiles', () => resolveTechnicalFiles({
1043
+ generatedSpecBody: generatedSpec.specBody,
1044
+ generatedTechnicalSection: generatedSpec.technicalSection,
1045
+ projectPath: params.projectPath ?? '',
1046
+ }));
1047
+ const groundedTechnical = await measureStep('groundTechnicalFiles', () => groundTechnicalFiles({
1048
+ files: technicalFiles,
1049
+ projectPath: params.projectPath ?? '',
1050
+ userInput: params.description,
1051
+ autopilot,
1052
+ }));
1053
+ const outOfScope = resolveOutOfScope(params.outOfScope, params.description);
1054
+ const scenarioTestPaths = groundedTechnical.files.test.map((file) => file.path);
1055
+ const criteria = contractCriteria.map((record) => ({ text: record.text, done: false }));
1056
+ const leanSpec = generateLeanSpecContent({
1057
+ spec,
1058
+ description: generatedSpec.specBody,
1059
+ estimation,
1060
+ criteriaOverride: criteria,
1061
+ groundingCriteria: contractCriteria,
1062
+ groundingTechnicalReferences: groundedTechnical.records,
1063
+ acFormat: params.acFormat,
1064
+ scenarioTestPaths,
1065
+ });
1066
+ const leanTechnical = generateLeanTechnicalContent({
1067
+ specId: spec.id,
1068
+ filesToCreate: groundedTechnical.files.create,
1069
+ filesToModify: groundedTechnical.files.modify,
1070
+ filesToTest: groundedTechnical.files.test,
1071
+ includeDecisionRequiredPlaceholder: spec.scope !== 'trivial',
1072
+ });
1073
+ const verificationCommands = await resolveGroundedVerificationCommands(params.projectPath ?? '', scenarioTestPaths);
1074
+ const implementationContract = buildImplementationContractSection({
1075
+ description: generatedSpec.specBody,
1076
+ criteria,
1077
+ files: groundedTechnical.files,
1078
+ outOfScope,
1079
+ verificationCommands,
1080
+ });
1081
+ const unifiedSpec = buildCanonicalUnifiedSpecContent({
1082
+ leanSpecBody: leanSpec,
1083
+ sourceDescription: params.description,
1084
+ technicalBody: leanTechnical,
1085
+ implementationContract,
1086
+ criteria,
1087
+ files: groundedTechnical.files,
1088
+ outOfScope,
1089
+ });
1090
+ const candidateValidation = validateUnifiedSpecCandidate(unifiedSpec, {
1091
+ groundedFilePaths: groundedTechnical.records.map((record) => record.path),
1092
+ });
1093
+ if (!candidateValidation.valid) {
1094
+ return earlyPreparation({
1095
+ content: [
1096
+ {
1097
+ type: 'text',
1098
+ text: `Unified spec candidate is invalid: ${candidateValidation.issues
1099
+ .slice(0, 3)
1100
+ .map((issue) => issue.message)
1101
+ .join('; ')}`,
1102
+ },
1103
+ ],
1104
+ isError: true,
1105
+ structuredContent: {
1106
+ error: 'SPEC_FORMAT_INVALID',
1107
+ code: 422,
1108
+ persisted: false,
1109
+ issues: candidateValidation.issues,
1110
+ },
1111
+ });
1112
+ }
1113
+ const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
1114
+ if (!genericOutputGate.passed) {
1115
+ return earlyPreparation({
1116
+ content: [
1117
+ {
1118
+ type: 'text',
1119
+ text: 'Spec quality gate blocked generic output before persistence. ' +
1120
+ genericOutputGate.issues
1121
+ .slice(0, 3)
1122
+ .map((issue) => `${issue.phrase}: ${issue.reason}`)
1123
+ .join('; '),
1124
+ },
1125
+ ],
1126
+ isError: true,
1127
+ structuredContent: {
1128
+ error: 'GENERIC_SPEC_OUTPUT_BLOCKED',
1129
+ sddConstitutionRuleId: 'sdd.no-generic-output',
1130
+ issues: genericOutputGate.issues,
1131
+ fixHint: 'Replace generic criteria or placeholder references with grounded, testable behavior.',
1132
+ },
1133
+ });
1134
+ }
1135
+ if (outOfScope.length > 0) {
1136
+ spec.outOfScope = outOfScope;
1137
+ }
1138
+ return {
1139
+ kind: 'ready',
1140
+ prepared: {
1141
+ params,
1142
+ context,
1143
+ knowledge,
1144
+ clarificationSession,
1145
+ constitutionWarnings: constitutionCheck.warnings,
1146
+ advisoryCriteria,
1147
+ actionableMetrics,
1148
+ unifiedSpec,
1149
+ simplicityResult: runSimplicityCheck([params.description, ...filteredCriteria].join('\n'), estimation.devHours),
1150
+ },
1151
+ };
1152
+ }
814
1153
  // eslint-disable-next-line max-lines-per-function
815
1154
  export async function handleCreateSpec(inputParams, server) {
1155
+ const enumValidation = validateCreateSpecEnums(inputParams);
1156
+ if (enumValidation) {
1157
+ return enumValidation;
1158
+ }
816
1159
  if (inputParams.idempotencyKey !== undefined &&
817
1160
  !/^[A-Za-z0-9._:-]{8,128}$/.test(inputParams.idempotencyKey)) {
818
1161
  return {
@@ -870,21 +1213,70 @@ export async function handleCreateSpec(inputParams, server) {
870
1213
  if (languageGate) {
871
1214
  return languageGate;
872
1215
  }
1216
+ // SPEC-584: Gate check — block if pending clarification token exists and no answers provided
1217
+ const gateError = await runClarificationGate(resolvedPath, 'create_spec', inputParams.clarificationAnswers);
1218
+ if (gateError) {
1219
+ return gateError;
1220
+ }
1221
+ const idempotencyKey = computeIdempotencyKey(inputParams.title, resolvedPath, inputParams.idempotencyKey);
1222
+ const cutoff = Date.now() - IDEMPOTENCY_MATCH_WINDOW_MS;
1223
+ const storeOnlyCandidate = await findStoreByIdempotencyKey(hashProjectPath(resolvedPath), idempotencyKey, cutoff);
1224
+ let preparation;
1225
+ try {
1226
+ preparation = await prepareCreateSpecCandidate(resolvedInputParams, server);
1227
+ }
1228
+ catch (error) {
1229
+ const message = error instanceof Error ? error.message : String(error);
1230
+ return {
1231
+ content: [{ type: 'text', text: ti('errors.internalError', { message }) }],
1232
+ isError: true,
1233
+ };
1234
+ }
1235
+ if (preparation.kind === 'early') {
1236
+ if (preparation.clarificationProjectId) {
1237
+ await persistClarificationToken(preparation.result, preparation.clarificationProjectId, 'create_spec');
1238
+ }
1239
+ return preparation.result;
1240
+ }
1241
+ const prepared = preparation.prepared;
1242
+ prepared.context.spec.idempotencyKey = idempotencyKey;
1243
+ if (storeOnlyCandidate) {
1244
+ const committedEvidence = await findByIdempotencyEvidence(resolvedPath, idempotencyKey, cutoff);
1245
+ if (!committedEvidence) {
1246
+ return {
1247
+ content: [
1248
+ {
1249
+ type: 'text',
1250
+ text: `A partial create_spec transaction exists for ${storeOnlyCandidate.id}: the store row has no committed idempotency evidence. Recovery is required before retrying.`,
1251
+ },
1252
+ ],
1253
+ isError: true,
1254
+ structuredContent: {
1255
+ error: 'PARTIAL_IDEMPOTENCY_STATE',
1256
+ code: 409,
1257
+ persisted: false,
1258
+ specId: storeOnlyCandidate.id,
1259
+ },
1260
+ };
1261
+ }
1262
+ }
873
1263
  // SPEC-665: Fire-and-forget TTL refresh for conventions.json (never blocks create_spec)
874
1264
  void import('../engine/conventions-ttl.js').then(({ triggerConventionsTtlRefresh }) => {
875
1265
  void triggerConventionsTtlRefresh(resolvedPath).catch(() => {
876
1266
  /* best-effort */
877
1267
  });
878
1268
  });
879
- // SPEC-584: Gate check — block if pending clarification token exists and no answers provided
880
- const gateError = await runClarificationGate(resolvedPath, 'create_spec', inputParams.clarificationAnswers);
881
- if (gateError) {
882
- return gateError;
883
- }
884
1269
  // SPEC-770: Idempotency check — return existing spec if same title+projectPath within 10 minutes
885
- const idempotencyKey = computeIdempotencyKey(inputParams.title, resolvedPath, inputParams.idempotencyKey);
886
1270
  const operationKey = computeOperationKey(resolvedInputParams, resolvedPath, idempotencyKey);
887
- const runtimeDatabase = new RuntimeDatabase({ path: resolveStorageLayout().runtimeDatabase });
1271
+ // SPEC-1351: create_spec holds its RuntimeDatabase connection for the whole
1272
+ // operation (idempotency journal + recovery payload). The default cap (4) is
1273
+ // exhausted by a handful of concurrent create_spec calls in one process,
1274
+ // which otherwise fail with RuntimeDatabaseResourceExhaustedError before any
1275
+ // persistence work starts. Raise the ceiling for this call site only.
1276
+ const runtimeDatabase = new RuntimeDatabase({
1277
+ path: resolveStorageLayout().runtimeDatabase,
1278
+ maxConnections: 16,
1279
+ });
888
1280
  const operationJournal = new OperationJournal(runtimeDatabase, createHash('sha256').update(hashProjectPath(resolvedPath)).digest('hex').slice(0, 32));
889
1281
  let journalEntry;
890
1282
  try {
@@ -915,6 +1307,25 @@ export async function handleCreateSpec(inputParams, server) {
915
1307
  }
916
1308
  catch (error) {
917
1309
  runtimeDatabase.close();
1310
+ if (error instanceof BoundaryFailure &&
1311
+ error.kind === 'Conflict' &&
1312
+ error.operation === 'operation begin') {
1313
+ return {
1314
+ content: [
1315
+ {
1316
+ type: 'text',
1317
+ text: 'The idempotency key is already bound to a different create_spec request.',
1318
+ },
1319
+ ],
1320
+ isError: true,
1321
+ structuredContent: {
1322
+ error: 'IDEMPOTENCY_CONFLICT',
1323
+ code: 'IDEMPOTENCY_CONFLICT',
1324
+ status: 409,
1325
+ persisted: false,
1326
+ },
1327
+ };
1328
+ }
918
1329
  throw error;
919
1330
  }
920
1331
  if ((journalEntry.state === 'committed' || journalEntry.state === 'acknowledged') &&
@@ -984,299 +1395,147 @@ export async function handleCreateSpec(inputParams, server) {
984
1395
  idempotencyKey,
985
1396
  ownerId: idempotencyClaim.ownerId,
986
1397
  });
987
- // eslint-disable-next-line max-lines-per-function -- cohesive transactional create boundary
988
1398
  return await trackCost(resolvedInputParams.projectPath ?? '', 'create_spec', async () => {
989
- // Allow internal re-enrichment without mutating function parameter
990
- let params = resolvedInputParams;
991
- const { description, clarificationSessionId } = params;
992
1399
  try {
993
1400
  // SPEC-713: Phase 1 — Critical path: build + persist spec within 25s hard ceiling.
994
1401
  // Only the steps required to produce spec.md on disk are inside this budget.
995
1402
  // Durable post-commit work is delegated to the spec.created outbox consumer.
996
1403
  const criticalResult = await withTotalBudget('create_spec-critical', getRuntimePolicy().createSpec.criticalTimeoutMs, async (criticalSignal) => {
997
- // Build spec context (ID, estimation, diagrams, scope filters)
998
- const buildResult = await measureStep('buildSpecContext', () => buildSpecContext(params));
999
- if (!buildResult.ok) {
1000
- return {
1001
- ok: false,
1002
- earlyReturn: {
1003
- content: [{ type: 'text', text: buildResult.errorMessage }],
1004
- isError: true,
1005
- },
1006
- };
1007
- }
1008
- const { spec, specDir, specPath, estimation, splitSuggestion, duplicate, projectId } = buildResult.context;
1009
- // Kept in external store/evidence for retries; the value-only serializer omits it.
1010
- spec.idempotencyKey = idempotencyKey;
1011
- // Load project knowledge and clarification session
1012
- const knowledge = await measureStep('loadKnowledge', () => knowledgeStore.getKnowledge(projectId));
1013
- const clarificationSession = clarificationSessionId
1014
- ? await knowledgeStore.getClarification(projectId, clarificationSessionId)
1015
- : null;
1016
- // Validate against Constitution
1017
- const constitutionCheck = await measureStep('validateConstitution', () => validateConstitution(projectId, spec.title, spec.tags));
1018
- if (constitutionCheck.blocked && constitutionCheck.errorResult) {
1019
- return { ok: false, earlyReturn: constitutionCheck.errorResult };
1020
- }
1021
- // Full project-wide analysis is delivered by the durable spec.created outbox consumer.
1022
- // A tiny inline budget preserves deterministic idea/clarification behavior when the
1023
- // answer is immediately available without allowing repository scans to delay ACK.
1024
- const inlineAnalysis = await withBudget('create-spec-inline-analysis', getRuntimePolicy().createSpec.inlineAnalysisBudgetMs, (signal) => analyzeProjectForSpec(params.projectPath ?? '', description, spec.title, knowledge, signal));
1025
- const autopilot = unwrapBudget(inlineAnalysis, getEmptyAutopilotResult());
1026
- // If very vague (<10 words), capture as idea instead of spec
1027
- if (autopilot.isIdea) {
1028
- return {
1029
- ok: false,
1030
- earlyReturn: {
1031
- content: [
1032
- {
1033
- type: 'text',
1034
- text: `💡 **Idea captured**: "${description}"\n\nThe description is too brief for a full spec. Use \`capture_idea\` to save it to the backlog, or provide more detail (>10 words) to create a spec.`,
1035
- },
1036
- ],
1037
- },
1038
- };
1039
- }
1040
- // SPEC-463 / SPEC-471: Interactive clarification
1041
- const clarificationResult = handleClarification(server, description, knowledge, autopilot, params);
1042
- if (clarificationResult !== null) {
1043
- if ('earlyReturn' in clarificationResult) {
1044
- // SPEC-584: Persist token so the gate blocks retries without answers
1045
- await persistClarificationToken(clarificationResult.earlyReturn, projectId, 'create_spec');
1046
- return { ok: false, earlyReturn: clarificationResult.earlyReturn };
1404
+ const { params, context, knowledge, clarificationSession, constitutionWarnings, advisoryCriteria, actionableMetrics, simplicityResult: committedSimplicity, } = prepared;
1405
+ const { splitSuggestion, duplicate, projectId } = context;
1406
+ let spec = context.spec;
1407
+ let specDir = context.specDir;
1408
+ let specPath = context.specPath;
1409
+ let unifiedSpec = prepared.unifiedSpec;
1410
+ // SPEC-1351: serialize the id re-derivation + persistence sequence
1411
+ // per project. The candidate id computed during (unlocked) prepare
1412
+ // can collide with another in-flight create_spec call; re-deriving
1413
+ // it here — inside the only place that ever writes spec.md for this
1414
+ // project — guarantees the id actually persisted is unique.
1415
+ return withFileLock(`create-spec:${projectId}`, async () => {
1416
+ let committedPlannerToken;
1417
+ try {
1418
+ const lockedSpecId = await computeNextSpecId(resolvedPath, projectId);
1419
+ if (lockedSpecId !== spec.id) {
1420
+ const newGitBranch = generateBranchName(lockedSpecId, spec.slug, spec.type);
1421
+ const newSpecDir = pathJoin(pathDirname(specDir), `${lockedSpecId}-${spec.slug}`);
1422
+ const newSpecPath = pathJoin(newSpecDir, pathBasename(specPath));
1423
+ unifiedSpec = unifiedSpec
1424
+ .replace(`id: ${spec.id}\n`, `id: ${lockedSpecId}\n`)
1425
+ .replace(`branch: ${spec.gitBranch}\n`, `branch: ${newGitBranch}\n`);
1426
+ spec = {
1427
+ ...spec,
1428
+ id: lockedSpecId,
1429
+ specPath: newSpecPath,
1430
+ technicalPath: newSpecPath,
1431
+ gitBranch: newGitBranch,
1432
+ };
1433
+ specDir = newSpecDir;
1434
+ specPath = newSpecPath;
1435
+ }
1436
+ committedPlannerToken = await issuePlannerToken(resolvedPath, spec.id, params.sessionId ?? 'unknown-session', params.modelId ?? 'unknown-model', params.host ?? 'unknown-host');
1047
1437
  }
1048
- params = clarificationResult.params;
1049
- }
1050
- const filteredCriteria = filterGroundedCriteria(autopilot.suggestedCriteria);
1051
- const technologyContract = await readTechnologySelectionContract(params.projectPath ?? '');
1052
- const contractNote = technologyContract
1053
- ? [
1054
- '',
1055
- 'Technology Contract:',
1056
- `- Mode: ${technologyContract.mode}`,
1057
- technologyContract.language ? `- Language: ${technologyContract.language}` : '',
1058
- technologyContract.framework
1059
- ? `- Framework: ${technologyContract.framework}`
1060
- : '- Framework: none or not selected',
1061
- technologyContract.platform ? `- Platform: ${technologyContract.platform}` : '',
1062
- technologyContract.projectType
1063
- ? `- Project type: ${technologyContract.projectType}`
1064
- : '',
1065
- '- Agents must not choose a different stack unless the user approves a new contract.',
1066
- ]
1067
- .filter(Boolean)
1068
- .join('\n')
1069
- : '';
1070
- const specGenerator = new FallbackGenerator();
1071
- const generatedSpec = await measureStep('generateSpecBody', () => specGenerator.generate({
1072
- title: spec.title,
1073
- description: `${description}${contractNote}`,
1074
- type: spec.type,
1075
- scope: spec.scope,
1076
- target: spec.target,
1077
- acFormat: params.acFormat,
1078
- projectContext: {
1079
- language: technologyContract?.language ?? knowledge?.language ?? undefined,
1080
- framework: technologyContract?.framework ?? knowledge?.framework ?? undefined,
1081
- architecture: knowledge?.architecture.primary ?? undefined,
1082
- },
1083
- }));
1084
- spec.generation = generatedSpec.generation;
1085
- spec.qualityWarnings =
1086
- generatedSpec.qualityWarnings.length > 0 ? generatedSpec.qualityWarnings : undefined;
1087
- const baseCriteria = extractCriteria(generatedSpec.specBody).map((criterion) => criterion.text);
1088
- const groundingCriteria = buildCriterionGroundingRecords({
1089
- criteria: [...baseCriteria, ...filteredCriteria],
1090
- userInput: description,
1091
- generatedEvidence: [
1092
- generatedSpec.generation.modelId ?? generatedSpec.generation.method,
1093
- ],
1094
- });
1095
- const contractCriteria = getContractCriteria(groundingCriteria);
1096
- const advisoryCriteria = getAdvisoryCriteria(groundingCriteria).map((record) => record.text);
1097
- if (contractCriteria.length === 0) {
1098
- return {
1099
- ok: false,
1100
- earlyReturn: {
1101
- content: [
1102
- {
1103
- type: 'text',
1104
- text: 'create_spec did not persist a spec because no acceptance criterion was grounded in the user request. ' +
1105
- 'Needs decision: provide at least one explicit, verifiable criterion as a checkbox or Given/When/Then outcome.',
1106
- },
1107
- ],
1108
- isError: true,
1109
- structuredContent: {
1110
- error: 'MISSING_GROUNDED_ACCEPTANCE_CRITERIA',
1111
- missingDecision: 'Provide at least one explicit, verifiable acceptance criterion.',
1112
- persisted: false,
1113
- },
1114
- },
1115
- };
1116
- }
1117
- const actionableMetrics = calculateActionableSpecMetrics({
1118
- criteria: [...baseCriteria, ...filteredCriteria],
1119
- groundingRecords: groundingCriteria,
1120
- }).filter(shouldExposeMetric);
1121
- const technicalFiles = await measureStep('resolveTechnicalFiles', () => resolveTechnicalFiles({
1122
- generatedSpecBody: generatedSpec.specBody,
1123
- generatedTechnicalSection: generatedSpec.technicalSection,
1124
- projectPath: params.projectPath ?? '',
1125
- }));
1126
- const groundedTechnical = await measureStep('groundTechnicalFiles', () => groundTechnicalFiles({
1127
- files: technicalFiles,
1128
- projectPath: params.projectPath ?? '',
1129
- userInput: description,
1130
- autopilot,
1131
- }));
1132
- const outOfScope = resolveOutOfScope(params.outOfScope);
1133
- const scenarioTestPaths = groundedTechnical.files.test.map((file) => file.path);
1134
- const leanSpec = generateLeanSpecContent({
1135
- spec,
1136
- description: generatedSpec.specBody,
1137
- estimation,
1138
- criteriaOverride: contractCriteria.map((record) => ({
1139
- text: record.text,
1140
- done: false,
1141
- })),
1142
- groundingCriteria: contractCriteria,
1143
- groundingTechnicalReferences: groundedTechnical.records,
1144
- acFormat: params.acFormat,
1145
- scenarioTestPaths,
1146
- });
1147
- const leanTechnical = generateLeanTechnicalContent({
1148
- specId: spec.id,
1149
- filesToCreate: groundedTechnical.files.create,
1150
- filesToModify: groundedTechnical.files.modify,
1151
- filesToTest: groundedTechnical.files.test,
1152
- includeDecisionRequiredPlaceholder: spec.scope !== 'trivial',
1153
- });
1154
- // SPEC-709: write unified spec.md from origin — no separate technical.md.
1155
- // The legacy two-file output is preserved by appending the technical body
1156
- // as a `## Technical` section inside spec.md.
1157
- const unifiedWithoutContract = buildUnifiedSpecContent(leanSpec, leanTechnical);
1158
- const unifiedSpec = appendImplementationContractIfMissing(unifiedWithoutContract, {
1159
- description: generatedSpec.specBody,
1160
- criteria: contractCriteria.map((record) => ({ text: record.text, done: false })),
1161
- files: groundedTechnical.files,
1162
- outOfScope,
1163
- verificationCommands: [],
1164
- });
1165
- const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
1166
- if (!genericOutputGate.passed) {
1167
- return {
1168
- ok: false,
1169
- earlyReturn: {
1170
- content: [
1171
- {
1172
- type: 'text',
1173
- text: 'Spec quality gate blocked generic output before persistence. ' +
1174
- genericOutputGate.issues
1175
- .slice(0, 3)
1176
- .map((issue) => `${issue.phrase}: ${issue.reason}`)
1177
- .join('; '),
1178
- },
1179
- ],
1180
- isError: true,
1181
- structuredContent: {
1182
- error: 'GENERIC_SPEC_OUTPUT_BLOCKED',
1183
- sddConstitutionRuleId: 'sdd.no-generic-output',
1184
- issues: genericOutputGate.issues,
1185
- fixHint: 'Replace generic criteria or placeholder references with grounded, testable behavior.',
1186
- },
1187
- },
1188
- };
1189
- }
1190
- // Contradiction analysis is durable post-commit work. Its synchronous scanners can
1191
- // block the event loop, so it must never run on the persistence acknowledgement path.
1192
- const contradictionHint = undefined;
1193
- const committedSimplicity = runSimplicityCheck([params.description, ...filteredCriteria].join('\n'), estimation.devHours);
1194
- if (outOfScope.length > 0) {
1195
- spec.outOfScope = outOfScope;
1196
- }
1197
- let committedPlannerToken;
1198
- try {
1199
- committedPlannerToken = await issuePlannerToken(resolvedPath, spec.id, params.sessionId ?? 'unknown-session', params.modelId ?? 'unknown-model', params.host ?? 'unknown-host');
1200
- }
1201
- catch {
1202
- // Best-effort evidence must not block durable spec creation.
1203
- }
1204
- const committedResult = buildCommittedCreateResult({
1205
- spec,
1206
- operationId: operationKey,
1207
- duplicate,
1208
- constitutionWarnings: constitutionCheck.warnings,
1209
- clarificationSession,
1210
- advisoryCriteria,
1211
- actionableMetrics,
1212
- splitSuggestion,
1213
- experienceLevel: knowledge?.experienceLevel,
1214
- contradictionHint,
1215
- simplicityResult: committedSimplicity,
1216
- plannerToken: committedPlannerToken,
1217
- });
1218
- let storeCreated = false;
1219
- try {
1220
- assertExecutionCanCommit(criticalSignal);
1221
- operationJournal.setRecoveryPayload('create_spec', operationKey, 1, {
1222
- operationVersion: 1,
1223
- projectPath: resolvedPath,
1224
- idempotencyKey,
1225
- ownerId: idempotencyClaim.ownerId,
1226
- projectId,
1227
- specId: spec.id,
1228
- specPath,
1229
- specContentDigest: createHash('sha256').update(unifiedSpec).digest('hex'),
1230
- result: committedResult,
1438
+ catch {
1439
+ // Best-effort evidence must not block durable spec creation.
1440
+ }
1441
+ const committedResult = buildCommittedCreateResult({
1442
+ spec,
1443
+ operationId: operationKey,
1444
+ duplicate,
1445
+ constitutionWarnings,
1446
+ clarificationSession,
1447
+ advisoryCriteria,
1448
+ actionableMetrics,
1449
+ splitSuggestion,
1450
+ experienceLevel: knowledge?.experienceLevel,
1451
+ contradictionHint: undefined,
1452
+ simplicityResult: committedSimplicity,
1453
+ plannerToken: committedPlannerToken,
1231
1454
  });
1232
- operationJournal.prepared('create_spec', operationKey);
1233
- await measureStep('mkdir-specDir', () => executionMkdir(specDir, { recursive: true }));
1234
- // SPEC-713: measure file write — this is the critical persistence step.
1235
- assertExecutionCanCommit(criticalSignal);
1236
- await measureStep('writeFile-specPath', () => atomicWriteFile(specPath, unifiedSpec, { encoding: 'utf-8' }));
1237
- assertExecutionCanCommit(criticalSignal);
1238
- await measureStep('specStore-createSpec', () => specStore.createSpec(projectId, spec));
1239
- storeCreated = true;
1240
- assertExecutionCanCommit(criticalSignal);
1241
- await measureStep('commit-idempotency-evidence', () => commitIdempotencyEvidence(resolvedPath, idempotencyKey, idempotencyClaim, spec, specPath));
1242
- operationJournal.commitWithOutbox('create_spec', operationKey, committedResult, {
1243
- topic: 'spec.created',
1244
- payload: {
1245
- schemaVersion: 1,
1246
- specId: spec.id,
1247
- projectId,
1455
+ let storeCreated = false;
1456
+ try {
1457
+ assertExecutionCanCommit(criticalSignal);
1458
+ operationJournal.setRecoveryPayload('create_spec', operationKey, 1, {
1459
+ operationVersion: 1,
1248
1460
  projectPath: resolvedPath,
1249
- postCommitTasks: SPEC_CREATED_POST_COMMIT_TASKS,
1250
- },
1251
- });
1252
- claimLifecycle.committed = true;
1253
- // SPEC-709/SPEC-461: unified spec.md; no progress or HTML artifacts.
1254
- }
1255
- catch (writeErr) {
1256
- const storeRolledBack = storeCreated
1257
- ? await specStore.deleteSpec(projectId, spec.id).catch(() => false)
1258
- : true;
1259
- if (storeRolledBack) {
1260
- await cleanupExecutionArtifact(getIdempotencyEvidencePath(resolvedPath, idempotencyKey), {
1261
- force: true,
1461
+ idempotencyKey,
1462
+ ownerId: idempotencyClaim.ownerId,
1463
+ projectId,
1464
+ specId: spec.id,
1465
+ specPath,
1466
+ specContentDigest: createHash('sha256').update(unifiedSpec).digest('hex'),
1467
+ result: committedResult,
1468
+ });
1469
+ operationJournal.prepared('create_spec', operationKey);
1470
+ await measureStep('mkdir-specDir', () => executionMkdir(specDir, { recursive: true }));
1471
+ // SPEC-713: measure file write — this is the critical persistence step.
1472
+ assertExecutionCanCommit(criticalSignal);
1473
+ await measureStep('writeFile-specPath', () => atomicWriteFile(specPath, unifiedSpec, { encoding: 'utf-8' }));
1474
+ assertExecutionCanCommit(criticalSignal);
1475
+ // SPEC-1351 (AC2): verify the write actually landed on disk
1476
+ // before any commit path is allowed to report persisted:true.
1477
+ // atomicWriteFile resolving without throwing is not sufficient
1478
+ // evidence under resource-exhaustion / concurrent-load
1479
+ // conditions that previously let the journal report success
1480
+ // with zero bytes ever reaching disk.
1481
+ const writtenStat = await fsStat(specPath).catch(() => null);
1482
+ if (!writtenStat || writtenStat.size === 0) {
1483
+ throw new Error(`create_spec failed to persist ${specPath}: file is missing or empty after write. ` +
1484
+ 'No spec was registered — retry create_spec.');
1485
+ }
1486
+ await measureStep('specStore-createSpec', () => specStore.createSpec(projectId, spec));
1487
+ storeCreated = true;
1488
+ assertExecutionCanCommit(criticalSignal);
1489
+ await measureStep('commit-idempotency-evidence', () => commitIdempotencyEvidence(resolvedPath, idempotencyKey, idempotencyClaim, spec, specPath));
1490
+ operationJournal.commitWithOutbox('create_spec', operationKey, committedResult, {
1491
+ topic: 'spec.created',
1492
+ payload: {
1493
+ schemaVersion: 1,
1494
+ specId: spec.id,
1495
+ projectId,
1496
+ projectPath: resolvedPath,
1497
+ postCommitTasks: SPEC_CREATED_POST_COMMIT_TASKS,
1498
+ },
1262
1499
  });
1500
+ claimLifecycle.committed = true;
1501
+ // SPEC-709/SPEC-461: unified spec.md; no progress or HTML artifacts.
1502
+ }
1503
+ catch (writeErr) {
1504
+ const storeRolledBack = storeCreated
1505
+ ? await specStore.deleteSpec(projectId, spec.id).catch(() => false)
1506
+ : true;
1507
+ await cleanupExecutionArtifact(getIdempotencyEvidencePath(resolvedPath, idempotencyKey), { force: true });
1263
1508
  await cleanupExecutionArtifact(specDir, { recursive: true, force: true });
1509
+ try {
1510
+ operationJournal.resolveRecovery('create_spec', operationKey, {
1511
+ action: storeRolledBack ? 'rollback' : 'quarantine',
1512
+ reason: storeRolledBack
1513
+ ? 'create_spec persistence failed and compensating cleanup completed'
1514
+ : 'create_spec persistence failed and the spec store rollback was incomplete',
1515
+ });
1516
+ }
1517
+ catch {
1518
+ // The original persistence failure remains the primary error.
1519
+ }
1520
+ throw writeErr;
1264
1521
  }
1265
- throw writeErr;
1266
- }
1267
- return {
1268
- ok: true,
1269
- data: {
1270
- committedResult,
1271
- },
1272
- };
1522
+ return {
1523
+ ok: true,
1524
+ data: {
1525
+ committedResult,
1526
+ },
1527
+ };
1528
+ });
1273
1529
  }); // end withTotalBudget critical path
1274
1530
  // SPEC-713: Handle total budget exceeded — return clear error before 60s MCP timeout
1275
1531
  if (criticalResult.timedOut) {
1276
1532
  claimLifecycle.retainForInFlightWork = criticalResult.warning.includes('exceeded');
1533
+ if (!claimLifecycle.retainForInFlightWork) {
1534
+ rollbackUncommittedOperation(operationJournal, operationKey, 'create_spec timed out before commit with no in-flight work to preserve');
1535
+ }
1277
1536
  // SPEC-770: Wait briefly for any in-flight spec write to complete, then scan for duplicates
1278
1537
  await new Promise((resolve) => setTimeout(resolve, 500));
1279
- const recentSpecs = await findRecentlyWrittenSpecs(resolvedPath, 2 * 60 * 1000);
1538
+ const recentSpecs = await findRecentlyWrittenSpecs(resolvedPath, 2 * 60 * 1000, resolvedInputParams.title);
1280
1539
  const possibleDuplicates = recentSpecs.length > 0 ? recentSpecs : undefined;
1281
1540
  const dupNote = possibleDuplicates
1282
1541
  ? `\n\nA spec may have been created server-side: ${possibleDuplicates.map((s) => s.id).join(', ')}. Retry with the same title to return it instead of creating a duplicate.`
@@ -1285,14 +1544,28 @@ export async function handleCreateSpec(inputParams, server) {
1285
1544
  content: [
1286
1545
  {
1287
1546
  type: 'text',
1288
- text: `❌ create_spec exceeded 25s internal budget.\n\n${criticalResult.warning}${dupNote}`,
1547
+ text: `❌ create_spec exceeded its ${String(criticalResult.ceilingMs)}ms internal budget.\n\n${criticalResult.warning}${dupNote}`,
1289
1548
  },
1290
1549
  ],
1291
1550
  isError: true,
1292
1551
  ...(possibleDuplicates ? { structuredContent: { possibleDuplicates } } : {}),
1293
1552
  };
1294
1553
  }
1554
+ if (criticalResult.status === 'failed') {
1555
+ rollbackUncommittedOperation(operationJournal, operationKey, 'create_spec critical path failed before commit');
1556
+ return {
1557
+ content: [{ type: 'text', text: criticalResult.warning }],
1558
+ isError: true,
1559
+ structuredContent: {
1560
+ error: 'OPERATION_FAILED',
1561
+ category: criticalResult.category,
1562
+ elapsedMs: criticalResult.elapsedMs,
1563
+ persisted: false,
1564
+ },
1565
+ };
1566
+ }
1295
1567
  if (!criticalResult.value.ok) {
1568
+ rollbackUncommittedOperation(operationJournal, operationKey, 'create_spec returned early before commit');
1296
1569
  return criticalResult.value.earlyReturn;
1297
1570
  }
1298
1571
  if (!claimLifecycle.committed) {
@@ -1306,9 +1579,11 @@ export async function handleCreateSpec(inputParams, server) {
1306
1579
  content: [{ type: 'text', text: ti('errors.internalError', { message }) }],
1307
1580
  isError: true,
1308
1581
  };
1309
- return claimLifecycle.committed
1310
- ? finishRecoveredOperation(operationJournal, operationKey, errorResult)
1311
- : errorResult;
1582
+ if (claimLifecycle.committed) {
1583
+ return finishRecoveredOperation(operationJournal, operationKey, errorResult);
1584
+ }
1585
+ rollbackUncommittedOperation(operationJournal, operationKey, 'create_spec failed before commit');
1586
+ return errorResult;
1312
1587
  }
1313
1588
  }); // end trackCost
1314
1589
  }