@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
|
@@ -6,15 +6,17 @@ import { readTechnologySelectionContract } from '../storage/technology-selection
|
|
|
6
6
|
import { toolResult, interactiveResult } from './response-helpers.js';
|
|
7
7
|
import { readFile, stat as fsStat } from 'node:fs/promises';
|
|
8
8
|
import { createHash, randomUUID } from 'node:crypto';
|
|
9
|
-
import { dirname as pathDirname, isAbsolute as pathIsAbsolute, join as pathJoin, relative as pathRelative, sep as pathSeparator, } from 'node:path';
|
|
10
|
-
import {
|
|
9
|
+
import { basename as pathBasename, dirname as pathDirname, isAbsolute as pathIsAbsolute, join as pathJoin, relative as pathRelative, sep as pathSeparator, } from 'node:path';
|
|
10
|
+
import { withFileLock } from '../storage/file-mutex.js';
|
|
11
|
+
import { generateBranchName } from './create-spec-helpers.js';
|
|
12
|
+
import { buildSpecContext, buildSplitResult, computeNextSpecId, } from './create-spec/spec-builder.js';
|
|
11
13
|
import { validateConstitution } from './create-spec/constitution-validator.js';
|
|
12
14
|
import { getAsyncAnalysisPath } from './create-spec/post-creation.js';
|
|
13
15
|
import { extractCriteria, generateLeanSpecContent, } from '../engine/spec-format/lean-spec-generator.js';
|
|
14
16
|
import { generateLeanTechnicalContent, } from '../engine/spec-format/lean-technical-generator.js';
|
|
15
17
|
import { extractFilesFromSpecBody } from '../engine/spec-format/technical-md-populator.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
+
import { buildCanonicalUnifiedSpecContent, validateUnifiedSpecCandidate, } from '../engine/spec-format/unified-spec-builder.js';
|
|
19
|
+
import { buildImplementationContractSection } from '../engine/implementation-contract/index.js';
|
|
18
20
|
import { validateEnglishOnlySpecText } from '../engine/spec-language/english-only.js';
|
|
19
21
|
import { FallbackGenerator } from '../engine/spec-generator/index.js';
|
|
20
22
|
import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/autopilot-analyzer.js';
|
|
@@ -31,6 +33,8 @@ import { buildCriterionGroundingRecords, filterGroundedCriteria, getAdvisoryCrit
|
|
|
31
33
|
import { checkGenericSpecOutput } from '../engine/spec-quality/generic-output-gate.js';
|
|
32
34
|
import { calculateActionableSpecMetrics, shouldExposeMetric, } from '../engine/spec-metrics/actionable-metrics.js';
|
|
33
35
|
import { RuntimeDatabase } from '../storage/runtime-db.js';
|
|
36
|
+
import { BoundaryFailure } from '../errors/error-taxonomy.js';
|
|
37
|
+
import { SpecAcFormatEnum, SpecScopeEnum, SpecTargetEnum, SpecTypeEnum } from './schemas/spec.js';
|
|
34
38
|
import { SPEC_CREATED_POST_COMMIT_TASKS } from '../types/outbox-worker.js';
|
|
35
39
|
import { resolveStorageLayout } from '../storage/storage-layout.js';
|
|
36
40
|
import { OperationJournal, recoverOperationJournal, } from '../engine/execution/operation-journal.js';
|
|
@@ -185,8 +189,15 @@ function buildAdvisoryCompat() {
|
|
|
185
189
|
};
|
|
186
190
|
}
|
|
187
191
|
/** Persist only out-of-scope items supplied explicitly by the caller. */
|
|
188
|
-
function resolveOutOfScope(provided) {
|
|
189
|
-
|
|
192
|
+
function resolveOutOfScope(provided, source = '') {
|
|
193
|
+
const explicit = provided?.filter((item) => item.trim().length > 0) ?? [];
|
|
194
|
+
if (explicit.length > 0) {
|
|
195
|
+
return explicit;
|
|
196
|
+
}
|
|
197
|
+
const section = /(?:^|\n)##\s+Out of scope\s*\n([\s\S]*?)(?=\n##\s|$)/i.exec(source)?.[1];
|
|
198
|
+
return section
|
|
199
|
+
? Array.from(section.matchAll(/^[-*]\s+(.+)$/gm), (match) => match[1]?.trim() ?? '').filter(Boolean)
|
|
200
|
+
: [];
|
|
190
201
|
}
|
|
191
202
|
/** SPEC-783: Synthesize agent team findings into unified spec.md ## Technical section. */
|
|
192
203
|
async function handleAgentTeamSynthesis(specId, projectPath, findings) {
|
|
@@ -275,6 +286,19 @@ function finishRecoveredOperation(journal, key, result) {
|
|
|
275
286
|
}
|
|
276
287
|
return entry?.result ?? result;
|
|
277
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* SPEC-1348: release the journal claim of an attempt that failed before commit.
|
|
291
|
+
* Rolling back marks the key re-claimable; committed entries are left untouched
|
|
292
|
+
* because recovery resolution only acts on active (intent/prepared) states.
|
|
293
|
+
*/
|
|
294
|
+
function rollbackUncommittedOperation(journal, key, reason) {
|
|
295
|
+
try {
|
|
296
|
+
journal.resolveRecovery('create_spec', key, { action: 'rollback', reason });
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Best-effort: the original failure remains the primary error.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
278
302
|
function buildCommittedCreateResult(input) {
|
|
279
303
|
const advisorySignals = [];
|
|
280
304
|
if (input.advisoryCriteria.length > 0) {
|
|
@@ -330,9 +354,12 @@ function buildCommittedCreateResult(input) {
|
|
|
330
354
|
deprecatedAlias: 'riskWarning',
|
|
331
355
|
}));
|
|
332
356
|
}
|
|
333
|
-
return toolResult(`Spec ${spec.id} created and
|
|
357
|
+
return toolResult(`Spec ${spec.id} created and persisted.`, {
|
|
334
358
|
operationId: input.operationId,
|
|
359
|
+
/** @deprecated Operation-journal durability; this is not Git evidence. */
|
|
335
360
|
committed: true,
|
|
361
|
+
persisted: true,
|
|
362
|
+
gitCommitPerformed: false,
|
|
336
363
|
pendingAnalysis: true,
|
|
337
364
|
specId: spec.id,
|
|
338
365
|
title: spec.title,
|
|
@@ -711,28 +738,27 @@ async function buildIdempotencyMatchResult(projectPath, existing) {
|
|
|
711
738
|
};
|
|
712
739
|
}
|
|
713
740
|
const FRONTMATTER_ID_RE = /^id:\s*(SPEC-\d+)/m;
|
|
714
|
-
const FRONTMATTER_TITLE_RE = /^title:\s*"?([^"\n]+?)"?\s*$/m;
|
|
715
741
|
const FRONTMATTER_STATUS_RE = /^status:\s*(\S+)/m;
|
|
716
|
-
|
|
717
|
-
async function findByIdempotencyKey(projectPath, key, projectId, windowMs = IDEMPOTENCY_MATCH_WINDOW_MS) {
|
|
718
|
-
const cutoff = Date.now() - windowMs;
|
|
719
|
-
// Fast path: check store (always authoritative for successfully created specs)
|
|
742
|
+
async function findStoreByIdempotencyKey(projectId, key, cutoff) {
|
|
720
743
|
try {
|
|
721
744
|
const existing = await specStore.listSpecs(projectId);
|
|
722
|
-
|
|
723
|
-
if (match !== undefined) {
|
|
724
|
-
return match;
|
|
725
|
-
}
|
|
745
|
+
return existing.find((s) => s.idempotencyKey === key && new Date(s.createdAt).getTime() > cutoff);
|
|
726
746
|
}
|
|
727
747
|
catch {
|
|
728
|
-
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/** SPEC-770: Scan store and filesystem for a spec with matching idempotency key within windowMs. */
|
|
752
|
+
async function findByIdempotencyKey(projectPath, key, projectId, windowMs = IDEMPOTENCY_MATCH_WINDOW_MS) {
|
|
753
|
+
const cutoff = Date.now() - windowMs;
|
|
754
|
+
const storeMatch = await findStoreByIdempotencyKey(projectId, key, cutoff);
|
|
755
|
+
if (storeMatch) {
|
|
756
|
+
return storeMatch;
|
|
729
757
|
}
|
|
730
|
-
// Transactional fallback: the request key stays in external project data, not spec.md.
|
|
731
758
|
const evidenceMatch = await findByIdempotencyEvidence(projectPath, key, cutoff);
|
|
732
759
|
if (evidenceMatch !== undefined) {
|
|
733
760
|
return evidenceMatch;
|
|
734
761
|
}
|
|
735
|
-
// Compatibility fallback for specs created before external evidence was introduced.
|
|
736
762
|
try {
|
|
737
763
|
const { glob } = await import('glob');
|
|
738
764
|
const { join: joinPath } = await import('node:path');
|
|
@@ -762,7 +788,7 @@ async function findByIdempotencyKey(projectPath, key, projectId, windowMs = IDEM
|
|
|
762
788
|
return undefined;
|
|
763
789
|
}
|
|
764
790
|
/** SPEC-770: Scan for recently written spec.md files (for possibleDuplicates in timeout response). */
|
|
765
|
-
async function findRecentlyWrittenSpecs(projectPath, windowMs) {
|
|
791
|
+
async function findRecentlyWrittenSpecs(projectPath, windowMs, requestedTitle) {
|
|
766
792
|
try {
|
|
767
793
|
const { glob } = await import('glob');
|
|
768
794
|
const { join: joinPath } = await import('node:path');
|
|
@@ -776,9 +802,11 @@ async function findRecentlyWrittenSpecs(projectPath, windowMs) {
|
|
|
776
802
|
}
|
|
777
803
|
const content = await readFile(specFile, 'utf-8').catch(() => '');
|
|
778
804
|
const idMatch = FRONTMATTER_ID_RE.exec(content);
|
|
779
|
-
const
|
|
780
|
-
if (idMatch?.[1] !== undefined
|
|
781
|
-
|
|
805
|
+
const title = extractFrontmatterTitle(content);
|
|
806
|
+
if (idMatch?.[1] !== undefined &&
|
|
807
|
+
normalizeDuplicateTitle(title) !== '' &&
|
|
808
|
+
normalizeDuplicateTitle(title) === normalizeDuplicateTitle(requestedTitle)) {
|
|
809
|
+
results.push({ id: idMatch[1], title });
|
|
782
810
|
}
|
|
783
811
|
}
|
|
784
812
|
return results;
|
|
@@ -787,6 +815,69 @@ async function findRecentlyWrittenSpecs(projectPath, windowMs) {
|
|
|
787
815
|
return [];
|
|
788
816
|
}
|
|
789
817
|
}
|
|
818
|
+
function extractFrontmatterTitle(content) {
|
|
819
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1];
|
|
820
|
+
const rawTitle = frontmatter ? /^title:\s*(.*?)\s*$/m.exec(frontmatter)?.[1] : undefined;
|
|
821
|
+
if (!rawTitle) {
|
|
822
|
+
return '';
|
|
823
|
+
}
|
|
824
|
+
if (rawTitle.startsWith('"') && rawTitle.endsWith('"')) {
|
|
825
|
+
try {
|
|
826
|
+
const parsed = JSON.parse(rawTitle);
|
|
827
|
+
return typeof parsed === 'string' ? parsed : '';
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return '';
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (rawTitle.startsWith("'") && rawTitle.endsWith("'")) {
|
|
834
|
+
return rawTitle.slice(1, -1).replace(/''/g, "'");
|
|
835
|
+
}
|
|
836
|
+
return rawTitle;
|
|
837
|
+
}
|
|
838
|
+
function normalizeDuplicateTitle(value) {
|
|
839
|
+
return value
|
|
840
|
+
.normalize('NFKC')
|
|
841
|
+
.toLocaleLowerCase('en-US')
|
|
842
|
+
.replace(/[\p{P}]+/gu, ' ')
|
|
843
|
+
.replace(/\s+/g, ' ')
|
|
844
|
+
.trim();
|
|
845
|
+
}
|
|
846
|
+
const CREATE_SPEC_ENUMS = {
|
|
847
|
+
type: SpecTypeEnum,
|
|
848
|
+
scope: SpecScopeEnum,
|
|
849
|
+
target: SpecTargetEnum,
|
|
850
|
+
acFormat: SpecAcFormatEnum,
|
|
851
|
+
};
|
|
852
|
+
function validateCreateSpecEnums(input) {
|
|
853
|
+
for (const [field, schema] of Object.entries(CREATE_SPEC_ENUMS)) {
|
|
854
|
+
const received = input[field];
|
|
855
|
+
if (received === undefined || schema.safeParse(received).success) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const boundedReceived = received === null || ['string', 'number', 'boolean'].includes(typeof received)
|
|
859
|
+
? received
|
|
860
|
+
: typeof received;
|
|
861
|
+
return {
|
|
862
|
+
content: [
|
|
863
|
+
{
|
|
864
|
+
type: 'text',
|
|
865
|
+
text: `${field} must be one of: ${schema.options.join(', ')}.`,
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
isError: true,
|
|
869
|
+
structuredContent: {
|
|
870
|
+
error: 'INVALID_INPUT',
|
|
871
|
+
code: 'INVALID_INPUT',
|
|
872
|
+
status: 422,
|
|
873
|
+
field,
|
|
874
|
+
received: boundedReceived,
|
|
875
|
+
allowed: schema.options,
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
790
881
|
const DESCRIPTION_MAX_CHARS = 10_000;
|
|
791
882
|
const DESCRIPTION_EXCEED_MSG = 'Description exceeds 10000 chars. Create with a summary (<3k) and use reconcile_spec to add the rest.';
|
|
792
883
|
function checkEnglishOnlyInput(params) {
|
|
@@ -811,8 +902,260 @@ function checkEnglishOnlyInput(params) {
|
|
|
811
902
|
},
|
|
812
903
|
};
|
|
813
904
|
}
|
|
905
|
+
function quotePosixShellArgument(value) {
|
|
906
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
907
|
+
}
|
|
908
|
+
function earlyPreparation(result, clarificationProjectId) {
|
|
909
|
+
return {
|
|
910
|
+
kind: 'early',
|
|
911
|
+
result,
|
|
912
|
+
...(clarificationProjectId ? { clarificationProjectId } : {}),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
export async function resolveGroundedVerificationCommands(projectPath, testPaths) {
|
|
916
|
+
if (testPaths.length === 0) {
|
|
917
|
+
return [];
|
|
918
|
+
}
|
|
919
|
+
const packageJsonRaw = await readFile(pathJoin(projectPath, 'package.json'), 'utf8').catch(() => '');
|
|
920
|
+
if (!packageJsonRaw) {
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
try {
|
|
924
|
+
const manifest = JSON.parse(packageJsonRaw);
|
|
925
|
+
if (typeof manifest.packageManager !== 'string') {
|
|
926
|
+
return [];
|
|
927
|
+
}
|
|
928
|
+
const packageManager = /^(pnpm|npm|yarn|bun)@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.exec(manifest.packageManager)?.[1];
|
|
929
|
+
if (!packageManager) {
|
|
930
|
+
return [];
|
|
931
|
+
}
|
|
932
|
+
if (typeof manifest.scripts?.test !== 'string' || manifest.scripts.test.trim() === '') {
|
|
933
|
+
return [];
|
|
934
|
+
}
|
|
935
|
+
return [`${packageManager} run test -- ${testPaths.map(quotePosixShellArgument).join(' ')}`];
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
return [];
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async function prepareCreateSpecCandidate(initialParams, server) {
|
|
942
|
+
let params = initialParams;
|
|
943
|
+
const buildResult = await measureStep('buildSpecContext', () => buildSpecContext(params));
|
|
944
|
+
if (!buildResult.ok) {
|
|
945
|
+
return earlyPreparation({
|
|
946
|
+
content: [{ type: 'text', text: buildResult.errorMessage }],
|
|
947
|
+
isError: true,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
const context = buildResult.context;
|
|
951
|
+
const { spec, estimation, projectId } = context;
|
|
952
|
+
const knowledge = await measureStep('loadKnowledge', () => knowledgeStore.getKnowledge(projectId));
|
|
953
|
+
const clarificationSession = params.clarificationSessionId
|
|
954
|
+
? await knowledgeStore.getClarification(projectId, params.clarificationSessionId)
|
|
955
|
+
: null;
|
|
956
|
+
const constitutionCheck = await measureStep('validateConstitution', () => validateConstitution(projectId, spec.title, spec.tags));
|
|
957
|
+
if (constitutionCheck.blocked && constitutionCheck.errorResult) {
|
|
958
|
+
return earlyPreparation(constitutionCheck.errorResult);
|
|
959
|
+
}
|
|
960
|
+
const inlineAnalysis = await withBudget('create-spec-inline-analysis', getRuntimePolicy().createSpec.inlineAnalysisBudgetMs, (signal) => analyzeProjectForSpec(params.projectPath ?? '', params.description, spec.title, knowledge, signal));
|
|
961
|
+
const autopilot = unwrapBudget(inlineAnalysis, getEmptyAutopilotResult());
|
|
962
|
+
if (autopilot.isIdea) {
|
|
963
|
+
return earlyPreparation({
|
|
964
|
+
content: [
|
|
965
|
+
{
|
|
966
|
+
type: 'text',
|
|
967
|
+
text: `💡 **Idea captured**: "${params.description}"\n\nThe description is too brief for a full spec. Use \`capture_idea\` to save it to the backlog, or provide more detail (>10 words) to create a spec.`,
|
|
968
|
+
},
|
|
969
|
+
],
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
const clarificationResult = handleClarification(server, params.description, knowledge, autopilot, params);
|
|
973
|
+
if (clarificationResult !== null) {
|
|
974
|
+
if ('earlyReturn' in clarificationResult) {
|
|
975
|
+
return earlyPreparation(clarificationResult.earlyReturn, projectId);
|
|
976
|
+
}
|
|
977
|
+
params = clarificationResult.params;
|
|
978
|
+
}
|
|
979
|
+
const filteredCriteria = filterGroundedCriteria(autopilot.suggestedCriteria);
|
|
980
|
+
const technologyContract = await readTechnologySelectionContract(params.projectPath ?? '');
|
|
981
|
+
const contractNote = technologyContract
|
|
982
|
+
? [
|
|
983
|
+
'',
|
|
984
|
+
'Technology Contract:',
|
|
985
|
+
`- Mode: ${technologyContract.mode}`,
|
|
986
|
+
technologyContract.language ? `- Language: ${technologyContract.language}` : '',
|
|
987
|
+
technologyContract.framework
|
|
988
|
+
? `- Framework: ${technologyContract.framework}`
|
|
989
|
+
: '- Framework: none or not selected',
|
|
990
|
+
technologyContract.platform ? `- Platform: ${technologyContract.platform}` : '',
|
|
991
|
+
technologyContract.projectType ? `- Project type: ${technologyContract.projectType}` : '',
|
|
992
|
+
'- Agents must not choose a different stack unless the user approves a new contract.',
|
|
993
|
+
]
|
|
994
|
+
.filter(Boolean)
|
|
995
|
+
.join('\n')
|
|
996
|
+
: '';
|
|
997
|
+
const generatedSpec = await measureStep('generateSpecBody', () => new FallbackGenerator().generate({
|
|
998
|
+
title: spec.title,
|
|
999
|
+
description: `${params.description}${contractNote}`,
|
|
1000
|
+
type: spec.type,
|
|
1001
|
+
scope: spec.scope,
|
|
1002
|
+
target: spec.target,
|
|
1003
|
+
acFormat: params.acFormat,
|
|
1004
|
+
projectContext: {
|
|
1005
|
+
language: technologyContract?.language ?? knowledge?.language ?? undefined,
|
|
1006
|
+
framework: technologyContract?.framework ?? knowledge?.framework ?? undefined,
|
|
1007
|
+
architecture: knowledge?.architecture.primary ?? undefined,
|
|
1008
|
+
},
|
|
1009
|
+
}));
|
|
1010
|
+
spec.generation = generatedSpec.generation;
|
|
1011
|
+
spec.qualityWarnings =
|
|
1012
|
+
generatedSpec.qualityWarnings.length > 0 ? generatedSpec.qualityWarnings : undefined;
|
|
1013
|
+
const baseCriteria = extractCriteria(generatedSpec.specBody).map((criterion) => criterion.text);
|
|
1014
|
+
const groundingCriteria = buildCriterionGroundingRecords({
|
|
1015
|
+
criteria: [...baseCriteria, ...filteredCriteria],
|
|
1016
|
+
userInput: params.description,
|
|
1017
|
+
generatedEvidence: [generatedSpec.generation.modelId ?? generatedSpec.generation.method],
|
|
1018
|
+
});
|
|
1019
|
+
const contractCriteria = getContractCriteria(groundingCriteria);
|
|
1020
|
+
const advisoryCriteria = getAdvisoryCriteria(groundingCriteria).map((record) => record.text);
|
|
1021
|
+
if (contractCriteria.length === 0) {
|
|
1022
|
+
return earlyPreparation({
|
|
1023
|
+
content: [
|
|
1024
|
+
{
|
|
1025
|
+
type: 'text',
|
|
1026
|
+
text: 'create_spec did not persist a spec because no acceptance criterion was grounded in the user request. ' +
|
|
1027
|
+
'Needs decision: provide at least one explicit, verifiable criterion as a checkbox or Given/When/Then outcome.',
|
|
1028
|
+
},
|
|
1029
|
+
],
|
|
1030
|
+
isError: true,
|
|
1031
|
+
structuredContent: {
|
|
1032
|
+
error: 'MISSING_GROUNDED_ACCEPTANCE_CRITERIA',
|
|
1033
|
+
missingDecision: 'Provide at least one explicit, verifiable acceptance criterion.',
|
|
1034
|
+
persisted: false,
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
const actionableMetrics = calculateActionableSpecMetrics({
|
|
1039
|
+
criteria: [...baseCriteria, ...filteredCriteria],
|
|
1040
|
+
groundingRecords: groundingCriteria,
|
|
1041
|
+
}).filter(shouldExposeMetric);
|
|
1042
|
+
const technicalFiles = await measureStep('resolveTechnicalFiles', () => resolveTechnicalFiles({
|
|
1043
|
+
generatedSpecBody: generatedSpec.specBody,
|
|
1044
|
+
generatedTechnicalSection: generatedSpec.technicalSection,
|
|
1045
|
+
projectPath: params.projectPath ?? '',
|
|
1046
|
+
}));
|
|
1047
|
+
const groundedTechnical = await measureStep('groundTechnicalFiles', () => groundTechnicalFiles({
|
|
1048
|
+
files: technicalFiles,
|
|
1049
|
+
projectPath: params.projectPath ?? '',
|
|
1050
|
+
userInput: params.description,
|
|
1051
|
+
autopilot,
|
|
1052
|
+
}));
|
|
1053
|
+
const outOfScope = resolveOutOfScope(params.outOfScope, params.description);
|
|
1054
|
+
const scenarioTestPaths = groundedTechnical.files.test.map((file) => file.path);
|
|
1055
|
+
const criteria = contractCriteria.map((record) => ({ text: record.text, done: false }));
|
|
1056
|
+
const leanSpec = generateLeanSpecContent({
|
|
1057
|
+
spec,
|
|
1058
|
+
description: generatedSpec.specBody,
|
|
1059
|
+
estimation,
|
|
1060
|
+
criteriaOverride: criteria,
|
|
1061
|
+
groundingCriteria: contractCriteria,
|
|
1062
|
+
groundingTechnicalReferences: groundedTechnical.records,
|
|
1063
|
+
acFormat: params.acFormat,
|
|
1064
|
+
scenarioTestPaths,
|
|
1065
|
+
});
|
|
1066
|
+
const leanTechnical = generateLeanTechnicalContent({
|
|
1067
|
+
specId: spec.id,
|
|
1068
|
+
filesToCreate: groundedTechnical.files.create,
|
|
1069
|
+
filesToModify: groundedTechnical.files.modify,
|
|
1070
|
+
filesToTest: groundedTechnical.files.test,
|
|
1071
|
+
includeDecisionRequiredPlaceholder: spec.scope !== 'trivial',
|
|
1072
|
+
});
|
|
1073
|
+
const verificationCommands = await resolveGroundedVerificationCommands(params.projectPath ?? '', scenarioTestPaths);
|
|
1074
|
+
const implementationContract = buildImplementationContractSection({
|
|
1075
|
+
description: generatedSpec.specBody,
|
|
1076
|
+
criteria,
|
|
1077
|
+
files: groundedTechnical.files,
|
|
1078
|
+
outOfScope,
|
|
1079
|
+
verificationCommands,
|
|
1080
|
+
});
|
|
1081
|
+
const unifiedSpec = buildCanonicalUnifiedSpecContent({
|
|
1082
|
+
leanSpecBody: leanSpec,
|
|
1083
|
+
sourceDescription: params.description,
|
|
1084
|
+
technicalBody: leanTechnical,
|
|
1085
|
+
implementationContract,
|
|
1086
|
+
criteria,
|
|
1087
|
+
files: groundedTechnical.files,
|
|
1088
|
+
outOfScope,
|
|
1089
|
+
});
|
|
1090
|
+
const candidateValidation = validateUnifiedSpecCandidate(unifiedSpec, {
|
|
1091
|
+
groundedFilePaths: groundedTechnical.records.map((record) => record.path),
|
|
1092
|
+
});
|
|
1093
|
+
if (!candidateValidation.valid) {
|
|
1094
|
+
return earlyPreparation({
|
|
1095
|
+
content: [
|
|
1096
|
+
{
|
|
1097
|
+
type: 'text',
|
|
1098
|
+
text: `Unified spec candidate is invalid: ${candidateValidation.issues
|
|
1099
|
+
.slice(0, 3)
|
|
1100
|
+
.map((issue) => issue.message)
|
|
1101
|
+
.join('; ')}`,
|
|
1102
|
+
},
|
|
1103
|
+
],
|
|
1104
|
+
isError: true,
|
|
1105
|
+
structuredContent: {
|
|
1106
|
+
error: 'SPEC_FORMAT_INVALID',
|
|
1107
|
+
code: 422,
|
|
1108
|
+
persisted: false,
|
|
1109
|
+
issues: candidateValidation.issues,
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
|
|
1114
|
+
if (!genericOutputGate.passed) {
|
|
1115
|
+
return earlyPreparation({
|
|
1116
|
+
content: [
|
|
1117
|
+
{
|
|
1118
|
+
type: 'text',
|
|
1119
|
+
text: 'Spec quality gate blocked generic output before persistence. ' +
|
|
1120
|
+
genericOutputGate.issues
|
|
1121
|
+
.slice(0, 3)
|
|
1122
|
+
.map((issue) => `${issue.phrase}: ${issue.reason}`)
|
|
1123
|
+
.join('; '),
|
|
1124
|
+
},
|
|
1125
|
+
],
|
|
1126
|
+
isError: true,
|
|
1127
|
+
structuredContent: {
|
|
1128
|
+
error: 'GENERIC_SPEC_OUTPUT_BLOCKED',
|
|
1129
|
+
sddConstitutionRuleId: 'sdd.no-generic-output',
|
|
1130
|
+
issues: genericOutputGate.issues,
|
|
1131
|
+
fixHint: 'Replace generic criteria or placeholder references with grounded, testable behavior.',
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
if (outOfScope.length > 0) {
|
|
1136
|
+
spec.outOfScope = outOfScope;
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
kind: 'ready',
|
|
1140
|
+
prepared: {
|
|
1141
|
+
params,
|
|
1142
|
+
context,
|
|
1143
|
+
knowledge,
|
|
1144
|
+
clarificationSession,
|
|
1145
|
+
constitutionWarnings: constitutionCheck.warnings,
|
|
1146
|
+
advisoryCriteria,
|
|
1147
|
+
actionableMetrics,
|
|
1148
|
+
unifiedSpec,
|
|
1149
|
+
simplicityResult: runSimplicityCheck([params.description, ...filteredCriteria].join('\n'), estimation.devHours),
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
814
1153
|
// eslint-disable-next-line max-lines-per-function
|
|
815
1154
|
export async function handleCreateSpec(inputParams, server) {
|
|
1155
|
+
const enumValidation = validateCreateSpecEnums(inputParams);
|
|
1156
|
+
if (enumValidation) {
|
|
1157
|
+
return enumValidation;
|
|
1158
|
+
}
|
|
816
1159
|
if (inputParams.idempotencyKey !== undefined &&
|
|
817
1160
|
!/^[A-Za-z0-9._:-]{8,128}$/.test(inputParams.idempotencyKey)) {
|
|
818
1161
|
return {
|
|
@@ -870,21 +1213,70 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
870
1213
|
if (languageGate) {
|
|
871
1214
|
return languageGate;
|
|
872
1215
|
}
|
|
1216
|
+
// SPEC-584: Gate check — block if pending clarification token exists and no answers provided
|
|
1217
|
+
const gateError = await runClarificationGate(resolvedPath, 'create_spec', inputParams.clarificationAnswers);
|
|
1218
|
+
if (gateError) {
|
|
1219
|
+
return gateError;
|
|
1220
|
+
}
|
|
1221
|
+
const idempotencyKey = computeIdempotencyKey(inputParams.title, resolvedPath, inputParams.idempotencyKey);
|
|
1222
|
+
const cutoff = Date.now() - IDEMPOTENCY_MATCH_WINDOW_MS;
|
|
1223
|
+
const storeOnlyCandidate = await findStoreByIdempotencyKey(hashProjectPath(resolvedPath), idempotencyKey, cutoff);
|
|
1224
|
+
let preparation;
|
|
1225
|
+
try {
|
|
1226
|
+
preparation = await prepareCreateSpecCandidate(resolvedInputParams, server);
|
|
1227
|
+
}
|
|
1228
|
+
catch (error) {
|
|
1229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1230
|
+
return {
|
|
1231
|
+
content: [{ type: 'text', text: ti('errors.internalError', { message }) }],
|
|
1232
|
+
isError: true,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
if (preparation.kind === 'early') {
|
|
1236
|
+
if (preparation.clarificationProjectId) {
|
|
1237
|
+
await persistClarificationToken(preparation.result, preparation.clarificationProjectId, 'create_spec');
|
|
1238
|
+
}
|
|
1239
|
+
return preparation.result;
|
|
1240
|
+
}
|
|
1241
|
+
const prepared = preparation.prepared;
|
|
1242
|
+
prepared.context.spec.idempotencyKey = idempotencyKey;
|
|
1243
|
+
if (storeOnlyCandidate) {
|
|
1244
|
+
const committedEvidence = await findByIdempotencyEvidence(resolvedPath, idempotencyKey, cutoff);
|
|
1245
|
+
if (!committedEvidence) {
|
|
1246
|
+
return {
|
|
1247
|
+
content: [
|
|
1248
|
+
{
|
|
1249
|
+
type: 'text',
|
|
1250
|
+
text: `A partial create_spec transaction exists for ${storeOnlyCandidate.id}: the store row has no committed idempotency evidence. Recovery is required before retrying.`,
|
|
1251
|
+
},
|
|
1252
|
+
],
|
|
1253
|
+
isError: true,
|
|
1254
|
+
structuredContent: {
|
|
1255
|
+
error: 'PARTIAL_IDEMPOTENCY_STATE',
|
|
1256
|
+
code: 409,
|
|
1257
|
+
persisted: false,
|
|
1258
|
+
specId: storeOnlyCandidate.id,
|
|
1259
|
+
},
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
873
1263
|
// SPEC-665: Fire-and-forget TTL refresh for conventions.json (never blocks create_spec)
|
|
874
1264
|
void import('../engine/conventions-ttl.js').then(({ triggerConventionsTtlRefresh }) => {
|
|
875
1265
|
void triggerConventionsTtlRefresh(resolvedPath).catch(() => {
|
|
876
1266
|
/* best-effort */
|
|
877
1267
|
});
|
|
878
1268
|
});
|
|
879
|
-
// SPEC-584: Gate check — block if pending clarification token exists and no answers provided
|
|
880
|
-
const gateError = await runClarificationGate(resolvedPath, 'create_spec', inputParams.clarificationAnswers);
|
|
881
|
-
if (gateError) {
|
|
882
|
-
return gateError;
|
|
883
|
-
}
|
|
884
1269
|
// SPEC-770: Idempotency check — return existing spec if same title+projectPath within 10 minutes
|
|
885
|
-
const idempotencyKey = computeIdempotencyKey(inputParams.title, resolvedPath, inputParams.idempotencyKey);
|
|
886
1270
|
const operationKey = computeOperationKey(resolvedInputParams, resolvedPath, idempotencyKey);
|
|
887
|
-
|
|
1271
|
+
// SPEC-1351: create_spec holds its RuntimeDatabase connection for the whole
|
|
1272
|
+
// operation (idempotency journal + recovery payload). The default cap (4) is
|
|
1273
|
+
// exhausted by a handful of concurrent create_spec calls in one process,
|
|
1274
|
+
// which otherwise fail with RuntimeDatabaseResourceExhaustedError before any
|
|
1275
|
+
// persistence work starts. Raise the ceiling for this call site only.
|
|
1276
|
+
const runtimeDatabase = new RuntimeDatabase({
|
|
1277
|
+
path: resolveStorageLayout().runtimeDatabase,
|
|
1278
|
+
maxConnections: 16,
|
|
1279
|
+
});
|
|
888
1280
|
const operationJournal = new OperationJournal(runtimeDatabase, createHash('sha256').update(hashProjectPath(resolvedPath)).digest('hex').slice(0, 32));
|
|
889
1281
|
let journalEntry;
|
|
890
1282
|
try {
|
|
@@ -915,6 +1307,25 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
915
1307
|
}
|
|
916
1308
|
catch (error) {
|
|
917
1309
|
runtimeDatabase.close();
|
|
1310
|
+
if (error instanceof BoundaryFailure &&
|
|
1311
|
+
error.kind === 'Conflict' &&
|
|
1312
|
+
error.operation === 'operation begin') {
|
|
1313
|
+
return {
|
|
1314
|
+
content: [
|
|
1315
|
+
{
|
|
1316
|
+
type: 'text',
|
|
1317
|
+
text: 'The idempotency key is already bound to a different create_spec request.',
|
|
1318
|
+
},
|
|
1319
|
+
],
|
|
1320
|
+
isError: true,
|
|
1321
|
+
structuredContent: {
|
|
1322
|
+
error: 'IDEMPOTENCY_CONFLICT',
|
|
1323
|
+
code: 'IDEMPOTENCY_CONFLICT',
|
|
1324
|
+
status: 409,
|
|
1325
|
+
persisted: false,
|
|
1326
|
+
},
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
918
1329
|
throw error;
|
|
919
1330
|
}
|
|
920
1331
|
if ((journalEntry.state === 'committed' || journalEntry.state === 'acknowledged') &&
|
|
@@ -984,299 +1395,147 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
984
1395
|
idempotencyKey,
|
|
985
1396
|
ownerId: idempotencyClaim.ownerId,
|
|
986
1397
|
});
|
|
987
|
-
// eslint-disable-next-line max-lines-per-function -- cohesive transactional create boundary
|
|
988
1398
|
return await trackCost(resolvedInputParams.projectPath ?? '', 'create_spec', async () => {
|
|
989
|
-
// Allow internal re-enrichment without mutating function parameter
|
|
990
|
-
let params = resolvedInputParams;
|
|
991
|
-
const { description, clarificationSessionId } = params;
|
|
992
1399
|
try {
|
|
993
1400
|
// SPEC-713: Phase 1 — Critical path: build + persist spec within 25s hard ceiling.
|
|
994
1401
|
// Only the steps required to produce spec.md on disk are inside this budget.
|
|
995
1402
|
// Durable post-commit work is delegated to the spec.created outbox consumer.
|
|
996
1403
|
const criticalResult = await withTotalBudget('create_spec-critical', getRuntimePolicy().createSpec.criticalTimeoutMs, async (criticalSignal) => {
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
earlyReturn: {
|
|
1031
|
-
content: [
|
|
1032
|
-
{
|
|
1033
|
-
type: 'text',
|
|
1034
|
-
text: `💡 **Idea captured**: "${description}"\n\nThe description is too brief for a full spec. Use \`capture_idea\` to save it to the backlog, or provide more detail (>10 words) to create a spec.`,
|
|
1035
|
-
},
|
|
1036
|
-
],
|
|
1037
|
-
},
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
// SPEC-463 / SPEC-471: Interactive clarification
|
|
1041
|
-
const clarificationResult = handleClarification(server, description, knowledge, autopilot, params);
|
|
1042
|
-
if (clarificationResult !== null) {
|
|
1043
|
-
if ('earlyReturn' in clarificationResult) {
|
|
1044
|
-
// SPEC-584: Persist token so the gate blocks retries without answers
|
|
1045
|
-
await persistClarificationToken(clarificationResult.earlyReturn, projectId, 'create_spec');
|
|
1046
|
-
return { ok: false, earlyReturn: clarificationResult.earlyReturn };
|
|
1404
|
+
const { params, context, knowledge, clarificationSession, constitutionWarnings, advisoryCriteria, actionableMetrics, simplicityResult: committedSimplicity, } = prepared;
|
|
1405
|
+
const { splitSuggestion, duplicate, projectId } = context;
|
|
1406
|
+
let spec = context.spec;
|
|
1407
|
+
let specDir = context.specDir;
|
|
1408
|
+
let specPath = context.specPath;
|
|
1409
|
+
let unifiedSpec = prepared.unifiedSpec;
|
|
1410
|
+
// SPEC-1351: serialize the id re-derivation + persistence sequence
|
|
1411
|
+
// per project. The candidate id computed during (unlocked) prepare
|
|
1412
|
+
// can collide with another in-flight create_spec call; re-deriving
|
|
1413
|
+
// it here — inside the only place that ever writes spec.md for this
|
|
1414
|
+
// project — guarantees the id actually persisted is unique.
|
|
1415
|
+
return withFileLock(`create-spec:${projectId}`, async () => {
|
|
1416
|
+
let committedPlannerToken;
|
|
1417
|
+
try {
|
|
1418
|
+
const lockedSpecId = await computeNextSpecId(resolvedPath, projectId);
|
|
1419
|
+
if (lockedSpecId !== spec.id) {
|
|
1420
|
+
const newGitBranch = generateBranchName(lockedSpecId, spec.slug, spec.type);
|
|
1421
|
+
const newSpecDir = pathJoin(pathDirname(specDir), `${lockedSpecId}-${spec.slug}`);
|
|
1422
|
+
const newSpecPath = pathJoin(newSpecDir, pathBasename(specPath));
|
|
1423
|
+
unifiedSpec = unifiedSpec
|
|
1424
|
+
.replace(`id: ${spec.id}\n`, `id: ${lockedSpecId}\n`)
|
|
1425
|
+
.replace(`branch: ${spec.gitBranch}\n`, `branch: ${newGitBranch}\n`);
|
|
1426
|
+
spec = {
|
|
1427
|
+
...spec,
|
|
1428
|
+
id: lockedSpecId,
|
|
1429
|
+
specPath: newSpecPath,
|
|
1430
|
+
technicalPath: newSpecPath,
|
|
1431
|
+
gitBranch: newGitBranch,
|
|
1432
|
+
};
|
|
1433
|
+
specDir = newSpecDir;
|
|
1434
|
+
specPath = newSpecPath;
|
|
1435
|
+
}
|
|
1436
|
+
committedPlannerToken = await issuePlannerToken(resolvedPath, spec.id, params.sessionId ?? 'unknown-session', params.modelId ?? 'unknown-model', params.host ?? 'unknown-host');
|
|
1047
1437
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
: '',
|
|
1065
|
-
'- Agents must not choose a different stack unless the user approves a new contract.',
|
|
1066
|
-
]
|
|
1067
|
-
.filter(Boolean)
|
|
1068
|
-
.join('\n')
|
|
1069
|
-
: '';
|
|
1070
|
-
const specGenerator = new FallbackGenerator();
|
|
1071
|
-
const generatedSpec = await measureStep('generateSpecBody', () => specGenerator.generate({
|
|
1072
|
-
title: spec.title,
|
|
1073
|
-
description: `${description}${contractNote}`,
|
|
1074
|
-
type: spec.type,
|
|
1075
|
-
scope: spec.scope,
|
|
1076
|
-
target: spec.target,
|
|
1077
|
-
acFormat: params.acFormat,
|
|
1078
|
-
projectContext: {
|
|
1079
|
-
language: technologyContract?.language ?? knowledge?.language ?? undefined,
|
|
1080
|
-
framework: technologyContract?.framework ?? knowledge?.framework ?? undefined,
|
|
1081
|
-
architecture: knowledge?.architecture.primary ?? undefined,
|
|
1082
|
-
},
|
|
1083
|
-
}));
|
|
1084
|
-
spec.generation = generatedSpec.generation;
|
|
1085
|
-
spec.qualityWarnings =
|
|
1086
|
-
generatedSpec.qualityWarnings.length > 0 ? generatedSpec.qualityWarnings : undefined;
|
|
1087
|
-
const baseCriteria = extractCriteria(generatedSpec.specBody).map((criterion) => criterion.text);
|
|
1088
|
-
const groundingCriteria = buildCriterionGroundingRecords({
|
|
1089
|
-
criteria: [...baseCriteria, ...filteredCriteria],
|
|
1090
|
-
userInput: description,
|
|
1091
|
-
generatedEvidence: [
|
|
1092
|
-
generatedSpec.generation.modelId ?? generatedSpec.generation.method,
|
|
1093
|
-
],
|
|
1094
|
-
});
|
|
1095
|
-
const contractCriteria = getContractCriteria(groundingCriteria);
|
|
1096
|
-
const advisoryCriteria = getAdvisoryCriteria(groundingCriteria).map((record) => record.text);
|
|
1097
|
-
if (contractCriteria.length === 0) {
|
|
1098
|
-
return {
|
|
1099
|
-
ok: false,
|
|
1100
|
-
earlyReturn: {
|
|
1101
|
-
content: [
|
|
1102
|
-
{
|
|
1103
|
-
type: 'text',
|
|
1104
|
-
text: 'create_spec did not persist a spec because no acceptance criterion was grounded in the user request. ' +
|
|
1105
|
-
'Needs decision: provide at least one explicit, verifiable criterion as a checkbox or Given/When/Then outcome.',
|
|
1106
|
-
},
|
|
1107
|
-
],
|
|
1108
|
-
isError: true,
|
|
1109
|
-
structuredContent: {
|
|
1110
|
-
error: 'MISSING_GROUNDED_ACCEPTANCE_CRITERIA',
|
|
1111
|
-
missingDecision: 'Provide at least one explicit, verifiable acceptance criterion.',
|
|
1112
|
-
persisted: false,
|
|
1113
|
-
},
|
|
1114
|
-
},
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
const actionableMetrics = calculateActionableSpecMetrics({
|
|
1118
|
-
criteria: [...baseCriteria, ...filteredCriteria],
|
|
1119
|
-
groundingRecords: groundingCriteria,
|
|
1120
|
-
}).filter(shouldExposeMetric);
|
|
1121
|
-
const technicalFiles = await measureStep('resolveTechnicalFiles', () => resolveTechnicalFiles({
|
|
1122
|
-
generatedSpecBody: generatedSpec.specBody,
|
|
1123
|
-
generatedTechnicalSection: generatedSpec.technicalSection,
|
|
1124
|
-
projectPath: params.projectPath ?? '',
|
|
1125
|
-
}));
|
|
1126
|
-
const groundedTechnical = await measureStep('groundTechnicalFiles', () => groundTechnicalFiles({
|
|
1127
|
-
files: technicalFiles,
|
|
1128
|
-
projectPath: params.projectPath ?? '',
|
|
1129
|
-
userInput: description,
|
|
1130
|
-
autopilot,
|
|
1131
|
-
}));
|
|
1132
|
-
const outOfScope = resolveOutOfScope(params.outOfScope);
|
|
1133
|
-
const scenarioTestPaths = groundedTechnical.files.test.map((file) => file.path);
|
|
1134
|
-
const leanSpec = generateLeanSpecContent({
|
|
1135
|
-
spec,
|
|
1136
|
-
description: generatedSpec.specBody,
|
|
1137
|
-
estimation,
|
|
1138
|
-
criteriaOverride: contractCriteria.map((record) => ({
|
|
1139
|
-
text: record.text,
|
|
1140
|
-
done: false,
|
|
1141
|
-
})),
|
|
1142
|
-
groundingCriteria: contractCriteria,
|
|
1143
|
-
groundingTechnicalReferences: groundedTechnical.records,
|
|
1144
|
-
acFormat: params.acFormat,
|
|
1145
|
-
scenarioTestPaths,
|
|
1146
|
-
});
|
|
1147
|
-
const leanTechnical = generateLeanTechnicalContent({
|
|
1148
|
-
specId: spec.id,
|
|
1149
|
-
filesToCreate: groundedTechnical.files.create,
|
|
1150
|
-
filesToModify: groundedTechnical.files.modify,
|
|
1151
|
-
filesToTest: groundedTechnical.files.test,
|
|
1152
|
-
includeDecisionRequiredPlaceholder: spec.scope !== 'trivial',
|
|
1153
|
-
});
|
|
1154
|
-
// SPEC-709: write unified spec.md from origin — no separate technical.md.
|
|
1155
|
-
// The legacy two-file output is preserved by appending the technical body
|
|
1156
|
-
// as a `## Technical` section inside spec.md.
|
|
1157
|
-
const unifiedWithoutContract = buildUnifiedSpecContent(leanSpec, leanTechnical);
|
|
1158
|
-
const unifiedSpec = appendImplementationContractIfMissing(unifiedWithoutContract, {
|
|
1159
|
-
description: generatedSpec.specBody,
|
|
1160
|
-
criteria: contractCriteria.map((record) => ({ text: record.text, done: false })),
|
|
1161
|
-
files: groundedTechnical.files,
|
|
1162
|
-
outOfScope,
|
|
1163
|
-
verificationCommands: [],
|
|
1164
|
-
});
|
|
1165
|
-
const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
|
|
1166
|
-
if (!genericOutputGate.passed) {
|
|
1167
|
-
return {
|
|
1168
|
-
ok: false,
|
|
1169
|
-
earlyReturn: {
|
|
1170
|
-
content: [
|
|
1171
|
-
{
|
|
1172
|
-
type: 'text',
|
|
1173
|
-
text: 'Spec quality gate blocked generic output before persistence. ' +
|
|
1174
|
-
genericOutputGate.issues
|
|
1175
|
-
.slice(0, 3)
|
|
1176
|
-
.map((issue) => `${issue.phrase}: ${issue.reason}`)
|
|
1177
|
-
.join('; '),
|
|
1178
|
-
},
|
|
1179
|
-
],
|
|
1180
|
-
isError: true,
|
|
1181
|
-
structuredContent: {
|
|
1182
|
-
error: 'GENERIC_SPEC_OUTPUT_BLOCKED',
|
|
1183
|
-
sddConstitutionRuleId: 'sdd.no-generic-output',
|
|
1184
|
-
issues: genericOutputGate.issues,
|
|
1185
|
-
fixHint: 'Replace generic criteria or placeholder references with grounded, testable behavior.',
|
|
1186
|
-
},
|
|
1187
|
-
},
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
// Contradiction analysis is durable post-commit work. Its synchronous scanners can
|
|
1191
|
-
// block the event loop, so it must never run on the persistence acknowledgement path.
|
|
1192
|
-
const contradictionHint = undefined;
|
|
1193
|
-
const committedSimplicity = runSimplicityCheck([params.description, ...filteredCriteria].join('\n'), estimation.devHours);
|
|
1194
|
-
if (outOfScope.length > 0) {
|
|
1195
|
-
spec.outOfScope = outOfScope;
|
|
1196
|
-
}
|
|
1197
|
-
let committedPlannerToken;
|
|
1198
|
-
try {
|
|
1199
|
-
committedPlannerToken = await issuePlannerToken(resolvedPath, spec.id, params.sessionId ?? 'unknown-session', params.modelId ?? 'unknown-model', params.host ?? 'unknown-host');
|
|
1200
|
-
}
|
|
1201
|
-
catch {
|
|
1202
|
-
// Best-effort evidence must not block durable spec creation.
|
|
1203
|
-
}
|
|
1204
|
-
const committedResult = buildCommittedCreateResult({
|
|
1205
|
-
spec,
|
|
1206
|
-
operationId: operationKey,
|
|
1207
|
-
duplicate,
|
|
1208
|
-
constitutionWarnings: constitutionCheck.warnings,
|
|
1209
|
-
clarificationSession,
|
|
1210
|
-
advisoryCriteria,
|
|
1211
|
-
actionableMetrics,
|
|
1212
|
-
splitSuggestion,
|
|
1213
|
-
experienceLevel: knowledge?.experienceLevel,
|
|
1214
|
-
contradictionHint,
|
|
1215
|
-
simplicityResult: committedSimplicity,
|
|
1216
|
-
plannerToken: committedPlannerToken,
|
|
1217
|
-
});
|
|
1218
|
-
let storeCreated = false;
|
|
1219
|
-
try {
|
|
1220
|
-
assertExecutionCanCommit(criticalSignal);
|
|
1221
|
-
operationJournal.setRecoveryPayload('create_spec', operationKey, 1, {
|
|
1222
|
-
operationVersion: 1,
|
|
1223
|
-
projectPath: resolvedPath,
|
|
1224
|
-
idempotencyKey,
|
|
1225
|
-
ownerId: idempotencyClaim.ownerId,
|
|
1226
|
-
projectId,
|
|
1227
|
-
specId: spec.id,
|
|
1228
|
-
specPath,
|
|
1229
|
-
specContentDigest: createHash('sha256').update(unifiedSpec).digest('hex'),
|
|
1230
|
-
result: committedResult,
|
|
1438
|
+
catch {
|
|
1439
|
+
// Best-effort evidence must not block durable spec creation.
|
|
1440
|
+
}
|
|
1441
|
+
const committedResult = buildCommittedCreateResult({
|
|
1442
|
+
spec,
|
|
1443
|
+
operationId: operationKey,
|
|
1444
|
+
duplicate,
|
|
1445
|
+
constitutionWarnings,
|
|
1446
|
+
clarificationSession,
|
|
1447
|
+
advisoryCriteria,
|
|
1448
|
+
actionableMetrics,
|
|
1449
|
+
splitSuggestion,
|
|
1450
|
+
experienceLevel: knowledge?.experienceLevel,
|
|
1451
|
+
contradictionHint: undefined,
|
|
1452
|
+
simplicityResult: committedSimplicity,
|
|
1453
|
+
plannerToken: committedPlannerToken,
|
|
1231
1454
|
});
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
assertExecutionCanCommit(criticalSignal);
|
|
1238
|
-
await measureStep('specStore-createSpec', () => specStore.createSpec(projectId, spec));
|
|
1239
|
-
storeCreated = true;
|
|
1240
|
-
assertExecutionCanCommit(criticalSignal);
|
|
1241
|
-
await measureStep('commit-idempotency-evidence', () => commitIdempotencyEvidence(resolvedPath, idempotencyKey, idempotencyClaim, spec, specPath));
|
|
1242
|
-
operationJournal.commitWithOutbox('create_spec', operationKey, committedResult, {
|
|
1243
|
-
topic: 'spec.created',
|
|
1244
|
-
payload: {
|
|
1245
|
-
schemaVersion: 1,
|
|
1246
|
-
specId: spec.id,
|
|
1247
|
-
projectId,
|
|
1455
|
+
let storeCreated = false;
|
|
1456
|
+
try {
|
|
1457
|
+
assertExecutionCanCommit(criticalSignal);
|
|
1458
|
+
operationJournal.setRecoveryPayload('create_spec', operationKey, 1, {
|
|
1459
|
+
operationVersion: 1,
|
|
1248
1460
|
projectPath: resolvedPath,
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
: true;
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1461
|
+
idempotencyKey,
|
|
1462
|
+
ownerId: idempotencyClaim.ownerId,
|
|
1463
|
+
projectId,
|
|
1464
|
+
specId: spec.id,
|
|
1465
|
+
specPath,
|
|
1466
|
+
specContentDigest: createHash('sha256').update(unifiedSpec).digest('hex'),
|
|
1467
|
+
result: committedResult,
|
|
1468
|
+
});
|
|
1469
|
+
operationJournal.prepared('create_spec', operationKey);
|
|
1470
|
+
await measureStep('mkdir-specDir', () => executionMkdir(specDir, { recursive: true }));
|
|
1471
|
+
// SPEC-713: measure file write — this is the critical persistence step.
|
|
1472
|
+
assertExecutionCanCommit(criticalSignal);
|
|
1473
|
+
await measureStep('writeFile-specPath', () => atomicWriteFile(specPath, unifiedSpec, { encoding: 'utf-8' }));
|
|
1474
|
+
assertExecutionCanCommit(criticalSignal);
|
|
1475
|
+
// SPEC-1351 (AC2): verify the write actually landed on disk
|
|
1476
|
+
// before any commit path is allowed to report persisted:true.
|
|
1477
|
+
// atomicWriteFile resolving without throwing is not sufficient
|
|
1478
|
+
// evidence under resource-exhaustion / concurrent-load
|
|
1479
|
+
// conditions that previously let the journal report success
|
|
1480
|
+
// with zero bytes ever reaching disk.
|
|
1481
|
+
const writtenStat = await fsStat(specPath).catch(() => null);
|
|
1482
|
+
if (!writtenStat || writtenStat.size === 0) {
|
|
1483
|
+
throw new Error(`create_spec failed to persist ${specPath}: file is missing or empty after write. ` +
|
|
1484
|
+
'No spec was registered — retry create_spec.');
|
|
1485
|
+
}
|
|
1486
|
+
await measureStep('specStore-createSpec', () => specStore.createSpec(projectId, spec));
|
|
1487
|
+
storeCreated = true;
|
|
1488
|
+
assertExecutionCanCommit(criticalSignal);
|
|
1489
|
+
await measureStep('commit-idempotency-evidence', () => commitIdempotencyEvidence(resolvedPath, idempotencyKey, idempotencyClaim, spec, specPath));
|
|
1490
|
+
operationJournal.commitWithOutbox('create_spec', operationKey, committedResult, {
|
|
1491
|
+
topic: 'spec.created',
|
|
1492
|
+
payload: {
|
|
1493
|
+
schemaVersion: 1,
|
|
1494
|
+
specId: spec.id,
|
|
1495
|
+
projectId,
|
|
1496
|
+
projectPath: resolvedPath,
|
|
1497
|
+
postCommitTasks: SPEC_CREATED_POST_COMMIT_TASKS,
|
|
1498
|
+
},
|
|
1262
1499
|
});
|
|
1500
|
+
claimLifecycle.committed = true;
|
|
1501
|
+
// SPEC-709/SPEC-461: unified spec.md; no progress or HTML artifacts.
|
|
1502
|
+
}
|
|
1503
|
+
catch (writeErr) {
|
|
1504
|
+
const storeRolledBack = storeCreated
|
|
1505
|
+
? await specStore.deleteSpec(projectId, spec.id).catch(() => false)
|
|
1506
|
+
: true;
|
|
1507
|
+
await cleanupExecutionArtifact(getIdempotencyEvidencePath(resolvedPath, idempotencyKey), { force: true });
|
|
1263
1508
|
await cleanupExecutionArtifact(specDir, { recursive: true, force: true });
|
|
1509
|
+
try {
|
|
1510
|
+
operationJournal.resolveRecovery('create_spec', operationKey, {
|
|
1511
|
+
action: storeRolledBack ? 'rollback' : 'quarantine',
|
|
1512
|
+
reason: storeRolledBack
|
|
1513
|
+
? 'create_spec persistence failed and compensating cleanup completed'
|
|
1514
|
+
: 'create_spec persistence failed and the spec store rollback was incomplete',
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
catch {
|
|
1518
|
+
// The original persistence failure remains the primary error.
|
|
1519
|
+
}
|
|
1520
|
+
throw writeErr;
|
|
1264
1521
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
};
|
|
1522
|
+
return {
|
|
1523
|
+
ok: true,
|
|
1524
|
+
data: {
|
|
1525
|
+
committedResult,
|
|
1526
|
+
},
|
|
1527
|
+
};
|
|
1528
|
+
});
|
|
1273
1529
|
}); // end withTotalBudget critical path
|
|
1274
1530
|
// SPEC-713: Handle total budget exceeded — return clear error before 60s MCP timeout
|
|
1275
1531
|
if (criticalResult.timedOut) {
|
|
1276
1532
|
claimLifecycle.retainForInFlightWork = criticalResult.warning.includes('exceeded');
|
|
1533
|
+
if (!claimLifecycle.retainForInFlightWork) {
|
|
1534
|
+
rollbackUncommittedOperation(operationJournal, operationKey, 'create_spec timed out before commit with no in-flight work to preserve');
|
|
1535
|
+
}
|
|
1277
1536
|
// SPEC-770: Wait briefly for any in-flight spec write to complete, then scan for duplicates
|
|
1278
1537
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1279
|
-
const recentSpecs = await findRecentlyWrittenSpecs(resolvedPath, 2 * 60 * 1000);
|
|
1538
|
+
const recentSpecs = await findRecentlyWrittenSpecs(resolvedPath, 2 * 60 * 1000, resolvedInputParams.title);
|
|
1280
1539
|
const possibleDuplicates = recentSpecs.length > 0 ? recentSpecs : undefined;
|
|
1281
1540
|
const dupNote = possibleDuplicates
|
|
1282
1541
|
? `\n\nA spec may have been created server-side: ${possibleDuplicates.map((s) => s.id).join(', ')}. Retry with the same title to return it instead of creating a duplicate.`
|
|
@@ -1285,14 +1544,28 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1285
1544
|
content: [
|
|
1286
1545
|
{
|
|
1287
1546
|
type: 'text',
|
|
1288
|
-
text: `❌ create_spec exceeded
|
|
1547
|
+
text: `❌ create_spec exceeded its ${String(criticalResult.ceilingMs)}ms internal budget.\n\n${criticalResult.warning}${dupNote}`,
|
|
1289
1548
|
},
|
|
1290
1549
|
],
|
|
1291
1550
|
isError: true,
|
|
1292
1551
|
...(possibleDuplicates ? { structuredContent: { possibleDuplicates } } : {}),
|
|
1293
1552
|
};
|
|
1294
1553
|
}
|
|
1554
|
+
if (criticalResult.status === 'failed') {
|
|
1555
|
+
rollbackUncommittedOperation(operationJournal, operationKey, 'create_spec critical path failed before commit');
|
|
1556
|
+
return {
|
|
1557
|
+
content: [{ type: 'text', text: criticalResult.warning }],
|
|
1558
|
+
isError: true,
|
|
1559
|
+
structuredContent: {
|
|
1560
|
+
error: 'OPERATION_FAILED',
|
|
1561
|
+
category: criticalResult.category,
|
|
1562
|
+
elapsedMs: criticalResult.elapsedMs,
|
|
1563
|
+
persisted: false,
|
|
1564
|
+
},
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1295
1567
|
if (!criticalResult.value.ok) {
|
|
1568
|
+
rollbackUncommittedOperation(operationJournal, operationKey, 'create_spec returned early before commit');
|
|
1296
1569
|
return criticalResult.value.earlyReturn;
|
|
1297
1570
|
}
|
|
1298
1571
|
if (!claimLifecycle.committed) {
|
|
@@ -1306,9 +1579,11 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1306
1579
|
content: [{ type: 'text', text: ti('errors.internalError', { message }) }],
|
|
1307
1580
|
isError: true,
|
|
1308
1581
|
};
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1582
|
+
if (claimLifecycle.committed) {
|
|
1583
|
+
return finishRecoveredOperation(operationJournal, operationKey, errorResult);
|
|
1584
|
+
}
|
|
1585
|
+
rollbackUncommittedOperation(operationJournal, operationKey, 'create_spec failed before commit');
|
|
1586
|
+
return errorResult;
|
|
1312
1587
|
}
|
|
1313
1588
|
}); // end trackCost
|
|
1314
1589
|
}
|