@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.
- package/CHANGELOG.md +82 -0
- package/LICENSE +55 -12
- package/README.md +10 -2
- package/dist/cli/commands/install.d.ts +1 -12
- package/dist/cli/commands/install.js +9 -65
- package/dist/cli/commands/job.js +4 -3
- package/dist/cli/commands/spec.js +12 -3
- package/dist/cli/commands/validate.js +2 -2
- package/dist/cli/router.js +1 -3
- package/dist/config/environment-schema.json +1 -3
- package/dist/config/official-sdd-tools.d.ts +4 -0
- package/dist/config/official-sdd-tools.js +35 -0
- package/dist/config/product-public.d.ts +139 -3
- package/dist/config/product-public.js +8 -1
- package/dist/config/product-public.json +7 -36
- package/dist/config/registries/hosts/codex.json +20 -363
- package/dist/config/release-policy.json +84 -2
- package/dist/config/runtime-policy.json +12 -1
- package/dist/config/skill-templates/planu-resume-work.md +2 -2
- package/dist/core/index.d.ts +1 -1
- package/dist/core/spec-validator.d.ts +4 -4
- package/dist/core/spec-validator.js +33 -19
- package/dist/engine/abortable-process-runner.d.ts +20 -2
- package/dist/engine/abortable-process-runner.js +243 -74
- package/dist/engine/ambiguity-scorer.js +4 -26
- package/dist/engine/auth/scope-mapper.js +1 -7
- package/dist/engine/autopilot/state-updater.js +4 -4
- package/dist/engine/cascade-hooks/hooks/review-enricher.hook.js +9 -6
- package/dist/engine/cascade-hooks/state-drift-detector.d.ts +3 -3
- package/dist/engine/cascade-hooks/state-drift-detector.js +1 -1
- package/dist/engine/ci-generator/planu-config.js +1 -7
- package/dist/engine/clarification-gate/gate.d.ts +3 -3
- package/dist/engine/code-scanner/layer-scanner.js +6 -22
- package/dist/engine/core-bridge.d.ts +3 -3
- package/dist/engine/cost-guardrails.d.ts +1 -1
- package/dist/engine/cost-guardrails.js +1 -1
- package/dist/engine/criterion-identity.d.ts +1 -0
- package/dist/engine/criterion-identity.js +7 -0
- package/dist/engine/dashboard/data-loader.js +2 -7
- package/dist/engine/dashboard/templates-layout.js +0 -3
- package/dist/engine/dashboard/templates-project.js +1 -13
- package/dist/engine/docs-site-generator/data-collector.d.ts +1 -1
- package/dist/engine/docs-site-generator/data-collector.js +7 -28
- package/dist/engine/docs-site-generator/tools-renderer.js +3 -10
- package/dist/engine/evidence-gates/artifact-reader.d.ts +12 -1
- package/dist/engine/evidence-gates/artifact-reader.js +174 -4
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +10 -0
- package/dist/engine/evidence-gates/evidence-autofill.js +148 -0
- package/dist/engine/evidence-gates/evidence-skeletons.d.ts +19 -0
- package/dist/engine/evidence-gates/evidence-skeletons.js +69 -0
- package/dist/engine/evidence-gates/lifecycle-gate.js +4 -3
- package/dist/engine/evidence-gates/reconciliation-freshness.d.ts +11 -0
- package/dist/engine/evidence-gates/reconciliation-freshness.js +72 -0
- package/dist/engine/evidence-index/done-drift.d.ts +1 -1
- package/dist/engine/evidence-index/done-drift.js +40 -18
- package/dist/engine/execution/context.js +3 -0
- package/dist/engine/execution/durable-job-heartbeat-runtime.d.ts +4 -0
- package/dist/engine/execution/durable-job-heartbeat-runtime.js +400 -0
- package/dist/engine/execution/job-runtime.d.ts +30 -0
- package/dist/engine/execution/job-runtime.js +114 -6
- package/dist/engine/execution/operation-journal.js +10 -4
- package/dist/engine/execution/public-job-sanitizer.d.ts +4 -0
- package/dist/engine/execution/public-job-sanitizer.js +31 -0
- package/dist/engine/execution/validate-job-executor.d.ts +12 -1
- package/dist/engine/execution/validate-job-executor.js +549 -86
- package/dist/engine/execution/validate-job-payload.js +107 -50
- package/dist/engine/execution/validate-job-recovery.js +1 -1
- package/dist/engine/execution/validate-worker-runtime.d.ts +5 -0
- package/dist/engine/execution/validate-worker-runtime.js +90 -2
- package/dist/engine/execution/validate-worker-thread.js +17 -24
- package/dist/engine/handoff-artifacts/schemas.d.ts +2 -1
- package/dist/engine/handoff-artifacts/schemas.js +33 -0
- package/dist/engine/handoff-packager.d.ts +19 -0
- package/dist/engine/handoff-packager.js +415 -13
- package/dist/engine/hooks/effort-configurator.js +1 -1
- package/dist/engine/impact-detector/test-break-predictor.js +3 -3
- package/dist/engine/impact-detector/tool-registration.js +1 -1
- package/dist/engine/implementation-contract/renderer.js +23 -8
- package/dist/engine/lifecycle-reconciliation-io.d.ts +11 -0
- package/dist/engine/lifecycle-reconciliation-io.js +180 -0
- package/dist/engine/lifecycle-reconciliation.d.ts +16 -0
- package/dist/engine/lifecycle-reconciliation.js +406 -0
- package/dist/engine/local-first/tool-classification.js +2 -0
- package/dist/engine/macos-keychain-native-port.d.ts +5 -0
- package/dist/engine/macos-keychain-native-port.js +19 -7
- package/dist/engine/migrations/remove-commercial-state.d.ts +3 -0
- package/dist/engine/migrations/remove-commercial-state.js +343 -0
- package/dist/engine/minimality/policy-loader.js +247 -6
- package/dist/engine/network-policy.js +1 -0
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +20 -20
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +20 -20
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +20 -20
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +20 -20
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +20 -20
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +20 -20
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +20 -20
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +20 -20
- package/dist/engine/readiness-checker.d.ts +1 -16
- package/dist/engine/readiness-checker.js +27 -88
- package/dist/engine/reverse-engineer/api-detector.js +2 -13
- package/dist/engine/reverse-engineer/complexity-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/config-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/dependency-graph.js +2 -13
- package/dist/engine/reverse-engineer/test-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/walk-ignore.d.ts +3 -0
- package/dist/engine/reverse-engineer/walk-ignore.js +26 -0
- package/dist/engine/runtime-policy.d.ts +2 -0
- package/dist/engine/runtime-policy.js +42 -5
- package/dist/engine/runtime-security/rate-limiter.js +0 -1
- package/dist/engine/safety/contained-project-file.d.ts +3 -0
- package/dist/engine/safety/contained-project-file.js +23 -0
- package/dist/engine/scope-boundaries/contradiction-checker.js +63 -1
- package/dist/engine/scope-boundaries/scope-validator.js +45 -7
- package/dist/engine/session/checkpoint-writer.js +0 -9
- package/dist/engine/spec-format/acceptance-criteria.d.ts +6 -2
- package/dist/engine/spec-format/acceptance-criteria.js +82 -22
- package/dist/engine/spec-format/markdown-sections.d.ts +14 -0
- package/dist/engine/spec-format/markdown-sections.js +105 -0
- package/dist/engine/spec-format/read-technical-section.js +33 -5
- package/dist/engine/spec-format/technical-md-populator.js +3 -2
- package/dist/engine/spec-format/text-fences.js +20 -2
- package/dist/engine/spec-format/unified-spec-builder.d.ts +7 -0
- package/dist/engine/spec-format/unified-spec-builder.js +202 -0
- package/dist/engine/spec-migrator/canonical-deduplicator.d.ts +18 -0
- package/dist/engine/spec-migrator/canonical-deduplicator.js +479 -0
- package/dist/engine/spec-migrator/filesystem-import.d.ts +4 -8
- package/dist/engine/spec-migrator/filesystem-import.js +121 -14
- package/dist/engine/spec-migrator/frontmatter-parser.js +32 -15
- package/dist/engine/spec-migrator/index.d.ts +1 -0
- package/dist/engine/spec-migrator/index.js +1 -0
- package/dist/engine/spec-state-machine/transition-spec.d.ts +6 -1
- package/dist/engine/spec-state-machine/transition-spec.js +10 -1
- package/dist/engine/spec-state-syncer.js +1 -1
- package/dist/engine/technical-enricher/index.js +16 -8
- package/dist/engine/technical-enricher/render-enriched.d.ts +1 -0
- package/dist/engine/technical-enricher/render-enriched.js +42 -14
- package/dist/engine/telemetry/health-checker.js +53 -17
- package/dist/engine/telemetry/telemetry-client.js +26 -8
- package/dist/engine/text-signal-boundaries.d.ts +2 -0
- package/dist/engine/text-signal-boundaries.js +25 -2
- package/dist/engine/time/relative-date.d.ts +3 -0
- package/dist/engine/time/relative-date.js +16 -0
- package/dist/engine/timing/budget.d.ts +11 -0
- package/dist/engine/timing/budget.js +31 -9
- package/dist/engine/token-optimizer/analytics.d.ts +1 -1
- package/dist/engine/token-optimizer/analytics.js +5 -6
- package/dist/engine/usage-tracker/core.d.ts +2 -2
- package/dist/engine/usage-tracker/core.js +1 -2
- package/dist/engine/usage-tracker/index.d.ts +0 -3
- package/dist/engine/usage-tracker/index.js +0 -3
- package/dist/engine/usage-tracker/stats.d.ts +2 -2
- package/dist/engine/usage-tracker/stats.js +1 -28
- package/dist/engine/validation/durable-validation.d.ts +1 -1
- package/dist/engine/validation/durable-validation.js +3 -82
- package/dist/engine/validation/validation-freshness.d.ts +11 -1
- package/dist/engine/validation/validation-freshness.js +231 -91
- package/dist/engine/validation/validation-receipt.d.ts +9 -6
- package/dist/engine/validation/validation-receipt.js +84 -24
- package/dist/engine/validation/validation-source-digest.d.ts +2 -0
- package/dist/engine/validation/validation-source-digest.js +86 -0
- package/dist/engine/validation/validation-submission.d.ts +5 -4
- package/dist/engine/validation/validation-submission.js +25 -11
- package/dist/engine/validation/validation-worktree.d.ts +21 -0
- package/dist/engine/validation/validation-worktree.js +260 -0
- package/dist/engine/validation-impact-planner.d.ts +14 -2
- package/dist/engine/validation-impact-planner.js +0 -3
- package/dist/engine/validator/dor-dod.d.ts +1 -0
- package/dist/engine/validator/dor-dod.js +5 -3
- package/dist/engine/validator/extractors.d.ts +1 -5
- package/dist/engine/validator/extractors.js +24 -69
- package/dist/engine/validator/spec-compliance-runner.d.ts +12 -24
- package/dist/engine/validator/spec-compliance-runner.js +429 -211
- package/dist/engine/validator.js +35 -22
- package/dist/index.js +4 -16
- package/dist/resources/usage-stats-resource.js +2 -18
- package/dist/security/adapters/linux-secret-service.js +7 -3
- package/dist/security/adapters/windows-credential-manager.js +6 -4
- package/dist/server/routes/specs.js +13 -1
- package/dist/storage/base-store.d.ts +1 -1
- package/dist/storage/base-store.js +1 -1
- package/dist/storage/feedback-remote.d.ts +2 -3
- package/dist/storage/feedback-remote.js +45 -31
- package/dist/storage/global-store.d.ts +1 -17
- package/dist/storage/global-store.js +1 -42
- package/dist/storage/index.d.ts +0 -2
- package/dist/storage/index.js +0 -2
- package/dist/storage/migrations/canonical-storage.js +124 -11
- package/dist/storage/runtime-db.d.ts +9 -0
- package/dist/storage/runtime-db.js +64 -8
- package/dist/storage/spec-store.d.ts +14 -2
- package/dist/storage/spec-store.js +104 -14
- package/dist/storage/status-store/atomic-status-write.d.ts +5 -0
- package/dist/storage/status-store/atomic-status-write.js +57 -0
- package/dist/storage/status-store/file-lock.js +30 -11
- package/dist/storage/status-store/self-healing.js +3 -3
- package/dist/storage/status-store/version-sync.d.ts +2 -1
- package/dist/storage/status-store/version-sync.js +35 -40
- package/dist/storage/usage-store.d.ts +1 -5
- package/dist/storage/usage-store.js +0 -35
- package/dist/tools/auto-update-handler.d.ts +3 -3
- package/dist/tools/auto-update-handler.js +3 -3
- package/dist/tools/challenge-spec/resilience-challenge-scenarios.d.ts +2 -1
- package/dist/tools/challenge-spec/resilience-challenge-scenarios.js +10 -7
- package/dist/tools/challenge-spec/scenario-collector.js +1 -1
- package/dist/tools/challenge-spec/scenarios-data.js +1 -1
- package/dist/tools/challenge-spec/scenarios-utils.d.ts +1 -1
- package/dist/tools/challenge-spec/scenarios-utils.js +35 -11
- package/dist/tools/challenge-spec-helpers.d.ts +10 -1
- package/dist/tools/challenge-spec-helpers.js +63 -22
- package/dist/tools/challenge-spec.js +23 -8
- package/dist/tools/check-readiness.js +38 -14
- package/dist/tools/configure-telemetry.js +30 -2
- package/dist/tools/create-spec/spec-builder.d.ts +7 -0
- package/dist/tools/create-spec/spec-builder.js +19 -15
- package/dist/tools/create-spec.d.ts +1 -0
- package/dist/tools/create-spec.js +584 -309
- package/dist/tools/git/branch-ops.d.ts +1 -1
- package/dist/tools/git/branch-ops.js +3 -2
- package/dist/tools/init-project/handler.js +10 -12
- package/dist/tools/init-project/host-assets-writer.js +26 -1
- package/dist/tools/init-project/lint-scaffolder.d.ts +3 -3
- package/dist/tools/init-project/migration-runner.d.ts +2 -4
- package/dist/tools/init-project/migration-runner.js +3 -2
- package/dist/tools/init-project/portable-index-reconciler.d.ts +4 -0
- package/dist/tools/init-project/portable-index-reconciler.js +14 -0
- package/dist/tools/init-project/result-builder.js +1 -3
- package/dist/tools/jobs/handlers.js +38 -6
- package/dist/tools/list-specs.js +5 -26
- package/dist/tools/merge-risk-handler.js +2 -2
- package/dist/tools/package-handoff.js +2 -0
- package/dist/tools/reconcile-spec.js +0 -27
- package/dist/tools/register-agent-squad-tools.js +2 -2
- package/dist/tools/register-platform-tools/design-stack-tools.js +11 -11
- package/dist/tools/register-platform-tools/lifecycle-infra-tools.js +7 -7
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-sdd-tools.js +2 -29
- package/dist/tools/register-spec-tools/analysis-tools.js +8 -8
- package/dist/tools/register-spec-tools/core-spec-tools.js +29 -19
- package/dist/tools/safe-handler.d.ts +5 -13
- package/dist/tools/safe-handler.js +7 -23
- package/dist/tools/schemas/index.d.ts +1 -1
- package/dist/tools/schemas/index.js +1 -1
- package/dist/tools/schemas/output-schemas.d.ts +0 -23
- package/dist/tools/schemas/output-schemas.js +0 -28
- package/dist/tools/schemas/spec.d.ts +4 -0
- package/dist/tools/schemas/spec.js +3 -0
- package/dist/tools/schemas/validate-output-schema.d.ts +11 -0
- package/dist/tools/schemas/validate-output-schema.js +7 -0
- package/dist/tools/spec-prompt-handler.js +3 -3
- package/dist/tools/sync-spec-state-handler.d.ts +1 -1
- package/dist/tools/sync-spec-state-handler.js +50 -2
- package/dist/tools/tool-entry.d.ts +4 -4
- package/dist/tools/tool-entry.js +3 -3
- package/dist/tools/tool-registry/core-tools.js +31 -15
- package/dist/tools/tool-registry/group-infra.js +27 -110
- package/dist/tools/tool-registry/group-quality-compliance.js +26 -26
- package/dist/tools/tool-registry/inventory.d.ts +2 -0
- package/dist/tools/tool-registry/inventory.js +2 -0
- package/dist/tools/tool-registry-helpers.d.ts +3 -3
- package/dist/tools/tool-registry-helpers.js +3 -3
- package/dist/tools/tool-usage-report/registered-tools.d.ts +1 -1
- package/dist/tools/tool-usage-report/registered-tools.js +3 -15
- package/dist/tools/update-status/approval-gates.js +3 -0
- package/dist/tools/update-status/batch.d.ts +6 -2
- package/dist/tools/update-status/batch.js +58 -1
- package/dist/tools/update-status/dod-gates.d.ts +16 -1
- package/dist/tools/update-status/dod-gates.js +191 -1
- package/dist/tools/update-status/done-receipt-verifier.d.ts +8 -0
- package/dist/tools/update-status/done-receipt-verifier.js +75 -27
- package/dist/tools/update-status/evidence-gate.d.ts +4 -0
- package/dist/tools/update-status/evidence-gate.js +103 -3
- package/dist/tools/update-status/file-sync.d.ts +4 -3
- package/dist/tools/update-status/file-sync.js +57 -6
- package/dist/tools/update-status/index.d.ts +25 -1
- package/dist/tools/update-status/index.js +292 -78
- package/dist/tools/update-status/response-builder.js +3 -3
- package/dist/tools/update-status/side-effects.d.ts +2 -0
- package/dist/tools/update-status/side-effects.js +2 -2
- package/dist/tools/update-status/transition-guard.js +45 -16
- package/dist/tools/update-status-actions.d.ts +4 -2
- package/dist/tools/update-status-actions.js +16 -14
- package/dist/tools/usage-report.js +1 -16
- package/dist/tools/usage-stats.js +1 -18
- package/dist/tools/usage-tracking.d.ts +4 -0
- package/dist/tools/usage-tracking.js +20 -0
- package/dist/tools/validate-lint.d.ts +1 -0
- package/dist/tools/validate-lint.js +1 -1
- package/dist/tools/validate.d.ts +2 -1
- package/dist/tools/validate.js +114 -34
- package/dist/tools/workspace-dashboard-handler.js +38 -0
- package/dist/transports/http-transport.js +6 -8
- package/dist/transports/mcp-types.d.ts +0 -2
- package/dist/transports/session-context.js +1 -1
- package/dist/types/cascade-hooks.d.ts +1 -1
- package/dist/types/ci.d.ts +0 -4
- package/dist/types/clarification-token.d.ts +1 -1
- package/dist/types/claude-code-runtime.d.ts +0 -27
- package/dist/types/commercial-migration.d.ts +17 -0
- package/dist/types/commercial-migration.js +2 -0
- package/dist/types/common/primitives.d.ts +0 -2
- package/dist/types/core-bridge.d.ts +4 -10
- package/dist/types/dashboard.d.ts +3 -7
- package/dist/types/docs.d.ts +0 -1
- package/dist/types/durable-job.d.ts +4 -4
- package/dist/types/durable-validation.d.ts +100 -2
- package/dist/types/durable-validation.js +165 -1
- package/dist/types/errors/structured-error.d.ts +3 -3
- package/dist/types/estimation.d.ts +2 -0
- package/dist/types/evidence-autofill.d.ts +34 -0
- package/dist/types/evidence-autofill.js +2 -0
- package/dist/types/evidence-gates.d.ts +9 -1
- package/dist/types/execution.d.ts +2 -0
- package/dist/types/gemini-integration.d.ts +0 -11
- package/dist/types/handoff-artifacts.d.ts +41 -0
- package/dist/types/hooks-advanced.d.ts +0 -13
- package/dist/types/impact-detection.d.ts +2 -2
- package/dist/types/index.d.ts +3 -6
- package/dist/types/index.js +2 -5
- package/dist/types/kiro-integration.d.ts +0 -6
- package/dist/types/network-policy.d.ts +1 -1
- package/dist/types/project/lint-scaffolder.d.ts +1 -1
- package/dist/types/runtime-policy.d.ts +11 -0
- package/dist/types/session-safeguard.d.ts +0 -31
- package/dist/types/spec/canonical-repair.d.ts +81 -0
- package/dist/types/spec/canonical-repair.js +2 -0
- package/dist/types/spec/core.d.ts +20 -0
- package/dist/types/spec/index.d.ts +1 -0
- package/dist/types/spec/index.js +1 -0
- package/dist/types/spec/inputs.d.ts +35 -1
- package/dist/types/spec-format.d.ts +25 -1
- package/dist/types/telemetry.d.ts +4 -1
- package/dist/types/transition-log.d.ts +1 -1
- package/dist/types/transport.d.ts +0 -2
- package/dist/types/usage.d.ts +1 -36
- package/dist/types/usage.js +1 -1
- package/dist/types/validate-worker.d.ts +6 -0
- package/dist/types/validation-receipt.d.ts +1 -1
- package/dist/types/validation.d.ts +10 -4
- package/package.json +17 -12
- package/planu-native.json +1 -1
- package/planu-plugin.json +23 -40
- package/scripts/lib/pending-release-file.mjs +93 -0
- package/src/i18n/messages/en.json +0 -41
- package/src/i18n/messages/es.json +0 -41
- package/src/i18n/messages/pt.json +0 -41
- package/dist/cli/commands/activate.d.ts +0 -14
- package/dist/cli/commands/activate.js +0 -174
- package/dist/config/license-plans.json +0 -251
- package/dist/config/product-public.ts +0 -148
- package/dist/config/server-instructions.ts +0 -121
- package/dist/config/version-defaults.ts +0 -76
- package/dist/config/version.ts +0 -21
- package/dist/engine/ai-integration/gemini/settings-generator.d.ts +0 -4
- package/dist/engine/ai-integration/gemini/settings-generator.js +0 -195
- package/dist/engine/ai-integration/kiro/hooks-generator.d.ts +0 -6
- package/dist/engine/ai-integration/kiro/hooks-generator.js +0 -75
- package/dist/engine/hooks/full-spectrum-generator.d.ts +0 -32
- package/dist/engine/hooks/full-spectrum-generator.js +0 -117
- package/dist/engine/license-validator/config-loader.d.ts +0 -5
- package/dist/engine/license-validator/config-loader.js +0 -16
- package/dist/engine/license-validator/core.d.ts +0 -15
- package/dist/engine/license-validator/core.js +0 -151
- package/dist/engine/license-validator/fingerprint.d.ts +0 -3
- package/dist/engine/license-validator/fingerprint.js +0 -17
- package/dist/engine/license-validator/lemon-squeezy.d.ts +0 -5
- package/dist/engine/license-validator/lemon-squeezy.js +0 -55
- package/dist/engine/license-validator.d.ts +0 -5
- package/dist/engine/license-validator.js +0 -6
- package/dist/engine/product-intelligence/index.d.ts +0 -7
- package/dist/engine/product-intelligence/index.js +0 -211
- package/dist/engine/session/auto-save.d.ts +0 -18
- package/dist/engine/session/auto-save.js +0 -98
- package/dist/engine/session/context-snapshot.d.ts +0 -8
- package/dist/engine/session/context-snapshot.js +0 -49
- package/dist/engine/session/memory-sync.d.ts +0 -12
- package/dist/engine/session/memory-sync.js +0 -34
- package/dist/engine/session/session-diff.d.ts +0 -11
- package/dist/engine/session/session-diff.js +0 -42
- package/dist/engine/session/session-merge.d.ts +0 -12
- package/dist/engine/session/session-merge.js +0 -55
- package/dist/engine/session-safeguard/checkpoint-runner.d.ts +0 -4
- package/dist/engine/session-safeguard/checkpoint-runner.js +0 -70
- package/dist/engine/session-safeguard/learnings-buffer.d.ts +0 -15
- package/dist/engine/session-safeguard/learnings-buffer.js +0 -91
- package/dist/engine/session-safeguard/precompaction-drain.d.ts +0 -10
- package/dist/engine/session-safeguard/precompaction-drain.js +0 -82
- package/dist/engine/session-safeguard/unpushed-detector.d.ts +0 -7
- package/dist/engine/session-safeguard/unpushed-detector.js +0 -59
- package/dist/engine/spec-changelog/core.d.ts +0 -16
- package/dist/engine/spec-changelog/core.js +0 -176
- package/dist/engine/spec-changelog/diff.d.ts +0 -18
- package/dist/engine/spec-changelog/diff.js +0 -121
- package/dist/engine/spec-changelog/index.d.ts +0 -3
- package/dist/engine/spec-changelog/index.js +0 -4
- package/dist/engine/trial-engine.d.ts +0 -5
- package/dist/engine/trial-engine.js +0 -14
- package/dist/engine/usage-tracker/rate-check.d.ts +0 -7
- package/dist/engine/usage-tracker/rate-check.js +0 -18
- package/dist/engine/usage-tracker/rate-limiter.d.ts +0 -14
- package/dist/engine/usage-tracker/rate-limiter.js +0 -73
- package/dist/engine/usage-tracker/trial.d.ts +0 -27
- package/dist/engine/usage-tracker/trial.js +0 -49
- package/dist/engine/webhook/event-handlers.d.ts +0 -25
- package/dist/engine/webhook/event-handlers.js +0 -89
- package/dist/engine/webhook/index.d.ts +0 -4
- package/dist/engine/webhook/index.js +0 -5
- package/dist/engine/webhook/server.d.ts +0 -12
- package/dist/engine/webhook/server.js +0 -249
- package/dist/engine/webhook/signature.d.ts +0 -16
- package/dist/engine/webhook/signature.js +0 -49
- package/dist/storage/license-store.d.ts +0 -6
- package/dist/storage/license-store.js +0 -63
- package/dist/storage/session-store.d.ts +0 -20
- package/dist/storage/session-store.js +0 -136
- package/dist/tools/activate-license.d.ts +0 -6
- package/dist/tools/activate-license.js +0 -132
- package/dist/tools/checkpoint-handler.d.ts +0 -3
- package/dist/tools/checkpoint-handler.js +0 -130
- package/dist/tools/export-session.d.ts +0 -3
- package/dist/tools/export-session.js +0 -90
- package/dist/tools/license-gate.d.ts +0 -18
- package/dist/tools/license-gate.js +0 -222
- package/dist/tools/license-status.d.ts +0 -3
- package/dist/tools/license-status.js +0 -93
- package/dist/tools/list-sessions.d.ts +0 -3
- package/dist/tools/list-sessions.js +0 -46
- package/dist/tools/planu-session-checkpoint.d.ts +0 -6
- package/dist/tools/planu-session-checkpoint.js +0 -42
- package/dist/tools/restore-session.d.ts +0 -3
- package/dist/tools/restore-session.js +0 -71
- package/dist/tools/spec-history.d.ts +0 -3
- package/dist/tools/spec-history.js +0 -59
- package/dist/tools/webhook.d.ts +0 -6
- package/dist/tools/webhook.js +0 -150
- package/dist/types/changelog.d.ts +0 -49
- package/dist/types/changelog.js +0 -3
- package/dist/types/licensing.d.ts +0 -109
- package/dist/types/licensing.js +0 -4
- package/dist/types/product-intelligence.d.ts +0 -60
- package/dist/types/product-intelligence.js +0 -3
- package/dist/types/session.d.ts +0 -84
- package/dist/types/session.js +0 -3
- package/dist/types/webhook.d.ts +0 -62
- 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
|
|
257
|
-
if (
|
|
258
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
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((
|
|
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
|
-
|
|
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
|