@planu/cli 4.14.1 → 5.0.0
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 +35 -1
- 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 +2 -2
- 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 +1 -1
- 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 +10 -1
- package/dist/engine/evidence-gates/artifact-reader.js +115 -2
- 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/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/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 +10 -10
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +10 -10
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +10 -10
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +10 -10
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +10 -10
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +10 -10
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +10 -10
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +10 -10
- package/dist/engine/readiness-checker.d.ts +1 -16
- package/dist/engine/readiness-checker.js +27 -88
- 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 +77 -18
- 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/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/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 +27 -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 +10 -0
- 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.js +5 -5
- package/dist/tools/check-readiness.js +1 -1
- package/dist/tools/configure-telemetry.js +30 -2
- package/dist/tools/create-spec/spec-builder.js +0 -11
- package/dist/tools/create-spec.d.ts +1 -0
- package/dist/tools/create-spec.js +444 -239
- 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 +17 -8
- 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 +1 -1
- 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/done-receipt-verifier.js +40 -27
- package/dist/tools/update-status/evidence-gate.js +36 -1
- package/dist/tools/update-status/file-sync.d.ts +2 -1
- package/dist/tools/update-status/file-sync.js +57 -6
- package/dist/tools/update-status/index.d.ts +2 -0
- package/dist/tools/update-status/index.js +95 -58
- 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 +32 -15
- 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/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-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 +2 -6
- package/dist/types/index.js +1 -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 +14 -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 +29 -0
- package/dist/types/spec-format.d.ts +24 -0
- package/dist/types/telemetry.d.ts +4 -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 +2 -2
- package/package.json +16 -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
|
@@ -13,8 +13,8 @@ import { getAsyncAnalysisPath } from './create-spec/post-creation.js';
|
|
|
13
13
|
import { extractCriteria, generateLeanSpecContent, } from '../engine/spec-format/lean-spec-generator.js';
|
|
14
14
|
import { generateLeanTechnicalContent, } from '../engine/spec-format/lean-technical-generator.js';
|
|
15
15
|
import { extractFilesFromSpecBody } from '../engine/spec-format/technical-md-populator.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { buildCanonicalUnifiedSpecContent, validateUnifiedSpecCandidate, } from '../engine/spec-format/unified-spec-builder.js';
|
|
17
|
+
import { buildImplementationContractSection } from '../engine/implementation-contract/index.js';
|
|
18
18
|
import { validateEnglishOnlySpecText } from '../engine/spec-language/english-only.js';
|
|
19
19
|
import { FallbackGenerator } from '../engine/spec-generator/index.js';
|
|
20
20
|
import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/autopilot-analyzer.js';
|
|
@@ -31,6 +31,8 @@ import { buildCriterionGroundingRecords, filterGroundedCriteria, getAdvisoryCrit
|
|
|
31
31
|
import { checkGenericSpecOutput } from '../engine/spec-quality/generic-output-gate.js';
|
|
32
32
|
import { calculateActionableSpecMetrics, shouldExposeMetric, } from '../engine/spec-metrics/actionable-metrics.js';
|
|
33
33
|
import { RuntimeDatabase } from '../storage/runtime-db.js';
|
|
34
|
+
import { BoundaryFailure } from '../errors/error-taxonomy.js';
|
|
35
|
+
import { SpecAcFormatEnum, SpecScopeEnum, SpecTargetEnum, SpecTypeEnum } from './schemas/spec.js';
|
|
34
36
|
import { SPEC_CREATED_POST_COMMIT_TASKS } from '../types/outbox-worker.js';
|
|
35
37
|
import { resolveStorageLayout } from '../storage/storage-layout.js';
|
|
36
38
|
import { OperationJournal, recoverOperationJournal, } from '../engine/execution/operation-journal.js';
|
|
@@ -185,8 +187,15 @@ function buildAdvisoryCompat() {
|
|
|
185
187
|
};
|
|
186
188
|
}
|
|
187
189
|
/** Persist only out-of-scope items supplied explicitly by the caller. */
|
|
188
|
-
function resolveOutOfScope(provided) {
|
|
189
|
-
|
|
190
|
+
function resolveOutOfScope(provided, source = '') {
|
|
191
|
+
const explicit = provided?.filter((item) => item.trim().length > 0) ?? [];
|
|
192
|
+
if (explicit.length > 0) {
|
|
193
|
+
return explicit;
|
|
194
|
+
}
|
|
195
|
+
const section = /(?:^|\n)##\s+Out of scope\s*\n([\s\S]*?)(?=\n##\s|$)/i.exec(source)?.[1];
|
|
196
|
+
return section
|
|
197
|
+
? Array.from(section.matchAll(/^[-*]\s+(.+)$/gm), (match) => match[1]?.trim() ?? '').filter(Boolean)
|
|
198
|
+
: [];
|
|
190
199
|
}
|
|
191
200
|
/** SPEC-783: Synthesize agent team findings into unified spec.md ## Technical section. */
|
|
192
201
|
async function handleAgentTeamSynthesis(specId, projectPath, findings) {
|
|
@@ -330,9 +339,12 @@ function buildCommittedCreateResult(input) {
|
|
|
330
339
|
deprecatedAlias: 'riskWarning',
|
|
331
340
|
}));
|
|
332
341
|
}
|
|
333
|
-
return toolResult(`Spec ${spec.id} created and
|
|
342
|
+
return toolResult(`Spec ${spec.id} created and persisted.`, {
|
|
334
343
|
operationId: input.operationId,
|
|
344
|
+
/** @deprecated Operation-journal durability; this is not Git evidence. */
|
|
335
345
|
committed: true,
|
|
346
|
+
persisted: true,
|
|
347
|
+
gitCommitPerformed: false,
|
|
336
348
|
pendingAnalysis: true,
|
|
337
349
|
specId: spec.id,
|
|
338
350
|
title: spec.title,
|
|
@@ -711,28 +723,27 @@ async function buildIdempotencyMatchResult(projectPath, existing) {
|
|
|
711
723
|
};
|
|
712
724
|
}
|
|
713
725
|
const FRONTMATTER_ID_RE = /^id:\s*(SPEC-\d+)/m;
|
|
714
|
-
const FRONTMATTER_TITLE_RE = /^title:\s*"?([^"\n]+?)"?\s*$/m;
|
|
715
726
|
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)
|
|
727
|
+
async function findStoreByIdempotencyKey(projectId, key, cutoff) {
|
|
720
728
|
try {
|
|
721
729
|
const existing = await specStore.listSpecs(projectId);
|
|
722
|
-
|
|
723
|
-
if (match !== undefined) {
|
|
724
|
-
return match;
|
|
725
|
-
}
|
|
730
|
+
return existing.find((s) => s.idempotencyKey === key && new Date(s.createdAt).getTime() > cutoff);
|
|
726
731
|
}
|
|
727
732
|
catch {
|
|
728
|
-
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/** SPEC-770: Scan store and filesystem for a spec with matching idempotency key within windowMs. */
|
|
737
|
+
async function findByIdempotencyKey(projectPath, key, projectId, windowMs = IDEMPOTENCY_MATCH_WINDOW_MS) {
|
|
738
|
+
const cutoff = Date.now() - windowMs;
|
|
739
|
+
const storeMatch = await findStoreByIdempotencyKey(projectId, key, cutoff);
|
|
740
|
+
if (storeMatch) {
|
|
741
|
+
return storeMatch;
|
|
729
742
|
}
|
|
730
|
-
// Transactional fallback: the request key stays in external project data, not spec.md.
|
|
731
743
|
const evidenceMatch = await findByIdempotencyEvidence(projectPath, key, cutoff);
|
|
732
744
|
if (evidenceMatch !== undefined) {
|
|
733
745
|
return evidenceMatch;
|
|
734
746
|
}
|
|
735
|
-
// Compatibility fallback for specs created before external evidence was introduced.
|
|
736
747
|
try {
|
|
737
748
|
const { glob } = await import('glob');
|
|
738
749
|
const { join: joinPath } = await import('node:path');
|
|
@@ -762,7 +773,7 @@ async function findByIdempotencyKey(projectPath, key, projectId, windowMs = IDEM
|
|
|
762
773
|
return undefined;
|
|
763
774
|
}
|
|
764
775
|
/** SPEC-770: Scan for recently written spec.md files (for possibleDuplicates in timeout response). */
|
|
765
|
-
async function findRecentlyWrittenSpecs(projectPath, windowMs) {
|
|
776
|
+
async function findRecentlyWrittenSpecs(projectPath, windowMs, requestedTitle) {
|
|
766
777
|
try {
|
|
767
778
|
const { glob } = await import('glob');
|
|
768
779
|
const { join: joinPath } = await import('node:path');
|
|
@@ -776,9 +787,11 @@ async function findRecentlyWrittenSpecs(projectPath, windowMs) {
|
|
|
776
787
|
}
|
|
777
788
|
const content = await readFile(specFile, 'utf-8').catch(() => '');
|
|
778
789
|
const idMatch = FRONTMATTER_ID_RE.exec(content);
|
|
779
|
-
const
|
|
780
|
-
if (idMatch?.[1] !== undefined
|
|
781
|
-
|
|
790
|
+
const title = extractFrontmatterTitle(content);
|
|
791
|
+
if (idMatch?.[1] !== undefined &&
|
|
792
|
+
normalizeDuplicateTitle(title) !== '' &&
|
|
793
|
+
normalizeDuplicateTitle(title) === normalizeDuplicateTitle(requestedTitle)) {
|
|
794
|
+
results.push({ id: idMatch[1], title });
|
|
782
795
|
}
|
|
783
796
|
}
|
|
784
797
|
return results;
|
|
@@ -787,6 +800,69 @@ async function findRecentlyWrittenSpecs(projectPath, windowMs) {
|
|
|
787
800
|
return [];
|
|
788
801
|
}
|
|
789
802
|
}
|
|
803
|
+
function extractFrontmatterTitle(content) {
|
|
804
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1];
|
|
805
|
+
const rawTitle = frontmatter ? /^title:\s*(.*?)\s*$/m.exec(frontmatter)?.[1] : undefined;
|
|
806
|
+
if (!rawTitle) {
|
|
807
|
+
return '';
|
|
808
|
+
}
|
|
809
|
+
if (rawTitle.startsWith('"') && rawTitle.endsWith('"')) {
|
|
810
|
+
try {
|
|
811
|
+
const parsed = JSON.parse(rawTitle);
|
|
812
|
+
return typeof parsed === 'string' ? parsed : '';
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
return '';
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (rawTitle.startsWith("'") && rawTitle.endsWith("'")) {
|
|
819
|
+
return rawTitle.slice(1, -1).replace(/''/g, "'");
|
|
820
|
+
}
|
|
821
|
+
return rawTitle;
|
|
822
|
+
}
|
|
823
|
+
function normalizeDuplicateTitle(value) {
|
|
824
|
+
return value
|
|
825
|
+
.normalize('NFKC')
|
|
826
|
+
.toLocaleLowerCase('en-US')
|
|
827
|
+
.replace(/[\p{P}]+/gu, ' ')
|
|
828
|
+
.replace(/\s+/g, ' ')
|
|
829
|
+
.trim();
|
|
830
|
+
}
|
|
831
|
+
const CREATE_SPEC_ENUMS = {
|
|
832
|
+
type: SpecTypeEnum,
|
|
833
|
+
scope: SpecScopeEnum,
|
|
834
|
+
target: SpecTargetEnum,
|
|
835
|
+
acFormat: SpecAcFormatEnum,
|
|
836
|
+
};
|
|
837
|
+
function validateCreateSpecEnums(input) {
|
|
838
|
+
for (const [field, schema] of Object.entries(CREATE_SPEC_ENUMS)) {
|
|
839
|
+
const received = input[field];
|
|
840
|
+
if (received === undefined || schema.safeParse(received).success) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const boundedReceived = received === null || ['string', 'number', 'boolean'].includes(typeof received)
|
|
844
|
+
? received
|
|
845
|
+
: typeof received;
|
|
846
|
+
return {
|
|
847
|
+
content: [
|
|
848
|
+
{
|
|
849
|
+
type: 'text',
|
|
850
|
+
text: `${field} must be one of: ${schema.options.join(', ')}.`,
|
|
851
|
+
},
|
|
852
|
+
],
|
|
853
|
+
isError: true,
|
|
854
|
+
structuredContent: {
|
|
855
|
+
error: 'INVALID_INPUT',
|
|
856
|
+
code: 'INVALID_INPUT',
|
|
857
|
+
status: 422,
|
|
858
|
+
field,
|
|
859
|
+
received: boundedReceived,
|
|
860
|
+
allowed: schema.options,
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
790
866
|
const DESCRIPTION_MAX_CHARS = 10_000;
|
|
791
867
|
const DESCRIPTION_EXCEED_MSG = 'Description exceeds 10000 chars. Create with a summary (<3k) and use reconcile_spec to add the rest.';
|
|
792
868
|
function checkEnglishOnlyInput(params) {
|
|
@@ -811,8 +887,260 @@ function checkEnglishOnlyInput(params) {
|
|
|
811
887
|
},
|
|
812
888
|
};
|
|
813
889
|
}
|
|
890
|
+
function quotePosixShellArgument(value) {
|
|
891
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
892
|
+
}
|
|
893
|
+
function earlyPreparation(result, clarificationProjectId) {
|
|
894
|
+
return {
|
|
895
|
+
kind: 'early',
|
|
896
|
+
result,
|
|
897
|
+
...(clarificationProjectId ? { clarificationProjectId } : {}),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
export async function resolveGroundedVerificationCommands(projectPath, testPaths) {
|
|
901
|
+
if (testPaths.length === 0) {
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
904
|
+
const packageJsonRaw = await readFile(pathJoin(projectPath, 'package.json'), 'utf8').catch(() => '');
|
|
905
|
+
if (!packageJsonRaw) {
|
|
906
|
+
return [];
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const manifest = JSON.parse(packageJsonRaw);
|
|
910
|
+
if (typeof manifest.packageManager !== 'string') {
|
|
911
|
+
return [];
|
|
912
|
+
}
|
|
913
|
+
const packageManager = /^(pnpm|npm|yarn|bun)@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.exec(manifest.packageManager)?.[1];
|
|
914
|
+
if (!packageManager) {
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
if (typeof manifest.scripts?.test !== 'string' || manifest.scripts.test.trim() === '') {
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
return [`${packageManager} run test -- ${testPaths.map(quotePosixShellArgument).join(' ')}`];
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
return [];
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async function prepareCreateSpecCandidate(initialParams, server) {
|
|
927
|
+
let params = initialParams;
|
|
928
|
+
const buildResult = await measureStep('buildSpecContext', () => buildSpecContext(params));
|
|
929
|
+
if (!buildResult.ok) {
|
|
930
|
+
return earlyPreparation({
|
|
931
|
+
content: [{ type: 'text', text: buildResult.errorMessage }],
|
|
932
|
+
isError: true,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
const context = buildResult.context;
|
|
936
|
+
const { spec, estimation, projectId } = context;
|
|
937
|
+
const knowledge = await measureStep('loadKnowledge', () => knowledgeStore.getKnowledge(projectId));
|
|
938
|
+
const clarificationSession = params.clarificationSessionId
|
|
939
|
+
? await knowledgeStore.getClarification(projectId, params.clarificationSessionId)
|
|
940
|
+
: null;
|
|
941
|
+
const constitutionCheck = await measureStep('validateConstitution', () => validateConstitution(projectId, spec.title, spec.tags));
|
|
942
|
+
if (constitutionCheck.blocked && constitutionCheck.errorResult) {
|
|
943
|
+
return earlyPreparation(constitutionCheck.errorResult);
|
|
944
|
+
}
|
|
945
|
+
const inlineAnalysis = await withBudget('create-spec-inline-analysis', getRuntimePolicy().createSpec.inlineAnalysisBudgetMs, (signal) => analyzeProjectForSpec(params.projectPath ?? '', params.description, spec.title, knowledge, signal));
|
|
946
|
+
const autopilot = unwrapBudget(inlineAnalysis, getEmptyAutopilotResult());
|
|
947
|
+
if (autopilot.isIdea) {
|
|
948
|
+
return earlyPreparation({
|
|
949
|
+
content: [
|
|
950
|
+
{
|
|
951
|
+
type: 'text',
|
|
952
|
+
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.`,
|
|
953
|
+
},
|
|
954
|
+
],
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
const clarificationResult = handleClarification(server, params.description, knowledge, autopilot, params);
|
|
958
|
+
if (clarificationResult !== null) {
|
|
959
|
+
if ('earlyReturn' in clarificationResult) {
|
|
960
|
+
return earlyPreparation(clarificationResult.earlyReturn, projectId);
|
|
961
|
+
}
|
|
962
|
+
params = clarificationResult.params;
|
|
963
|
+
}
|
|
964
|
+
const filteredCriteria = filterGroundedCriteria(autopilot.suggestedCriteria);
|
|
965
|
+
const technologyContract = await readTechnologySelectionContract(params.projectPath ?? '');
|
|
966
|
+
const contractNote = technologyContract
|
|
967
|
+
? [
|
|
968
|
+
'',
|
|
969
|
+
'Technology Contract:',
|
|
970
|
+
`- Mode: ${technologyContract.mode}`,
|
|
971
|
+
technologyContract.language ? `- Language: ${technologyContract.language}` : '',
|
|
972
|
+
technologyContract.framework
|
|
973
|
+
? `- Framework: ${technologyContract.framework}`
|
|
974
|
+
: '- Framework: none or not selected',
|
|
975
|
+
technologyContract.platform ? `- Platform: ${technologyContract.platform}` : '',
|
|
976
|
+
technologyContract.projectType ? `- Project type: ${technologyContract.projectType}` : '',
|
|
977
|
+
'- Agents must not choose a different stack unless the user approves a new contract.',
|
|
978
|
+
]
|
|
979
|
+
.filter(Boolean)
|
|
980
|
+
.join('\n')
|
|
981
|
+
: '';
|
|
982
|
+
const generatedSpec = await measureStep('generateSpecBody', () => new FallbackGenerator().generate({
|
|
983
|
+
title: spec.title,
|
|
984
|
+
description: `${params.description}${contractNote}`,
|
|
985
|
+
type: spec.type,
|
|
986
|
+
scope: spec.scope,
|
|
987
|
+
target: spec.target,
|
|
988
|
+
acFormat: params.acFormat,
|
|
989
|
+
projectContext: {
|
|
990
|
+
language: technologyContract?.language ?? knowledge?.language ?? undefined,
|
|
991
|
+
framework: technologyContract?.framework ?? knowledge?.framework ?? undefined,
|
|
992
|
+
architecture: knowledge?.architecture.primary ?? undefined,
|
|
993
|
+
},
|
|
994
|
+
}));
|
|
995
|
+
spec.generation = generatedSpec.generation;
|
|
996
|
+
spec.qualityWarnings =
|
|
997
|
+
generatedSpec.qualityWarnings.length > 0 ? generatedSpec.qualityWarnings : undefined;
|
|
998
|
+
const baseCriteria = extractCriteria(generatedSpec.specBody).map((criterion) => criterion.text);
|
|
999
|
+
const groundingCriteria = buildCriterionGroundingRecords({
|
|
1000
|
+
criteria: [...baseCriteria, ...filteredCriteria],
|
|
1001
|
+
userInput: params.description,
|
|
1002
|
+
generatedEvidence: [generatedSpec.generation.modelId ?? generatedSpec.generation.method],
|
|
1003
|
+
});
|
|
1004
|
+
const contractCriteria = getContractCriteria(groundingCriteria);
|
|
1005
|
+
const advisoryCriteria = getAdvisoryCriteria(groundingCriteria).map((record) => record.text);
|
|
1006
|
+
if (contractCriteria.length === 0) {
|
|
1007
|
+
return earlyPreparation({
|
|
1008
|
+
content: [
|
|
1009
|
+
{
|
|
1010
|
+
type: 'text',
|
|
1011
|
+
text: 'create_spec did not persist a spec because no acceptance criterion was grounded in the user request. ' +
|
|
1012
|
+
'Needs decision: provide at least one explicit, verifiable criterion as a checkbox or Given/When/Then outcome.',
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
isError: true,
|
|
1016
|
+
structuredContent: {
|
|
1017
|
+
error: 'MISSING_GROUNDED_ACCEPTANCE_CRITERIA',
|
|
1018
|
+
missingDecision: 'Provide at least one explicit, verifiable acceptance criterion.',
|
|
1019
|
+
persisted: false,
|
|
1020
|
+
},
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
const actionableMetrics = calculateActionableSpecMetrics({
|
|
1024
|
+
criteria: [...baseCriteria, ...filteredCriteria],
|
|
1025
|
+
groundingRecords: groundingCriteria,
|
|
1026
|
+
}).filter(shouldExposeMetric);
|
|
1027
|
+
const technicalFiles = await measureStep('resolveTechnicalFiles', () => resolveTechnicalFiles({
|
|
1028
|
+
generatedSpecBody: generatedSpec.specBody,
|
|
1029
|
+
generatedTechnicalSection: generatedSpec.technicalSection,
|
|
1030
|
+
projectPath: params.projectPath ?? '',
|
|
1031
|
+
}));
|
|
1032
|
+
const groundedTechnical = await measureStep('groundTechnicalFiles', () => groundTechnicalFiles({
|
|
1033
|
+
files: technicalFiles,
|
|
1034
|
+
projectPath: params.projectPath ?? '',
|
|
1035
|
+
userInput: params.description,
|
|
1036
|
+
autopilot,
|
|
1037
|
+
}));
|
|
1038
|
+
const outOfScope = resolveOutOfScope(params.outOfScope, params.description);
|
|
1039
|
+
const scenarioTestPaths = groundedTechnical.files.test.map((file) => file.path);
|
|
1040
|
+
const criteria = contractCriteria.map((record) => ({ text: record.text, done: false }));
|
|
1041
|
+
const leanSpec = generateLeanSpecContent({
|
|
1042
|
+
spec,
|
|
1043
|
+
description: generatedSpec.specBody,
|
|
1044
|
+
estimation,
|
|
1045
|
+
criteriaOverride: criteria,
|
|
1046
|
+
groundingCriteria: contractCriteria,
|
|
1047
|
+
groundingTechnicalReferences: groundedTechnical.records,
|
|
1048
|
+
acFormat: params.acFormat,
|
|
1049
|
+
scenarioTestPaths,
|
|
1050
|
+
});
|
|
1051
|
+
const leanTechnical = generateLeanTechnicalContent({
|
|
1052
|
+
specId: spec.id,
|
|
1053
|
+
filesToCreate: groundedTechnical.files.create,
|
|
1054
|
+
filesToModify: groundedTechnical.files.modify,
|
|
1055
|
+
filesToTest: groundedTechnical.files.test,
|
|
1056
|
+
includeDecisionRequiredPlaceholder: spec.scope !== 'trivial',
|
|
1057
|
+
});
|
|
1058
|
+
const verificationCommands = await resolveGroundedVerificationCommands(params.projectPath ?? '', scenarioTestPaths);
|
|
1059
|
+
const implementationContract = buildImplementationContractSection({
|
|
1060
|
+
description: generatedSpec.specBody,
|
|
1061
|
+
criteria,
|
|
1062
|
+
files: groundedTechnical.files,
|
|
1063
|
+
outOfScope,
|
|
1064
|
+
verificationCommands,
|
|
1065
|
+
});
|
|
1066
|
+
const unifiedSpec = buildCanonicalUnifiedSpecContent({
|
|
1067
|
+
leanSpecBody: leanSpec,
|
|
1068
|
+
sourceDescription: params.description,
|
|
1069
|
+
technicalBody: leanTechnical,
|
|
1070
|
+
implementationContract,
|
|
1071
|
+
criteria,
|
|
1072
|
+
files: groundedTechnical.files,
|
|
1073
|
+
outOfScope,
|
|
1074
|
+
});
|
|
1075
|
+
const candidateValidation = validateUnifiedSpecCandidate(unifiedSpec, {
|
|
1076
|
+
groundedFilePaths: groundedTechnical.records.map((record) => record.path),
|
|
1077
|
+
});
|
|
1078
|
+
if (!candidateValidation.valid) {
|
|
1079
|
+
return earlyPreparation({
|
|
1080
|
+
content: [
|
|
1081
|
+
{
|
|
1082
|
+
type: 'text',
|
|
1083
|
+
text: `Unified spec candidate is invalid: ${candidateValidation.issues
|
|
1084
|
+
.slice(0, 3)
|
|
1085
|
+
.map((issue) => issue.message)
|
|
1086
|
+
.join('; ')}`,
|
|
1087
|
+
},
|
|
1088
|
+
],
|
|
1089
|
+
isError: true,
|
|
1090
|
+
structuredContent: {
|
|
1091
|
+
error: 'SPEC_FORMAT_INVALID',
|
|
1092
|
+
code: 422,
|
|
1093
|
+
persisted: false,
|
|
1094
|
+
issues: candidateValidation.issues,
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
|
|
1099
|
+
if (!genericOutputGate.passed) {
|
|
1100
|
+
return earlyPreparation({
|
|
1101
|
+
content: [
|
|
1102
|
+
{
|
|
1103
|
+
type: 'text',
|
|
1104
|
+
text: 'Spec quality gate blocked generic output before persistence. ' +
|
|
1105
|
+
genericOutputGate.issues
|
|
1106
|
+
.slice(0, 3)
|
|
1107
|
+
.map((issue) => `${issue.phrase}: ${issue.reason}`)
|
|
1108
|
+
.join('; '),
|
|
1109
|
+
},
|
|
1110
|
+
],
|
|
1111
|
+
isError: true,
|
|
1112
|
+
structuredContent: {
|
|
1113
|
+
error: 'GENERIC_SPEC_OUTPUT_BLOCKED',
|
|
1114
|
+
sddConstitutionRuleId: 'sdd.no-generic-output',
|
|
1115
|
+
issues: genericOutputGate.issues,
|
|
1116
|
+
fixHint: 'Replace generic criteria or placeholder references with grounded, testable behavior.',
|
|
1117
|
+
},
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
if (outOfScope.length > 0) {
|
|
1121
|
+
spec.outOfScope = outOfScope;
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
kind: 'ready',
|
|
1125
|
+
prepared: {
|
|
1126
|
+
params,
|
|
1127
|
+
context,
|
|
1128
|
+
knowledge,
|
|
1129
|
+
clarificationSession,
|
|
1130
|
+
constitutionWarnings: constitutionCheck.warnings,
|
|
1131
|
+
advisoryCriteria,
|
|
1132
|
+
actionableMetrics,
|
|
1133
|
+
unifiedSpec,
|
|
1134
|
+
simplicityResult: runSimplicityCheck([params.description, ...filteredCriteria].join('\n'), estimation.devHours),
|
|
1135
|
+
},
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
814
1138
|
// eslint-disable-next-line max-lines-per-function
|
|
815
1139
|
export async function handleCreateSpec(inputParams, server) {
|
|
1140
|
+
const enumValidation = validateCreateSpecEnums(inputParams);
|
|
1141
|
+
if (enumValidation) {
|
|
1142
|
+
return enumValidation;
|
|
1143
|
+
}
|
|
816
1144
|
if (inputParams.idempotencyKey !== undefined &&
|
|
817
1145
|
!/^[A-Za-z0-9._:-]{8,128}$/.test(inputParams.idempotencyKey)) {
|
|
818
1146
|
return {
|
|
@@ -870,19 +1198,60 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
870
1198
|
if (languageGate) {
|
|
871
1199
|
return languageGate;
|
|
872
1200
|
}
|
|
1201
|
+
// SPEC-584: Gate check — block if pending clarification token exists and no answers provided
|
|
1202
|
+
const gateError = await runClarificationGate(resolvedPath, 'create_spec', inputParams.clarificationAnswers);
|
|
1203
|
+
if (gateError) {
|
|
1204
|
+
return gateError;
|
|
1205
|
+
}
|
|
1206
|
+
const idempotencyKey = computeIdempotencyKey(inputParams.title, resolvedPath, inputParams.idempotencyKey);
|
|
1207
|
+
const cutoff = Date.now() - IDEMPOTENCY_MATCH_WINDOW_MS;
|
|
1208
|
+
const storeOnlyCandidate = await findStoreByIdempotencyKey(hashProjectPath(resolvedPath), idempotencyKey, cutoff);
|
|
1209
|
+
let preparation;
|
|
1210
|
+
try {
|
|
1211
|
+
preparation = await prepareCreateSpecCandidate(resolvedInputParams, server);
|
|
1212
|
+
}
|
|
1213
|
+
catch (error) {
|
|
1214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1215
|
+
return {
|
|
1216
|
+
content: [{ type: 'text', text: ti('errors.internalError', { message }) }],
|
|
1217
|
+
isError: true,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
if (preparation.kind === 'early') {
|
|
1221
|
+
if (preparation.clarificationProjectId) {
|
|
1222
|
+
await persistClarificationToken(preparation.result, preparation.clarificationProjectId, 'create_spec');
|
|
1223
|
+
}
|
|
1224
|
+
return preparation.result;
|
|
1225
|
+
}
|
|
1226
|
+
const prepared = preparation.prepared;
|
|
1227
|
+
prepared.context.spec.idempotencyKey = idempotencyKey;
|
|
1228
|
+
if (storeOnlyCandidate) {
|
|
1229
|
+
const committedEvidence = await findByIdempotencyEvidence(resolvedPath, idempotencyKey, cutoff);
|
|
1230
|
+
if (!committedEvidence) {
|
|
1231
|
+
return {
|
|
1232
|
+
content: [
|
|
1233
|
+
{
|
|
1234
|
+
type: 'text',
|
|
1235
|
+
text: `A partial create_spec transaction exists for ${storeOnlyCandidate.id}: the store row has no committed idempotency evidence. Recovery is required before retrying.`,
|
|
1236
|
+
},
|
|
1237
|
+
],
|
|
1238
|
+
isError: true,
|
|
1239
|
+
structuredContent: {
|
|
1240
|
+
error: 'PARTIAL_IDEMPOTENCY_STATE',
|
|
1241
|
+
code: 409,
|
|
1242
|
+
persisted: false,
|
|
1243
|
+
specId: storeOnlyCandidate.id,
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
873
1248
|
// SPEC-665: Fire-and-forget TTL refresh for conventions.json (never blocks create_spec)
|
|
874
1249
|
void import('../engine/conventions-ttl.js').then(({ triggerConventionsTtlRefresh }) => {
|
|
875
1250
|
void triggerConventionsTtlRefresh(resolvedPath).catch(() => {
|
|
876
1251
|
/* best-effort */
|
|
877
1252
|
});
|
|
878
1253
|
});
|
|
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
1254
|
// SPEC-770: Idempotency check — return existing spec if same title+projectPath within 10 minutes
|
|
885
|
-
const idempotencyKey = computeIdempotencyKey(inputParams.title, resolvedPath, inputParams.idempotencyKey);
|
|
886
1255
|
const operationKey = computeOperationKey(resolvedInputParams, resolvedPath, idempotencyKey);
|
|
887
1256
|
const runtimeDatabase = new RuntimeDatabase({ path: resolveStorageLayout().runtimeDatabase });
|
|
888
1257
|
const operationJournal = new OperationJournal(runtimeDatabase, createHash('sha256').update(hashProjectPath(resolvedPath)).digest('hex').slice(0, 32));
|
|
@@ -915,6 +1284,25 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
915
1284
|
}
|
|
916
1285
|
catch (error) {
|
|
917
1286
|
runtimeDatabase.close();
|
|
1287
|
+
if (error instanceof BoundaryFailure &&
|
|
1288
|
+
error.kind === 'Conflict' &&
|
|
1289
|
+
error.operation === 'operation begin') {
|
|
1290
|
+
return {
|
|
1291
|
+
content: [
|
|
1292
|
+
{
|
|
1293
|
+
type: 'text',
|
|
1294
|
+
text: 'The idempotency key is already bound to a different create_spec request.',
|
|
1295
|
+
},
|
|
1296
|
+
],
|
|
1297
|
+
isError: true,
|
|
1298
|
+
structuredContent: {
|
|
1299
|
+
error: 'IDEMPOTENCY_CONFLICT',
|
|
1300
|
+
code: 'IDEMPOTENCY_CONFLICT',
|
|
1301
|
+
status: 409,
|
|
1302
|
+
persisted: false,
|
|
1303
|
+
},
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
918
1306
|
throw error;
|
|
919
1307
|
}
|
|
920
1308
|
if ((journalEntry.state === 'committed' || journalEntry.state === 'acknowledged') &&
|
|
@@ -984,216 +1372,14 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
984
1372
|
idempotencyKey,
|
|
985
1373
|
ownerId: idempotencyClaim.ownerId,
|
|
986
1374
|
});
|
|
987
|
-
// eslint-disable-next-line max-lines-per-function -- cohesive transactional create boundary
|
|
988
1375
|
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
1376
|
try {
|
|
993
1377
|
// SPEC-713: Phase 1 — Critical path: build + persist spec within 25s hard ceiling.
|
|
994
1378
|
// Only the steps required to produce spec.md on disk are inside this budget.
|
|
995
1379
|
// Durable post-commit work is delegated to the spec.created outbox consumer.
|
|
996
1380
|
const criticalResult = await withTotalBudget('create_spec-critical', getRuntimePolicy().createSpec.criticalTimeoutMs, async (criticalSignal) => {
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
if (!buildResult.ok) {
|
|
1000
|
-
return {
|
|
1001
|
-
ok: false,
|
|
1002
|
-
earlyReturn: {
|
|
1003
|
-
content: [{ type: 'text', text: buildResult.errorMessage }],
|
|
1004
|
-
isError: true,
|
|
1005
|
-
},
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
const { spec, specDir, specPath, estimation, splitSuggestion, duplicate, projectId } = buildResult.context;
|
|
1009
|
-
// Kept in external store/evidence for retries; the value-only serializer omits it.
|
|
1010
|
-
spec.idempotencyKey = idempotencyKey;
|
|
1011
|
-
// Load project knowledge and clarification session
|
|
1012
|
-
const knowledge = await measureStep('loadKnowledge', () => knowledgeStore.getKnowledge(projectId));
|
|
1013
|
-
const clarificationSession = clarificationSessionId
|
|
1014
|
-
? await knowledgeStore.getClarification(projectId, clarificationSessionId)
|
|
1015
|
-
: null;
|
|
1016
|
-
// Validate against Constitution
|
|
1017
|
-
const constitutionCheck = await measureStep('validateConstitution', () => validateConstitution(projectId, spec.title, spec.tags));
|
|
1018
|
-
if (constitutionCheck.blocked && constitutionCheck.errorResult) {
|
|
1019
|
-
return { ok: false, earlyReturn: constitutionCheck.errorResult };
|
|
1020
|
-
}
|
|
1021
|
-
// Full project-wide analysis is delivered by the durable spec.created outbox consumer.
|
|
1022
|
-
// A tiny inline budget preserves deterministic idea/clarification behavior when the
|
|
1023
|
-
// answer is immediately available without allowing repository scans to delay ACK.
|
|
1024
|
-
const inlineAnalysis = await withBudget('create-spec-inline-analysis', getRuntimePolicy().createSpec.inlineAnalysisBudgetMs, (signal) => analyzeProjectForSpec(params.projectPath ?? '', description, spec.title, knowledge, signal));
|
|
1025
|
-
const autopilot = unwrapBudget(inlineAnalysis, getEmptyAutopilotResult());
|
|
1026
|
-
// If very vague (<10 words), capture as idea instead of spec
|
|
1027
|
-
if (autopilot.isIdea) {
|
|
1028
|
-
return {
|
|
1029
|
-
ok: false,
|
|
1030
|
-
earlyReturn: {
|
|
1031
|
-
content: [
|
|
1032
|
-
{
|
|
1033
|
-
type: 'text',
|
|
1034
|
-
text: `💡 **Idea captured**: "${description}"\n\nThe description is too brief for a full spec. Use \`capture_idea\` to save it to the backlog, or provide more detail (>10 words) to create a spec.`,
|
|
1035
|
-
},
|
|
1036
|
-
],
|
|
1037
|
-
},
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
// SPEC-463 / SPEC-471: Interactive clarification
|
|
1041
|
-
const clarificationResult = handleClarification(server, description, knowledge, autopilot, params);
|
|
1042
|
-
if (clarificationResult !== null) {
|
|
1043
|
-
if ('earlyReturn' in clarificationResult) {
|
|
1044
|
-
// SPEC-584: Persist token so the gate blocks retries without answers
|
|
1045
|
-
await persistClarificationToken(clarificationResult.earlyReturn, projectId, 'create_spec');
|
|
1046
|
-
return { ok: false, earlyReturn: clarificationResult.earlyReturn };
|
|
1047
|
-
}
|
|
1048
|
-
params = clarificationResult.params;
|
|
1049
|
-
}
|
|
1050
|
-
const filteredCriteria = filterGroundedCriteria(autopilot.suggestedCriteria);
|
|
1051
|
-
const technologyContract = await readTechnologySelectionContract(params.projectPath ?? '');
|
|
1052
|
-
const contractNote = technologyContract
|
|
1053
|
-
? [
|
|
1054
|
-
'',
|
|
1055
|
-
'Technology Contract:',
|
|
1056
|
-
`- Mode: ${technologyContract.mode}`,
|
|
1057
|
-
technologyContract.language ? `- Language: ${technologyContract.language}` : '',
|
|
1058
|
-
technologyContract.framework
|
|
1059
|
-
? `- Framework: ${technologyContract.framework}`
|
|
1060
|
-
: '- Framework: none or not selected',
|
|
1061
|
-
technologyContract.platform ? `- Platform: ${technologyContract.platform}` : '',
|
|
1062
|
-
technologyContract.projectType
|
|
1063
|
-
? `- Project type: ${technologyContract.projectType}`
|
|
1064
|
-
: '',
|
|
1065
|
-
'- Agents must not choose a different stack unless the user approves a new contract.',
|
|
1066
|
-
]
|
|
1067
|
-
.filter(Boolean)
|
|
1068
|
-
.join('\n')
|
|
1069
|
-
: '';
|
|
1070
|
-
const specGenerator = new FallbackGenerator();
|
|
1071
|
-
const generatedSpec = await measureStep('generateSpecBody', () => specGenerator.generate({
|
|
1072
|
-
title: spec.title,
|
|
1073
|
-
description: `${description}${contractNote}`,
|
|
1074
|
-
type: spec.type,
|
|
1075
|
-
scope: spec.scope,
|
|
1076
|
-
target: spec.target,
|
|
1077
|
-
acFormat: params.acFormat,
|
|
1078
|
-
projectContext: {
|
|
1079
|
-
language: technologyContract?.language ?? knowledge?.language ?? undefined,
|
|
1080
|
-
framework: technologyContract?.framework ?? knowledge?.framework ?? undefined,
|
|
1081
|
-
architecture: knowledge?.architecture.primary ?? undefined,
|
|
1082
|
-
},
|
|
1083
|
-
}));
|
|
1084
|
-
spec.generation = generatedSpec.generation;
|
|
1085
|
-
spec.qualityWarnings =
|
|
1086
|
-
generatedSpec.qualityWarnings.length > 0 ? generatedSpec.qualityWarnings : undefined;
|
|
1087
|
-
const baseCriteria = extractCriteria(generatedSpec.specBody).map((criterion) => criterion.text);
|
|
1088
|
-
const groundingCriteria = buildCriterionGroundingRecords({
|
|
1089
|
-
criteria: [...baseCriteria, ...filteredCriteria],
|
|
1090
|
-
userInput: description,
|
|
1091
|
-
generatedEvidence: [
|
|
1092
|
-
generatedSpec.generation.modelId ?? generatedSpec.generation.method,
|
|
1093
|
-
],
|
|
1094
|
-
});
|
|
1095
|
-
const contractCriteria = getContractCriteria(groundingCriteria);
|
|
1096
|
-
const advisoryCriteria = getAdvisoryCriteria(groundingCriteria).map((record) => record.text);
|
|
1097
|
-
if (contractCriteria.length === 0) {
|
|
1098
|
-
return {
|
|
1099
|
-
ok: false,
|
|
1100
|
-
earlyReturn: {
|
|
1101
|
-
content: [
|
|
1102
|
-
{
|
|
1103
|
-
type: 'text',
|
|
1104
|
-
text: 'create_spec did not persist a spec because no acceptance criterion was grounded in the user request. ' +
|
|
1105
|
-
'Needs decision: provide at least one explicit, verifiable criterion as a checkbox or Given/When/Then outcome.',
|
|
1106
|
-
},
|
|
1107
|
-
],
|
|
1108
|
-
isError: true,
|
|
1109
|
-
structuredContent: {
|
|
1110
|
-
error: 'MISSING_GROUNDED_ACCEPTANCE_CRITERIA',
|
|
1111
|
-
missingDecision: 'Provide at least one explicit, verifiable acceptance criterion.',
|
|
1112
|
-
persisted: false,
|
|
1113
|
-
},
|
|
1114
|
-
},
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
const actionableMetrics = calculateActionableSpecMetrics({
|
|
1118
|
-
criteria: [...baseCriteria, ...filteredCriteria],
|
|
1119
|
-
groundingRecords: groundingCriteria,
|
|
1120
|
-
}).filter(shouldExposeMetric);
|
|
1121
|
-
const technicalFiles = await measureStep('resolveTechnicalFiles', () => resolveTechnicalFiles({
|
|
1122
|
-
generatedSpecBody: generatedSpec.specBody,
|
|
1123
|
-
generatedTechnicalSection: generatedSpec.technicalSection,
|
|
1124
|
-
projectPath: params.projectPath ?? '',
|
|
1125
|
-
}));
|
|
1126
|
-
const groundedTechnical = await measureStep('groundTechnicalFiles', () => groundTechnicalFiles({
|
|
1127
|
-
files: technicalFiles,
|
|
1128
|
-
projectPath: params.projectPath ?? '',
|
|
1129
|
-
userInput: description,
|
|
1130
|
-
autopilot,
|
|
1131
|
-
}));
|
|
1132
|
-
const outOfScope = resolveOutOfScope(params.outOfScope);
|
|
1133
|
-
const scenarioTestPaths = groundedTechnical.files.test.map((file) => file.path);
|
|
1134
|
-
const leanSpec = generateLeanSpecContent({
|
|
1135
|
-
spec,
|
|
1136
|
-
description: generatedSpec.specBody,
|
|
1137
|
-
estimation,
|
|
1138
|
-
criteriaOverride: contractCriteria.map((record) => ({
|
|
1139
|
-
text: record.text,
|
|
1140
|
-
done: false,
|
|
1141
|
-
})),
|
|
1142
|
-
groundingCriteria: contractCriteria,
|
|
1143
|
-
groundingTechnicalReferences: groundedTechnical.records,
|
|
1144
|
-
acFormat: params.acFormat,
|
|
1145
|
-
scenarioTestPaths,
|
|
1146
|
-
});
|
|
1147
|
-
const leanTechnical = generateLeanTechnicalContent({
|
|
1148
|
-
specId: spec.id,
|
|
1149
|
-
filesToCreate: groundedTechnical.files.create,
|
|
1150
|
-
filesToModify: groundedTechnical.files.modify,
|
|
1151
|
-
filesToTest: groundedTechnical.files.test,
|
|
1152
|
-
includeDecisionRequiredPlaceholder: spec.scope !== 'trivial',
|
|
1153
|
-
});
|
|
1154
|
-
// SPEC-709: write unified spec.md from origin — no separate technical.md.
|
|
1155
|
-
// The legacy two-file output is preserved by appending the technical body
|
|
1156
|
-
// as a `## Technical` section inside spec.md.
|
|
1157
|
-
const unifiedWithoutContract = buildUnifiedSpecContent(leanSpec, leanTechnical);
|
|
1158
|
-
const unifiedSpec = appendImplementationContractIfMissing(unifiedWithoutContract, {
|
|
1159
|
-
description: generatedSpec.specBody,
|
|
1160
|
-
criteria: contractCriteria.map((record) => ({ text: record.text, done: false })),
|
|
1161
|
-
files: groundedTechnical.files,
|
|
1162
|
-
outOfScope,
|
|
1163
|
-
verificationCommands: [],
|
|
1164
|
-
});
|
|
1165
|
-
const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
|
|
1166
|
-
if (!genericOutputGate.passed) {
|
|
1167
|
-
return {
|
|
1168
|
-
ok: false,
|
|
1169
|
-
earlyReturn: {
|
|
1170
|
-
content: [
|
|
1171
|
-
{
|
|
1172
|
-
type: 'text',
|
|
1173
|
-
text: 'Spec quality gate blocked generic output before persistence. ' +
|
|
1174
|
-
genericOutputGate.issues
|
|
1175
|
-
.slice(0, 3)
|
|
1176
|
-
.map((issue) => `${issue.phrase}: ${issue.reason}`)
|
|
1177
|
-
.join('; '),
|
|
1178
|
-
},
|
|
1179
|
-
],
|
|
1180
|
-
isError: true,
|
|
1181
|
-
structuredContent: {
|
|
1182
|
-
error: 'GENERIC_SPEC_OUTPUT_BLOCKED',
|
|
1183
|
-
sddConstitutionRuleId: 'sdd.no-generic-output',
|
|
1184
|
-
issues: genericOutputGate.issues,
|
|
1185
|
-
fixHint: 'Replace generic criteria or placeholder references with grounded, testable behavior.',
|
|
1186
|
-
},
|
|
1187
|
-
},
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
// Contradiction analysis is durable post-commit work. Its synchronous scanners can
|
|
1191
|
-
// block the event loop, so it must never run on the persistence acknowledgement path.
|
|
1192
|
-
const contradictionHint = undefined;
|
|
1193
|
-
const committedSimplicity = runSimplicityCheck([params.description, ...filteredCriteria].join('\n'), estimation.devHours);
|
|
1194
|
-
if (outOfScope.length > 0) {
|
|
1195
|
-
spec.outOfScope = outOfScope;
|
|
1196
|
-
}
|
|
1381
|
+
const { params, context, knowledge, clarificationSession, constitutionWarnings, advisoryCriteria, actionableMetrics, unifiedSpec, simplicityResult: committedSimplicity, } = prepared;
|
|
1382
|
+
const { spec, specDir, specPath, splitSuggestion, duplicate, projectId } = context;
|
|
1197
1383
|
let committedPlannerToken;
|
|
1198
1384
|
try {
|
|
1199
1385
|
committedPlannerToken = await issuePlannerToken(resolvedPath, spec.id, params.sessionId ?? 'unknown-session', params.modelId ?? 'unknown-model', params.host ?? 'unknown-host');
|
|
@@ -1205,13 +1391,13 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1205
1391
|
spec,
|
|
1206
1392
|
operationId: operationKey,
|
|
1207
1393
|
duplicate,
|
|
1208
|
-
constitutionWarnings
|
|
1394
|
+
constitutionWarnings,
|
|
1209
1395
|
clarificationSession,
|
|
1210
1396
|
advisoryCriteria,
|
|
1211
1397
|
actionableMetrics,
|
|
1212
1398
|
splitSuggestion,
|
|
1213
1399
|
experienceLevel: knowledge?.experienceLevel,
|
|
1214
|
-
contradictionHint,
|
|
1400
|
+
contradictionHint: undefined,
|
|
1215
1401
|
simplicityResult: committedSimplicity,
|
|
1216
1402
|
plannerToken: committedPlannerToken,
|
|
1217
1403
|
});
|
|
@@ -1256,11 +1442,18 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1256
1442
|
const storeRolledBack = storeCreated
|
|
1257
1443
|
? await specStore.deleteSpec(projectId, spec.id).catch(() => false)
|
|
1258
1444
|
: true;
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1445
|
+
await cleanupExecutionArtifact(getIdempotencyEvidencePath(resolvedPath, idempotencyKey), { force: true });
|
|
1446
|
+
await cleanupExecutionArtifact(specDir, { recursive: true, force: true });
|
|
1447
|
+
try {
|
|
1448
|
+
operationJournal.resolveRecovery('create_spec', operationKey, {
|
|
1449
|
+
action: storeRolledBack ? 'rollback' : 'quarantine',
|
|
1450
|
+
reason: storeRolledBack
|
|
1451
|
+
? 'create_spec persistence failed and compensating cleanup completed'
|
|
1452
|
+
: 'create_spec persistence failed and the spec store rollback was incomplete',
|
|
1262
1453
|
});
|
|
1263
|
-
|
|
1454
|
+
}
|
|
1455
|
+
catch {
|
|
1456
|
+
// The original persistence failure remains the primary error.
|
|
1264
1457
|
}
|
|
1265
1458
|
throw writeErr;
|
|
1266
1459
|
}
|
|
@@ -1276,7 +1469,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1276
1469
|
claimLifecycle.retainForInFlightWork = criticalResult.warning.includes('exceeded');
|
|
1277
1470
|
// SPEC-770: Wait briefly for any in-flight spec write to complete, then scan for duplicates
|
|
1278
1471
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1279
|
-
const recentSpecs = await findRecentlyWrittenSpecs(resolvedPath, 2 * 60 * 1000);
|
|
1472
|
+
const recentSpecs = await findRecentlyWrittenSpecs(resolvedPath, 2 * 60 * 1000, resolvedInputParams.title);
|
|
1280
1473
|
const possibleDuplicates = recentSpecs.length > 0 ? recentSpecs : undefined;
|
|
1281
1474
|
const dupNote = possibleDuplicates
|
|
1282
1475
|
? `\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,13 +1478,25 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1285
1478
|
content: [
|
|
1286
1479
|
{
|
|
1287
1480
|
type: 'text',
|
|
1288
|
-
text: `❌ create_spec exceeded
|
|
1481
|
+
text: `❌ create_spec exceeded its ${String(criticalResult.ceilingMs)}ms internal budget.\n\n${criticalResult.warning}${dupNote}`,
|
|
1289
1482
|
},
|
|
1290
1483
|
],
|
|
1291
1484
|
isError: true,
|
|
1292
1485
|
...(possibleDuplicates ? { structuredContent: { possibleDuplicates } } : {}),
|
|
1293
1486
|
};
|
|
1294
1487
|
}
|
|
1488
|
+
if (criticalResult.status === 'failed') {
|
|
1489
|
+
return {
|
|
1490
|
+
content: [{ type: 'text', text: criticalResult.warning }],
|
|
1491
|
+
isError: true,
|
|
1492
|
+
structuredContent: {
|
|
1493
|
+
error: 'OPERATION_FAILED',
|
|
1494
|
+
category: criticalResult.category,
|
|
1495
|
+
elapsedMs: criticalResult.elapsedMs,
|
|
1496
|
+
persisted: false,
|
|
1497
|
+
},
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1295
1500
|
if (!criticalResult.value.ok) {
|
|
1296
1501
|
return criticalResult.value.earlyReturn;
|
|
1297
1502
|
}
|