@opengsd/gsd-core 1.14.0 → 1.16.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +85 -5
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +5 -3
- package/agents/gsd-code-reviewer.md +8 -6
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +3 -3
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -12
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +2 -2
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +15 -9
- package/agents/gsd-planner.md +15 -11
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +21 -30
- package/agents/gsd-ui-auditor.md +166 -37
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +37 -14
- package/bin/install.js +764 -287
- package/commands/gsd/add-tests.md +6 -1
- package/commands/gsd/ai-integration-phase.md +6 -1
- package/commands/gsd/audit-fix.md +5 -0
- package/commands/gsd/audit-milestone.md +6 -1
- package/commands/gsd/autonomous.md +7 -2
- package/commands/gsd/capture.md +9 -5
- package/commands/gsd/code-review.md +7 -2
- package/commands/gsd/complete-milestone.md +4 -0
- package/commands/gsd/config.md +7 -3
- package/commands/gsd/debug.md +11 -7
- package/commands/gsd/discuss-phase.md +7 -3
- package/commands/gsd/docs-update.md +12 -7
- package/commands/gsd/eval-review.md +6 -1
- package/commands/gsd/execute-phase.md +12 -7
- package/commands/gsd/extract-learnings.md +5 -0
- package/commands/gsd/fast.md +4 -0
- package/commands/gsd/forensics.md +5 -1
- package/commands/gsd/graphify.md +10 -6
- package/commands/gsd/health.md +5 -0
- package/commands/gsd/help.md +7 -2
- package/commands/gsd/import.md +7 -3
- package/commands/gsd/inbox.md +5 -0
- package/commands/gsd/ingest-docs.md +5 -1
- package/commands/gsd/manager.md +6 -1
- package/commands/gsd/map-codebase.md +7 -3
- package/commands/gsd/mempalace-capture.md +12 -4
- package/commands/gsd/mempalace-recall.md +5 -1
- package/commands/gsd/milestone-summary.md +5 -1
- package/commands/gsd/mvp-phase.md +8 -3
- package/commands/gsd/new-milestone.md +6 -1
- package/commands/gsd/new-project.md +5 -0
- package/commands/gsd/next.md +6 -1
- package/commands/gsd/ns-context.md +4 -0
- package/commands/gsd/ns-ideate.md +4 -0
- package/commands/gsd/ns-manage.md +4 -0
- package/commands/gsd/ns-project.md +4 -0
- package/commands/gsd/ns-review.md +4 -0
- package/commands/gsd/ns-workflow.md +4 -0
- package/commands/gsd/onboard.md +6 -1
- package/commands/gsd/pause-work.md +5 -1
- package/commands/gsd/phase.md +8 -4
- package/commands/gsd/plan-phase.md +6 -1
- package/commands/gsd/plan-review-convergence.md +11 -7
- package/commands/gsd/pr-branch.md +4 -0
- package/commands/gsd/profile-user.md +5 -1
- package/commands/gsd/progress.md +7 -2
- package/commands/gsd/quick-batch.md +21 -9
- package/commands/gsd/quick.md +12 -7
- package/commands/gsd/review.md +7 -4
- package/commands/gsd/secure-phase.md +6 -1
- package/commands/gsd/ship.md +5 -0
- package/commands/gsd/sketch.md +7 -2
- package/commands/gsd/spec-phase.md +5 -1
- package/commands/gsd/spike.md +8 -3
- package/commands/gsd/surface.md +5 -1
- package/commands/gsd/thread.md +4 -0
- package/commands/gsd/ui-phase.md +6 -1
- package/commands/gsd/ui-review.md +6 -1
- package/commands/gsd/ultraplan-phase.md +5 -1
- package/commands/gsd/undo.md +5 -1
- package/commands/gsd/update.md +6 -2
- package/commands/gsd/validate-phase.md +6 -1
- package/commands/gsd/verify-work.md +6 -1
- package/commands/gsd/workspace.md +7 -3
- package/gsd-core/bin/gsd-tools.cjs +477 -78
- package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
- package/gsd-core/bin/lib/audit.cjs +144 -42
- package/gsd-core/bin/lib/broken-windows.cjs +13 -13
- package/gsd-core/bin/lib/capability-activation.cjs +9 -4
- package/gsd-core/bin/lib/capability-registry.cjs +197 -222
- package/gsd-core/bin/lib/capability-validator.cjs +16 -1
- package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
- package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
- package/gsd-core/bin/lib/cli-exit.cjs +12 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
- package/gsd-core/bin/lib/command-aliases.cjs +7 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
- package/gsd-core/bin/lib/commands.cjs +343 -207
- package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +76 -19
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/coverage.cjs +4 -8
- package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/drift.cjs +177 -42
- package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
- package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
- package/gsd-core/bin/lib/frontmatter.cjs +426 -234
- package/gsd-core/bin/lib/gap-checker.cjs +72 -29
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
- package/gsd-core/bin/lib/gate-args.cjs +53 -0
- package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
- package/gsd-core/bin/lib/gate-config.cjs +46 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
- package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
- package/gsd-core/bin/lib/gate-exit.cjs +27 -0
- package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
- package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
- package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
- package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
- package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
- package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
- package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
- package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
- package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
- package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
- package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
- package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +364 -132
- package/gsd-core/bin/lib/install-engine.cjs +30 -33
- package/gsd-core/bin/lib/install-profiles.cjs +7 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/io.cjs +122 -3
- package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
- package/gsd-core/bin/lib/milestone.cjs +37 -6
- package/gsd-core/bin/lib/model-resolver.cjs +171 -62
- package/gsd-core/bin/lib/observability/event.cjs +1 -1
- package/gsd-core/bin/lib/observability/logger.cjs +46 -1
- package/gsd-core/bin/lib/pattern.cjs +10 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
- package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +110 -8
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase-status.cjs +360 -0
- package/gsd-core/bin/lib/phase.cjs +489 -88
- package/gsd-core/bin/lib/plan-document.cjs +142 -20
- package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
- package/gsd-core/bin/lib/planning-document.cjs +692 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/profile-output.cjs +6 -3
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
- package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
- package/gsd-core/bin/lib/report-parser.cjs +269 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
- package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
- package/gsd-core/bin/lib/roadmap.cjs +405 -88
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
- package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
- package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
- package/gsd-core/bin/lib/state-contract.cjs +15 -18
- package/gsd-core/bin/lib/state-document.cjs +100 -22
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +256 -105
- package/gsd-core/bin/lib/surface.cjs +19 -2
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
- package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
- package/gsd-core/bin/lib/uat.cjs +432 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/vendor/README.md +31 -9
- package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
- package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
- package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +1378 -274
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
- package/gsd-core/bin/lib/verify.cjs +357 -531
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
- package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
- package/gsd-core/bin/lib/workstream.cjs +11 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/references/autonomous-smart-discuss.md +2 -1
- package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-mvp-tdd.md +5 -10
- package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +7 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/offer-next.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +12 -3
- package/gsd-core/references/spidr-splitting.md +1 -1
- package/gsd-core/references/tdd.md +37 -8
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verifier-phase-gates.md +5 -2
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -3
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/UAT.md +17 -1
- package/gsd-core/templates/config.json +2 -11
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +8 -7
- package/gsd-core/workflows/add-tests.md +4 -3
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +13 -4
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +4 -3
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +43 -23
- package/gsd-core/workflows/check-todos.md +7 -6
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
- package/gsd-core/workflows/code-review-fix.md +112 -25
- package/gsd-core/workflows/code-review.md +146 -135
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
- package/gsd-core/workflows/complete-milestone.md +13 -8
- package/gsd-core/workflows/debug.md +32 -7
- package/gsd-core/workflows/diagnose-issues.md +3 -2
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +4 -3
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +4 -3
- package/gsd-core/workflows/eval-review.md +14 -5
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +96 -178
- package/gsd-core/workflows/execute-plan.md +18 -23
- package/gsd-core/workflows/explore.md +4 -4
- package/gsd-core/workflows/extract-learnings.md +4 -2
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +3 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +4 -3
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +4 -3
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +6 -4
- package/gsd-core/workflows/map-codebase.md +5 -4
- package/gsd-core/workflows/milestone-summary.md +3 -2
- package/gsd-core/workflows/mvp-phase.md +14 -14
- package/gsd-core/workflows/new-milestone.md +11 -11
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +2 -2
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +50 -24
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +19 -49
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +15 -10
- package/gsd-core/workflows/quick.md +62 -39
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +3 -2
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +3 -2
- package/gsd-core/workflows/review.md +33 -17
- package/gsd-core/workflows/scan.md +3 -2
- package/gsd-core/workflows/secure-phase.md +13 -13
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +14 -13
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +2 -2
- package/gsd-core/workflows/transition.md +13 -23
- package/gsd-core/workflows/ui-phase.md +48 -11
- package/gsd-core/workflows/ui-review.md +21 -6
- package/gsd-core/workflows/ultraplan-phase.md +3 -2
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +12 -13
- package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
- package/gsd-core/workflows/verify-work.md +129 -55
- package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
- package/hooks/dist/gsd-check-update-worker.js +8 -0
- package/hooks/dist/gsd-check-update.js +8 -0
- package/hooks/dist/gsd-context-monitor.js +31 -8
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
- package/hooks/dist/gsd-secret-read-guard.js +161 -4
- package/hooks/dist/gsd-statusline.js +85 -20
- package/hooks/dist/gsd-update-banner.js +8 -0
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
- package/hooks/dist/gsd-workflow-guard.js +5 -4
- package/hooks/dist/gsd-worktree-path-guard.js +6 -2
- package/hooks/dist/lib/cli-exit.js +12 -0
- package/hooks/dist/lib/git-probe.js +17 -1
- package/hooks/dist/lib/isolation-sentinel.js +2 -2
- package/hooks/gsd-agent-isolation-guard.js +32 -0
- package/hooks/gsd-check-update-worker.js +8 -0
- package/hooks/gsd-check-update.js +8 -0
- package/hooks/gsd-context-monitor.js +31 -8
- package/hooks/gsd-cursor-subagent-start.js +8 -0
- package/hooks/gsd-secret-read-guard.js +161 -4
- package/hooks/gsd-statusline.js +85 -20
- package/hooks/gsd-update-banner.js +8 -0
- package/hooks/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-windsurf-pre-write.js +11 -2
- package/hooks/gsd-workflow-guard.js +5 -4
- package/hooks/gsd-worktree-path-guard.js +6 -2
- package/hooks/hooks.json +5 -5
- package/hooks/lib/cli-exit.js +12 -0
- package/hooks/lib/git-probe.js +17 -1
- package/hooks/lib/isolation-sentinel.js +2 -2
- package/package.json +22 -4
- package/scripts/build-hooks.js +15 -6
- package/scripts/changeset/parse.cjs +52 -4
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/ci-timeout-report.cjs +770 -4
- package/scripts/command-contract-helpers.cjs +15 -8
- package/scripts/docs-guard-registry.cjs +34 -0
- package/scripts/gen-features.cjs +13 -8
- package/scripts/gen-hooks-cli-exit.cjs +12 -28
- package/scripts/gen-loop-host-contract.cjs +79 -1
- package/scripts/gen-platform-conformance-tier.cjs +187 -1
- package/scripts/gen-plugin-skills.cjs +87 -1
- package/scripts/gen-research-agents.cjs +24 -31
- package/scripts/gen-scripts-cli-exit.cjs +30 -3
- package/scripts/gen-test-timings.cjs +32 -7
- package/scripts/lib/cli-exit.cjs +12 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
- package/scripts/lib/ndjson-reporter.cjs +31 -5
- package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
- package/scripts/lib/registration-ledger-preload.cjs +155 -0
- package/scripts/lib/vendor-bundle.cjs +59 -0
- package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-completion-predicate-drift.cjs +18 -19
- package/scripts/lint-descriptions.cjs +7 -3
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
- package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
- package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-enumeration-drift.cjs +12 -8
- package/scripts/lint-phase-id-drift.cjs +319 -5
- package/scripts/lint-planning-document-positive-control.cjs +329 -0
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +3 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-skill-deps.cjs +7 -3
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +38 -9
- package/scripts/lint-test-file-count.cjs +34 -1
- package/scripts/lint-vendored-deps.cjs +41 -5
- package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
- package/scripts/mutation-matrix.cjs +50 -5
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/require-issue-link-policy.cjs +6 -2
- package/scripts/sync-runtime-launcher.cjs +184 -2
- package/scripts/verify-npm-publish.cjs +76 -20
- package/skills/gsd-add-tests/SKILL.md +6 -1
- package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
- package/skills/gsd-audit-fix/SKILL.md +5 -0
- package/skills/gsd-audit-milestone/SKILL.md +6 -1
- package/skills/gsd-autonomous/SKILL.md +7 -2
- package/skills/gsd-capture/SKILL.md +9 -5
- package/skills/gsd-code-review/SKILL.md +7 -2
- package/skills/gsd-complete-milestone/SKILL.md +4 -0
- package/skills/gsd-config/SKILL.md +8 -4
- package/skills/gsd-debug/SKILL.md +11 -7
- package/skills/gsd-discuss-phase/SKILL.md +7 -3
- package/skills/gsd-docs-update/SKILL.md +12 -7
- package/skills/gsd-eval-review/SKILL.md +6 -1
- package/skills/gsd-execute-phase/SKILL.md +12 -7
- package/skills/gsd-extract-learnings/SKILL.md +5 -0
- package/skills/gsd-fast/SKILL.md +4 -0
- package/skills/gsd-forensics/SKILL.md +5 -1
- package/skills/gsd-graphify/SKILL.md +10 -6
- package/skills/gsd-health/SKILL.md +5 -0
- package/skills/gsd-help/SKILL.md +7 -2
- package/skills/gsd-import/SKILL.md +7 -3
- package/skills/gsd-inbox/SKILL.md +5 -0
- package/skills/gsd-ingest-docs/SKILL.md +5 -1
- package/skills/gsd-manager/SKILL.md +6 -1
- package/skills/gsd-map-codebase/SKILL.md +7 -3
- package/skills/gsd-mempalace-capture/SKILL.md +12 -4
- package/skills/gsd-mempalace-recall/SKILL.md +5 -1
- package/skills/gsd-milestone-summary/SKILL.md +5 -1
- package/skills/gsd-mvp-phase/SKILL.md +8 -3
- package/skills/gsd-new-milestone/SKILL.md +6 -1
- package/skills/gsd-new-project/SKILL.md +5 -0
- package/skills/gsd-next/SKILL.md +6 -1
- package/skills/gsd-ns-context/SKILL.md +4 -0
- package/skills/gsd-ns-ideate/SKILL.md +4 -0
- package/skills/gsd-ns-manage/SKILL.md +4 -0
- package/skills/gsd-ns-project/SKILL.md +4 -0
- package/skills/gsd-ns-review/SKILL.md +4 -0
- package/skills/gsd-ns-workflow/SKILL.md +4 -0
- package/skills/gsd-onboard/SKILL.md +6 -1
- package/skills/gsd-pause-work/SKILL.md +5 -1
- package/skills/gsd-phase/SKILL.md +8 -4
- package/skills/gsd-plan-phase/SKILL.md +6 -1
- package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
- package/skills/gsd-pr-branch/SKILL.md +4 -0
- package/skills/gsd-profile-user/SKILL.md +5 -1
- package/skills/gsd-progress/SKILL.md +7 -2
- package/skills/gsd-quick/SKILL.md +16 -10
- package/skills/gsd-quick-batch/SKILL.md +21 -9
- package/skills/gsd-review/SKILL.md +7 -4
- package/skills/gsd-review-backlog/SKILL.md +3 -2
- package/skills/gsd-secure-phase/SKILL.md +6 -1
- package/skills/gsd-ship/SKILL.md +5 -0
- package/skills/gsd-sketch/SKILL.md +7 -2
- package/skills/gsd-spec-phase/SKILL.md +5 -1
- package/skills/gsd-spike/SKILL.md +8 -3
- package/skills/gsd-surface/SKILL.md +5 -1
- package/skills/gsd-thread/SKILL.md +4 -0
- package/skills/gsd-ui-phase/SKILL.md +6 -1
- package/skills/gsd-ui-review/SKILL.md +6 -1
- package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
- package/skills/gsd-undo/SKILL.md +5 -1
- package/skills/gsd-update/SKILL.md +6 -2
- package/skills/gsd-validate-phase/SKILL.md +6 -1
- package/skills/gsd-verify-work/SKILL.md +6 -1
- package/skills/gsd-workspace/SKILL.md +7 -3
- package/skills/gsd-workstreams/SKILL.md +6 -6
- package/vscode/package.json +1 -1
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Check subcommand router —
|
|
3
|
+
* Check subcommand router — parses argv, dispatches to one gate module per verb, and formats the
|
|
4
|
+
* gate's result (#5139, epic #5056, ADR-5057 §4 first bullet, design D2).
|
|
5
|
+
*
|
|
6
|
+
* The router decides nothing: every gate lives in a `src/gate-<verb>.cts` module (or, for
|
|
7
|
+
* `auto-mode`, `check-auto-mode.cts`) that returns a `GateResult` (`src/gate-verdict.cts`). The
|
|
8
|
+
* router's single output site, `emitGateResult`, prints a verdict's payload or turns a usage
|
|
9
|
+
* failure into `error()`. It imports no filesystem or subprocess primitive (eslint.config.mjs,
|
|
10
|
+
* `no-restricted-imports`; tests/check-router-gate-boundaries.test.cjs).
|
|
4
11
|
*
|
|
5
12
|
* ADR-457 build-at-publish: the hand-written bin/lib/check-command-router.cjs collapsed
|
|
6
|
-
* to a TypeScript source of truth.
|
|
7
|
-
* from the prior hand-written .cjs; only strict types are added.
|
|
13
|
+
* to a TypeScript source of truth.
|
|
8
14
|
*/
|
|
9
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
-
};
|
|
12
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
-
const node_child_process_1 = require("node:child_process");
|
|
15
15
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
16
|
const io = require("./io.cjs");
|
|
17
17
|
const { output, ERROR_REASON } = io;
|
|
@@ -21,1550 +21,99 @@ const { output, ERROR_REASON } = io;
|
|
|
21
21
|
// or an annotated const. Without the annotation every `error(...)` guard below would
|
|
22
22
|
// need a dead `throw` after it to convince the checker that the value is non-null.
|
|
23
23
|
const error = io.error;
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
-
const gatePredicateEval = require("./gate-predicate-evaluator.cjs");
|
|
51
|
-
const { evaluatePredicate } = gatePredicateEval;
|
|
52
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
-
const apiCoverageMod = require("./api-coverage.cjs");
|
|
54
|
-
const { detectApiIntegration, validateCoverageMatrix } = apiCoverageMod;
|
|
55
|
-
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
57
|
-
const planScanMod = require("./plan-scan.cjs");
|
|
58
|
-
const { scanPhasePlans } = planScanMod;
|
|
59
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
60
|
-
const planningScopeMod = require("./planning-scope.cjs");
|
|
61
|
-
const { SCOPE } = planningScopeMod;
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
63
|
-
const verifyCommandGroundingMod = require("./verify-command-grounding.cjs");
|
|
64
|
-
const { probePhaseVerifyCommands, probePhaseFailingDirections } = verifyCommandGroundingMod;
|
|
65
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
66
|
-
function normalizePhrase(text) {
|
|
67
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
68
|
-
return String(text || '')
|
|
69
|
-
.toLowerCase()
|
|
70
|
-
.replace(/[^a-z0-9\s]/g, ' ')
|
|
71
|
-
.replace(/\s+/g, ' ')
|
|
72
|
-
.trim();
|
|
73
|
-
}
|
|
74
|
-
const SOFT_PHRASE_MIN_WORDS = 6;
|
|
75
|
-
function softPhrase(text) {
|
|
76
|
-
const words = normalizePhrase(text).split(' ').filter(Boolean);
|
|
77
|
-
if (words.length < SOFT_PHRASE_MIN_WORDS)
|
|
78
|
-
return '';
|
|
79
|
-
return words.slice(0, SOFT_PHRASE_MIN_WORDS).join(' ');
|
|
80
|
-
}
|
|
81
|
-
function decisionMentioned(haystack, decision) {
|
|
82
|
-
if (!haystack)
|
|
83
|
-
return false;
|
|
84
|
-
if (new RegExp(`\\b${decision.id}\\b`).test(haystack))
|
|
85
|
-
return true;
|
|
86
|
-
const phrase = softPhrase(decision.text);
|
|
87
|
-
return phrase ? normalizePhrase(haystack).includes(phrase) : false;
|
|
88
|
-
}
|
|
89
|
-
function readIfExists(filePath) {
|
|
90
|
-
try {
|
|
91
|
-
return node_fs_1.default.readFileSync(filePath, 'utf-8');
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return '';
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
function resolvePath(inputPath, projectDir) {
|
|
98
|
-
const candidate = node_path_1.default.isAbsolute(inputPath) ? inputPath : node_path_1.default.join(projectDir, inputPath);
|
|
99
|
-
const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
100
|
-
if (contained === null) {
|
|
101
|
-
error(`path escapes its allowed directory: ${inputPath}`, ERROR_REASON.USAGE);
|
|
102
|
-
}
|
|
103
|
-
return contained;
|
|
104
|
-
}
|
|
105
|
-
function readWorkflowConfig(projectDir) {
|
|
106
|
-
const configPath = node_path_1.default.join(projectDir, '.planning', 'config.json');
|
|
107
|
-
try {
|
|
108
|
-
const parsed = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
109
|
-
const wf = parsed['workflow'] || {};
|
|
110
|
-
return {
|
|
111
|
-
...wf,
|
|
112
|
-
auto_advance: (wf['auto_advance'] ?? parsed['auto_advance']),
|
|
113
|
-
_auto_chain_active: (wf['_auto_chain_active'] ?? parsed['_auto_chain_active']),
|
|
114
|
-
context_coverage_gate: (wf['context_coverage_gate'] ?? parsed['context_coverage_gate']),
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return {};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function cmdAutoMode(projectDir, raw) {
|
|
122
|
-
const workflow = readWorkflowConfig(projectDir);
|
|
123
|
-
const autoAdvance = Boolean(workflow.auto_advance ?? false);
|
|
124
|
-
const autoChainActive = Boolean(workflow._auto_chain_active ?? false);
|
|
125
|
-
let source = 'none';
|
|
126
|
-
if (autoChainActive && autoAdvance)
|
|
127
|
-
source = 'both';
|
|
128
|
-
else if (autoChainActive)
|
|
129
|
-
source = 'auto_chain';
|
|
130
|
-
else if (autoAdvance)
|
|
131
|
-
source = 'auto_advance';
|
|
132
|
-
output({
|
|
133
|
-
active: autoChainActive || autoAdvance,
|
|
134
|
-
source,
|
|
135
|
-
auto_chain_active: autoChainActive,
|
|
136
|
-
auto_advance: autoAdvance,
|
|
137
|
-
}, raw, undefined);
|
|
138
|
-
}
|
|
139
|
-
function gateEnabled(projectDir) {
|
|
140
|
-
const value = readWorkflowConfig(projectDir).context_coverage_gate;
|
|
141
|
-
if (typeof value === 'boolean')
|
|
142
|
-
return value;
|
|
143
|
-
if (typeof value === 'string') {
|
|
144
|
-
const lower = value.toLowerCase();
|
|
145
|
-
if (lower === 'false' || lower === 'true')
|
|
146
|
-
return lower !== 'false';
|
|
147
|
-
}
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
function loadPlanContents(phaseDir) {
|
|
151
|
-
if (!node_fs_1.default.existsSync(phaseDir))
|
|
152
|
-
return [];
|
|
153
|
-
// #3183 (lint-plan-count-drift): source live plan files from the single
|
|
154
|
-
// owner (scanPhasePlans) instead of a local `-PLAN.md` readdirSync filter
|
|
155
|
-
// — picks up bare PLAN.md and nested plans/, and excludes plans marked
|
|
156
|
-
// `status: superseded`, which the prior root-only exact-suffix filter did
|
|
157
|
-
// neither for.
|
|
158
|
-
return scanPhasePlans(phaseDir).planFiles
|
|
159
|
-
.map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)));
|
|
160
|
-
}
|
|
161
|
-
const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
|
|
162
|
-
// #2372: scanned-tag set must match the planner-canonical surfaces where a D-NN citation
|
|
163
|
-
// is meaningful. `<objective>`/`<tasks>`/`<task>`/`<action>` are the historical core. The
|
|
164
|
-
// planner is also explicitly told (plan-phase.md) to cite decisions in `<read_first>`,
|
|
165
|
-
// `<behavior>`, `<verify>`, `<acceptance_criteria>`, and `<done>` — those are now scanned too,
|
|
166
|
-
// so the gate no longer reports a false coverage gap when a decision is cited in any of them.
|
|
167
|
-
//
|
|
168
|
-
// Implementation: per-tag matching, NOT a single wide alternation. A single alternation
|
|
169
|
-
// like `<(?:a|b|c)>...<\/(?:a|b|c)>` halts the outer tag's body capture at any inner tag
|
|
170
|
-
// in the set, dropping any citation in the outer tag's prefix prose — e.g.
|
|
171
|
-
// `<action>per D-05 <verify>npm test</verify></action>` would lose D-05 because `<verify>`
|
|
172
|
-
// halts the `<action>` body before the citation. Per-tag matching avoids this: each tag's
|
|
173
|
-
// body terminates only at its OWN closing tag, so `<verify>` inside `<action>` is absorbed
|
|
174
|
-
// into `<action>`'s body (D-05 caught) AND `<verify>` is matched separately on its own pass.
|
|
175
|
-
// Each per-tag regex keeps the ReDoS-safe negative-lookahead tempering (#2128).
|
|
176
|
-
const XML_DECISION_TAG_NAMES = ['objective', 'tasks', 'task', 'action', 'read_first', 'behavior', 'verify', 'acceptance_criteria', 'done'];
|
|
177
|
-
function buildXmlDecisionTagRegex(tagName) {
|
|
178
|
-
// Per-tag: body tempering stops only at the SAME tag's reopening or closing — other
|
|
179
|
-
// scanned tags pass through as text into this body. Non-greedy `*?` to first close.
|
|
180
|
-
return new RegExp(`<${tagName}(?:\\s[^>]{0,1000})?>((?:(?!<${tagName}[\\s>])[\\s\\S])*?)<\\/${tagName}>`, 'gi');
|
|
181
|
-
}
|
|
182
|
-
function stripCommentsAndFences(text) {
|
|
183
|
-
// HTML-comment stripping stays caller-side (the seam does not strip HTML comments).
|
|
184
|
-
// Stop-at-next-open body (ReDoS-safe, #2128); an UNCLOSED `<!--` does not match,
|
|
185
|
-
// so downstream tags are preserved (unlike a `(?:-->|$)` fallback, which would
|
|
186
|
-
// wipe to EOF and fail-close the decision-coverage gate).
|
|
187
|
-
const htmlStripped = text.replace(/<!--(?:(?!<!--)[\s\S])*?-->/g, ' ');
|
|
188
|
-
// Fenced-code stripping: delegate to the canonical CommonMark-correct seam.
|
|
189
|
-
// replaces the prior independent regex copy (```` ``` ``` ```` + `~~~ ~~~`).
|
|
190
|
-
return (0, markdown_sectionizer_cjs_1.stripFencedCode)(htmlStripped).text;
|
|
191
|
-
}
|
|
192
|
-
function extractYamlBlock(frontmatter, key) {
|
|
193
|
-
const match = frontmatter.match(new RegExp(`^${key}\\s*:(.*)$`, 'm'));
|
|
194
|
-
if (!match)
|
|
195
|
-
return '';
|
|
196
|
-
const startIdx = (match.index || 0) + match[0].length;
|
|
197
|
-
const rest = frontmatter.slice(startIdx + 1).split(/\r?\n/);
|
|
198
|
-
const block = [match[1] || ''];
|
|
199
|
-
for (const line of rest) {
|
|
200
|
-
if (line === '' || /^\s/.test(line))
|
|
201
|
-
block.push(line);
|
|
202
|
-
else
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
return block.join('\n');
|
|
206
|
-
}
|
|
207
|
-
function extractXmlTagBodies(text) {
|
|
208
|
-
const parts = [];
|
|
209
|
-
for (const tagName of XML_DECISION_TAG_NAMES) {
|
|
210
|
-
const re = buildXmlDecisionTagRegex(tagName);
|
|
211
|
-
for (const match of text.matchAll(re)) {
|
|
212
|
-
if (match[1])
|
|
213
|
-
parts.push(match[1]);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return parts.join('\n');
|
|
217
|
-
}
|
|
218
|
-
function extractPlanDesignatedSections(planContent) {
|
|
219
|
-
if (!planContent)
|
|
220
|
-
return '';
|
|
221
|
-
const cleaned = stripCommentsAndFences(planContent);
|
|
222
|
-
const fmMatch = cleaned.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
223
|
-
const frontmatter = fmMatch ? fmMatch[1] : '';
|
|
224
|
-
const body = fmMatch ? fmMatch[2] : cleaned;
|
|
225
|
-
const parts = [];
|
|
226
|
-
for (const key of ['must_haves', 'truths', 'objective']) {
|
|
227
|
-
const block = extractYamlBlock(frontmatter, key);
|
|
228
|
-
if (block)
|
|
229
|
-
parts.push(block);
|
|
230
|
-
}
|
|
231
|
-
// Replace hand-rolled split(/\r?\n/) + heading walk with the seam's collectSections.
|
|
232
|
-
// stopPredicate fires on EVERY heading (collectSections needs to start a section at
|
|
233
|
-
// each heading), then we filter to designated ones — same semantics as the prior
|
|
234
|
-
// inDesignated flag: emit the heading line + body only when DESIGNATED_HEADINGS_RE matches.
|
|
235
|
-
const sections = (0, markdown_sectionizer_cjs_1.collectSections)(body, () => true);
|
|
236
|
-
const bodyParts = [];
|
|
237
|
-
for (const section of sections) {
|
|
238
|
-
const headingLine = '#'.repeat(section.heading.level) + ' ' + section.heading.text;
|
|
239
|
-
if (DESIGNATED_HEADINGS_RE.test(headingLine)) {
|
|
240
|
-
bodyParts.push(headingLine);
|
|
241
|
-
if (section.body)
|
|
242
|
-
bodyParts.push(section.body);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
parts.push(bodyParts.join('\n'));
|
|
246
|
-
parts.push(extractXmlTagBodies(cleaned));
|
|
247
|
-
return parts.join('\n\n');
|
|
248
|
-
}
|
|
249
|
-
function buildPlanMessage(uncovered) {
|
|
250
|
-
if (uncovered.length === 0)
|
|
251
|
-
return 'All trackable CONTEXT.md decisions are covered by plans.';
|
|
252
|
-
return [
|
|
253
|
-
'## Decision Coverage Gap',
|
|
254
|
-
'',
|
|
255
|
-
`${uncovered.length} CONTEXT.md decision(s) are not covered by any plan:`,
|
|
256
|
-
'',
|
|
257
|
-
...uncovered.map((item) => `- **${item.id}** (${item.category || 'uncategorized'}): ${item.text}`),
|
|
258
|
-
'',
|
|
259
|
-
'Resolve by citing `D-NN:` in any of the scanned plan surfaces: front-matter',
|
|
260
|
-
'`must_haves`/`truths`/`objective`, a `## must_haves`/`truths`/`tasks`/`objective`',
|
|
261
|
-
'heading, or an `<objective>`/`<tasks>`/`<task>`/`<action>`/`<read_first>`/`<behavior>`/`<verify>`/`<acceptance_criteria>`/`<done>`',
|
|
262
|
-
'tag body. Other locations (prose outside those headings, comments, other XML tags) are not scanned.',
|
|
263
|
-
'OR move the decision to `### Claude\'s Discretion` / tag it `[informational]` if it should not be tracked.',
|
|
264
|
-
].join('\n');
|
|
265
|
-
}
|
|
266
|
-
function buildVerifyMessage(notHonored) {
|
|
267
|
-
if (notHonored.length === 0)
|
|
268
|
-
return 'All trackable CONTEXT.md decisions are honored by shipped artifacts.';
|
|
269
|
-
return [
|
|
270
|
-
'### Decision Coverage (warning)',
|
|
271
|
-
'',
|
|
272
|
-
`${notHonored.length} decision(s) not found in shipped artifacts:`,
|
|
273
|
-
'',
|
|
274
|
-
...notHonored.map((item) => `- **${item.id}** (${item.category || 'uncategorized'}): ${item.text}`),
|
|
275
|
-
'',
|
|
276
|
-
'This is a soft warning - verification status is unchanged.',
|
|
277
|
-
].join('\n');
|
|
278
|
-
}
|
|
279
|
-
function loadDecisionExtraction(contextPath) {
|
|
280
|
-
const extraction = (0, decisions_cjs_1.extractDecisions)(readIfExists(contextPath));
|
|
281
|
-
return {
|
|
282
|
-
trackable: extraction.decisions.filter((d) => d.trackable),
|
|
283
|
-
outcome: extraction.outcome,
|
|
284
|
-
};
|
|
24
|
+
const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
|
|
25
|
+
const gate_exit_cjs_1 = require("./gate-exit.cjs");
|
|
26
|
+
const gate_args_cjs_1 = require("./gate-args.cjs");
|
|
27
|
+
const gate_decision_coverage_plan_cjs_1 = require("./gate-decision-coverage-plan.cjs");
|
|
28
|
+
const gate_decision_coverage_verify_cjs_1 = require("./gate-decision-coverage-verify.cjs");
|
|
29
|
+
const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
|
|
30
|
+
const gate_ui_plan_cjs_1 = require("./gate-ui-plan.cjs");
|
|
31
|
+
const gate_ui_safety_cjs_1 = require("./gate-ui-safety.cjs");
|
|
32
|
+
const gate_tdd_review_checkpoint_cjs_1 = require("./gate-tdd-review-checkpoint.cjs");
|
|
33
|
+
const gate_tdd_red_evidence_cjs_1 = require("./gate-tdd-red-evidence.cjs");
|
|
34
|
+
const gate_verify_command_paths_cjs_1 = require("./gate-verify-command-paths.cjs");
|
|
35
|
+
const gate_verify_failure_directions_cjs_1 = require("./gate-verify-failure-directions.cjs");
|
|
36
|
+
const gate_gap_analysis_plan_post_cjs_1 = require("./gate-gap-analysis-plan-post.cjs");
|
|
37
|
+
const gate_predicate_cjs_1 = require("./gate-predicate.cjs");
|
|
38
|
+
const gate_api_coverage_verify_pre_cjs_1 = require("./gate-api-coverage-verify-pre.cjs");
|
|
39
|
+
const decision_coverage_support_cjs_1 = require("./decision-coverage-support.cjs");
|
|
40
|
+
const check_auto_mode_cjs_1 = require("./check-auto-mode.cjs");
|
|
41
|
+
const gate_schema_drift_cjs_1 = require("./gate-schema-drift.cjs");
|
|
42
|
+
const gate_codebase_drift_cjs_1 = require("./gate-codebase-drift.cjs");
|
|
43
|
+
const gate_context_drift_cjs_1 = require("./gate-context-drift.cjs");
|
|
44
|
+
const gate_prohibition_enforcement_cjs_1 = require("./gate-prohibition-enforcement.cjs");
|
|
45
|
+
/** A gate's usage failure as `error()`: the gate's own message, and its code when it is a known reason. */
|
|
46
|
+
function failGate(failed) {
|
|
47
|
+
const reason = Object.values(ERROR_REASON).find((value) => value === failed.failure.code);
|
|
48
|
+
return error(failed.failure.message, reason);
|
|
285
49
|
}
|
|
286
50
|
/**
|
|
287
|
-
*
|
|
288
|
-
* (
|
|
289
|
-
*
|
|
290
|
-
* Invocation (the context path may be supplied EITHER way; #4130 follow-up):
|
|
291
|
-
* gsd_run check decision-coverage-plan <phase-dir> <context-path> (positional, the workflow caller's form)
|
|
292
|
-
* gsd_run check decision-coverage-plan --context <path> [<phase-dir>]
|
|
293
|
-
*
|
|
294
|
-
* `--context <path>` follows the sibling flag convention (`check predicate`,
|
|
295
|
-
* #2008): `--flag value` pairs parsed by the shared partitionPredicateArgs
|
|
296
|
-
* pass, the flag WINNING over a same-purpose positional when both appear,
|
|
297
|
-
* and a valueless `--context` counting as no context at all (it falls
|
|
298
|
-
* through to the #2770 caller-error branch, not to the "CONTEXT.md missing"
|
|
299
|
-
* green skip). The positional form keeps working unchanged — no sibling
|
|
300
|
-
* check verb deprecates positionals and the plan-phase workflow passes them.
|
|
51
|
+
* The router's single output site for a gate: a verdict prints its payload; a usage failure
|
|
52
|
+
* fails through `error()` with the gate's own message and code (design D2).
|
|
301
53
|
*/
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const { flags, positionals } = partitionPredicateArgs(args.slice(2));
|
|
306
|
-
const phaseDir = positionals[0] ? resolvePath(positionals[0], projectDir) : '';
|
|
307
|
-
// A VALUELESS `--context` stays a bare token in the positionals (sibling
|
|
308
|
-
// parser semantics); it must not then be read as the context PATH — a
|
|
309
|
-
// `--`-prefixed "path" is a caller mistake, and #2770's law says a missing
|
|
310
|
-
// context argument fails CLOSED, never a silent "CONTEXT.md missing" green
|
|
311
|
-
// skip. So only a non-flag positional may serve as the context.
|
|
312
|
-
const positionalContext = positionals[1] && !positionals[1].startsWith('--') ? positionals[1] : '';
|
|
313
|
-
const contextArg = flags['context'] ?? positionalContext ?? '';
|
|
314
|
-
const contextPath = contextArg ? resolvePath(contextArg, projectDir) : '';
|
|
315
|
-
if (!gateEnabled(projectDir)) {
|
|
316
|
-
output({ passed: true, skipped: true, reason: 'workflow.context_coverage_gate is false', total: 0, covered: 0, uncovered: [], message: 'Decision coverage gate disabled by config.' }, raw, undefined);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
// #2770: an EMPTY/MISSING contextPath argument is a CALLER ERROR (the workflow
|
|
320
|
-
// forgot to pass the path — e.g. a shell variable lost between Bash blocks), not
|
|
321
|
-
// evidence the phase has no CONTEXT.md. Fail closed (mirrors #1365 fail-loud) so a
|
|
322
|
-
// blocking gate cannot silently certify success on a caller mistake.
|
|
323
|
-
if (!contextArg || contextArg === '') {
|
|
324
|
-
output({ passed: false, skipped: false, reason: 'missing context path argument', total: 0, covered: 0, uncovered: [], message: 'Decision coverage gate called without a context path argument — the caller (e.g. the plan-phase workflow) must pass the CONTEXT.md path. An empty argument is a caller error, not evidence there is nothing to check (#2770).' }, raw, undefined);
|
|
325
|
-
return;
|
|
54
|
+
function emitGateResult(result, raw) {
|
|
55
|
+
if ((0, gate_verdict_cjs_1.isGateUsageFailure)(result)) {
|
|
56
|
+
failGate(result);
|
|
326
57
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
const { trackable: decisions, outcome } = loadDecisionExtraction(contextPath);
|
|
333
|
-
// #1365 fail-loud gate: any could-not-parse outcome must NOT silently pass —
|
|
334
|
-
// even when some decisions were extracted (e.g. D-01 valid but D-02 malformed).
|
|
335
|
-
// A parse-miss on ANY bullet means the gate cannot certify full coverage.
|
|
336
|
-
// Fire independent of decisions.length so a partial-parse still blocks.
|
|
337
|
-
if (outcome === 'could-not-parse') {
|
|
338
|
-
const partialParse = decisions.length > 0;
|
|
339
|
-
output({
|
|
340
|
-
passed: false,
|
|
341
|
-
skipped: false,
|
|
342
|
-
reason: 'could-not-parse',
|
|
343
|
-
total: decisions.length,
|
|
344
|
-
covered: 0,
|
|
345
|
-
uncovered: [],
|
|
346
|
-
message: partialParse
|
|
347
|
-
? 'Decision coverage gate: decisions could not be fully parsed — one or more ' +
|
|
348
|
-
'`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
|
|
349
|
-
'prefix that is not a digit run, e.g. `D4x-01`). Fix the bullet format so all decisions ' +
|
|
350
|
-
'can be read before re-running the gate.'
|
|
351
|
-
: 'Decision coverage gate: could not parse decisions — possible format mismatch. ' +
|
|
352
|
-
'The CONTEXT.md appears to be decision-shaped (has a <decisions> block, a decisions heading, ' +
|
|
353
|
-
'or D- tokens) but no decision bullets could be extracted. Check the formatting of the decisions ' +
|
|
354
|
-
'block and ensure bullets follow the `- **D-NN:** text`, `- **D4-NN:** text` (phase-prefixed), ' +
|
|
355
|
-
'or `- **D-NN — title** body` form. An ID grammar the parser does not support (e.g. `DEC-01`) ' +
|
|
356
|
-
'also lands here.',
|
|
357
|
-
}, raw, undefined);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
if (decisions.length === 0) {
|
|
361
|
-
output({ passed: true, skipped: true, reason: 'no trackable decisions', total: 0, covered: 0, uncovered: [], message: 'No trackable decisions in CONTEXT.md.' }, raw, undefined);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const sections = loadPlanContents(phaseDir).map(extractPlanDesignatedSections);
|
|
365
|
-
const uncovered = [];
|
|
366
|
-
let covered = 0;
|
|
367
|
-
for (const decision of decisions) {
|
|
368
|
-
if (sections.some((section) => decisionMentioned(section, decision)))
|
|
369
|
-
covered++;
|
|
370
|
-
else
|
|
371
|
-
uncovered.push({ id: decision.id, text: decision.text, category: decision.category });
|
|
372
|
-
}
|
|
373
|
-
output({
|
|
374
|
-
passed: uncovered.length === 0,
|
|
375
|
-
skipped: false,
|
|
376
|
-
total: decisions.length,
|
|
377
|
-
covered,
|
|
378
|
-
uncovered,
|
|
379
|
-
message: buildPlanMessage(uncovered),
|
|
380
|
-
}, raw, undefined);
|
|
58
|
+
output(result.payload, raw, undefined);
|
|
59
|
+
// After output(): it rewrites the pending-outcome cell on every call. Payload mode — a delivered
|
|
60
|
+
// blocking verdict is still exit 0 (the dispatch contract); an unreadable one is UNAVAILABLE (#5170).
|
|
61
|
+
(0, gate_exit_cjs_1.declareGateExit)(result, 'payload');
|
|
381
62
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
cwd: projectDir,
|
|
386
|
-
encoding: 'utf-8',
|
|
387
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
388
|
-
windowsHide: true,
|
|
389
|
-
timeout: 15_000,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
return '';
|
|
394
|
-
}
|
|
63
|
+
// ─── Thin wrappers: argv[0]='check', argv[1]=verb — a gate takes the argv AFTER the verb ──────────
|
|
64
|
+
function cmdAutoMode(projectDir, raw) {
|
|
65
|
+
emitGateResult((0, gate_verdict_cjs_1.gateVerdict)('pass', false, { ...(0, check_auto_mode_cjs_1.readAutoModeState)(projectDir) }), raw);
|
|
395
66
|
}
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
let total = 0;
|
|
399
|
-
for (const summary of summaries) {
|
|
400
|
-
if (!summary)
|
|
401
|
-
continue;
|
|
402
|
-
for (const blockMatch of summary.matchAll(/files_modified:\s*\n((?:[ \t]*-\s+.+\n?)+)/g)) {
|
|
403
|
-
const files = [...(blockMatch[1] || '').matchAll(/-\s+(.+)/g)]
|
|
404
|
-
.map((match) => match[1].trim().replace(/^["']|["']$/g, ''));
|
|
405
|
-
for (const file of files) {
|
|
406
|
-
if (total >= 50)
|
|
407
|
-
break;
|
|
408
|
-
if (!file)
|
|
409
|
-
continue;
|
|
410
|
-
// Migrated off the hand-rolled prefix check (ADR-4650): resolve+contain in one
|
|
411
|
-
// step via the canonical realpath predicate — the eventual read below follows
|
|
412
|
-
// symlinks, so containment must be decided on the resolved target, not a lexical
|
|
413
|
-
// prefix. Read the value the predicate RETURNED; do not re-derive the path.
|
|
414
|
-
const candidate = node_path_1.default.isAbsolute(file) ? file : node_path_1.default.join(projectDir, file);
|
|
415
|
-
const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
416
|
-
if (contained === null)
|
|
417
|
-
continue;
|
|
418
|
-
const raw = readIfExists(contained);
|
|
419
|
-
out.push(raw.length > 256 * 1024 ? raw.slice(0, 256 * 1024) : raw);
|
|
420
|
-
total++;
|
|
421
|
-
}
|
|
422
|
-
if (total >= 50)
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
if (total >= 50)
|
|
426
|
-
break;
|
|
427
|
-
}
|
|
428
|
-
return out.join('\n\n');
|
|
67
|
+
function cmdDecisionCoveragePlan(projectDir, args, raw) {
|
|
68
|
+
emitGateResult((0, gate_decision_coverage_plan_cjs_1.evaluateDecisionCoveragePlan)({ projectDir, args: args.slice(2) }), raw);
|
|
429
69
|
}
|
|
430
70
|
function cmdDecisionCoverageVerify(projectDir, args, raw) {
|
|
431
|
-
|
|
432
|
-
const contextPath = args[3] ? resolvePath(args[3], projectDir) : '';
|
|
433
|
-
if (!gateEnabled(projectDir)) {
|
|
434
|
-
output({ skipped: true, blocking: false, reason: 'workflow.context_coverage_gate is false', total: 0, honored: 0, not_honored: [], message: 'Decision coverage gate disabled by config.' }, raw, undefined);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
if (!contextPath || !node_fs_1.default.existsSync(contextPath)) {
|
|
438
|
-
output({ skipped: true, blocking: false, reason: 'CONTEXT.md missing', total: 0, honored: 0, not_honored: [], message: 'No CONTEXT.md - nothing to check.' }, raw, undefined);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
const { trackable: decisions, outcome: decisionOutcome } = loadDecisionExtraction(contextPath);
|
|
442
|
-
// Mirror could-not-parse surface for verify (non-blocking advisory WARN).
|
|
443
|
-
// Fire independent of decisions.length — a parse-miss on any bullet must surface,
|
|
444
|
-
// even when some decisions were partially extracted (#1365 fix-parity with plan gate).
|
|
445
|
-
if (decisionOutcome === 'could-not-parse') {
|
|
446
|
-
const partialParse = decisions.length > 0;
|
|
447
|
-
output({
|
|
448
|
-
skipped: false,
|
|
449
|
-
blocking: false,
|
|
450
|
-
reason: 'could-not-parse',
|
|
451
|
-
total: decisions.length,
|
|
452
|
-
honored: 0,
|
|
453
|
-
not_honored: [],
|
|
454
|
-
message: partialParse
|
|
455
|
-
? 'Decision coverage verify (warning): decisions could not be fully parsed — one or more ' +
|
|
456
|
-
'`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
|
|
457
|
-
'prefix that is not a digit run). Fix the bullet format in the CONTEXT.md decisions block.'
|
|
458
|
-
: 'Decision coverage verify (warning): could not parse decisions — possible format mismatch. ' +
|
|
459
|
-
'Check the formatting of the CONTEXT.md decisions block (accepted forms: `- **D-NN:** text`, ' +
|
|
460
|
-
'`- **D4-NN:** text` (phase-prefixed), `- **D-NN — title** body`).',
|
|
461
|
-
}, raw, undefined);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (decisions.length === 0) {
|
|
465
|
-
output({ skipped: true, blocking: false, reason: 'no trackable decisions', total: 0, honored: 0, not_honored: [], message: 'No trackable decisions in CONTEXT.md.' }, raw, undefined);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const planContents = loadPlanContents(phaseDir);
|
|
469
|
-
// #3183 (lint-plan-count-drift): same single-owner sourcing as
|
|
470
|
-
// loadPlanContents above — scanPhasePlans's summaryFiles instead of a
|
|
471
|
-
// local `-SUMMARY.md` readdirSync filter.
|
|
472
|
-
const summaryParts = node_fs_1.default.existsSync(phaseDir)
|
|
473
|
-
? scanPhasePlans(phaseDir).summaryFiles.map((entry) => readIfExists(node_path_1.default.join(phaseDir, entry)))
|
|
474
|
-
: [];
|
|
475
|
-
const haystack = [
|
|
476
|
-
planContents.join('\n\n'),
|
|
477
|
-
summaryParts.join('\n\n'),
|
|
478
|
-
readModifiedFilesContent(projectDir, summaryParts),
|
|
479
|
-
recentCommitMessages(projectDir),
|
|
480
|
-
].join('\n\n');
|
|
481
|
-
const notHonored = [];
|
|
482
|
-
let honored = 0;
|
|
483
|
-
for (const decision of decisions) {
|
|
484
|
-
if (decisionMentioned(haystack, decision))
|
|
485
|
-
honored++;
|
|
486
|
-
else
|
|
487
|
-
notHonored.push({ id: decision.id, text: decision.text, category: decision.category });
|
|
488
|
-
}
|
|
489
|
-
output({
|
|
490
|
-
skipped: false,
|
|
491
|
-
blocking: false,
|
|
492
|
-
total: decisions.length,
|
|
493
|
-
honored,
|
|
494
|
-
not_honored: notHonored,
|
|
495
|
-
message: buildVerifyMessage(notHonored),
|
|
496
|
-
}, raw, undefined);
|
|
71
|
+
emitGateResult((0, gate_decision_coverage_verify_cjs_1.evaluateDecisionCoverageVerify)({ projectDir, args: args.slice(2) }), raw);
|
|
497
72
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
* ui-plan-gate: given a phase number, checks whether the phase has frontend
|
|
501
|
-
* indicators and whether a *-UI-SPEC.md already exists in the phase directory.
|
|
502
|
-
*
|
|
503
|
-
* Returns JSON: { frontend, hasFrontendEvidence, hasUiSpec, block, uiSpecPath, matchedToken, matchedLine }
|
|
504
|
-
* block = frontend && hasFrontendEvidence && !hasUiSpec (#3312: gate fires when
|
|
505
|
-
* UI work is detected AND the repo has static frontend evidence but no spec exists)
|
|
506
|
-
*
|
|
507
|
-
* Invocable as: gsd_run check ui-plan-gate <phase>
|
|
508
|
-
*
|
|
509
|
-
* Uses checkUiPresence from ui-safety-gate.cjs — does NOT reimplement frontend detection.
|
|
510
|
-
* Uses getRoadmapPhaseWithFallback + findPhaseInternal from leaf modules for phase data.
|
|
511
|
-
*/
|
|
512
|
-
function findUiSpecInDir(phaseDir) {
|
|
513
|
-
if (!phaseDir || !node_fs_1.default.existsSync(phaseDir))
|
|
514
|
-
return '';
|
|
515
|
-
try {
|
|
516
|
-
const files = node_fs_1.default.readdirSync(phaseDir);
|
|
517
|
-
const found = files.find((f) => /-UI-SPEC\.md$/.test(f));
|
|
518
|
-
return found ? node_path_1.default.join(phaseDir, found) : '';
|
|
519
|
-
}
|
|
520
|
-
catch {
|
|
521
|
-
return '';
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Pure logic for ui-plan-gate — exposed for direct behavioral testing.
|
|
526
|
-
*
|
|
527
|
-
* Given a projectDir and phase number:
|
|
528
|
-
* (a) Reads the phase section from ROADMAP.md via getRoadmapPhaseWithFallback —
|
|
529
|
-
* same two-pass lookup (current milestone → full roadmap) as `roadmap.get-phase`
|
|
530
|
-
* (cmdRoadmapGetPhase). Cross-milestone / older frontend phases resolve correctly.
|
|
531
|
-
* If ROADMAP.md is missing, phaseSection is '' (ROADMAP.md not present = project
|
|
532
|
-
* has no roadmap = cannot be frontend). If the phase truly can't be found after
|
|
533
|
-
* both passes, phaseSection is '' and phaseLookupFailed is set so callers can
|
|
534
|
-
* surface the miss — we do NOT silently degrade to frontend:false if the roadmap
|
|
535
|
-
* exists but the phase header is absent.
|
|
536
|
-
* (b) Runs checkUiPresence (frontend detection) — no reimplementation.
|
|
537
|
-
* (c) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md.
|
|
538
|
-
*
|
|
539
|
-
* Returns: { frontend, hasFrontendEvidence, hasUiSpec, block, uiSpecPath, matchedToken, matchedLine, phaseLookupFailed }
|
|
540
|
-
* block = frontend && hasFrontendEvidence && !hasUiSpec (#3312)
|
|
541
|
-
* phaseLookupFailed = ROADMAP.md present but phase header not found (surfaced for
|
|
542
|
-
* onError:halt gates so a missing phase doesn't silently bypass)
|
|
543
|
-
*
|
|
544
|
-
* #3312 — structural corroboration: `frontend` is a vocabulary signal only. A
|
|
545
|
-
* hyphen is a word boundary, so a phase naming the repo `dashboard-financeiro`
|
|
546
|
-
* matches the token `dashboard` exactly like the real compound `micro-frontend`
|
|
547
|
-
* (the boundary rule of #3718 is intentional and untouched). The gate therefore
|
|
548
|
-
* blocks only when the token match is corroborated by static frontend evidence
|
|
549
|
-
* in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep or
|
|
550
|
-
* a component-framework file). This mirrors the sibling post-wave gate
|
|
551
|
-
* computeUiSafetyGate, which requires `hasUiFiles` (git diff) before blocking.
|
|
552
|
-
* matchedToken/matchedLine surface what tripped the sniffer so an operator can
|
|
553
|
-
* judge the flag in one second instead of reaching for --skip-ui.
|
|
554
|
-
*/
|
|
555
|
-
function computeUiPlanGate(projectDir, phase) {
|
|
556
|
-
// (a) Read the phase section text using the same two-pass lookup as roadmap.get-phase.
|
|
557
|
-
// getRoadmapPhaseWithFallback: current-milestone first, then stripShippedMilestones
|
|
558
|
-
// fallback — mirrors cmdRoadmapGetPhase exactly.
|
|
559
|
-
let phaseSection = '';
|
|
560
|
-
let phaseLookupFailed;
|
|
561
|
-
try {
|
|
562
|
-
const section = getRoadmapPhaseWithFallback(projectDir, phase);
|
|
563
|
-
if (section === null) {
|
|
564
|
-
// Distinguish: ROADMAP.md missing (no-roadmap project) vs phase not found in ROADMAP.
|
|
565
|
-
// planningDir(cwd) resolves the .planning/ root for workstream-aware paths.
|
|
566
|
-
const planDir = planningDir(projectDir);
|
|
567
|
-
const roadmapPath = node_path_1.default.join(planDir, 'ROADMAP.md');
|
|
568
|
-
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
569
|
-
// ROADMAP.md exists but phase was not found → surface the miss
|
|
570
|
-
phaseLookupFailed = true;
|
|
571
|
-
}
|
|
572
|
-
// phaseSection stays ''
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
phaseSection = section;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
catch { /* roadmap read failure → treat as empty (non-frontend) */ }
|
|
579
|
-
// (b) Run checkUiPresence (frontend detection) — reuse existing helper; no reimplementation
|
|
580
|
-
const presenceResult = (0, ui_safety_gate_cjs_1.checkUiPresence)(phaseSection);
|
|
581
|
-
const frontend = presenceResult.hasUI;
|
|
582
|
-
// (b') #3312 — static structural corroboration. Only probed when the sniffer
|
|
583
|
-
// matched (evidence is irrelevant otherwise); failures degrade to false.
|
|
584
|
-
const hasFrontendEvidence = frontend ? (0, ui_frontend_evidence_cjs_1.hasStaticFrontendEvidence)(projectDir) : false;
|
|
585
|
-
// (c) Resolve phase directory via findPhaseInternal and check for *-UI-SPEC.md
|
|
586
|
-
let phaseDir = '';
|
|
587
|
-
try {
|
|
588
|
-
const result = findPhaseInternal(projectDir, phase);
|
|
589
|
-
if (result && typeof result === 'object') {
|
|
590
|
-
// findPhaseInternal returns { directory: '<relative-posix-path>', ... }
|
|
591
|
-
// directory is relative to cwd — resolve it to absolute.
|
|
592
|
-
const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
|
|
593
|
-
if (relDir) {
|
|
594
|
-
phaseDir = node_path_1.default.resolve(projectDir, relDir);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
else if (typeof result === 'string') {
|
|
598
|
-
phaseDir = result;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
catch { /* phase dir lookup failure → hasUiSpec=false */ }
|
|
602
|
-
const uiSpecPath = findUiSpecInDir(phaseDir);
|
|
603
|
-
const hasUiSpec = uiSpecPath !== '';
|
|
604
|
-
// block = frontend phase with structural frontend evidence and no UI-SPEC (#3312)
|
|
605
|
-
const block = frontend && hasFrontendEvidence && !hasUiSpec;
|
|
606
|
-
const result = {
|
|
607
|
-
frontend, hasFrontendEvidence, hasUiSpec, block,
|
|
608
|
-
uiSpecPath: hasUiSpec ? uiSpecPath : null,
|
|
609
|
-
matchedToken: presenceResult.matchedToken,
|
|
610
|
-
matchedLine: presenceResult.matchedLine,
|
|
611
|
-
};
|
|
612
|
-
if (phaseLookupFailed)
|
|
613
|
-
result.phaseLookupFailed = true;
|
|
614
|
-
return result;
|
|
73
|
+
function cmdEvaluationScope(projectDir, args, raw) {
|
|
74
|
+
emitGateResult((0, gate_evaluation_scope_cjs_1.evaluateEvaluationScope)({ projectDir, args: args.slice(2) }), raw);
|
|
615
75
|
}
|
|
616
76
|
function cmdUiPlanGate(projectDir, args, raw) {
|
|
617
|
-
|
|
618
|
-
const phase = args[2] || '';
|
|
619
|
-
if (!phase) {
|
|
620
|
-
error('ui-plan-gate requires a phase argument: check ui-plan-gate <phase>', ERROR_REASON.SDK_MISSING_ARG);
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
output(computeUiPlanGate(projectDir, phase), raw, undefined);
|
|
624
|
-
}
|
|
625
|
-
// ─── ui-safety-gate ───────────────────────────────────────────────────────────
|
|
626
|
-
/**
|
|
627
|
-
* ui-safety-gate: post-wave check that verifies UI-changed files conform to
|
|
628
|
-
* the active UI-SPEC for the phase. Called after each wave by execute:wave:post.
|
|
629
|
-
*
|
|
630
|
-
* Returns JSON: { frontend: boolean, hasUiFiles: boolean, hasUiSpec: boolean, block: boolean, message?: string }
|
|
631
|
-
* block = frontend && hasUiFiles && !hasUiSpec
|
|
632
|
-
*
|
|
633
|
-
* Args: check ui-safety-gate <phase>
|
|
634
|
-
* Invocable as: gsd_run check ui-safety-gate <phase>
|
|
635
|
-
* or gsd_run check ui.safety-gate <phase> (dots normalized to hyphens)
|
|
636
|
-
*
|
|
637
|
-
* Uses checkUiPresence from ui-safety-gate.cjs — does NOT reimplement frontend detection.
|
|
638
|
-
* Checks whether any files changed in recent git history match frontend file patterns.
|
|
639
|
-
* Also checks whether a *-UI-SPEC.md exists in the phase directory (same as ui-plan-gate).
|
|
640
|
-
*
|
|
641
|
-
* Limitation: uses git diff HEAD~1..HEAD which covers only the last commit; in a
|
|
642
|
-
* multi-plan wave the wave-start commit would be more accurate but is not yet stored
|
|
643
|
-
* in the wave manifest. This is tracked as a known limitation.
|
|
644
|
-
*/
|
|
645
|
-
const UI_FILE_EXTENSIONS_RE = /\.(tsx|jsx|css|scss|sass|less|vue|svelte|html)$/i;
|
|
646
|
-
const UI_PATH_PATTERNS_RE = /\/(components|pages|views|screens|layouts|ui|frontend)\//i;
|
|
647
|
-
/**
|
|
648
|
-
* Pure logic for ui-safety-gate — exposed for direct behavioral testing.
|
|
649
|
-
*
|
|
650
|
-
* Given a projectDir and phase number:
|
|
651
|
-
* (a) Reads the phase section from ROADMAP.md via getRoadmapPhaseWithFallback —
|
|
652
|
-
* same lookup as computeUiPlanGate — to determine if this is a frontend phase.
|
|
653
|
-
* (b) Runs checkUiPresence (frontend detection) — no reimplementation.
|
|
654
|
-
* (c) Checks git diff HEAD~1..HEAD for UI file changes in the current worktree.
|
|
655
|
-
* (d) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md.
|
|
656
|
-
*
|
|
657
|
-
* Returns: { frontend, hasUiFiles, hasUiSpec, block, message?, phaseLookupFailed? }
|
|
658
|
-
* block = frontend && hasUiFiles && !hasUiSpec
|
|
659
|
-
* phaseLookupFailed = ROADMAP.md present but phase header not found
|
|
660
|
-
*/
|
|
661
|
-
function computeUiSafetyGate(projectDir, phase) {
|
|
662
|
-
// (a) Read the phase section text (same two-pass lookup as computeUiPlanGate)
|
|
663
|
-
let phaseSection = '';
|
|
664
|
-
let phaseLookupFailed;
|
|
665
|
-
try {
|
|
666
|
-
const section = getRoadmapPhaseWithFallback(projectDir, phase);
|
|
667
|
-
if (section === null) {
|
|
668
|
-
const planDir = planningDir(projectDir);
|
|
669
|
-
const roadmapPath = node_path_1.default.join(planDir, 'ROADMAP.md');
|
|
670
|
-
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
671
|
-
phaseLookupFailed = true;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
else {
|
|
675
|
-
phaseSection = section;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
catch { /* roadmap read failure → treat as empty (non-frontend) */ }
|
|
679
|
-
// (b) Run checkUiPresence (frontend detection) — reuse existing helper; no reimplementation
|
|
680
|
-
const presenceResult = (0, ui_safety_gate_cjs_1.checkUiPresence)(phaseSection);
|
|
681
|
-
const frontend = presenceResult.hasUI;
|
|
682
|
-
// (c) Check whether any UI files were changed in recent git commits
|
|
683
|
-
// Uses git diff HEAD~1..HEAD to detect frontend file changes since last commit.
|
|
684
|
-
// Known limitation: multi-plan waves may need the wave-start commit for full coverage.
|
|
685
|
-
let hasUiFiles = false;
|
|
686
|
-
try {
|
|
687
|
-
const changed = (0, node_child_process_1.execFileSync)('git', ['diff', '--name-only', 'HEAD~1', 'HEAD'], {
|
|
688
|
-
cwd: projectDir,
|
|
689
|
-
encoding: 'utf-8',
|
|
690
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
691
|
-
windowsHide: true,
|
|
692
|
-
timeout: 10_000,
|
|
693
|
-
});
|
|
694
|
-
hasUiFiles = changed.split('\n').some((f) => f.trim() && (UI_FILE_EXTENSIONS_RE.test(f) || UI_PATH_PATTERNS_RE.test(f)));
|
|
695
|
-
}
|
|
696
|
-
catch { /* git unavailable or no prior commit — treat as no UI files changed */ }
|
|
697
|
-
// (d) Resolve phase directory and check for *-UI-SPEC.md (same as computeUiPlanGate)
|
|
698
|
-
let phaseDir = '';
|
|
699
|
-
try {
|
|
700
|
-
const result = findPhaseInternal(projectDir, phase);
|
|
701
|
-
if (result && typeof result === 'object') {
|
|
702
|
-
const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
|
|
703
|
-
if (relDir) {
|
|
704
|
-
phaseDir = node_path_1.default.resolve(projectDir, relDir);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
else if (typeof result === 'string') {
|
|
708
|
-
phaseDir = result;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
catch { /* phase dir lookup failure → hasUiSpec=false */ }
|
|
712
|
-
const uiSpecPath = findUiSpecInDir(phaseDir);
|
|
713
|
-
const hasUiSpec = uiSpecPath !== '';
|
|
714
|
-
// block only when: this is a frontend phase AND UI files were changed AND no UI-SPEC exists
|
|
715
|
-
const block = frontend && hasUiFiles && !hasUiSpec;
|
|
716
|
-
const result = { frontend, hasUiFiles, hasUiSpec, block };
|
|
717
|
-
if (block) {
|
|
718
|
-
result.message = `UI files changed in this wave but no UI-SPEC.md exists for Phase ${phase}. ` +
|
|
719
|
-
`Run /gsd:ui-phase ${phase} to generate the design contract before continuing.`;
|
|
720
|
-
}
|
|
721
|
-
if (phaseLookupFailed)
|
|
722
|
-
result.phaseLookupFailed = true;
|
|
723
|
-
return result;
|
|
77
|
+
emitGateResult((0, gate_ui_plan_cjs_1.evaluateUiPlanGate)({ projectDir, args: args.slice(2) }), raw);
|
|
724
78
|
}
|
|
725
79
|
function cmdUiSafetyGate(projectDir, args, raw) {
|
|
726
|
-
|
|
727
|
-
const phase = args[2] || '';
|
|
728
|
-
if (!phase) {
|
|
729
|
-
error('ui-safety-gate requires a phase argument: check ui-safety-gate <phase>', ERROR_REASON.SDK_MISSING_ARG);
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
output(computeUiSafetyGate(projectDir, phase), raw, undefined);
|
|
80
|
+
emitGateResult((0, gate_ui_safety_cjs_1.evaluateUiSafetyGate)({ projectDir, args: args.slice(2) }), raw);
|
|
733
81
|
}
|
|
734
82
|
function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
735
|
-
|
|
736
|
-
const phase = args[2] || '';
|
|
737
|
-
if (!phase) {
|
|
738
|
-
error('tdd.review-checkpoint requires a phase argument: check tdd.review-checkpoint <phase>', ERROR_REASON.SDK_MISSING_ARG);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
// Resolve phase directory
|
|
742
|
-
let phaseDir = '';
|
|
743
|
-
try {
|
|
744
|
-
const result = findPhaseInternal(projectDir, phase);
|
|
745
|
-
if (result && typeof result === 'object') {
|
|
746
|
-
const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
|
|
747
|
-
if (relDir)
|
|
748
|
-
phaseDir = node_path_1.default.resolve(projectDir, relDir);
|
|
749
|
-
}
|
|
750
|
-
else if (typeof result === 'string') {
|
|
751
|
-
phaseDir = result;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
catch { /* phase dir lookup failure */ }
|
|
755
|
-
// Find all PLAN.md files with type: tdd in frontmatter
|
|
756
|
-
const tddPlanFiles = [];
|
|
757
|
-
if (phaseDir) {
|
|
758
|
-
try {
|
|
759
|
-
// #3183: canonical plan set (root+nested, superseded-excluded) from the
|
|
760
|
-
// single owner, rather than a root-only hand-rolled readdirSync filter.
|
|
761
|
-
const files = scanPhasePlans(phaseDir).planFiles;
|
|
762
|
-
for (const file of files) {
|
|
763
|
-
const planPath = node_path_1.default.join(phaseDir, file);
|
|
764
|
-
const content = readIfExists(planPath);
|
|
765
|
-
// Check frontmatter for type: tdd
|
|
766
|
-
// CRLF-tolerant: a PLAN.md written with Windows line endings (---\r\n...---)
|
|
767
|
-
// must still match. The same CRLF-tolerant form is already used at line 205
|
|
768
|
-
// (extractPlanDesignatedSections); this is the same canonical pattern, applied
|
|
769
|
-
// here for the tdd-classification path. Fixes #2449.
|
|
770
|
-
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
771
|
-
if (frontmatterMatch) {
|
|
772
|
-
const fm = frontmatterMatch[1];
|
|
773
|
-
if (/^type:\s*tdd\s*$/m.test(fm)) {
|
|
774
|
-
tddPlanFiles.push(planPath);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
catch { /* directory read failure */ }
|
|
780
|
-
}
|
|
781
|
-
if (tddPlanFiles.length === 0) {
|
|
782
|
-
const result = {
|
|
783
|
-
// Uniform gate contract: block = violations > 0 (advisory; never truly blocks).
|
|
784
|
-
block: false,
|
|
785
|
-
passed: true,
|
|
786
|
-
tddPlans: 0,
|
|
787
|
-
violations: 0,
|
|
788
|
-
table: '',
|
|
789
|
-
rows: [],
|
|
790
|
-
message: `No type:tdd plans found in phase ${phase}. TDD review skipped.`,
|
|
791
|
-
};
|
|
792
|
-
// Pass undefined as rawValue so --raw emits JSON (not plain text).
|
|
793
|
-
// The human-readable report is carried in `result.message` for the
|
|
794
|
-
// dispatch's advisory branch to surface.
|
|
795
|
-
output(result, raw, undefined);
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
// For each TDD plan, extract the plan ID (padded plan number) and check git log
|
|
799
|
-
const rows = [];
|
|
800
|
-
for (const planPath of tddPlanFiles) {
|
|
801
|
-
// Extract plan ID from filename (e.g. "01-02-PLAN.md" → "01-02", or "03-PLAN.md" → "03")
|
|
802
|
-
const basename = node_path_1.default.basename(planPath, '-PLAN.md');
|
|
803
|
-
// planId for commit grep: phase-plan format, e.g. "01-02"
|
|
804
|
-
const planId = basename;
|
|
805
|
-
// Check for RED gate commit: test({planId}):
|
|
806
|
-
let red = false;
|
|
807
|
-
let green = false;
|
|
808
|
-
let refactor = false;
|
|
809
|
-
try {
|
|
810
|
-
const redCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^test(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
|
|
811
|
-
red = redCommit.trim().length > 0;
|
|
812
|
-
}
|
|
813
|
-
catch { /* git unavailable or no match */ }
|
|
814
|
-
try {
|
|
815
|
-
const greenCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^feat(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
|
|
816
|
-
green = greenCommit.trim().length > 0;
|
|
817
|
-
}
|
|
818
|
-
catch { /* git unavailable or no match */ }
|
|
819
|
-
try {
|
|
820
|
-
const refactorCommit = (0, node_child_process_1.execFileSync)('git', ['log', '--oneline', `--grep=^refactor(${planId}):`, '--', '.'], { cwd: projectDir, encoding: 'utf-8', maxBuffer: 1024 * 1024, windowsHide: true, timeout: 10_000 });
|
|
821
|
-
refactor = refactorCommit.trim().length > 0;
|
|
822
|
-
}
|
|
823
|
-
catch { /* git unavailable or no match */ }
|
|
824
|
-
const missing = [];
|
|
825
|
-
if (!red)
|
|
826
|
-
missing.push('RED');
|
|
827
|
-
if (!green)
|
|
828
|
-
missing.push('GREEN');
|
|
829
|
-
const status = missing.length === 0 ? 'Pass' : 'FAIL';
|
|
830
|
-
rows.push({ planId, red, green, refactor, status, missing });
|
|
831
|
-
}
|
|
832
|
-
const violations = rows.filter(r => r.status === 'FAIL').length;
|
|
833
|
-
// Build review table
|
|
834
|
-
const tableHeader = '| Plan | RED | GREEN | REFACTOR | Status |';
|
|
835
|
-
const tableDivider = '|------|-----|-------|----------|--------|';
|
|
836
|
-
const tableRows = rows.map(r => `| ${r.planId.padEnd(4)} | ${r.red ? ' ✓ ' : ' ✗ '} | ${r.green ? ' ✓ ' : ' ✗ '} | ${r.refactor ? ' ✓ ' : ' — '} | ${r.status.padEnd(6)} |`);
|
|
837
|
-
let table = [
|
|
838
|
-
`### TDD REVIEW — Phase ${phase}`,
|
|
839
|
-
'',
|
|
840
|
-
`TDD Plans: ${tddPlanFiles.length} | Gate violations: ${violations}`,
|
|
841
|
-
'',
|
|
842
|
-
tableHeader,
|
|
843
|
-
tableDivider,
|
|
844
|
-
...tableRows,
|
|
845
|
-
].join('\n');
|
|
846
|
-
if (violations > 0) {
|
|
847
|
-
table += '\n\n⚠ Gate violations are advisory — review before advancing.';
|
|
848
|
-
for (const r of rows.filter(row => row.status === 'FAIL')) {
|
|
849
|
-
table += `\n Plan ${r.planId} missing: ${r.missing.join(', ')} gate commit(s).`;
|
|
850
|
-
table += `\n Expected commit pattern: test(${r.planId}): ... → feat(${r.planId}): ...`;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
const result = {
|
|
854
|
-
// Uniform gate contract: block = violations > 0.
|
|
855
|
-
// This gate is advisory (blocking: false in capability.json) so block:true
|
|
856
|
-
// only surfaces as a warning, never halts. Kept here so the host-loop
|
|
857
|
-
// dispatch can read a single consistent `block` field.
|
|
858
|
-
block: violations > 0,
|
|
859
|
-
passed: true,
|
|
860
|
-
tddPlans: tddPlanFiles.length,
|
|
861
|
-
violations,
|
|
862
|
-
table,
|
|
863
|
-
rows,
|
|
864
|
-
// Human-readable report in `message` so the dispatch's advisory branch
|
|
865
|
-
// can surface it. --raw emits JSON (rawValue=undefined), not plain text.
|
|
866
|
-
message: table,
|
|
867
|
-
};
|
|
868
|
-
// Pass undefined as rawValue so --raw emits JSON (not the raw table text).
|
|
869
|
-
// The review table is carried in `result.message` and `result.table` so
|
|
870
|
-
// the host-loop dispatch's advisory branch can surface it.
|
|
871
|
-
output(result, raw, undefined);
|
|
872
|
-
}
|
|
873
|
-
// ─── tdd-red-evidence (#3770) ──────────────────────────────────────────────────
|
|
874
|
-
/**
|
|
875
|
-
* tdd-red-evidence: validates a persisted RED-phase test-run record for a
|
|
876
|
-
* `type: tdd` plan (#3770). Only an INTENTIONAL failure of the target test
|
|
877
|
-
* (verdict RED_EVIDENCE_OK) may authorize GREEN; zero-test discovery, fixture/
|
|
878
|
-
* load crashes, nonzero exits without a failing test, unrelated failures, and
|
|
879
|
-
* unexpected greens are INVALID_RED and block GREEN.
|
|
880
|
-
*
|
|
881
|
-
* The record is the JSON the executor persists after running the RED command:
|
|
882
|
-
* { command, exitCode, output, targetTest, targetFile?, expected?, actual? }
|
|
883
|
-
* Fail-closed: a missing/unreadable/unparseable record is INVALID_RED
|
|
884
|
-
* (reason unreadable_record), never a pass.
|
|
885
|
-
*
|
|
886
|
-
* Args: check tdd-red-evidence <record.json>
|
|
887
|
-
*/
|
|
888
|
-
function cmdTddRedEvidence(_projectDir, args, raw) {
|
|
889
|
-
const recordPath = typeof args[2] === 'string' ? args[2] : '';
|
|
890
|
-
if (!recordPath) {
|
|
891
|
-
error('tdd-red-evidence requires a record path: check tdd-red-evidence <record.json>', ERROR_REASON.SDK_MISSING_ARG);
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
const resolved = node_path_1.default.resolve(recordPath);
|
|
895
|
-
const text = readIfExists(resolved);
|
|
896
|
-
const input = (() => {
|
|
897
|
-
if (!text)
|
|
898
|
-
return null;
|
|
899
|
-
try {
|
|
900
|
-
return (JSON.parse(text) ?? {});
|
|
901
|
-
}
|
|
902
|
-
catch {
|
|
903
|
-
return null;
|
|
904
|
-
}
|
|
905
|
-
})();
|
|
906
|
-
if (!input) {
|
|
907
|
-
output({
|
|
908
|
-
passed: false,
|
|
909
|
-
block: true,
|
|
910
|
-
verdict: 'INVALID_RED',
|
|
911
|
-
reason: 'unreadable_record',
|
|
912
|
-
record: resolved,
|
|
913
|
-
readError: text ? `record is not valid JSON: ${resolved}` : `record not found or unreadable: ${resolved}`,
|
|
914
|
-
}, raw, undefined);
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
const evidenceInput = {
|
|
918
|
-
command: input['command'],
|
|
919
|
-
exitCode: input['exitCode'],
|
|
920
|
-
output: input['output'],
|
|
921
|
-
targetTest: input['targetTest'],
|
|
922
|
-
targetFile: input['targetFile'],
|
|
923
|
-
expected: input['expected'],
|
|
924
|
-
actual: input['actual'],
|
|
925
|
-
};
|
|
926
|
-
const result = (0, tdd_red_evidence_cjs_1.classifyRedEvidence)(evidenceInput);
|
|
927
|
-
const record = (0, tdd_red_evidence_cjs_1.buildRedEvidenceRecord)(evidenceInput, result);
|
|
928
|
-
output({
|
|
929
|
-
// Uniform gate contract: block = !passed. INVALID_RED blocks GREEN.
|
|
930
|
-
passed: result.verdict === 'RED_EVIDENCE_OK',
|
|
931
|
-
block: result.verdict !== 'RED_EVIDENCE_OK',
|
|
932
|
-
verdict: result.verdict,
|
|
933
|
-
reason: result.reason,
|
|
934
|
-
evidence: result.evidence,
|
|
935
|
-
record,
|
|
936
|
-
message: result.verdict === 'RED_EVIDENCE_OK'
|
|
937
|
-
? `RED evidence verified: target test "${result.evidence.target_test}" failed as expected (exit ${result.evidence.exit_code}). GREEN authorized.`
|
|
938
|
-
: `INVALID_RED (${result.reason}): GREEN blocked. Fix the RED phase — only an intentional failure of target test "${result.evidence.target_test}" authorizes production edits.`,
|
|
939
|
-
}, raw, undefined);
|
|
83
|
+
emitGateResult((0, gate_tdd_review_checkpoint_cjs_1.evaluateTddReviewCheckpoint)({ projectDir, args: args.slice(2) }), raw);
|
|
940
84
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
* cannot be resolved. Shared by every `check` arm that probes a phase's
|
|
944
|
-
* PLAN.md files, so the two never drift (DEFECT.GENERATIVE-FIX-DIVERGENCE).
|
|
945
|
-
* Never throws — the callers emit a degraded JSON payload instead, because
|
|
946
|
-
* a consumer must be able to tell "nothing to report" from "could not look".
|
|
947
|
-
*/
|
|
948
|
-
function resolvePhaseDirOrEmpty(projectDir, phase) {
|
|
949
|
-
try {
|
|
950
|
-
const result = findPhaseInternal(projectDir, phase);
|
|
951
|
-
if (result && typeof result === 'object') {
|
|
952
|
-
// findPhaseInternal returns { directory: '<relative-posix-path>', ... }
|
|
953
|
-
// directory is relative to cwd — resolve it to absolute.
|
|
954
|
-
const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
|
|
955
|
-
if (relDir) {
|
|
956
|
-
return node_path_1.default.resolve(projectDir, relDir);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
else if (typeof result === 'string') {
|
|
960
|
-
return result;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
catch { /* phase dir lookup failure → caller emits degraded payload */ }
|
|
964
|
-
return '';
|
|
85
|
+
function cmdTddRedEvidence(projectDir, args, raw) {
|
|
86
|
+
emitGateResult((0, gate_tdd_red_evidence_cjs_1.evaluateTddRedEvidence)({ projectDir, args: args.slice(2) }), raw);
|
|
965
87
|
}
|
|
966
|
-
// ─── verify-command-paths (#2401) ──────────────────────────────────────────────
|
|
967
|
-
/**
|
|
968
|
-
* verify-command-paths: probes every `<automated>` verify command declared in a
|
|
969
|
-
* phase's `-PLAN.md` files against the filesystem WITHOUT executing anything —
|
|
970
|
-
* see verify-command-grounding.cjs for the recognizer contract.
|
|
971
|
-
*
|
|
972
|
-
* Args: check verify-command-paths <phase>
|
|
973
|
-
* Invocable as: gsd_run check verify-command-paths <phase>
|
|
974
|
-
*
|
|
975
|
-
* When the phase cannot be resolved to a directory, this emits a non-throwing
|
|
976
|
-
* degraded JSON payload (status/commands/counts all zeroed, `readError`
|
|
977
|
-
* populated) rather than calling `error()` — the plan-checker parses this
|
|
978
|
-
* result and must be able to distinguish "nothing to report" from "could not
|
|
979
|
-
* look", which a non-zero exit / thrown error would collapse.
|
|
980
|
-
*/
|
|
981
88
|
function cmdVerifyCommandPaths(projectDir, args, raw) {
|
|
982
|
-
|
|
983
|
-
const phase = args[2] || '';
|
|
984
|
-
if (!phase) {
|
|
985
|
-
output({
|
|
986
|
-
status: 'unresolvable',
|
|
987
|
-
commands: [],
|
|
988
|
-
counts: { blocker: 0, warning: 0, total: 0 },
|
|
989
|
-
readError: 'verify-command-paths requires a phase argument: check verify-command-paths <phase>',
|
|
990
|
-
}, raw, undefined);
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
const phaseDir = resolvePhaseDirOrEmpty(projectDir, phase);
|
|
994
|
-
if (!phaseDir) {
|
|
995
|
-
output({
|
|
996
|
-
status: 'unresolvable',
|
|
997
|
-
commands: [],
|
|
998
|
-
counts: { blocker: 0, warning: 0, total: 0 },
|
|
999
|
-
readError: `could not resolve phase directory for phase ${phase}`,
|
|
1000
|
-
}, raw, undefined);
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
const probed = probePhaseVerifyCommands({ phaseDir, projectRoot: projectDir });
|
|
1004
|
-
output(probed, raw, undefined);
|
|
89
|
+
emitGateResult((0, gate_verify_command_paths_cjs_1.evaluateVerifyCommandPaths)({ projectDir, args: args.slice(2) }), raw);
|
|
1005
90
|
}
|
|
1006
|
-
// ─── verify-failure-directions (#3172) ─────────────────────────────────────────
|
|
1007
|
-
/**
|
|
1008
|
-
* verify-failure-directions: probes every `<automated>` verify command
|
|
1009
|
-
* declared in a phase's `-PLAN.md` files for a stated `<fails_when>` failing
|
|
1010
|
-
* direction — see verify-command-grounding.cjs for the recognizer contract.
|
|
1011
|
-
*
|
|
1012
|
-
* Args: check verify-failure-directions <phase>
|
|
1013
|
-
* Invocable as: gsd_run check verify-failure-directions <phase>
|
|
1014
|
-
*
|
|
1015
|
-
* When the phase cannot be resolved to a directory, this emits a non-throwing
|
|
1016
|
-
* degraded JSON payload (status/commands/counts all zeroed, `readError`
|
|
1017
|
-
* populated) rather than calling `error()` — the plan-checker parses this
|
|
1018
|
-
* result and must be able to distinguish "nothing to report" from "could not
|
|
1019
|
-
* look", which a non-zero exit / thrown error would collapse.
|
|
1020
|
-
*/
|
|
1021
91
|
function cmdVerifyFailureDirections(projectDir, args, raw) {
|
|
1022
|
-
|
|
1023
|
-
const phase = args[2] || '';
|
|
1024
|
-
if (!phase) {
|
|
1025
|
-
output({
|
|
1026
|
-
status: 'unresolvable',
|
|
1027
|
-
commands: [],
|
|
1028
|
-
counts: { blocker: 0, warning: 0, total: 0 },
|
|
1029
|
-
readError: 'verify-failure-directions requires a phase argument: check verify-failure-directions <phase>',
|
|
1030
|
-
}, raw, undefined);
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
const phaseDir = resolvePhaseDirOrEmpty(projectDir, phase);
|
|
1034
|
-
if (!phaseDir) {
|
|
1035
|
-
output({
|
|
1036
|
-
status: 'unresolvable',
|
|
1037
|
-
commands: [],
|
|
1038
|
-
counts: { blocker: 0, warning: 0, total: 0 },
|
|
1039
|
-
readError: `could not resolve phase directory for phase ${phase}`,
|
|
1040
|
-
}, raw, undefined);
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
const result = probePhaseFailingDirections({ phaseDir });
|
|
1044
|
-
output(result, raw, undefined);
|
|
92
|
+
emitGateResult((0, gate_verify_failure_directions_cjs_1.evaluateVerifyFailureDirections)({ projectDir, args: args.slice(2) }), raw);
|
|
1045
93
|
}
|
|
1046
|
-
// ─── gap-analysis-plan-post ───────────────────────────────────────────────────
|
|
1047
|
-
/**
|
|
1048
|
-
* gap-analysis-plan-post: non-blocking advisory check that runs the post-planning
|
|
1049
|
-
* gap analysis after all PLAN.md files are generated for a phase.
|
|
1050
|
-
*
|
|
1051
|
-
* Cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md
|
|
1052
|
-
* against the concatenated text of all *-PLAN.md files, emitting a coverage table.
|
|
1053
|
-
*
|
|
1054
|
-
* This gate is always advisory (passed: true) — it never blocks phase advancement.
|
|
1055
|
-
*
|
|
1056
|
-
* Args: check gap-analysis.plan-post <phase-dir> [phase-req-ids]
|
|
1057
|
-
* Invocable as: gsd_run check gap-analysis.plan-post <phase-dir> [phase-req-ids]
|
|
1058
|
-
*/
|
|
1059
94
|
function cmdGapAnalysisPlanPost(projectDir, args, raw) {
|
|
1060
|
-
|
|
1061
|
-
const phaseDir = args[2] || '';
|
|
1062
|
-
if (!phaseDir) {
|
|
1063
|
-
error('gap-analysis.plan-post requires a phase-dir argument: check gap-analysis.plan-post <phase-dir> [phase-req-ids]', ERROR_REASON.SDK_MISSING_ARG);
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
const resolvedPhaseDir = resolvePath(phaseDir, projectDir);
|
|
1067
|
-
const phaseReqIds = args[3] ?? undefined;
|
|
1068
|
-
const result = runGapAnalysis(projectDir, resolvedPhaseDir, { phaseReqIds });
|
|
1069
|
-
// Uniform gate contract: block = false (gap-analysis is always advisory, never blocks).
|
|
1070
|
-
// `message` carries the human-readable gap analysis report so the dispatch's
|
|
1071
|
-
// advisory branch can surface it. --raw emits JSON (rawValue=undefined), not
|
|
1072
|
-
// plain markdown text.
|
|
1073
|
-
output({
|
|
1074
|
-
block: false,
|
|
1075
|
-
passed: true,
|
|
1076
|
-
enabled: result.enabled,
|
|
1077
|
-
table: result.table,
|
|
1078
|
-
summary: result.summary,
|
|
1079
|
-
counts: result.counts,
|
|
1080
|
-
// Human-readable report in `message` for the host-loop advisory branch.
|
|
1081
|
-
message: result.table || result.summary || '',
|
|
1082
|
-
}, raw, undefined);
|
|
95
|
+
emitGateResult((0, gate_gap_analysis_plan_post_cjs_1.evaluateGapAnalysisPlanPost)({ projectDir, args: args.slice(2) }), raw);
|
|
1083
96
|
}
|
|
1084
|
-
// ─── predicate (generic gate-predicate evaluator, #2008) ──────────────────────
|
|
1085
|
-
/**
|
|
1086
|
-
* Production subprocess binding for the gate-predicate evaluator. Wraps the
|
|
1087
|
-
* bounded `execTool` seam (shell-command-projection) as a `runBoundedShell`
|
|
1088
|
-
* the pure evaluator consumes. `sh -c` runs the interpolated command; the
|
|
1089
|
-
* subprocess inherits the process env and is killed (SIGTERM) on timeout.
|
|
1090
|
-
*
|
|
1091
|
-
* `timedOut` is derived from the kill signal: spawnSync sets `signal: 'SIGTERM'`
|
|
1092
|
-
* when the `timeout` fires, distinct from a normal non-zero exit code. A command
|
|
1093
|
-
* that self-terminates with SIGTERM is indistinguishable at this seam and is
|
|
1094
|
-
* reported as a timeout — either way the gate blocks (non-zero), so the outcome
|
|
1095
|
-
* is fail-closed and correct. See ADR-2008.
|
|
1096
|
-
*/
|
|
1097
|
-
function buildPredicateDeps() {
|
|
1098
|
-
return {
|
|
1099
|
-
runBoundedShell(opts) {
|
|
1100
|
-
const r = (0, shell_command_projection_cjs_1.execTool)('sh', ['-c', opts.command], { cwd: opts.cwd, timeout: opts.timeoutMs });
|
|
1101
|
-
return {
|
|
1102
|
-
exitCode: r.exitCode,
|
|
1103
|
-
stdout: r.stdout,
|
|
1104
|
-
stderr: r.stderr,
|
|
1105
|
-
signal: r.signal,
|
|
1106
|
-
timedOut: r.timedOut,
|
|
1107
|
-
};
|
|
1108
|
-
},
|
|
1109
|
-
findPhaseArtifact(phaseDir, artifactSuffix) {
|
|
1110
|
-
if (!node_fs_1.default.existsSync(phaseDir))
|
|
1111
|
-
return null;
|
|
1112
|
-
if (artifactSuffix === '.' ||
|
|
1113
|
-
artifactSuffix === '..' ||
|
|
1114
|
-
artifactSuffix.includes('\0') ||
|
|
1115
|
-
node_path_1.default.basename(artifactSuffix) !== artifactSuffix ||
|
|
1116
|
-
node_path_1.default.win32.basename(artifactSuffix) !== artifactSuffix) {
|
|
1117
|
-
return null;
|
|
1118
|
-
}
|
|
1119
|
-
const directContained = (0, security_cjs_1.tryWithinRoot)(artifactSuffix, phaseDir);
|
|
1120
|
-
if (directContained !== null && node_fs_1.default.existsSync(directContained) && node_fs_1.default.statSync(directContained).isFile()) {
|
|
1121
|
-
return directContained;
|
|
1122
|
-
}
|
|
1123
|
-
const planningContained = (0, security_cjs_1.tryWithinRoot)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
|
|
1124
|
-
if (planningContained !== null && node_fs_1.default.existsSync(planningContained) && node_fs_1.default.statSync(planningContained).isFile()) {
|
|
1125
|
-
return planningContained;
|
|
1126
|
-
}
|
|
1127
|
-
try {
|
|
1128
|
-
const files = node_fs_1.default.readdirSync(phaseDir);
|
|
1129
|
-
for (const f of files) {
|
|
1130
|
-
if (f.endsWith('-' + artifactSuffix) || f === artifactSuffix) {
|
|
1131
|
-
const candidateContained = (0, security_cjs_1.tryWithinRoot)(f, phaseDir);
|
|
1132
|
-
if (candidateContained !== null && node_fs_1.default.statSync(candidateContained).isFile())
|
|
1133
|
-
return candidateContained;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
catch { /* ignore */ }
|
|
1138
|
-
return null;
|
|
1139
|
-
},
|
|
1140
|
-
readFrontmatter(filePath) {
|
|
1141
|
-
const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
|
|
1142
|
-
if (content === null)
|
|
1143
|
-
throw new Error(`predicate artifact disappeared before it could be read: ${filePath}`);
|
|
1144
|
-
const parsed = extractFrontmatter(content, filePath);
|
|
1145
|
-
return parsed;
|
|
1146
|
-
}
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Split an args array into `--flag value` pairs and the leftover positional
|
|
1151
|
-
* tokens, in ONE pass, with the semantics `check predicate` established
|
|
1152
|
-
* (#2008): a `--flag` followed by a non-`--` token consumes it as the value
|
|
1153
|
-
* (last write wins); a `--flag` with no value stays a bare token and moves to
|
|
1154
|
-
* the positionals; everything else is positional. `parsePredicateFlags` is
|
|
1155
|
-
* the flags half of this same pass — there is exactly one parser, so the
|
|
1156
|
-
* flag-taking check verbs cannot drift apart (#4130 follow-up: `check
|
|
1157
|
-
* decision-coverage-plan --context <path>` shares it).
|
|
1158
|
-
*/
|
|
1159
|
-
function partitionPredicateArgs(args) {
|
|
1160
|
-
const flags = {};
|
|
1161
|
-
const positionals = [];
|
|
1162
|
-
for (let i = 0; i < args.length; i++) {
|
|
1163
|
-
const a = args[i];
|
|
1164
|
-
if (typeof a !== 'string')
|
|
1165
|
-
continue;
|
|
1166
|
-
if (!a.startsWith('--')) {
|
|
1167
|
-
positionals.push(a);
|
|
1168
|
-
continue;
|
|
1169
|
-
}
|
|
1170
|
-
const key = a.slice(2);
|
|
1171
|
-
const next = args[i + 1];
|
|
1172
|
-
if (key.length > 0 && typeof next === 'string' && !next.startsWith('--')) {
|
|
1173
|
-
flags[key] = next;
|
|
1174
|
-
i++;
|
|
1175
|
-
}
|
|
1176
|
-
else {
|
|
1177
|
-
positionals.push(a);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
return { flags, positionals };
|
|
1181
|
-
}
|
|
1182
|
-
/** Parse `--flag value` pairs from an args array into a map (last write wins). */
|
|
1183
|
-
function parsePredicateFlags(args) {
|
|
1184
|
-
return partitionPredicateArgs(args).flags;
|
|
1185
|
-
}
|
|
1186
|
-
/**
|
|
1187
|
-
* `check predicate` — generic evaluator for capability gate `check.predicate`
|
|
1188
|
-
* blocks (#2008). The workflow gate-dispatch invokes this for any gate whose
|
|
1189
|
-
* `check` carries a `predicate` (instead of a `query`); the predicate object is
|
|
1190
|
-
* passed as `--predicate '<json>'`. Emits the standard `{ block, message,
|
|
1191
|
-
* details? }` gate contract on success. A malformed predicate / unknown kind
|
|
1192
|
-
* THROWS inside the evaluator and is mapped here to `error()` (non-zero exit),
|
|
1193
|
-
* which the workflow's two-step gate contract treats as a step-1 command failure
|
|
1194
|
-
* routed per the gate's `onError`.
|
|
1195
|
-
*
|
|
1196
|
-
* Invocation:
|
|
1197
|
-
* gsd_run check predicate --predicate '<json>' \
|
|
1198
|
-
* [--phase-dir <dir>] [--phase-number <n>] [--phase-req-ids <ids>] --raw
|
|
1199
|
-
*
|
|
1200
|
-
* The subprocess runs at the runtime project root (the `cwd` passed to this
|
|
1201
|
-
* router), inheriting the process env. Interpolation placeholders
|
|
1202
|
-
* ${PHASE_NUMBER}/${PHASE_DIR}/${PHASE_REQ_IDS} are substituted from the flags.
|
|
1203
|
-
*/
|
|
1204
97
|
function cmdCheckPredicate(projectDir, args, raw) {
|
|
1205
|
-
|
|
1206
|
-
const predicateJson = flags['predicate'];
|
|
1207
|
-
if (!predicateJson) {
|
|
1208
|
-
error('predicate requires --predicate <json> (the gate hook check.predicate object)', ERROR_REASON.SDK_MISSING_ARG);
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
let predicate;
|
|
1212
|
-
try {
|
|
1213
|
-
predicate = JSON.parse(predicateJson);
|
|
1214
|
-
}
|
|
1215
|
-
catch {
|
|
1216
|
-
error('predicate --predicate value must be valid JSON', ERROR_REASON.USAGE);
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
const rawPhaseDir = flags['phase-dir'];
|
|
1220
|
-
let resolvedPhaseDir = rawPhaseDir;
|
|
1221
|
-
if (typeof rawPhaseDir === 'string' && rawPhaseDir !== '') {
|
|
1222
|
-
resolvedPhaseDir = resolvePath(rawPhaseDir, projectDir);
|
|
1223
|
-
}
|
|
1224
|
-
const ctx = {
|
|
1225
|
-
cwd: projectDir,
|
|
1226
|
-
phaseNumber: flags['phase-number'],
|
|
1227
|
-
phaseDir: resolvedPhaseDir,
|
|
1228
|
-
phaseReqIds: flags['phase-req-ids'],
|
|
1229
|
-
};
|
|
1230
|
-
let result;
|
|
1231
|
-
try {
|
|
1232
|
-
result = evaluatePredicate(predicate, ctx, buildPredicateDeps());
|
|
1233
|
-
}
|
|
1234
|
-
catch (e) {
|
|
1235
|
-
error(`gate predicate evaluation failed: ${e.message}`, ERROR_REASON.USAGE);
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
output(result, raw, undefined);
|
|
98
|
+
emitGateResult((0, gate_predicate_cjs_1.evaluateCheckPredicate)({ projectDir, args: args.slice(2) }), raw);
|
|
1239
99
|
}
|
|
1240
|
-
// ─── api-coverage-verify-pre ──────────────────────────────────────────────────
|
|
1241
|
-
/**
|
|
1242
|
-
* api-coverage.verify-pre: BLOCKING seal-time gate for the ai-integration
|
|
1243
|
-
* capability (#1562). Enforces "Full API Coverage by Default — Opt Out, Never
|
|
1244
|
-
* Opt In." A phase that integrates an external API/SDK/service may not seal
|
|
1245
|
-
* until a COVERAGE.md matrix enumerates the surface and every non-integrated
|
|
1246
|
-
* capability is an explicit, reasoned opt-out.
|
|
1247
|
-
*
|
|
1248
|
-
* Contract (two touch points composed into one check):
|
|
1249
|
-
* 1. If COVERAGE.md exists in the phase dir → validate it (acceptance #2).
|
|
1250
|
-
* Block on any validation error (empty matrix, OPT-OUT without reason,
|
|
1251
|
-
* duplicate/empty capability).
|
|
1252
|
-
* 2. If COVERAGE.md is absent → run detectApiIntegration over the phase scope
|
|
1253
|
-
* (PLAN.md body, then ROADMAP phase section as fallback). If a strong
|
|
1254
|
-
* external-API-integration signal is detected → BLOCK ("integration
|
|
1255
|
-
* detected without coverage matrix"). If no signal → PASS (treat as a
|
|
1256
|
-
* non-API phase; acceptance #4 — low false positives).
|
|
1257
|
-
*
|
|
1258
|
-
* The detector is the FALLBACK for the "nobody decided / forgot the matrix"
|
|
1259
|
-
* case; the primary path is the plan:pre contribution prompting COVERAGE.md.
|
|
1260
|
-
*
|
|
1261
|
-
* Args: check api-coverage.verify-pre <phase-dir>
|
|
1262
|
-
* Emits the uniform gate contract: { block, passed, message, ...details }.
|
|
1263
|
-
*/
|
|
1264
100
|
function cmdApiCoverageVerifyPre(projectDir, args, raw) {
|
|
1265
|
-
|
|
1266
|
-
if (!phaseArg) {
|
|
1267
|
-
error('api-coverage.verify-pre requires a phase argument: check api-coverage.verify-pre <phase-dir-or-token>', ERROR_REASON.SDK_MISSING_ARG);
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
const pDir = planningDir(projectDir);
|
|
1271
|
-
const phasesRoot = node_path_1.default.join(pDir, 'phases');
|
|
1272
|
-
// SECURITY (path traversal): the phase argument is taken ONLY as a phase
|
|
1273
|
-
// token — its basename — and resolved by findPhaseInternal strictly under
|
|
1274
|
-
// .planning/phases/ (or a milestone archive). The raw arg is never used as a
|
|
1275
|
-
// path, so `..`, absolute paths, and arbitrary directories cannot reach a
|
|
1276
|
-
// file read. Mirrors cmdVerifySchemaDrift's token-match approach.
|
|
1277
|
-
let token = (0, shell_command_projection_cjs_1.posixNormalize)(phaseArg).split('/').filter(Boolean).pop() || '';
|
|
1278
|
-
// A token like ".." or "." carries no phase identity → unresolvable.
|
|
1279
|
-
if (token === '.' || token === '..')
|
|
1280
|
-
token = '';
|
|
1281
|
-
// Not a GSD project (no phases tree at all) → fail-open: nothing to gate.
|
|
1282
|
-
if (!node_fs_1.default.existsSync(phasesRoot)) {
|
|
1283
|
-
output({
|
|
1284
|
-
block: false,
|
|
1285
|
-
passed: true,
|
|
1286
|
-
coverage_present: false,
|
|
1287
|
-
detected: false,
|
|
1288
|
-
message: 'api-coverage: no .planning/phases directory; gate skipped (not a GSD project layout)',
|
|
1289
|
-
}, raw, undefined);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
// Resolve the phase dir under the contained phases root.
|
|
1293
|
-
let resolvedDir = null;
|
|
1294
|
-
let phaseNumber = '';
|
|
1295
|
-
if (token) {
|
|
1296
|
-
const found = findPhaseInternal(projectDir, token);
|
|
1297
|
-
if (found && found.directory) {
|
|
1298
|
-
resolvedDir = found.directory;
|
|
1299
|
-
phaseNumber = found.phase_number || '';
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
if (!resolvedDir) {
|
|
1303
|
-
// The phases tree EXISTS but THIS phase could not be resolved. For a
|
|
1304
|
-
// BLOCKING gate, fail-closed: a missing phase dir must not silently bypass
|
|
1305
|
-
// the coverage requirement. (Distinguished from "no .planning at all"
|
|
1306
|
-
// above, which is a genuine non-GSD-project → pass.)
|
|
1307
|
-
output({
|
|
1308
|
-
block: true,
|
|
1309
|
-
passed: false,
|
|
1310
|
-
coverage_present: false,
|
|
1311
|
-
detected: false,
|
|
1312
|
-
phase_lookup_failed: true,
|
|
1313
|
-
message: `api-coverage: could not resolve phase "${phaseArg}" under .planning/phases/. ` +
|
|
1314
|
-
'Resolve the phase directory (or produce COVERAGE.md) before sealing.',
|
|
1315
|
-
}, raw, undefined);
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
// Defense-in-depth: the resolved dir must be inside the phases root (or a
|
|
1319
|
-
// milestone archive under .planning/milestones).
|
|
1320
|
-
const milestonesRoot = node_path_1.default.join(pDir, 'milestones');
|
|
1321
|
-
// Lexical containment (ADR-4650): resolvedDir is a directory path, not read
|
|
1322
|
-
// through here — mirrors the prior path.resolve(root, candidate)-based check
|
|
1323
|
-
// without introducing a filesystem/realpath dependency this defense-in-depth
|
|
1324
|
-
// recheck never had.
|
|
1325
|
-
if ((0, security_cjs_1.tryWithinRootLexical)(resolvedDir, phasesRoot) === null &&
|
|
1326
|
-
(0, security_cjs_1.tryWithinRootLexical)(resolvedDir, milestonesRoot) === null) {
|
|
1327
|
-
output({
|
|
1328
|
-
block: true,
|
|
1329
|
-
passed: false,
|
|
1330
|
-
coverage_present: false,
|
|
1331
|
-
detected: false,
|
|
1332
|
-
message: 'api-coverage: resolved phase dir escapes .planning/ — refusing to evaluate',
|
|
1333
|
-
}, raw, undefined);
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
// (1) locate COVERAGE.md — prefer the exact name, then a single *-COVERAGE.md.
|
|
1337
|
-
let coverageFile = '';
|
|
1338
|
-
let suffixed = [];
|
|
1339
|
-
try {
|
|
1340
|
-
const entries = node_fs_1.default.readdirSync(resolvedDir, { withFileTypes: true });
|
|
1341
|
-
const files = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
1342
|
-
const exact = files.find((f) => /^COVERAGE\.md$/i.test(f));
|
|
1343
|
-
if (exact) {
|
|
1344
|
-
coverageFile = exact;
|
|
1345
|
-
}
|
|
1346
|
-
else {
|
|
1347
|
-
suffixed = files.filter((f) => /-COVERAGE\.md$/i.test(f)).sort();
|
|
1348
|
-
if (suffixed.length === 1)
|
|
1349
|
-
coverageFile = suffixed[0];
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
catch {
|
|
1353
|
-
// readdir failure → treat as no matrix readable; fall through to detection.
|
|
1354
|
-
}
|
|
1355
|
-
if (coverageFile) {
|
|
1356
|
-
let matrixText;
|
|
1357
|
-
try {
|
|
1358
|
-
matrixText = node_fs_1.default.readFileSync(node_path_1.default.join(resolvedDir, coverageFile), 'utf8');
|
|
1359
|
-
}
|
|
1360
|
-
catch {
|
|
1361
|
-
// COVERAGE.md exists but is unreadable (EACCES/EIO/encoding). Fail-closed
|
|
1362
|
-
// with a useful message rather than a raw throw.
|
|
1363
|
-
output({
|
|
1364
|
-
block: true,
|
|
1365
|
-
passed: false,
|
|
1366
|
-
coverage_present: true,
|
|
1367
|
-
message: `api-coverage: COVERAGE.md exists but is unreadable — fix file permissions/encoding before sealing`,
|
|
1368
|
-
}, raw, undefined);
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
const v = validateCoverageMatrix(matrixText);
|
|
1372
|
-
if (v.valid) {
|
|
1373
|
-
if (v.none_declared) {
|
|
1374
|
-
// The declaration is the human override for the detector — it PASSES
|
|
1375
|
-
// even when detection fires (that is acceptance #5's point: the
|
|
1376
|
-
// detector is fallible and the declaration is the reasoned overrule).
|
|
1377
|
-
// But a contradiction must be VISIBLE, not silent: re-run detection
|
|
1378
|
-
// over the phase scope and surface any signals it still finds
|
|
1379
|
-
// (#2365 review S-1).
|
|
1380
|
-
const declScope = readPhaseScope(projectDir, resolvedDir, phaseNumber);
|
|
1381
|
-
const declDetection = detectApiIntegration(declScope.text);
|
|
1382
|
-
const declSignals = declDetection.signals.map((s) => ({ verb: s.verb, noun: s.noun }));
|
|
1383
|
-
// The declaration legitimately wins even over a read error (it is the
|
|
1384
|
-
// human overrule), but if scope was incomplete we say so — the contract
|
|
1385
|
-
// is that contradictions stay visible, not silent (#2365 review).
|
|
1386
|
-
const baseMsg = declDetection.detected
|
|
1387
|
-
? `api-coverage: COVERAGE.md declares no external API integration, overriding ${declSignals.length} detected signal(s) — confirm the declaration is accurate`
|
|
1388
|
-
: 'api-coverage: COVERAGE.md declares no external API integration — matrix not required';
|
|
1389
|
-
output({
|
|
1390
|
-
block: false,
|
|
1391
|
-
passed: true,
|
|
1392
|
-
coverage_present: true,
|
|
1393
|
-
matrix: coverageFile,
|
|
1394
|
-
counts: v.counts,
|
|
1395
|
-
none_declared: true,
|
|
1396
|
-
detected: declDetection.detected,
|
|
1397
|
-
...(declDetection.detected ? { signals: declSignals } : {}),
|
|
1398
|
-
...(declScope.readError ? { scope_read_error: declScope.readError } : {}),
|
|
1399
|
-
message: declScope.readError
|
|
1400
|
-
? `${baseMsg} (note: phase scope was incompletely read — ${declScope.readError})`
|
|
1401
|
-
: baseMsg,
|
|
1402
|
-
}, raw, undefined);
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1405
|
-
output({
|
|
1406
|
-
block: false,
|
|
1407
|
-
passed: true,
|
|
1408
|
-
coverage_present: true,
|
|
1409
|
-
matrix: coverageFile,
|
|
1410
|
-
counts: v.counts,
|
|
1411
|
-
message: `api-coverage: matrix present (${v.counts.surface} capabilities, ${v.counts.optout} opt-out)`,
|
|
1412
|
-
}, raw, undefined);
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
// Fixed-template message (no raw cell content echoed into the LLM-facing
|
|
1416
|
-
// message). The structured `errors` array is safe (row-indexed, no cell
|
|
1417
|
-
// values) and travels as data for tooling that wants detail.
|
|
1418
|
-
output({
|
|
1419
|
-
block: true,
|
|
1420
|
-
passed: false,
|
|
1421
|
-
coverage_present: true,
|
|
1422
|
-
matrix: coverageFile,
|
|
1423
|
-
error_count: v.errors.length,
|
|
1424
|
-
errors: v.errors,
|
|
1425
|
-
message: `api-coverage: COVERAGE.md has ${v.errors.length} problem(s) — fix the matrix (every capability INTEGRATE or OPT-OUT with a reason) before sealing`,
|
|
1426
|
-
}, raw, undefined);
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
if (suffixed.length > 1) {
|
|
1430
|
-
output({
|
|
1431
|
-
block: true,
|
|
1432
|
-
passed: false,
|
|
1433
|
-
coverage_present: false,
|
|
1434
|
-
message: `api-coverage: multiple *-COVERAGE.md files found (${suffixed.length}) — consolidate into one COVERAGE.md before sealing`,
|
|
1435
|
-
}, raw, undefined);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
// (2) no matrix — detect whether this phase integrates an external API.
|
|
1439
|
-
const scope = readPhaseScope(projectDir, resolvedDir, phaseNumber);
|
|
1440
|
-
if (scope.readError) {
|
|
1441
|
-
// Fail-closed: an unreadable plan could be the one describing the
|
|
1442
|
-
// integration, so we cannot certify "no integration" — block and surface it.
|
|
1443
|
-
output({
|
|
1444
|
-
block: true,
|
|
1445
|
-
passed: false,
|
|
1446
|
-
coverage_present: false,
|
|
1447
|
-
detected: false,
|
|
1448
|
-
message: `api-coverage: could not read the phase scope (${scope.readError}); ` +
|
|
1449
|
-
'refusing to certify no external-API integration from incomplete scope. ' +
|
|
1450
|
-
'Fix the unreadable plan file, or add a COVERAGE.md declaration.',
|
|
1451
|
-
}, raw, undefined);
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
// An EMPTY scope is not a negative verdict. This gate's neighbouring arms
|
|
1455
|
-
// already fail closed (unresolvable phase → block; unreadable plan → block),
|
|
1456
|
-
// but a phase with no plan body AND no roadmap section fell through to
|
|
1457
|
-
// detection over zero bytes and CERTIFIED "no external-API integration" —
|
|
1458
|
-
// clearing a blocking seal gate on a probe that examined nothing
|
|
1459
|
-
// (ADR-3889 failure class (c), #3909). The discriminator is BYTES EXAMINED,
|
|
1460
|
-
// never SIGNALS FOUND: a phase with real plans and no API vocabulary still
|
|
1461
|
-
// reaches the pass below unchanged.
|
|
1462
|
-
if (scope.text.trim() === '') {
|
|
1463
|
-
output({
|
|
1464
|
-
block: true,
|
|
1465
|
-
passed: false,
|
|
1466
|
-
coverage_present: false,
|
|
1467
|
-
detected: false,
|
|
1468
|
-
scope_unavailable: true,
|
|
1469
|
-
message: 'api-coverage: the phase scope is empty — no plan body and no roadmap section were ' +
|
|
1470
|
-
'found, so nothing was examined. Refusing to certify no external-API integration ' +
|
|
1471
|
-
'from an unestablished scope. Add the phase plan, or add a COVERAGE.md declaration.',
|
|
1472
|
-
}, raw, undefined);
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
const detection = detectApiIntegration(scope.text);
|
|
1476
|
-
if (detection.detected) {
|
|
1477
|
-
// Surface only verb/noun (typed, bounded) — NOT raw prose snippets — so the
|
|
1478
|
-
// gate output cannot relay injected PLAN.md instructions to the orchestrator.
|
|
1479
|
-
const signals = detection.signals.map((s) => ({ verb: s.verb, noun: s.noun }));
|
|
1480
|
-
output({
|
|
1481
|
-
block: true,
|
|
1482
|
-
passed: false,
|
|
1483
|
-
coverage_present: false,
|
|
1484
|
-
detected: true,
|
|
1485
|
-
signals,
|
|
1486
|
-
message: 'api-coverage: external-API integration detected without a coverage matrix. ' +
|
|
1487
|
-
'Produce COVERAGE.md enumerating the API surface (every capability INTEGRATE or ' +
|
|
1488
|
-
'OPT-OUT with a reason) before sealing. Full coverage is the default.',
|
|
1489
|
-
}, raw, undefined);
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
output({
|
|
1493
|
-
block: false,
|
|
1494
|
-
passed: true,
|
|
1495
|
-
coverage_present: false,
|
|
1496
|
-
detected: false,
|
|
1497
|
-
message: 'api-coverage: no external-API integration detected; coverage matrix not required',
|
|
1498
|
-
}, raw, undefined);
|
|
101
|
+
emitGateResult((0, gate_api_coverage_verify_pre_cjs_1.evaluateApiCoverageVerifyPre)({ projectDir, args: args.slice(2) }), raw);
|
|
1499
102
|
}
|
|
1500
|
-
/**
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
function isRealReadFailure(err) {
|
|
1504
|
-
const code = err?.code;
|
|
1505
|
-
return err != null && code !== 'ENOENT';
|
|
103
|
+
/** `GSD_SKIP_SCHEMA_CHECK` is the schema-drift bypass: it arrives as `env`, read here, never by the gate. */
|
|
104
|
+
function cmdSchemaDriftGate(projectDir, args, raw, env) {
|
|
105
|
+
emitGateResult((0, gate_schema_drift_cjs_1.evaluateSchemaDriftGate)({ projectDir, args: args.slice(2), env }), raw);
|
|
1506
106
|
}
|
|
1507
|
-
function
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
// #3183 (lint-plan-count-drift) now sources the plan-file list from the
|
|
1513
|
-
// single owner (scanPhasePlans) instead of a local `-PLAN\.md$` readdirSync
|
|
1514
|
-
// filter — picks up bare PLAN.md and nested plans/, and excludes
|
|
1515
|
-
// superseded plans, none of which the prior root-only exact-suffix filter
|
|
1516
|
-
// did.
|
|
1517
|
-
if (node_fs_1.default.existsSync(phaseDir)) {
|
|
1518
|
-
const scan = scanPhasePlans(phaseDir);
|
|
1519
|
-
if (scan.scope === SCOPE.UNREADABLE) {
|
|
1520
|
-
// Directory exists but scanPhasePlans's own readdirSync(phaseDir) call
|
|
1521
|
-
// failed (EACCES/EIO race) — a real read failure the gate must not
|
|
1522
|
-
// silently pass (#2365 review), mirroring the prior isRealReadFailure
|
|
1523
|
-
// branch below for the readdirSync-throws case.
|
|
1524
|
-
return {
|
|
1525
|
-
text: '',
|
|
1526
|
-
readError: 'could not read the phase directory: scanPhasePlans reported scope UNREADABLE',
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
const plans = [...scan.planFiles].sort();
|
|
1530
|
-
for (const p of plans) {
|
|
1531
|
-
try {
|
|
1532
|
-
chunks.push(node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, p), 'utf8'));
|
|
1533
|
-
}
|
|
1534
|
-
catch (err) {
|
|
1535
|
-
// A plan file that exists but cannot be read — record it and keep
|
|
1536
|
-
// reading the rest so the message names the first failure.
|
|
1537
|
-
if (!readError) {
|
|
1538
|
-
readError = `could not read ${p}: ${err instanceof Error ? err.message : String(err)}`;
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
if (readError)
|
|
1544
|
-
return { text: chunks.join('\n\n'), readError };
|
|
1545
|
-
if (chunks.join('').trim().length > 0)
|
|
1546
|
-
return { text: chunks.join('\n\n'), readError: null };
|
|
1547
|
-
// Fallback: ONLY this phase's ROADMAP section (not the whole file, which
|
|
1548
|
-
// would pollute detection with sibling-phase prose). A MISSING roadmap/section
|
|
1549
|
-
// is non-fatal; a roadmap that exists but cannot be read is a real failure.
|
|
1550
|
-
if (phaseNumber) {
|
|
1551
|
-
try {
|
|
1552
|
-
const section = getRoadmapPhaseWithFallback(projectDir, phaseNumber);
|
|
1553
|
-
if (section)
|
|
1554
|
-
return { text: section, readError: null };
|
|
1555
|
-
}
|
|
1556
|
-
catch (err) {
|
|
1557
|
-
if (isRealReadFailure(err)) {
|
|
1558
|
-
return {
|
|
1559
|
-
text: '',
|
|
1560
|
-
readError: `could not read the roadmap fallback: ${err instanceof Error ? err.message : String(err)}`,
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
return { text: '', readError: null };
|
|
107
|
+
function cmdCodebaseDriftGate(projectDir, args, raw) {
|
|
108
|
+
emitGateResult((0, gate_codebase_drift_cjs_1.evaluateCodebaseDriftGate)({ projectDir, args: args.slice(2) }), raw);
|
|
109
|
+
}
|
|
110
|
+
function cmdContextDriftGate(projectDir, args, raw) {
|
|
111
|
+
emitGateResult((0, gate_context_drift_cjs_1.evaluateContextDriftGate)({ projectDir, args: args.slice(2) }), raw);
|
|
1566
112
|
}
|
|
1567
|
-
function
|
|
113
|
+
function cmdProhibitionEnforcement(projectDir, args, raw) {
|
|
114
|
+
emitGateResult((0, gate_prohibition_enforcement_cjs_1.evaluateProhibitionEnforcementGate)({ projectDir, args: args.slice(2) }), raw);
|
|
115
|
+
}
|
|
116
|
+
function routeCheckCommand({ args, cwd, raw, env = process.env }) {
|
|
1568
117
|
// Normalize dots to hyphens in the subcommand so both forms are accepted.
|
|
1569
118
|
// This makes `check.query = "ui.plan-gate"` (dotted form in capability.json gates)
|
|
1570
119
|
// directly runnable as `gsd_run check ui.plan-gate` — the dot is normalized to
|
|
@@ -1573,117 +122,107 @@ function routeCheckCommand({ args, cwd, raw }) {
|
|
|
1573
122
|
// so the declared query must be dispatchable exactly as declared.
|
|
1574
123
|
const rawSubcommand = args[1];
|
|
1575
124
|
const subcommand = typeof rawSubcommand === 'string' ? rawSubcommand.replace(/\./g, '-') : rawSubcommand;
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
if (subcommand === 'prohibition-enforcement') {
|
|
1662
|
-
// The deterministic test-tier prohibition PRODUCER/gate (#1259, ADR-550 D5d). Locates the
|
|
1663
|
-
// wired mechanical check (node-test or lint-rule), confirms fail-first, runs it, builds
|
|
1664
|
-
// enforcementEvidence, and emits the dispositionForProhibition verdict. Invocable as
|
|
1665
|
-
// `gsd_run check prohibition-enforcement <request.json>`.
|
|
1666
|
-
(0, prohibition_enforcement_cjs_1.routeProhibitionEnforcement)(args, raw);
|
|
1667
|
-
return;
|
|
125
|
+
switch (subcommand) {
|
|
126
|
+
case 'auto-mode':
|
|
127
|
+
cmdAutoMode(cwd, raw);
|
|
128
|
+
return;
|
|
129
|
+
case 'decision-coverage-plan':
|
|
130
|
+
cmdDecisionCoveragePlan(cwd, args, raw);
|
|
131
|
+
return;
|
|
132
|
+
case 'decision-coverage-verify':
|
|
133
|
+
cmdDecisionCoverageVerify(cwd, args, raw);
|
|
134
|
+
return;
|
|
135
|
+
case 'evaluation-scope':
|
|
136
|
+
// ADR-5057 §4 / #5164: the one resolver for which commits and files a gate or
|
|
137
|
+
// workflow step evaluates (`--phase N`, `--plan P-N` or `--quick ID`).
|
|
138
|
+
cmdEvaluationScope(cwd, args, raw);
|
|
139
|
+
return;
|
|
140
|
+
case 'ui-plan-gate':
|
|
141
|
+
cmdUiPlanGate(cwd, args, raw);
|
|
142
|
+
return;
|
|
143
|
+
case 'gap-analysis-plan-post':
|
|
144
|
+
cmdGapAnalysisPlanPost(cwd, args, raw);
|
|
145
|
+
return;
|
|
146
|
+
case 'verify-command-paths':
|
|
147
|
+
// Deterministic filesystem probe for <automated> verify commands (#2401) —
|
|
148
|
+
// never executes anything; see verify-command-grounding.cjs.
|
|
149
|
+
cmdVerifyCommandPaths(cwd, args, raw);
|
|
150
|
+
return;
|
|
151
|
+
case 'verify-failure-directions':
|
|
152
|
+
// Presence probe for a stated <fails_when> per <automated> command
|
|
153
|
+
// (#3172) — never executes anything; see verify-command-grounding.cjs.
|
|
154
|
+
cmdVerifyFailureDirections(cwd, args, raw);
|
|
155
|
+
return;
|
|
156
|
+
case 'api-coverage-verify-pre':
|
|
157
|
+
// ai-integration capability blocking gate at verify:pre (#1562). Dot-to-
|
|
158
|
+
// hyphen normalization means query "api-coverage.verify-pre" routes here.
|
|
159
|
+
cmdApiCoverageVerifyPre(cwd, args, raw);
|
|
160
|
+
return;
|
|
161
|
+
case 'tdd-review-checkpoint':
|
|
162
|
+
cmdTddReviewCheckpoint(cwd, args, raw);
|
|
163
|
+
return;
|
|
164
|
+
case 'tdd-red-evidence':
|
|
165
|
+
// #3770: intentional-RED evidence gate — only a target-test failure may
|
|
166
|
+
// authorize GREEN. Validates the persisted record; never executes anything.
|
|
167
|
+
cmdTddRedEvidence(cwd, args, raw);
|
|
168
|
+
return;
|
|
169
|
+
case 'ui-safety-gate':
|
|
170
|
+
cmdUiSafetyGate(cwd, args, raw);
|
|
171
|
+
return;
|
|
172
|
+
case 'verify-schema-drift':
|
|
173
|
+
// The drift capability's gate at execute:wave:post (blocking; src/gate-schema-drift.cts).
|
|
174
|
+
// Dot-to-hyphen normalization means query "verify.schema-drift" routes here.
|
|
175
|
+
// GSD_SKIP_SCHEMA_CHECK=true bypasses the gate: read from `env` here, passed to the gate.
|
|
176
|
+
cmdSchemaDriftGate(cwd, args, raw, env);
|
|
177
|
+
return;
|
|
178
|
+
case 'verify-codebase-drift':
|
|
179
|
+
// The drift capability's gate at execute:wave:post (non-blocking; src/gate-codebase-drift.cts).
|
|
180
|
+
// Dot-to-hyphen normalization means query "verify.codebase-drift" routes here.
|
|
181
|
+
cmdCodebaseDriftGate(cwd, args, raw);
|
|
182
|
+
return;
|
|
183
|
+
case 'verify-context-drift':
|
|
184
|
+
// The drift capability's gate at plan:pre (non-blocking; src/gate-context-drift.cts).
|
|
185
|
+
// Dot-to-hyphen normalization means query "verify.context-drift" routes here.
|
|
186
|
+
cmdContextDriftGate(cwd, args, raw);
|
|
187
|
+
return;
|
|
188
|
+
case 'predicate':
|
|
189
|
+
// Generic gate-predicate evaluator (#2008). The workflow gate-dispatch calls
|
|
190
|
+
// this for any gate whose `check` carries a `predicate` (instead of a `query`),
|
|
191
|
+
// passing the predicate object as --predicate '<json>'. NOTE: unlike the
|
|
192
|
+
// `check.query` subcommands above (which take positional phase args), this
|
|
193
|
+
// subcommand is flag-driven. `decision-coverage-plan` above now ALSO accepts
|
|
194
|
+
// `--context <path>` (its positionals still work) — both share
|
|
195
|
+
// partitionPredicateArgs, the one flag parser.
|
|
196
|
+
cmdCheckPredicate(cwd, args, raw);
|
|
197
|
+
return;
|
|
198
|
+
case 'prohibition-enforcement':
|
|
199
|
+
// The deterministic test-tier prohibition PRODUCER/gate (#1259, ADR-550 D5d). Locates the
|
|
200
|
+
// wired mechanical check (node-test or lint-rule), confirms fail-first, runs it, builds
|
|
201
|
+
// enforcementEvidence, and emits the dispositionForProhibition verdict
|
|
202
|
+
// (src/gate-prohibition-enforcement.cts). Invocable as
|
|
203
|
+
// `gsd_run check prohibition-enforcement <request.json>`.
|
|
204
|
+
cmdProhibitionEnforcement(cwd, args, raw);
|
|
205
|
+
return;
|
|
206
|
+
default:
|
|
207
|
+
error('Unknown check subcommand. Available: api-coverage-verify-pre, auto-mode, decision-coverage-plan, decision-coverage-verify, evaluation-scope, gap-analysis-plan-post, predicate, prohibition-enforcement, tdd-red-evidence, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-command-paths, verify-failure-directions, verify-schema-drift, verify-codebase-drift, verify-context-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1668
208
|
}
|
|
1669
|
-
error('Unknown check subcommand. Available: api-coverage-verify-pre, auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, predicate, prohibition-enforcement, tdd-red-evidence, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-command-paths, verify-failure-directions, verify-schema-drift, verify-codebase-drift, verify-context-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1670
209
|
}
|
|
1671
210
|
module.exports = {
|
|
1672
211
|
routeCheckCommand,
|
|
1673
|
-
decisionMentioned,
|
|
1674
|
-
extractPlanDesignatedSections,
|
|
1675
|
-
computeUiPlanGate,
|
|
1676
|
-
computeUiSafetyGate,
|
|
212
|
+
decisionMentioned: decision_coverage_support_cjs_1.decisionMentioned,
|
|
213
|
+
extractPlanDesignatedSections: decision_coverage_support_cjs_1.extractPlanDesignatedSections,
|
|
214
|
+
computeUiPlanGate: gate_ui_plan_cjs_1.computeUiPlanGate,
|
|
215
|
+
computeUiSafetyGate: gate_ui_safety_cjs_1.computeUiSafetyGate,
|
|
1677
216
|
cmdGapAnalysisPlanPost,
|
|
1678
217
|
cmdVerifyCommandPaths,
|
|
1679
218
|
cmdVerifyFailureDirections,
|
|
1680
219
|
cmdTddReviewCheckpoint,
|
|
1681
220
|
cmdTddRedEvidence,
|
|
1682
221
|
cmdCheckPredicate,
|
|
1683
|
-
buildPredicateDeps,
|
|
1684
|
-
parsePredicateFlags,
|
|
1685
|
-
partitionPredicateArgs,
|
|
222
|
+
buildPredicateDeps: gate_predicate_cjs_1.buildPredicateDeps,
|
|
223
|
+
parsePredicateFlags: gate_args_cjs_1.parsePredicateFlags,
|
|
224
|
+
partitionPredicateArgs: gate_args_cjs_1.partitionPredicateArgs,
|
|
1686
225
|
// Fail-closed phase-scope reader for the api-coverage gate — exported for
|
|
1687
226
|
// in-process failure-injection tests (#2365 review).
|
|
1688
|
-
readPhaseScope,
|
|
227
|
+
readPhaseScope: gate_api_coverage_verify_pre_cjs_1.readPhaseScope,
|
|
1689
228
|
};
|