@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
@@ -1,19 +1,80 @@
1
+ /* eslint-disable max-lines -- durable validation, resumable receipt publication, and fencing stay co-located for auditability */
1
2
  import { randomUUID } from 'node:crypto';
3
+ import { join } from 'node:path';
2
4
  import { DurableJobRuntime } from './job-runtime.js';
3
5
  import { RuntimeDatabase } from '../../storage/runtime-db.js';
4
6
  import { createExecutionScope } from './context.js';
5
7
  import { runValidateWorker } from './validate-worker-runtime.js';
6
8
  import { redactValidationProgress, computeDurableValidationBindings, toValidationReceiptBindings, writeValidationArtifactProvenance, } from '../validation/durable-validation.js';
9
+ import { parseValidationReceiptPendingCheckpoint, isValidationReceiptPendingIntent, } from '../../types/durable-validation.js';
7
10
  import { resolveStorageLayout } from '../../storage/storage-layout.js';
8
11
  import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
9
- import { createValidationReceiptAuthority, ensureValidationIssuerKey, issueValidationReceipt, LOCAL_VALIDATION_RECEIPT_ISSUER, } from '../validation/validation-receipt.js';
12
+ import { createValidationReceiptAuthority, ensureValidationIssuerKey, issueValidationReceipt, LOCAL_VALIDATION_RECEIPT_ISSUER, preflightValidationReceiptSigner, } from '../validation/validation-receipt.js';
10
13
  import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
11
- import { assertValidationFreshnessBarrier, rebaselineValidationFreshnessLeaseAfterRecompute, } from '../validation/validation-freshness.js';
14
+ import { assertValidationFreshnessBarrier, beginValidationFreshnessBarrier, bindValidationFreshnessLease, closeValidationFreshnessLease, rebaselineValidationFreshnessLeaseAfterRecompute, } from '../validation/validation-freshness.js';
12
15
  import { recoverValidateJobs } from './validate-job-recovery.js';
13
16
  import { getRuntimePolicy } from '../runtime-policy.js';
14
17
  import { isValidateJob, parseValidatePayload, rehydrateValidationPayload, } from './validate-job-payload.js';
18
+ import { revalidateValidationWorktree } from '../validation/validation-worktree.js';
19
+ import { projectDataDir } from '../../storage/base-store.js';
20
+ import { redactSensitiveValue, redactText } from '../../security/redactor.js';
21
+ import { MacOSKeychainOperationError } from '../../security/adapters/macos-keychain.js';
22
+ import { createDurableJobHeartbeat } from './durable-job-heartbeat-runtime.js';
15
23
  const WORKSPACE_ID = 'global';
16
- const activeScopes = new Set();
24
+ const activeScopes = new Map();
25
+ const RECEIPT_PENDING_ERROR = '[Planu] VALIDATION_RECEIPT_PENDING action=restart_job';
26
+ class ReceiptPublicationTimeoutError extends Error {
27
+ constructor() {
28
+ super('[Planu] Receipt publication deadline elapsed');
29
+ this.name = 'ReceiptPublicationTimeoutError';
30
+ }
31
+ }
32
+ class ReceiptPublicationInterruptedError extends Error {
33
+ constructor(options) {
34
+ super('[Planu] Receipt publication was interrupted after persistence', options);
35
+ this.name = 'ReceiptPublicationInterruptedError';
36
+ }
37
+ }
38
+ class ValidationAuthorityLostError extends Error {
39
+ reason;
40
+ constructor(reason, options) {
41
+ super(`[Planu] Durable validate job authority was lost (${reason})`, options);
42
+ this.reason = reason;
43
+ this.name = 'ValidationAuthorityLostError';
44
+ }
45
+ }
46
+ function recoverableReceiptError(error) {
47
+ return (error instanceof ReceiptPublicationTimeoutError ||
48
+ error instanceof ReceiptPublicationInterruptedError ||
49
+ (error instanceof MacOSKeychainOperationError && error.code === 'KEYCHAIN_TIMEOUT'));
50
+ }
51
+ function decodeUtf8Prefix(encoded, prefixLength) {
52
+ try {
53
+ return {
54
+ valid: true,
55
+ value: new TextDecoder('utf-8', { fatal: true }).decode(encoded.subarray(0, prefixLength)),
56
+ };
57
+ }
58
+ catch {
59
+ return { valid: false };
60
+ }
61
+ }
62
+ function truncateUtf8ByBytes(value, maximumBytes) {
63
+ const encoded = Buffer.from(value, 'utf8');
64
+ if (encoded.length <= maximumBytes) {
65
+ return value;
66
+ }
67
+ for (let length = maximumBytes; length >= 0; length -= 1) {
68
+ const decoded = decodeUtf8Prefix(encoded, length);
69
+ if (decoded.valid) {
70
+ return decoded.value;
71
+ }
72
+ }
73
+ return '';
74
+ }
75
+ function canonicalJson(value) {
76
+ return JSON.stringify(value);
77
+ }
17
78
  function hasReceipt(result) {
18
79
  const structured = result.structuredContent;
19
80
  return typeof structured?.receiptId === 'string' && typeof structured.receiptKey === 'string';
@@ -23,6 +84,28 @@ function validationPassed(result) {
23
84
  const report = structured?.validationReport;
24
85
  return structured?.ready === true && report?.passed === true;
25
86
  }
87
+ async function closeFreshnessLeaseSafely(lease) {
88
+ if (!lease) {
89
+ return;
90
+ }
91
+ try {
92
+ await closeValidationFreshnessLease(lease);
93
+ }
94
+ catch (error) {
95
+ reportClassifiedDegradation('VALIDATION_FRESHNESS_CHECK_FAILED', error);
96
+ }
97
+ }
98
+ async function stopHeartbeatSafely(heartbeat) {
99
+ if (!heartbeat) {
100
+ return;
101
+ }
102
+ try {
103
+ await heartbeat.stop();
104
+ }
105
+ catch (error) {
106
+ reportClassifiedDegradation('VALIDATE_JOB_HEARTBEAT_FAILURE', error);
107
+ }
108
+ }
26
109
  function toolResultError(result) {
27
110
  return result.content
28
111
  .map((item) => item.text)
@@ -53,11 +136,32 @@ export class ValidateJobExecutor {
53
136
  return Promise.all(candidates.map((job) => this.execute(job)));
54
137
  }
55
138
  async runUntilIdle(projectId, workspaceId = WORKSPACE_ID) {
139
+ const capacity = getRuntimePolicy().validation.workerConcurrency;
140
+ const active = new Set();
56
141
  for (;;) {
57
- const completed = await this.runOnce(projectId, workspaceId);
58
- if (completed.length === 0) {
142
+ const available = capacity - active.size;
143
+ const candidates = this.runtime
144
+ .list(projectId, workspaceId)
145
+ .filter((job) => (job.state === 'accepted' || job.state === 'checkpointed') &&
146
+ isValidateJob(job.payload))
147
+ .slice(0, available);
148
+ for (const candidate of candidates) {
149
+ const execution = this.execute(candidate)
150
+ .then(() => undefined)
151
+ .catch((error) => {
152
+ reportClassifiedDegradation('VALIDATE_JOB_EXECUTION_FAILURE', error);
153
+ })
154
+ .finally(() => {
155
+ active.delete(execution);
156
+ });
157
+ active.add(execution);
158
+ }
159
+ if (active.size === 0) {
59
160
  return;
60
161
  }
162
+ if (active.size >= capacity || candidates.length === 0) {
163
+ await Promise.race(active);
164
+ }
61
165
  }
62
166
  }
63
167
  async execute(job) {
@@ -69,12 +173,24 @@ export class ValidateJobExecutor {
69
173
  const claimed = this.runtime.claim({
70
174
  ...identity,
71
175
  workerId: this.workerId,
72
- leaseMs: getRuntimePolicy().validation.workerLeaseMs,
176
+ leaseMs: this.workerTimingPolicy().workerLeaseMs,
73
177
  });
74
178
  if (!claimed) {
75
179
  return this.runtime.get(identity);
76
180
  }
77
- const parsedPayload = parseValidatePayload(claimed.payload);
181
+ let parsedPayload;
182
+ try {
183
+ parsedPayload = parseValidatePayload(claimed.payload);
184
+ }
185
+ catch (error) {
186
+ reportClassifiedDegradation('VALIDATE_JOB_PAYLOAD_PARSE_FAILURE', error);
187
+ return this.runtime.fail({
188
+ ...identity,
189
+ workerId: this.workerId,
190
+ fencingToken: claimed.fencingToken,
191
+ error: error instanceof Error ? error.message : '[Planu] Invalid validate job payload',
192
+ });
193
+ }
78
194
  if (!parsedPayload) {
79
195
  return this.runtime.fail({
80
196
  ...identity,
@@ -83,54 +199,73 @@ export class ValidateJobExecutor {
83
199
  error: '[Planu] Invalid validate job payload',
84
200
  });
85
201
  }
86
- const deadlineAt = parsedPayload.deadlineAt ?? Date.now() + getRuntimePolicy().validation.defaultDeadlineMs;
87
- if (deadlineAt <= Date.now()) {
202
+ const pendingCheckpoint = parseValidationReceiptPendingCheckpoint(claimed.checkpoint, this.receiptCheckpointLimits());
203
+ if (pendingCheckpoint) {
204
+ return this.resumeReceiptPublication(identity, parsedPayload, pendingCheckpoint, claimed.fencingToken);
205
+ }
206
+ if (isValidationReceiptPendingIntent(claimed.checkpoint)) {
88
207
  return this.runtime.fail({
89
208
  ...identity,
90
209
  workerId: this.workerId,
91
210
  fencingToken: claimed.fencingToken,
92
- error: '[Planu] Durable validate job deadline elapsed',
211
+ error: '[Planu] Invalid validation receipt checkpoint',
93
212
  });
94
213
  }
95
- let payload;
96
- try {
97
- payload = await rehydrateValidationPayload(parsedPayload);
98
- }
99
- catch (error) {
100
- /* reliability-optional: VALIDATE_JOB_SUBMISSION_FAILURE — runtime records typed failure */
101
- reportClassifiedDegradation('VALIDATE_JOB_SUBMISSION_FAILURE', error);
214
+ const deadlineAt = parsedPayload.deadlineAt ?? Date.now() + getRuntimePolicy().validation.defaultDeadlineMs;
215
+ if (deadlineAt <= Date.now()) {
102
216
  return this.runtime.fail({
103
217
  ...identity,
104
218
  workerId: this.workerId,
105
219
  fencingToken: claimed.fencingToken,
106
- error: (error instanceof Error ? error.message : String(error)).slice(0, 4_096),
220
+ error: '[Planu] Durable validate job deadline elapsed',
107
221
  });
108
222
  }
109
- return this.runClaimedValidation(identity, payload, deadlineAt, claimed.fencingToken);
223
+ return this.runClaimedValidation(identity, parsedPayload, deadlineAt, claimed.fencingToken);
110
224
  }
111
- async runClaimedValidation(identity, payload, deadlineAt, fencingToken) {
112
- const scope = createExecutionScope({
113
- requestId: identity.operationId,
114
- deadlineAt,
115
- });
116
- const heartbeat = this.startHeartbeat(identity, fencingToken, (reason) => {
117
- scope.cancel(reason);
118
- });
225
+ // eslint-disable-next-line max-lines-per-function -- worker and receipt failure cleanup share one lease boundary
226
+ async runClaimedValidation(identity, submittedPayload, deadlineAt, fencingToken) {
227
+ let scope;
228
+ let heartbeat;
229
+ let payload;
119
230
  try {
231
+ const executionScope = createExecutionScope({ requestId: identity.operationId, deadlineAt });
232
+ scope = executionScope;
233
+ heartbeat = await this.startHeartbeat(identity, fencingToken, (reason) => {
234
+ executionScope.cancel(reason);
235
+ });
236
+ let hydratedPayload;
237
+ try {
238
+ hydratedPayload = await rehydrateValidationPayload(submittedPayload);
239
+ }
240
+ catch (error) {
241
+ reportClassifiedDegradation('VALIDATE_JOB_SUBMISSION_FAILURE', error);
242
+ throw error;
243
+ }
244
+ payload = hydratedPayload;
245
+ await executionScope.run(() => preflightValidationReceiptSigner({
246
+ projectId: identity.projectId,
247
+ issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
248
+ }));
249
+ const afterPreflight = this.observeForSettlement(identity);
250
+ if (afterPreflight.state === 'cancelled' || afterPreflight.cancelRequested) {
251
+ return afterPreflight;
252
+ }
253
+ this.requireCurrentFence(afterPreflight, fencingToken);
120
254
  const startedAt = Date.now();
121
- const result = await scope.run(() => (this.options.runWorker ?? runValidateWorker)({
255
+ const result = await executionScope.run(() => (this.options.runWorker ?? runValidateWorker)({
122
256
  requestId: identity.operationId,
123
- input: payload.input,
257
+ input: { ...hydratedPayload.input, specPath: hydratedPayload.specPath },
124
258
  deadlineAt,
125
- signal: scope.context.signal,
259
+ signal: executionScope.context.signal,
126
260
  onProgress: (progress) => {
127
261
  this.persistProgress(identity, fencingToken, progress);
128
262
  },
129
263
  }));
130
- const current = this.runtime.get(identity);
131
- if (current.state === 'cancelled') {
264
+ const current = this.observeForSettlement(identity);
265
+ if (current.state === 'cancelled' || current.cancelRequested) {
132
266
  return current;
133
267
  }
268
+ this.requireCurrentFence(current, fencingToken);
134
269
  if (result.isError === true) {
135
270
  return this.runtime.fail({
136
271
  ...identity,
@@ -143,8 +278,9 @@ export class ValidateJobExecutor {
143
278
  throw new Error('[Planu] Validate workers cannot self-issue authoritative receipts');
144
279
  }
145
280
  const durableResult = validationPassed(result)
146
- ? await this.issueReceipt(identity, payload, result, Date.now() - startedAt, scope.context.signal)
281
+ ? await this.publishInitialReceipt(identity, payload, result, Date.now() - startedAt, fencingToken, executionScope.context.signal)
147
282
  : result;
283
+ this.currentFence(identity, fencingToken);
148
284
  return this.runtime.complete({
149
285
  ...identity,
150
286
  workerId: this.workerId,
@@ -155,10 +291,34 @@ export class ValidateJobExecutor {
155
291
  catch (error) {
156
292
  /* reliability-optional: VALIDATE_JOB_EXECUTION_FAILURE — runtime records typed failure */
157
293
  reportClassifiedDegradation('VALIDATE_JOB_EXECUTION_FAILURE', error);
158
- const current = this.runtime.get(identity);
294
+ const disposition = this.failureDisposition(identity, fencingToken, error);
295
+ if (disposition.settleLocally) {
296
+ if (disposition.current) {
297
+ return disposition.current;
298
+ }
299
+ throw error;
300
+ }
301
+ const current = disposition.current;
159
302
  if (current.state === 'cancelled') {
160
303
  return current;
161
304
  }
305
+ if (recoverableReceiptError(error) &&
306
+ parseValidationReceiptPendingCheckpoint(current.checkpoint, this.receiptCheckpointLimits())) {
307
+ return this.runtime.fail({
308
+ ...identity,
309
+ workerId: this.workerId,
310
+ fencingToken,
311
+ error: RECEIPT_PENDING_ERROR,
312
+ });
313
+ }
314
+ if (parseValidationReceiptPendingCheckpoint(current.checkpoint, this.receiptCheckpointLimits())) {
315
+ this.recordFencedProgress(identity, fencingToken, {
316
+ phase: 'receipt-publication',
317
+ command: '[Planu] Receipt publication failed closed',
318
+ durationMs: 0,
319
+ status: 'failed',
320
+ });
321
+ }
162
322
  return this.runtime.fail({
163
323
  ...identity,
164
324
  workerId: this.workerId,
@@ -172,49 +332,325 @@ export class ValidateJobExecutor {
172
332
  });
173
333
  }
174
334
  finally {
175
- clearInterval(heartbeat);
176
- scope.dispose();
335
+ await stopHeartbeatSafely(heartbeat);
336
+ scope?.dispose();
337
+ await closeFreshnessLeaseSafely(payload?.freshnessLease);
177
338
  }
178
339
  }
179
- startHeartbeat(identity, fencingToken, cancel) {
180
- const heartbeat = setInterval(() => {
181
- const current = this.runtime.get(identity);
182
- if (current.state === 'cancelled' || current.cancelRequested) {
183
- cancel(new Error('[Planu] Durable validate job cancelled'));
184
- return;
340
+ // eslint-disable-next-line max-lines-per-function -- receipt resume must keep its fresh lease and fail-closed cleanup together
341
+ async resumeReceiptPublication(identity, payload, checkpoint, fencingToken) {
342
+ const publicationDeadlineMs = getRuntimePolicy().validation.receiptPublicationDeadlineMs;
343
+ const deadlineAt = Date.now() + publicationDeadlineMs;
344
+ const scope = createExecutionScope({ requestId: identity.operationId, deadlineAt });
345
+ const deadlineTimer = setTimeout(() => {
346
+ scope.cancel(new ReceiptPublicationTimeoutError());
347
+ }, publicationDeadlineMs);
348
+ deadlineTimer.unref();
349
+ let heartbeat;
350
+ let ownedLease;
351
+ try {
352
+ heartbeat = await this.startHeartbeat(identity, fencingToken, (reason) => {
353
+ scope.cancel(reason);
354
+ });
355
+ const receiptBindings = checkpoint.receiptInput.bindings;
356
+ const resolved = await revalidateValidationWorktree({
357
+ projectPath: payload.input.projectPath ?? '',
358
+ specPath: payload.specPath,
359
+ specId: payload.input.specId,
360
+ validationRootId: payload.validationRootId,
361
+ });
362
+ const lease = await beginValidationFreshnessBarrier({
363
+ projectId: identity.projectId,
364
+ specId: payload.input.specId,
365
+ projectPath: resolved.projectPath,
366
+ handoffPath: join(projectDataDir(identity.projectId), 'handoffs', payload.input.specId),
367
+ invalidateCurrentBindings: false,
368
+ });
369
+ ownedLease = lease;
370
+ const observed = await computeDurableValidationBindings({
371
+ canonicalProjectId: identity.projectId,
372
+ specId: payload.input.specId,
373
+ projectPath: resolved.projectPath,
374
+ specPath: resolved.specPath,
375
+ });
376
+ const observedBindings = toValidationReceiptBindings(observed.bindings, observed.artifactDigests);
377
+ if (canonicalJson(observedBindings) !== canonicalJson(receiptBindings) ||
378
+ !(await bindValidationFreshnessLease(lease, observedBindings))) {
379
+ throw new Error('[Planu] Durable validation bindings became stale; submit a new validation');
185
380
  }
186
- if (!this.runtime.renew({
381
+ const hydratedPayload = {
382
+ ...payload,
383
+ input: { ...payload.input, projectPath: resolved.projectPath },
384
+ specPath: resolved.specPath,
385
+ bindings: observed.bindings,
386
+ artifactDigests: observed.artifactDigests,
387
+ freshnessLease: lease,
388
+ };
389
+ const result = await scope.run(() => {
390
+ this.currentFence(identity, fencingToken);
391
+ return this.issueReceipt(identity, hydratedPayload, checkpoint, lease, fencingToken, scope.context.signal);
392
+ });
393
+ this.currentFence(identity, fencingToken);
394
+ return this.runtime.complete({
187
395
  ...identity,
188
396
  workerId: this.workerId,
189
397
  fencingToken,
190
- leaseMs: getRuntimePolicy().validation.workerLeaseMs,
191
- })) {
192
- cancel(new Error('[Planu] Durable validate job lease was lost'));
398
+ result,
399
+ });
400
+ }
401
+ catch (error) {
402
+ reportClassifiedDegradation('VALIDATE_JOB_EXECUTION_FAILURE', error);
403
+ const disposition = this.failureDisposition(identity, fencingToken, error);
404
+ if (disposition.settleLocally) {
405
+ if (disposition.current) {
406
+ return disposition.current;
407
+ }
408
+ throw error;
193
409
  }
194
- }, getRuntimePolicy().validation.workerHeartbeatMs);
195
- heartbeat.unref();
196
- return heartbeat;
197
- }
198
- persistProgress(identity, fencingToken, progress) {
199
- const current = this.runtime.get(identity);
200
- if (current.state !== 'running') {
201
- return;
410
+ const current = disposition.current;
411
+ if (current.state === 'cancelled') {
412
+ return current;
413
+ }
414
+ if (recoverableReceiptError(error)) {
415
+ return this.runtime.fail({
416
+ ...identity,
417
+ workerId: this.workerId,
418
+ fencingToken,
419
+ error: RECEIPT_PENDING_ERROR,
420
+ });
421
+ }
422
+ this.recordFencedProgress(identity, fencingToken, {
423
+ phase: 'receipt-publication',
424
+ command: '[Planu] Receipt publication failed closed',
425
+ durationMs: 0,
426
+ status: 'failed',
427
+ });
428
+ return this.runtime.fail({
429
+ ...identity,
430
+ workerId: this.workerId,
431
+ fencingToken,
432
+ error: '[Planu] Receipt publication failed closed',
433
+ });
202
434
  }
203
- this.runtime.recordProgress({
435
+ finally {
436
+ clearTimeout(deadlineTimer);
437
+ await stopHeartbeatSafely(heartbeat);
438
+ scope.dispose();
439
+ await closeFreshnessLeaseSafely(ownedLease);
440
+ }
441
+ }
442
+ startHeartbeat(identity, fencingToken, cancel) {
443
+ const timing = this.workerTimingPolicy();
444
+ const policy = getRuntimePolicy().validation;
445
+ const factory = this.options.heartbeatFactory ?? createDurableJobHeartbeat;
446
+ return factory({
204
447
  ...identity,
205
448
  workerId: this.workerId,
206
449
  fencingToken,
207
- checkpoint: redactValidationProgress(progress),
450
+ databasePath: this.runtime.runtimeDatabasePath(),
451
+ leaseMs: timing.workerLeaseMs,
452
+ heartbeatMs: timing.workerHeartbeatMs,
453
+ databaseBusyTimeoutMs: policy.heartbeatDatabaseBusyTimeoutMs,
454
+ startupTimeoutMs: policy.heartbeatStartupTimeoutMs,
455
+ shutdownTimeoutMs: policy.heartbeatShutdownTimeoutMs,
456
+ onAuthorityLost: (reason) => {
457
+ reportClassifiedDegradation('VALIDATE_JOB_HEARTBEAT_FAILURE', reason);
458
+ cancel(new ValidationAuthorityLostError('infrastructure', { cause: reason }));
459
+ },
460
+ }).catch((error) => {
461
+ const authorityError = new ValidationAuthorityLostError('infrastructure', { cause: error });
462
+ reportClassifiedDegradation('VALIDATE_JOB_HEARTBEAT_FAILURE', error);
463
+ cancel(authorityError);
464
+ throw authorityError;
208
465
  });
209
466
  }
210
- async issueReceipt(identity, payload, result, durationMs, signal) {
467
+ persistProgress(identity, fencingToken, progress) {
468
+ const current = this.observeForSettlement(identity);
469
+ if (current.state !== 'running') {
470
+ return;
471
+ }
472
+ const previous = current.checkpoint;
473
+ if (previous &&
474
+ (progress.completedGates < (previous.completedGates ?? 0) ||
475
+ (typeof previous.lastProgressAt === 'string' &&
476
+ progress.lastProgressAt < previous.lastProgressAt) ||
477
+ (progress.completedGates === previous.completedGates &&
478
+ progress.phase === previous.phase &&
479
+ progress.status === previous.status))) {
480
+ return;
481
+ }
482
+ this.recordFencedProgress(identity, fencingToken, redactValidationProgress(progress));
483
+ }
484
+ async publishInitialReceipt(identity, payload, result, durationMs, fencingToken, signal) {
485
+ this.currentFence(identity, fencingToken);
211
486
  if (!hasReceiptInputs(payload)) {
212
487
  throw new Error('[Planu] Passing durable validation is missing receipt bindings');
213
488
  }
214
489
  const receiptBindings = toValidationReceiptBindings(payload.bindings, payload.artifactDigests);
215
- const publicationLease = await this.preparePublicationLease(payload, receiptBindings);
216
- if (executionAborted(signal) ||
217
- !(await assertValidationFreshnessBarrier(publicationLease, receiptBindings))) {
490
+ const checkpoint = this.buildReceiptPendingCheckpoint(payload, receiptBindings, result, durationMs);
491
+ this.recordFencedProgress(identity, fencingToken, checkpoint);
492
+ const publicationDeadlineMs = getRuntimePolicy().validation.receiptPublicationDeadlineMs;
493
+ const publicationScope = createExecutionScope({
494
+ requestId: identity.operationId,
495
+ deadlineAt: Date.now() + publicationDeadlineMs,
496
+ upstreamSignal: signal,
497
+ });
498
+ const deadlineTimer = setTimeout(() => {
499
+ publicationScope.cancel(new ReceiptPublicationTimeoutError());
500
+ }, publicationDeadlineMs);
501
+ deadlineTimer.unref();
502
+ try {
503
+ return await publicationScope.run(async () => {
504
+ const publicationLease = await this.preparePublicationLease(payload, receiptBindings);
505
+ this.currentFence(identity, fencingToken);
506
+ return this.issueReceipt(identity, payload, checkpoint, publicationLease, fencingToken, publicationScope.context.signal);
507
+ });
508
+ }
509
+ finally {
510
+ clearTimeout(deadlineTimer);
511
+ publicationScope.dispose();
512
+ }
513
+ }
514
+ workerTimingPolicy() {
515
+ const production = getRuntimePolicy().validation;
516
+ const injected = this.options.workerTimingPolicy;
517
+ if (!injected) {
518
+ return production;
519
+ }
520
+ const { workerLeaseMs, workerHeartbeatMs } = injected;
521
+ if (!Number.isSafeInteger(workerLeaseMs) ||
522
+ workerLeaseMs <= 0 ||
523
+ !Number.isSafeInteger(workerHeartbeatMs) ||
524
+ workerHeartbeatMs <= 0 ||
525
+ workerHeartbeatMs >= workerLeaseMs ||
526
+ BigInt(workerHeartbeatMs) * BigInt(production.workerLeaseMs) !==
527
+ BigInt(workerLeaseMs) * BigInt(production.workerHeartbeatMs) ||
528
+ production.heartbeatDatabaseBusyTimeoutMs >= workerLeaseMs - workerHeartbeatMs) {
529
+ throw new Error('[Planu] Invalid validation worker timing policy');
530
+ }
531
+ return injected;
532
+ }
533
+ requireCurrentFence(current, fencingToken) {
534
+ if (!this.hasCurrentFence(current, fencingToken)) {
535
+ throw new ValidationAuthorityLostError('reclaimed');
536
+ }
537
+ if (!current.leaseExpiresAt || Date.parse(current.leaseExpiresAt) <= Date.now()) {
538
+ throw new ValidationAuthorityLostError('expired');
539
+ }
540
+ }
541
+ hasCurrentFence(current, fencingToken) {
542
+ return (current.state === 'running' &&
543
+ current.leaseOwner === this.workerId &&
544
+ current.fencingToken === fencingToken &&
545
+ !current.cancelRequested);
546
+ }
547
+ currentFence(identity, fencingToken) {
548
+ const current = this.observeForSettlement(identity);
549
+ this.requireCurrentFence(current, fencingToken);
550
+ return current;
551
+ }
552
+ observeForSettlement(identity) {
553
+ try {
554
+ return this.runtime.get(identity);
555
+ }
556
+ catch (error) {
557
+ reportClassifiedDegradation('VALIDATE_JOB_STATE_READ_FAILURE', error);
558
+ throw new ValidationAuthorityLostError('infrastructure', { cause: error });
559
+ }
560
+ }
561
+ recordFencedProgress(identity, fencingToken, checkpoint) {
562
+ try {
563
+ this.runtime.recordProgress({
564
+ ...identity,
565
+ workerId: this.workerId,
566
+ fencingToken,
567
+ leaseMs: this.workerTimingPolicy().workerLeaseMs,
568
+ checkpoint,
569
+ });
570
+ }
571
+ catch (error) {
572
+ reportClassifiedDegradation('VALIDATE_JOB_PROGRESS_FAILURE', error);
573
+ throw new ValidationAuthorityLostError('infrastructure', { cause: error });
574
+ }
575
+ }
576
+ failureDisposition(identity, fencingToken, error) {
577
+ let current;
578
+ try {
579
+ current = this.runtime.get(identity);
580
+ }
581
+ catch (readError) {
582
+ reportClassifiedDegradation('VALIDATE_JOB_STATE_READ_FAILURE', readError);
583
+ return { settleLocally: true };
584
+ }
585
+ if (current.state === 'cancelled' || current.cancelRequested) {
586
+ return { settleLocally: true, current };
587
+ }
588
+ if (!this.hasCurrentFence(current, fencingToken) ||
589
+ !current.leaseExpiresAt ||
590
+ Date.parse(current.leaseExpiresAt) <= Date.now()) {
591
+ return { settleLocally: true, current };
592
+ }
593
+ return error instanceof ValidationAuthorityLostError
594
+ ? { settleLocally: true, current }
595
+ : { settleLocally: false, current };
596
+ }
597
+ buildReceiptPendingCheckpoint(payload, receiptBindings, result, durationMs) {
598
+ const { logMaxBytes, resultMaxBytes } = this.receiptCheckpointLimits();
599
+ if (!Number.isSafeInteger(logMaxBytes) ||
600
+ logMaxBytes <= 0 ||
601
+ !Number.isSafeInteger(resultMaxBytes) ||
602
+ resultMaxBytes <= 0 ||
603
+ logMaxBytes > resultMaxBytes) {
604
+ throw new Error('[Planu] Invalid receipt checkpoint policy');
605
+ }
606
+ const redactedResult = redactSensitiveValue(result);
607
+ if (!redactedResult ||
608
+ typeof redactedResult !== 'object' ||
609
+ Buffer.byteLength(canonicalJson(redactedResult), 'utf8') > resultMaxBytes) {
610
+ throw new Error('[Planu] VALIDATION_CHECKPOINT_TOO_LARGE');
611
+ }
612
+ const receiptInput = {
613
+ bindings: receiptBindings,
614
+ command: {
615
+ executable: 'planu',
616
+ args: ['validate', payload.bindings.specId],
617
+ cwd: '<project>',
618
+ },
619
+ versions: { node: process.version, receiptSchema: '1' },
620
+ exitCode: 0,
621
+ durationMs,
622
+ log: truncateUtf8ByBytes(redactText(result.content.map((item) => item.text).join('\n')), logMaxBytes),
623
+ gates: [{ gate: 'validation', status: 'passed', durationMs }],
624
+ issuedAt: new Date().toISOString(),
625
+ };
626
+ const checkpoint = {
627
+ kind: 'validation-receipt-pending',
628
+ schemaVersion: 1,
629
+ errorCode: 'VALIDATION_RECEIPT_PENDING',
630
+ recoveryAction: 'restart_job',
631
+ result: redactedResult,
632
+ receiptInput,
633
+ };
634
+ const parsed = parseValidationReceiptPendingCheckpoint(checkpoint, this.receiptCheckpointLimits());
635
+ if (!parsed) {
636
+ throw new Error('[Planu] Validation receipt checkpoint is not persistable');
637
+ }
638
+ return parsed;
639
+ }
640
+ receiptCheckpointLimits() {
641
+ const policy = getRuntimePolicy().validation;
642
+ return {
643
+ logMaxBytes: this.options.receiptCheckpointPolicy?.logMaxBytes ?? policy.receiptCheckpointLogMaxBytes,
644
+ resultMaxBytes: this.options.receiptCheckpointPolicy?.resultMaxBytes ??
645
+ policy.receiptCheckpointResultMaxBytes,
646
+ };
647
+ }
648
+ async issueReceipt(identity, payload, checkpoint, publicationLease, fencingToken, signal) {
649
+ const receiptBindings = checkpoint.receiptInput.bindings;
650
+ if (executionAborted(signal)) {
651
+ signal.throwIfAborted();
652
+ }
653
+ if (!(await assertValidationFreshnessBarrier(publicationLease, receiptBindings))) {
218
654
  throw new Error('[Planu] Validation inputs changed before receipt publication');
219
655
  }
220
656
  const database = new RuntimeDatabase({ path: resolveStorageLayout().runtimeDatabase });
@@ -223,40 +659,47 @@ export class ValidateJobExecutor {
223
659
  projectId: identity.projectId,
224
660
  issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
225
661
  });
662
+ signal.throwIfAborted();
226
663
  const { store } = createValidationReceiptAuthority({
227
664
  database,
228
665
  workspaceId: identity.workspaceId,
229
666
  projectId: identity.projectId,
230
667
  issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
231
- });
232
- const receipt = await issueValidationReceipt({
233
- bindings: receiptBindings,
234
- command: {
235
- executable: 'planu',
236
- args: ['validate', payload.bindings.specId],
237
- ...(payload.input.projectPath === undefined ? {} : { cwd: payload.input.projectPath }),
668
+ authorizePersistence: (persistenceDatabase) => {
669
+ persistenceDatabase.assertActiveJobLease({
670
+ id: identity.operationId,
671
+ projectId: identity.projectId,
672
+ workspaceId: identity.workspaceId,
673
+ workerId: this.workerId,
674
+ fencingToken,
675
+ leaseMs: this.workerTimingPolicy().workerLeaseMs,
676
+ now: new Date().toISOString(),
677
+ });
238
678
  },
239
- versions: { node: process.version, receiptSchema: '1' },
240
- exitCode: 0,
241
- durationMs,
242
- log: result.content.map((item) => item.text).join('\n'),
243
- gates: [{ gate: 'validation', status: 'passed', durationMs }],
244
- }, {
679
+ });
680
+ const receipt = await issueValidationReceipt(checkpoint.receiptInput, {
245
681
  issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
246
682
  signingKeyReference,
247
683
  store,
248
684
  });
249
- if (executionAborted(signal) ||
250
- !(await assertValidationFreshnessBarrier(publicationLease, receiptBindings))) {
685
+ if (executionAborted(signal)) {
686
+ signal.throwIfAborted();
687
+ }
688
+ if (!(await assertValidationFreshnessBarrier(publicationLease, receiptBindings))) {
251
689
  await store.invalidateCurrentBindings(identity.projectId, payload.bindings.specId);
252
690
  throw new Error('[Planu] Validation inputs changed during receipt publication');
253
691
  }
254
692
  const receiptKey = `validation-receipt:${receipt.receiptId.slice('sha256:'.length)}`;
255
- await this.persistReceiptArtifacts(identity, payload, receipt.receiptId, receiptKey);
693
+ try {
694
+ await this.persistReceiptArtifacts(identity, payload, receipt.receiptId, receiptKey);
695
+ }
696
+ catch (error) {
697
+ throw new ReceiptPublicationInterruptedError({ cause: error });
698
+ }
256
699
  return {
257
- ...result,
700
+ ...checkpoint.result,
258
701
  structuredContent: {
259
- ...(result.structuredContent ?? {}),
702
+ ...(checkpoint.result.structuredContent ?? {}),
260
703
  receiptId: receipt.receiptId,
261
704
  receiptKey,
262
705
  },
@@ -275,8 +718,14 @@ export class ValidateJobExecutor {
275
718
  return lease;
276
719
  }
277
720
  const bindings = payload.bindings;
278
- const projectPath = payload.input.projectPath;
279
- const specPath = payload.specPath;
721
+ const resolved = await revalidateValidationWorktree({
722
+ projectPath: payload.input.projectPath,
723
+ specPath: payload.specPath,
724
+ specId: payload.input.specId,
725
+ validationRootId: payload.validationRootId,
726
+ });
727
+ const projectPath = resolved.projectPath;
728
+ const specPath = resolved.specPath;
280
729
  const rebaselined = await rebaselineValidationFreshnessLeaseAfterRecompute({
281
730
  lease,
282
731
  expectedBindings,
@@ -322,19 +771,33 @@ export class ValidateJobExecutor {
322
771
  /** Schedules background execution after the submit response can be flushed. */
323
772
  export function scheduleValidateJobExecution(projectId, workspaceId = WORKSPACE_ID) {
324
773
  const scope = `${projectId}:${workspaceId}`;
325
- if (activeScopes.has(scope)) {
774
+ const active = activeScopes.get(scope);
775
+ if (active) {
776
+ active.rescheduleRequested = true;
326
777
  return;
327
778
  }
328
- activeScopes.add(scope);
779
+ const state = { rescheduleRequested: false };
780
+ activeScopes.set(scope, state);
329
781
  setImmediate(() => {
330
782
  const runtime = new DurableJobRuntime();
331
783
  const executor = new ValidateJobExecutor(runtime);
332
- void executor.runUntilIdle(projectId, workspaceId).finally(() => {
333
- runtime.close();
334
- activeScopes.delete(scope);
335
- });
784
+ void (async () => {
785
+ try {
786
+ do {
787
+ state.rescheduleRequested = false;
788
+ await executor.runUntilIdle(projectId, workspaceId);
789
+ } while (rescheduleRequested(state));
790
+ }
791
+ finally {
792
+ runtime.close();
793
+ activeScopes.delete(scope);
794
+ }
795
+ })();
336
796
  });
337
797
  }
798
+ function rescheduleRequested(state) {
799
+ return state.rescheduleRequested;
800
+ }
338
801
  /** Re-enqueues accepted/checkpointed validate work discovered after a process restart. */
339
802
  export function recoverValidateJobsAtStartup(options = {}) {
340
803
  recoverValidateJobs(scheduleValidateJobExecution, options);