@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
@@ -253,27 +253,129 @@ function specKey(value, index) {
253
253
  }
254
254
  return `anonymous:${index}`;
255
255
  }
256
- function mergeSpecs(source, destination) {
257
- if (!Array.isArray(source) || !Array.isArray(destination)) {
258
- throw new Error('[Planu] specs.json must contain arrays');
256
+ function unwrapSpecs(value) {
257
+ if (Array.isArray(value)) {
258
+ return { rows: value, envelope: false };
259
259
  }
260
+ if (value && typeof value === 'object' && Array.isArray(value.data)) {
261
+ return { rows: value.data, envelope: true };
262
+ }
263
+ throw new Error('[Planu] specs.json must contain an array or canonical envelope');
264
+ }
265
+ function conflictingUuidIdentity(left, right) {
266
+ if (!left || typeof left !== 'object' || !right || typeof right !== 'object') {
267
+ return false;
268
+ }
269
+ const leftRecord = left;
270
+ const rightRecord = right;
271
+ return (typeof leftRecord.uuid === 'string' &&
272
+ leftRecord.uuid.length > 0 &&
273
+ leftRecord.uuid === rightRecord.uuid &&
274
+ typeof leftRecord.id === 'string' &&
275
+ typeof rightRecord.id === 'string' &&
276
+ leftRecord.id !== rightRecord.id);
277
+ }
278
+ /** Reject split-brain source rows before any merge Map can discard one of them. */
279
+ function validateRawSpecRows(rows, owningProjectId) {
280
+ const ids = new Map();
281
+ const identities = new Map();
282
+ for (const record of rows) {
283
+ if (!record || typeof record !== 'object') {
284
+ throw new Error('[Planu] Canonical spec ownership conflict during migration');
285
+ }
286
+ const value = record;
287
+ const id = typeof value.id === 'string' && value.id.length > 0 ? value.id : undefined;
288
+ const uuid = typeof value.uuid === 'string' && value.uuid.length > 0 ? value.uuid : undefined;
289
+ if (id !== undefined) {
290
+ if (ids.has(id)) {
291
+ throw new Error('[Planu] Canonical spec identity conflict during migration');
292
+ }
293
+ ids.set(id, uuid);
294
+ }
295
+ if (typeof value.projectId !== 'string' || value.projectId !== owningProjectId) {
296
+ throw new Error('[Planu] Canonical spec ownership conflict during migration');
297
+ }
298
+ const aliases = Array.isArray(value.uuidAliases)
299
+ ? value.uuidAliases
300
+ : [];
301
+ for (const identity of [uuid, ...aliases]) {
302
+ if (typeof identity !== 'string' || identity.length === 0 || id === undefined) {
303
+ continue;
304
+ }
305
+ const owner = identities.get(identity);
306
+ if (owner !== undefined && owner !== id) {
307
+ throw new Error('[Planu] Canonical spec identity conflict during migration');
308
+ }
309
+ identities.set(identity, id);
310
+ }
311
+ }
312
+ }
313
+ function validateSpecsDocument(value, owningProjectId) {
314
+ validateRawSpecRows(unwrapSpecs(value).rows, owningProjectId);
315
+ }
316
+ function mergeSpecs(source, destination, owningProjectId) {
317
+ const sourceStore = unwrapSpecs(source);
318
+ const destinationStore = unwrapSpecs(destination);
319
+ validateRawSpecRows(sourceStore.rows, owningProjectId);
320
+ validateRawSpecRows(destinationStore.rows, owningProjectId);
260
321
  const merged = new Map();
261
- destination.forEach((record, index) => merged.set(specKey(record, index), record));
322
+ for (const [index, record] of destinationStore.rows.entries()) {
323
+ const key = specKey(record, index);
324
+ const previous = merged.get(key);
325
+ if (previous !== undefined && conflictingUuidIdentity(previous, record)) {
326
+ throw new Error('[Planu] Canonical spec identity conflict during migration');
327
+ }
328
+ merged.set(key, record);
329
+ }
262
330
  let sourceWon = false;
263
- for (const [index, record] of source.entries()) {
331
+ for (const [index, record] of sourceStore.rows.entries()) {
264
332
  const key = specKey(record, index);
265
333
  const current = merged.get(key);
266
334
  if (current === undefined) {
267
335
  merged.set(key, record);
268
336
  continue;
269
337
  }
338
+ if (conflictingUuidIdentity(current, record)) {
339
+ throw new Error('[Planu] Canonical spec identity conflict during migration');
340
+ }
270
341
  if (recordTimestamp(record) > recordTimestamp(current)) {
271
342
  merged.set(key, record);
272
343
  sourceWon = true;
273
344
  }
274
345
  }
346
+ // Legacy fixtures may omit UUID/project metadata, but a merger must never
347
+ // silently publish two canonical IDs or UUID identities when they are present.
348
+ const seenIds = new Set();
349
+ const seenUuids = new Set();
350
+ for (const record of merged.values()) {
351
+ if (!record || typeof record !== 'object') {
352
+ continue;
353
+ }
354
+ const value = record;
355
+ if (typeof value.id === 'string' && value.id.length > 0) {
356
+ if (seenIds.has(value.id)) {
357
+ throw new Error('[Planu] Canonical spec identity conflict during migration');
358
+ }
359
+ seenIds.add(value.id);
360
+ }
361
+ const aliases = Array.isArray(value.uuidAliases)
362
+ ? value.uuidAliases
363
+ : [];
364
+ const identities = [value.uuid, ...aliases];
365
+ for (const identity of identities) {
366
+ if (typeof identity !== 'string' || identity.length === 0) {
367
+ continue;
368
+ }
369
+ if (seenUuids.has(identity)) {
370
+ throw new Error('[Planu] Canonical spec identity conflict during migration');
371
+ }
372
+ seenUuids.add(identity);
373
+ }
374
+ }
275
375
  return {
276
- value: [...merged.values()],
376
+ value: sourceStore.envelope || destinationStore.envelope
377
+ ? { schemaVersion: 1, data: [...merged.values()] }
378
+ : [...merged.values()],
277
379
  resolution: sourceWon ? 'source-newer' : 'destination-newer',
278
380
  };
279
381
  }
@@ -285,6 +387,9 @@ async function mergeFile(sourceRoot, sourceDigest, stagingRoot, destinationRoot,
285
387
  if (sha256(current) !== file.sha256) {
286
388
  throw new Error(`[Planu] Legacy source changed during migration: ${sourcePath}`);
287
389
  }
390
+ if (file.relativePath === 'specs.json') {
391
+ validateSpecsDocument(JSON.parse(current.toString('utf8')), basename(destinationRoot));
392
+ }
288
393
  await mkdir(dirname(stagingPath), { recursive: true });
289
394
  if (!(await exists(stagingPath))) {
290
395
  await copyFile(sourcePath, stagingPath);
@@ -301,7 +406,7 @@ async function mergeFile(sourceRoot, sourceDigest, stagingRoot, destinationRoot,
301
406
  const destinationJson = JSON.parse(await readFile(stagingPath, 'utf8'));
302
407
  const merged = file.relativePath === 'knowledge.json'
303
408
  ? mergeKnowledge(sourceJson, destinationJson)
304
- : mergeSpecs(sourceJson, destinationJson);
409
+ : mergeSpecs(sourceJson, destinationJson, basename(destinationRoot));
305
410
  await writeFile(stagingPath, `${JSON.stringify(merged.value, null, 2)}\n`, { mode: 0o600 });
306
411
  resolution = merged.resolution;
307
412
  }
@@ -342,11 +447,15 @@ async function planCollisions(sourceInventories, destination, destinationRoot) {
342
447
  const current = state.get(file.relativePath);
343
448
  const schemaFile = file.relativePath === 'knowledge.json' || file.relativePath === 'specs.json';
344
449
  if (!current) {
450
+ const schemaValue = schemaFile
451
+ ? JSON.parse(await readFile(sourcePath, 'utf8'))
452
+ : undefined;
453
+ if (file.relativePath === 'specs.json') {
454
+ validateSpecsDocument(schemaValue, basename(destinationRoot));
455
+ }
345
456
  state.set(file.relativePath, {
346
457
  sha256: file.sha256,
347
- ...(schemaFile
348
- ? { value: JSON.parse(await readFile(sourcePath, 'utf8')) }
349
- : {}),
458
+ ...(schemaFile ? { value: schemaValue } : {}),
350
459
  });
351
460
  continue;
352
461
  }
@@ -361,7 +470,7 @@ async function planCollisions(sourceInventories, destination, destinationRoot) {
361
470
  JSON.parse(await readFile(join(destinationRoot, ...file.relativePath.split('/')), 'utf8'));
362
471
  const merged = file.relativePath === 'knowledge.json'
363
472
  ? mergeKnowledge(source, target)
364
- : mergeSpecs(source, target);
473
+ : mergeSpecs(source, target, basename(destinationRoot));
365
474
  resolution = merged.resolution;
366
475
  state.set(file.relativePath, {
367
476
  sha256: sha256(JSON.stringify(merged.value)),
@@ -403,6 +512,10 @@ async function prepare(journal) {
403
512
  await mergeFile(inventory.root, inventory.digest, journal.stagingRoot, journal.destinationRoot, file, journal.collisions);
404
513
  }
405
514
  }
515
+ const stagedSpecs = join(journal.stagingRoot, 'specs.json');
516
+ if (await exists(stagedSpecs)) {
517
+ validateSpecsDocument(JSON.parse(await readFile(stagedSpecs, 'utf8')), basename(journal.destinationRoot));
518
+ }
406
519
  journal.expectedInventory = await inventoryStorageRoot(journal.stagingRoot);
407
520
  }
408
521
  async function switchDestination(journal) {
@@ -1,3 +1,10 @@
1
+ import { type ValidationReceiptCheckpointLimits } from '../types/durable-validation.js';
2
+ /** Count-only process diagnostic for deterministic resource lifecycle tests. */
3
+ export declare function getRuntimeDatabaseDiagnosticsForTests(): {
4
+ readonly openConnections: number;
5
+ readonly integrityCheckedOpens: number;
6
+ readonly integrityBypassedOpens: number;
7
+ };
1
8
  export type RuntimeRecordKind = 'mutation' | 'index' | 'audit-state';
2
9
  export interface RuntimeDatabaseOptions {
3
10
  readonly path: string;
@@ -131,6 +138,7 @@ export declare class RuntimeDatabase implements Disposable {
131
138
  recordJobProgress(input: JobLeaseInput & {
132
139
  readonly checkpoint: unknown;
133
140
  }): DurableJobRecord;
141
+ assertActiveJobLease(input: JobLeaseInput): void;
134
142
  checkpointJob(input: JobLeaseInput & {
135
143
  readonly checkpoint: unknown;
136
144
  }): DurableJobRecord;
@@ -142,6 +150,7 @@ export declare class RuntimeDatabase implements Disposable {
142
150
  }): DurableJobRecord;
143
151
  retryJob(projectId: string, workspaceId: string, id: string): DurableJobRecord;
144
152
  restartJob(projectId: string, workspaceId: string, id: string): DurableJobRecord;
153
+ restartReceiptPublication(projectId: string, workspaceId: string, id: string, limits: ValidationReceiptCheckpointLimits): DurableJobRecord;
145
154
  restartJobWithPayload(projectId: string, workspaceId: string, id: string, payload: unknown): DurableJobRecord;
146
155
  cancelJob(projectId: string, workspaceId: string, id: string): DurableJobRecord;
147
156
  private archiveCheckpointBeforeRestart;
@@ -2,10 +2,21 @@
2
2
  import { mkdirSync } from 'node:fs';
3
3
  import { dirname, isAbsolute, resolve } from 'node:path';
4
4
  import { DatabaseSync } from 'node:sqlite';
5
+ import { parseValidationReceiptPendingCheckpoint, } from '../types/durable-validation.js';
5
6
  const CURRENT_SCHEMA_VERSION = 4;
6
7
  const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
7
8
  const DEFAULT_MAX_CONNECTIONS = 4;
8
9
  const openConnections = new Map();
10
+ let integrityCheckedOpens = 0;
11
+ let integrityBypassedOpens = 0;
12
+ /** Count-only process diagnostic for deterministic resource lifecycle tests. */
13
+ export function getRuntimeDatabaseDiagnosticsForTests() {
14
+ let count = 0;
15
+ for (const connections of openConnections.values()) {
16
+ count += connections;
17
+ }
18
+ return { openConnections: count, integrityCheckedOpens, integrityBypassedOpens };
19
+ }
9
20
  export const DURABLE_JOB_STATES = [
10
21
  'accepted',
11
22
  'running',
@@ -101,6 +112,10 @@ export class RuntimeDatabase {
101
112
  this.migrate();
102
113
  if (options.checkIntegrity !== false) {
103
114
  this.assertIntegrity();
115
+ integrityCheckedOpens += 1;
116
+ }
117
+ else {
118
+ integrityBypassedOpens += 1;
104
119
  }
105
120
  }
106
121
  catch (error) {
@@ -464,27 +479,39 @@ export class RuntimeDatabase {
464
479
  const now = input.now ?? new Date().toISOString();
465
480
  const expiresAt = new Date(Date.parse(now) + input.leaseMs).toISOString();
466
481
  const result = this.database
467
- .prepare(`UPDATE jobs SET lease_expires_at = ?, updated_at = ?
482
+ .prepare(`UPDATE jobs
483
+ SET lease_expires_at = CASE WHEN lease_expires_at > ? THEN lease_expires_at ELSE ? END,
484
+ updated_at = ?
468
485
  WHERE project_id = ? AND workspace_id = ? AND id = ?
469
486
  AND state = 'running' AND lease_owner = ? AND fencing_token = ? AND cancel_requested = 0
470
487
  AND lease_expires_at > ?`)
471
- .run(expiresAt, now, input.projectId, input.workspaceId, input.id, input.workerId, requireFencingToken(input), now);
488
+ .run(expiresAt, expiresAt, now, input.projectId, input.workspaceId, input.id, input.workerId, requireFencingToken(input), now);
472
489
  return result.changes === 1;
473
490
  }
474
491
  recordJobProgress(input) {
475
- this.requireLeasedJob(input);
492
+ this.assertOpen();
493
+ validateLeaseInput(input);
476
494
  const now = input.now ?? new Date().toISOString();
495
+ const expiresAt = new Date(Date.parse(now) + input.leaseMs).toISOString();
477
496
  const result = this.database
478
- .prepare(`UPDATE jobs SET checkpoint_json = ?, updated_at = ?
497
+ .prepare(`UPDATE jobs
498
+ SET checkpoint_json = ?,
499
+ lease_expires_at = CASE WHEN lease_expires_at > ? THEN lease_expires_at ELSE ? END,
500
+ updated_at = ?
479
501
  WHERE project_id = ? AND workspace_id = ? AND id = ?
480
502
  AND state = 'running' AND lease_owner = ? AND fencing_token = ? AND cancel_requested = 0
481
503
  AND lease_expires_at > ?`)
482
- .run(stringifyJson(input.checkpoint), now, input.projectId, input.workspaceId, input.id, input.workerId, requireFencingToken(input), now);
504
+ .run(stringifyJson(input.checkpoint), expiresAt, expiresAt, now, input.projectId, input.workspaceId, input.id, input.workerId, requireFencingToken(input), now);
483
505
  if (result.changes !== 1) {
506
+ this.requireLeasedJob(input);
484
507
  throw new Error('[Planu] Job lease was lost before checkpoint');
485
508
  }
486
509
  return requireJobRecord(this.getJob(input.projectId, input.workspaceId, input.id), input.id);
487
510
  }
511
+ assertActiveJobLease(input) {
512
+ this.assertOpen();
513
+ this.requireLeasedJob(input);
514
+ }
488
515
  checkpointJob(input) {
489
516
  return this.finishLeasedUpdate(input, 'checkpointed', {
490
517
  checkpointJson: stringifyJson(input.checkpoint),
@@ -492,6 +519,7 @@ export class RuntimeDatabase {
492
519
  }
493
520
  completeJob(input) {
494
521
  return this.finishLeasedUpdate(input, 'completed', {
522
+ clearCheckpoint: true,
495
523
  resultJson: stringifyJson(input.result),
496
524
  });
497
525
  }
@@ -545,6 +573,33 @@ export class RuntimeDatabase {
545
573
  });
546
574
  return requireJobRecord(this.getJob(projectId, workspaceId, id), id);
547
575
  }
576
+ restartReceiptPublication(projectId, workspaceId, id, limits) {
577
+ this.assertOpen();
578
+ requireIdentifier(projectId, 'projectId');
579
+ requireIdentifier(workspaceId, 'workspaceId');
580
+ requireIdentifier(id, 'job id');
581
+ const now = new Date().toISOString();
582
+ this.transaction(() => {
583
+ const current = this.getJob(projectId, workspaceId, id);
584
+ if (!current ||
585
+ !['failed', 'dead-letter'].includes(current.state) ||
586
+ current.cancelRequested ||
587
+ !parseValidationReceiptPendingCheckpoint(current.checkpoint, limits)) {
588
+ throw new Error('[Planu] Job receipt publication is not restartable');
589
+ }
590
+ const changed = this.database
591
+ .prepare(`UPDATE jobs SET state = 'accepted', attempts = 0, cancel_requested = 0,
592
+ result_json = NULL, error_message = NULL, lease_owner = NULL,
593
+ lease_expires_at = NULL, updated_at = ?
594
+ WHERE project_id = ? AND workspace_id = ? AND id = ?
595
+ AND state IN ('failed', 'dead-letter') AND cancel_requested = 0`)
596
+ .run(now, projectId, workspaceId, id);
597
+ if (changed.changes !== 1) {
598
+ throw new Error('[Planu] Job receipt publication is not restartable');
599
+ }
600
+ });
601
+ return requireJobRecord(this.getJob(projectId, workspaceId, id), id);
602
+ }
548
603
  restartJobWithPayload(projectId, workspaceId, id, payload) {
549
604
  this.assertOpen();
550
605
  requireIdentifier(projectId, 'projectId');
@@ -641,7 +696,8 @@ export class RuntimeDatabase {
641
696
  const current = this.getJob(input.projectId, input.workspaceId, input.id);
642
697
  if (current?.state !== 'running' ||
643
698
  current.leaseOwner !== input.workerId ||
644
- current.fencingToken !== requireFencingToken(input)) {
699
+ current.fencingToken !== requireFencingToken(input) ||
700
+ current.cancelRequested) {
645
701
  throw new Error('[Planu] Job lease is stale or not owned by this worker');
646
702
  }
647
703
  const now = input.now ?? new Date().toISOString();
@@ -654,13 +710,13 @@ export class RuntimeDatabase {
654
710
  this.requireLeasedJob(input);
655
711
  const now = input.now ?? new Date().toISOString();
656
712
  const result = this.database
657
- .prepare(`UPDATE jobs SET state = ?, checkpoint_json = COALESCE(?, checkpoint_json),
713
+ .prepare(`UPDATE jobs SET state = ?, checkpoint_json = CASE WHEN ? = 1 THEN NULL ELSE COALESCE(?, checkpoint_json) END,
658
714
  result_json = COALESCE(?, result_json), error_message = ?,
659
715
  lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
660
716
  WHERE project_id = ? AND workspace_id = ? AND id = ?
661
717
  AND state = 'running' AND lease_owner = ? AND fencing_token = ?
662
718
  AND lease_expires_at > ? AND cancel_requested = 0`)
663
- .run(state, values.checkpointJson ?? null, values.resultJson ?? null, values.errorMessage ?? null, now, input.projectId, input.workspaceId, input.id, input.workerId, requireFencingToken(input), now);
719
+ .run(state, values.clearCheckpoint === true ? 1 : 0, values.checkpointJson ?? null, values.resultJson ?? null, values.errorMessage ?? null, now, input.projectId, input.workspaceId, input.id, input.workerId, requireFencingToken(input), now);
664
720
  if (result.changes !== 1) {
665
721
  throw new Error('[Planu] Job lease was lost before commit');
666
722
  }
@@ -1,4 +1,4 @@
1
- import type { Spec, SpecStatus, SpecVersion, SpecSummary, DefinitionOfReady, DefinitionOfDone, PostCommitTaskClaim } from '../types/index.js';
1
+ import type { Spec, SpecStatus, SpecVersion, SpecSummary, DefinitionOfReady, DefinitionOfDone, StatusHistoryEntry, PostCommitTaskClaim } from '../types/index.js';
2
2
  /**
3
3
  * Thrown when code tries to write `status` via `updateSpec` directly.
4
4
  * All status transitions must go through `handleUpdateStatus` → `transitionSpec`.
@@ -6,6 +6,18 @@ import type { Spec, SpecStatus, SpecVersion, SpecSummary, DefinitionOfReady, Def
6
6
  export declare class DirectStatusWriteForbiddenError extends Error {
7
7
  constructor(specId: string, attemptedStatus: string, caller: string);
8
8
  }
9
+ /** Stable, privacy-safe canonical identity failure. */
10
+ export declare class DuplicateSpecIdError extends Error {
11
+ readonly code: 'DUPLICATE_SPEC_ID';
12
+ constructor(projectId: string, specIds: readonly string[]);
13
+ }
14
+ /**
15
+ * Validate the single canonical identity namespace shared by every spec row.
16
+ * Keep the diagnostic deliberately small: UUIDs and storage paths are private.
17
+ */
18
+ export declare function validateCanonicalSpecRows(projectId: string, specs: readonly Spec[]): void;
19
+ /** Administrative-only lock boundary for the digest-bound raw-store repair. */
20
+ export declare function withCanonicalSpecRepairLock<T>(projectId: string, fn: () => Promise<T>): Promise<T>;
9
21
  /** Evict the cache for a project (e.g. after external file changes). */
10
22
  export declare function invalidateSpecsCache(projectId: string): void;
11
23
  /**
@@ -34,7 +46,7 @@ export declare function createSpec(projectId: string, spec: Spec): Promise<Spec>
34
46
  * Importable exclusively from `src/engine/spec-state-machine/transition-spec.ts`.
35
47
  * All status transitions must flow through `transitionSpec` → `__internalSetStatus`.
36
48
  */
37
- export declare function __internalSetStatus(projectId: string, specId: string, status: SpecStatus, updates?: Omit<Partial<Spec>, 'status'>, pendingBackgroundActions?: readonly string[], expectedSnapshot?: Pick<Spec, 'status' | 'updatedAt'>): Promise<Spec>;
49
+ export declare function __internalSetStatus(projectId: string, specId: string, status: SpecStatus, updates?: Omit<Partial<Spec>, 'status'>, pendingBackgroundActions?: readonly string[], expectedSnapshot?: Pick<Spec, 'status' | 'updatedAt'>, transitionRootBinding?: string, reconciliation?: Pick<StatusHistoryEntry, 'reconciliationRequestId' | 'reconciliationBindingDigest' | 'trigger'>): Promise<Spec>;
38
50
  /**
39
51
  * Restore the exact snapshot captured before a lifecycle transition.
40
52
  * This is only used by transitionSpec compensation while the caller still
@@ -22,6 +22,15 @@ export class DirectStatusWriteForbiddenError extends Error {
22
22
  this.name = 'DirectStatusWriteForbiddenError';
23
23
  }
24
24
  }
25
+ /** Stable, privacy-safe canonical identity failure. */
26
+ export class DuplicateSpecIdError extends Error {
27
+ code = 'DUPLICATE_SPEC_ID';
28
+ constructor(projectId, specIds) {
29
+ const ids = [...new Set(specIds)].sort((left, right) => left.localeCompare(right));
30
+ super(`[Planu] Canonical spec identity conflict in project ${projectId}: ${ids.join(', ')}`);
31
+ this.name = 'DuplicateSpecIdError';
32
+ }
33
+ }
25
34
  // --- internal helpers ---
26
35
  function specsFile(projectId) {
27
36
  return `${projectDataDir(projectId)}/specs.json`;
@@ -41,8 +50,52 @@ function normalizeSpec(raw) {
41
50
  }
42
51
  // SPEC-601: Lazy UUID migration — generate if missing
43
52
  spec.uuid ??= crypto.randomUUID();
53
+ if (spec.uuidAliases !== undefined) {
54
+ spec.uuidAliases = [
55
+ ...new Set(spec.uuidAliases.filter((alias) => typeof alias === 'string' && alias)),
56
+ ].sort((left, right) => left.localeCompare(right));
57
+ }
44
58
  return spec;
45
59
  }
60
+ /**
61
+ * Validate the single canonical identity namespace shared by every spec row.
62
+ * Keep the diagnostic deliberately small: UUIDs and storage paths are private.
63
+ */
64
+ export function validateCanonicalSpecRows(projectId, specs) {
65
+ const ids = new Map();
66
+ const uuids = new Map();
67
+ const conflicts = new Set();
68
+ for (const spec of specs) {
69
+ const priorId = ids.get(spec.id);
70
+ if (priorId !== undefined) {
71
+ conflicts.add(spec.id);
72
+ conflicts.add(priorId);
73
+ }
74
+ else {
75
+ ids.set(spec.id, spec.id);
76
+ }
77
+ if (spec.projectId !== projectId) {
78
+ conflicts.add(spec.id);
79
+ }
80
+ const identities = [spec.uuid, ...(spec.uuidAliases ?? [])];
81
+ for (const identity of identities) {
82
+ if (typeof identity !== 'string' || identity.length === 0) {
83
+ continue;
84
+ }
85
+ const owner = uuids.get(identity);
86
+ if (owner !== undefined) {
87
+ conflicts.add(owner);
88
+ conflicts.add(spec.id);
89
+ }
90
+ else {
91
+ uuids.set(identity, spec.id);
92
+ }
93
+ }
94
+ }
95
+ if (conflicts.size > 0) {
96
+ throw new DuplicateSpecIdError(projectId, [...conflicts]);
97
+ }
98
+ }
46
99
  // Write-through in-memory cache: projectId → Spec[].
47
100
  // Populated on first read; invalidated (updated) on every mutation.
48
101
  // Eliminates repeated disk I/O for a local single-user MCP server.
@@ -93,6 +146,10 @@ async function withSpecMutationLock(projectId, fn) {
93
146
  }
94
147
  });
95
148
  }
149
+ /** Administrative-only lock boundary for the digest-bound raw-store repair. */
150
+ export async function withCanonicalSpecRepairLock(projectId, fn) {
151
+ return withSpecMutationLock(projectId, fn);
152
+ }
96
153
  /**
97
154
  * Load all specs — from cache on subsequent calls, disk only on first access.
98
155
  *
@@ -120,12 +177,15 @@ async function loadAll(projectId) {
120
177
  }
121
178
  const raw = decodeCriticalSchema(persisted, 'spec-store').data;
122
179
  const specs = raw.map(normalizeSpec);
180
+ validateCanonicalSpecRows(projectId, specs);
123
181
  specsCache.set(projectId, specs);
124
182
  return specs;
125
183
  }
126
184
  async function saveAll(projectId, specs) {
127
- await writeJson(specsFile(projectId), { schemaVersion: 1, data: specs });
128
- specsCache.set(projectId, specs);
185
+ const normalized = specs.map((spec) => normalizeSpec(spec));
186
+ validateCanonicalSpecRows(projectId, normalized);
187
+ await writeJson(specsFile(projectId), { schemaVersion: 1, data: normalized });
188
+ specsCache.set(projectId, normalized);
129
189
  }
130
190
  /** Evict the cache for a project (e.g. after external file changes). */
131
191
  export function invalidateSpecsCache(projectId) {
@@ -160,7 +220,7 @@ export async function getSpecFresh(projectId, specId) {
160
220
  /** SPEC-601: Lookup a spec by its globally unique UUID. */
161
221
  export async function getSpecByUuid(projectId, uuid) {
162
222
  const specs = await loadAll(projectId);
163
- return specs.find((s) => s.uuid === uuid) ?? null;
223
+ return (specs.find((spec) => spec.uuid === uuid || spec.uuidAliases?.includes(uuid) === true) ?? null);
164
224
  }
165
225
  /**
166
226
  * Create a new spec. Throws if a spec with the same ID already exists.
@@ -186,7 +246,12 @@ export async function createSpec(projectId, spec) {
186
246
  * NOT exported from the barrel. Called only by `__internalSetStatus` and itself.
187
247
  * All external callers must use `updateSpec` (non-status) or `transitionSpec` (status).
188
248
  */
189
- async function __internalUpdateSpec(projectId, specId, updates, pendingBackgroundActions = [], expectedSnapshot) {
249
+ function snapshotChanged(spec, expectedSnapshot) {
250
+ return Boolean(expectedSnapshot &&
251
+ (spec.status !== expectedSnapshot.status || spec.updatedAt !== expectedSnapshot.updatedAt));
252
+ }
253
+ // eslint-disable-next-line max-lines-per-function -- lifecycle status persistence is one atomic mutation boundary
254
+ async function __internalUpdateSpec(projectId, specId, updates, pendingBackgroundActions = [], expectedSnapshot, transitionRootBinding, reconciliation) {
190
255
  return withSpecMutationLock(projectId, async () => {
191
256
  const specs = await loadAll(projectId);
192
257
  const idx = specs.findIndex((s) => s.id === specId);
@@ -197,9 +262,7 @@ async function __internalUpdateSpec(projectId, specId, updates, pendingBackgroun
197
262
  if (!existing) {
198
263
  throw new Error(`Spec "${specId}" not found in project "${projectId}"`);
199
264
  }
200
- if (expectedSnapshot &&
201
- (existing.status !== expectedSnapshot.status ||
202
- existing.updatedAt !== expectedSnapshot.updatedAt)) {
265
+ if (snapshotChanged(existing, expectedSnapshot)) {
203
266
  throw new Error(`Spec "${specId}" changed after lifecycle gates ran; retry update_status with the fresh state.`);
204
267
  }
205
268
  const now = new Date().toISOString();
@@ -224,6 +287,8 @@ async function __internalUpdateSpec(projectId, specId, updates, pendingBackgroun
224
287
  fromStatus: existing.status,
225
288
  transitionId,
226
289
  ...(pendingBackgroundActions.length > 0 && { pendingBackgroundActions }),
290
+ ...(pendingBackgroundActions.length > 0 &&
291
+ transitionRootBinding && { transitionRootBinding }),
227
292
  ...(pendingBackgroundActions.length > 0 && {
228
293
  postCommitTasks: pendingBackgroundActions.map((name) => ({
229
294
  name,
@@ -232,6 +297,13 @@ async function __internalUpdateSpec(projectId, specId, updates, pendingBackgroun
232
297
  updatedAt: now,
233
298
  })),
234
299
  }),
300
+ ...(reconciliation?.reconciliationRequestId
301
+ ? {
302
+ reconciliationRequestId: reconciliation.reconciliationRequestId,
303
+ reconciliationBindingDigest: reconciliation.reconciliationBindingDigest,
304
+ trigger: reconciliation.trigger,
305
+ }
306
+ : {}),
235
307
  };
236
308
  updatedStatusHistory = [...(existing.statusHistory ?? []), entry];
237
309
  }
@@ -257,8 +329,8 @@ async function __internalUpdateSpec(projectId, specId, updates, pendingBackgroun
257
329
  * Importable exclusively from `src/engine/spec-state-machine/transition-spec.ts`.
258
330
  * All status transitions must flow through `transitionSpec` → `__internalSetStatus`.
259
331
  */
260
- export async function __internalSetStatus(projectId, specId, status, updates = {}, pendingBackgroundActions = [], expectedSnapshot) {
261
- return __internalUpdateSpec(projectId, specId, { ...updates, status }, pendingBackgroundActions, expectedSnapshot);
332
+ export async function __internalSetStatus(projectId, specId, status, updates = {}, pendingBackgroundActions = [], expectedSnapshot, transitionRootBinding, reconciliation) {
333
+ return __internalUpdateSpec(projectId, specId, { ...updates, status }, pendingBackgroundActions, expectedSnapshot, transitionRootBinding, reconciliation);
262
334
  }
263
335
  /**
264
336
  * Restore the exact snapshot captured before a lifecycle transition.
@@ -313,6 +385,21 @@ function isPendingPostCommitTask(task) {
313
385
  return (task.status !== 'done' &&
314
386
  !(task.status === 'failed' && task.attempts >= MAX_POST_COMMIT_ATTEMPTS));
315
387
  }
388
+ function terminalizeExhaustedPostCommitTask(entry, tasks, taskIndex, current, now) {
389
+ const failedTask = {
390
+ name: current.name,
391
+ status: 'failed',
392
+ attempts: current.attempts,
393
+ updatedAt: now,
394
+ ...(current.lastError ? { lastError: current.lastError } : {}),
395
+ };
396
+ const nextTasks = tasks.map((task, index) => (index === taskIndex ? failedTask : task));
397
+ return {
398
+ ...entry,
399
+ postCommitTasks: nextTasks,
400
+ pendingBackgroundActions: nextTasks.filter(isPendingPostCommitTask).map((task) => task.name),
401
+ };
402
+ }
316
403
  function pruneCompletedOutbox(history) {
317
404
  let completedOutboxesKept = 0;
318
405
  return [...history]
@@ -336,6 +423,7 @@ function pruneCompletedOutbox(history) {
336
423
  * Atomically claim one durable post-commit task.
337
424
  * Legacy transition receipts are materialized only when the task was explicitly planned.
338
425
  */
426
+ // eslint-disable-next-line max-lines-per-function -- claim and exhaustion normalization share one atomic mutation boundary
339
427
  export async function claimPostCommitTask(projectId, specId, transitionId, taskName, options = {}) {
340
428
  return withSpecMutationLock(projectId, async () => {
341
429
  const specs = await loadAll(projectId);
@@ -349,6 +437,7 @@ export async function claimPostCommitTask(projectId, specId, transitionId, taskN
349
437
  const claimResult = {
350
438
  value: { claimed: false, reason: 'missing' },
351
439
  };
440
+ const normalizedExhausted = { value: false };
352
441
  const nextHistory = updateOutboxReceipt(existing.statusHistory, transitionId, (entry) => {
353
442
  const tasks = materializePostCommitTasks(entry);
354
443
  const taskIndex = tasks.findIndex((task) => task.name === taskName);
@@ -360,10 +449,6 @@ export async function claimPostCommitTask(projectId, specId, transitionId, taskN
360
449
  claimResult.value = { claimed: false, reason: 'done' };
361
450
  return entry;
362
451
  }
363
- if (current.attempts >= MAX_POST_COMMIT_ATTEMPTS) {
364
- claimResult.value = { claimed: false, reason: 'done' };
365
- return entry;
366
- }
367
452
  if (current.status === 'running' &&
368
453
  current.leaseExpiresAt &&
369
454
  Date.parse(current.leaseExpiresAt) > nowMs) {
@@ -374,6 +459,11 @@ export async function claimPostCommitTask(projectId, specId, transitionId, taskN
374
459
  };
375
460
  return entry;
376
461
  }
462
+ if (current.attempts >= MAX_POST_COMMIT_ATTEMPTS) {
463
+ claimResult.value = { claimed: false, reason: 'done' };
464
+ normalizedExhausted.value = true;
465
+ return terminalizeExhaustedPostCommitTask(entry, tasks, taskIndex, current, now);
466
+ }
377
467
  const executionId = randomUUID();
378
468
  const attempts = current.attempts + 1;
379
469
  const claimedTask = {
@@ -394,7 +484,7 @@ export async function claimPostCommitTask(projectId, specId, transitionId, taskN
394
484
  .map((task) => task.name),
395
485
  };
396
486
  });
397
- if (!nextHistory || !claimResult.value.claimed) {
487
+ if (!nextHistory || (!claimResult.value.claimed && !normalizedExhausted.value)) {
398
488
  return claimResult.value;
399
489
  }
400
490
  const updated = { ...existing, statusHistory: nextHistory };
@@ -0,0 +1,5 @@
1
+ /** Publish one complete canonical status snapshot with same-directory atomic replacement. */
2
+ export declare function atomicStatusWrite(statusPath: string, status: unknown): Promise<void>;
3
+ /** Remove only default atomic-write temporaries whose local owner process is dead. */
4
+ export declare function cleanupDeadStatusTemps(statusPath: string): Promise<number>;
5
+ //# sourceMappingURL=atomic-status-write.d.ts.map