@opengsd/gsd-core 1.15.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/agents/gsd-code-fixer.compact.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- 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 +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +4 -4
- 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 +1 -1
- package/agents/gsd-plan-checker.md +7 -2
- package/agents/gsd-planner.md +3 -3
- 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 +42 -51
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +31 -9
- package/bin/install.js +126 -196
- 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 +5 -0
- package/commands/gsd/capture.md +8 -4
- 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 +5 -1
- 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 +5 -1
- package/commands/gsd/pr-branch.md +4 -0
- package/commands/gsd/profile-user.md +5 -1
- package/commands/gsd/progress.md +6 -1
- package/commands/gsd/quick-batch.md +20 -8
- package/commands/gsd/quick.md +12 -7
- package/commands/gsd/review.md +5 -1
- 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 +142 -56
- package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
- package/gsd-core/bin/lib/audit.cjs +78 -44
- 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 +183 -103
- package/gsd-core/bin/lib/capability-validator.cjs +16 -0
- package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
- package/gsd-core/bin/lib/check-command-router.cjs +164 -1712
- 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 +11 -8
- 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 +173 -170
- package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
- package/gsd-core/bin/lib/config.cjs +43 -12
- 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/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 +413 -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/health-diagnostic-rules/phase-structure.cjs +43 -47
- package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
- package/gsd-core/bin/lib/init.cjs +162 -96
- package/gsd-core/bin/lib/install-engine.cjs +19 -35
- package/gsd-core/bin/lib/install-profiles.cjs +7 -4
- 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 +19 -1
- package/gsd-core/bin/lib/model-resolver.cjs +18 -18
- 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 +11 -4
- package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
- package/gsd-core/bin/lib/phase-id.cjs +1 -1
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
- package/gsd-core/bin/lib/phase-status.cjs +360 -0
- package/gsd-core/bin/lib/phase.cjs +280 -80
- package/gsd-core/bin/lib/plan-document.cjs +93 -19
- package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
- package/gsd-core/bin/lib/planning-document.cjs +273 -40
- package/gsd-core/bin/lib/planning-inspect.cjs +42 -8
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +76 -53
- package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
- 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/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/roadmap-command-router.cjs +13 -15
- package/gsd-core/bin/lib/roadmap-parser.cjs +82 -15
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +127 -65
- package/gsd-core/bin/lib/roadmap.cjs +169 -72
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +91 -157
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +13 -10
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +307 -591
- package/gsd-core/bin/lib/runtime-name-policy.cjs +142 -27
- package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +37 -2
- 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.cjs +214 -105
- package/gsd-core/bin/lib/surface.cjs +2 -1
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -152
- package/gsd-core/bin/lib/uat-predicate.cjs +313 -35
- package/gsd-core/bin/lib/uat.cjs +424 -7
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +94 -58
- 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 +1165 -314
- package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
- package/gsd-core/bin/lib/verify.cjs +286 -567
- 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/shared/config-defaults.manifest.json +5 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +4 -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/execute-mvp-tdd.md +5 -10
- package/gsd-core/references/execute-phase-between-wave-reset.md +3 -0
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +7 -1
- package/gsd-core/references/offer-next.md +1 -1
- package/gsd-core/references/planning-config.md +1 -1
- package/gsd-core/references/spidr-splitting.md +1 -1
- package/gsd-core/references/tdd.md +10 -4
- package/gsd-core/references/verifier-phase-gates.md +5 -2
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -3
- 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 +3 -2
- package/gsd-core/workflows/add-todo.md +4 -3
- package/gsd-core/workflows/ai-integration-phase.md +3 -2
- 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.md +28 -18
- package/gsd-core/workflows/check-todos.md +6 -5
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
- package/gsd-core/workflows/code-review-fix.md +5 -4
- package/gsd-core/workflows/code-review.md +84 -90
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
- package/gsd-core/workflows/complete-milestone.md +12 -7
- package/gsd-core/workflows/debug.md +30 -5
- 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 +1 -1
- package/gsd-core/workflows/docs-update.md +4 -3
- package/gsd-core/workflows/edit-phase.md +4 -3
- package/gsd-core/workflows/eval-review.md +5 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +4 -2
- 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 +1 -1
- 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 +1 -1
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- 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/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.md +78 -170
- package/gsd-core/workflows/execute-plan.md +13 -19
- package/gsd-core/workflows/explore.md +2 -2
- package/gsd-core/workflows/extract-learnings.md +3 -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 +2 -1
- package/gsd-core/workflows/import.md +3 -2
- package/gsd-core/workflows/inbox.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- 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 +5 -3
- package/gsd-core/workflows/map-codebase.md +4 -3
- 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 +10 -10
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +1 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.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 +1 -1
- 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 +1 -1
- package/gsd-core/workflows/plan-phase.md +27 -18
- package/gsd-core/workflows/plan-review-convergence.md +1 -1
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +19 -49
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -2
- 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 +42 -31
- package/gsd-core/workflows/remove-phase.md +3 -2
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +3 -2
- package/gsd-core/workflows/review.md +3 -2
- package/gsd-core/workflows/scan.md +3 -2
- package/gsd-core/workflows/secure-phase.md +11 -12
- package/gsd-core/workflows/settings-advanced.md +1 -1
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +12 -12
- 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 +1 -1
- package/gsd-core/workflows/spec-phase.md +1 -1
- 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 +1 -1
- package/gsd-core/workflows/thread.md +2 -2
- package/gsd-core/workflows/transition.md +13 -23
- package/gsd-core/workflows/ui-phase.md +5 -4
- package/gsd-core/workflows/ui-review.md +4 -3
- package/gsd-core/workflows/ultraplan-phase.md +3 -2
- package/gsd-core/workflows/undo.md +1 -1
- package/gsd-core/workflows/validate-phase.md +10 -12
- 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 +68 -59
- package/hooks/dist/gsd-agent-isolation-guard.js +8 -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 +147 -16
- package/hooks/dist/gsd-statusline.js +15 -7
- package/hooks/dist/gsd-update-banner.js +8 -0
- 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 +8 -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 +147 -16
- package/hooks/gsd-statusline.js +15 -7
- package/hooks/gsd-update-banner.js +8 -0
- 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 +21 -4
- package/scripts/changeset/parse.cjs +52 -4
- package/scripts/ci-timeout-report.cjs +770 -4
- package/scripts/command-contract-helpers.cjs +12 -8
- package/scripts/docs-guard-registry.cjs +6 -0
- package/scripts/gen-features.cjs +13 -8
- package/scripts/gen-hooks-cli-exit.cjs +12 -28
- package/scripts/gen-loop-host-contract.cjs +10 -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 +20 -2
- package/scripts/lib/ndjson-reporter.cjs +28 -3
- package/scripts/lib/platform-conformance-tier.generated.cjs +34 -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 +10 -0
- 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-enumeration-drift.cjs +12 -8
- package/scripts/lint-phase-id-drift.cjs +29 -0
- package/scripts/lint-planning-document-positive-control.cjs +329 -0
- package/scripts/lint-response-language-coverage.cjs +3 -0
- package/scripts/lint-skill-deps.cjs +7 -3
- package/scripts/lint-test-file-count.allowlist.json +11 -1
- package/scripts/lint-test-file-count.cjs +34 -1
- package/scripts/lint-vendored-deps.cjs +41 -5
- package/scripts/lint-workflow-shellcheck-baseline.json +10 -10
- package/scripts/mutation-matrix.cjs +50 -5
- 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 +5 -0
- package/skills/gsd-capture/SKILL.md +8 -4
- 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 +5 -1
- 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 +5 -1
- package/skills/gsd-pr-branch/SKILL.md +4 -0
- package/skills/gsd-profile-user/SKILL.md +5 -1
- package/skills/gsd-progress/SKILL.md +6 -1
- package/skills/gsd-quick/SKILL.md +16 -10
- package/skills/gsd-quick-batch/SKILL.md +20 -8
- package/skills/gsd-review/SKILL.md +5 -1
- 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
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +0 -24
|
@@ -22,27 +22,18 @@ const frontmatterMod = require("./frontmatter.cjs");
|
|
|
22
22
|
const stateMod = require("./state.cjs");
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-profiles.cjs is an export= CommonJS module
|
|
24
24
|
const modelProfilesMod = require("./model-profiles.cjs");
|
|
25
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
26
|
-
const planScanMod = require("./plan-scan.cjs");
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
|
|
28
|
-
const verificationMod = require("./verification.cjs");
|
|
29
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
|
|
30
26
|
const coreUtilsMod = require("./core-utils.cjs");
|
|
31
27
|
const { findOrphanSummaries, findUnsummarizedPlans } = coreUtilsMod;
|
|
32
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
|
|
33
|
-
const planningScopeMod = require("./planning-scope.cjs");
|
|
34
|
-
const { SCOPE } = planningScopeMod;
|
|
35
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports -- worktree-safety.cjs is an export= CommonJS module
|
|
36
|
-
const worktreeSafetyMod = require("./worktree-safety.cjs");
|
|
37
|
-
// Single owner of git C-quoted-path decoding (see #4081 note at the
|
|
38
|
-
// codebase-drift --name-status parse loop).
|
|
39
|
-
const { decodeGitQuotedPath } = worktreeSafetyMod;
|
|
40
28
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
41
29
|
const security_cjs_2 = require("./security.cjs");
|
|
42
30
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
43
31
|
const schema_detect_cjs_1 = require("./schema-detect.cjs");
|
|
44
32
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
45
33
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
34
|
+
const gate_exit_cjs_1 = require("./gate-exit.cjs");
|
|
35
|
+
const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
|
|
36
|
+
const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
|
|
46
37
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
|
|
47
38
|
const agentInstallCheck = require("./agent-install-check.cjs");
|
|
48
39
|
const { checkAgentsInstalled, checkCodexModelPosture, checkCodexSandboxPosture } = agentInstallCheck;
|
|
@@ -50,9 +41,6 @@ const { checkAgentsInstalled, checkCodexModelPosture, checkCodexSandboxPosture }
|
|
|
50
41
|
const ioMod = require("./io.cjs");
|
|
51
42
|
const { output, error } = ioMod;
|
|
52
43
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
-
const phaseIdMod = require("./phase-id.cjs");
|
|
54
|
-
const { normalizePhaseName, matchPhaseDirs } = phaseIdMod;
|
|
55
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
56
44
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
57
45
|
const { findPhaseInternal } = phaseLocatorMod;
|
|
58
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -68,8 +56,7 @@ const { buildPlanningSnapshot } = planningSnapshotMod;
|
|
|
68
56
|
const onboardProjectionMod = require("./onboard-projection.cjs");
|
|
69
57
|
const { REQUIRED_CODEBASE_MAP_FILES } = onboardProjectionMod;
|
|
70
58
|
const clock_cjs_1 = require("./clock.cjs");
|
|
71
|
-
const { planningDir,
|
|
72
|
-
const { defaultPhaseCleanCommitTimesMs } = verificationMod;
|
|
59
|
+
const { planningDir, withPlanningLock } = planningWorkspace;
|
|
73
60
|
const { extractFrontmatter, parseMustHavesBlock } = frontmatterMod;
|
|
74
61
|
const { readStateHeadFreshness } = stateMod;
|
|
75
62
|
/**
|
|
@@ -273,8 +260,8 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
|
|
273
260
|
}
|
|
274
261
|
/**
|
|
275
262
|
* Issue #429 — negative-grep comment-text echo gate.
|
|
276
|
-
* A literal that an acceptance criterion negative-greps for (grep -c 'LIT' file == 0
|
|
277
|
-
* must not also appear verbatim inside an <action> body, or the executor's commit-time
|
|
263
|
+
* A literal that an acceptance criterion negative-greps for (grep -c 'LIT' file == 0, or the
|
|
264
|
+
* negated spellings `! grep -q 'LIT' file` / `if ! grep ...`, #4541) must not also appear verbatim inside an <action> body, or the executor's commit-time
|
|
278
265
|
* verify gate fails on the comment echo rather than a real regression. Conservative:
|
|
279
266
|
* errors only on a confidently-extracted QUOTED literal; ambiguous (bareword) → warning.
|
|
280
267
|
*/
|
|
@@ -339,6 +326,12 @@ function splitShellSegments(line) {
|
|
|
339
326
|
segments.push(current.trim());
|
|
340
327
|
return segments.filter((s) => s !== '');
|
|
341
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* The status-test alternation of a negated grep gate (`grep -q 'LIT' f; test $? -ne 0`): the one
|
|
331
|
+
* definition `statusNegatedTest` (the segment test) and `statusNegatedSpanRe` (the span removal)
|
|
332
|
+
* share, so the two cannot drift apart.
|
|
333
|
+
*/
|
|
334
|
+
const STATUS_NEGATED_ALTERNATION = String.raw `(?:-ne\s+0|-eq\s+1|!=\s*0|==\s*1)`;
|
|
342
335
|
function scanNegativeGrepCommentEcho(content) {
|
|
343
336
|
const errors = [];
|
|
344
337
|
const warnings = [];
|
|
@@ -376,6 +369,18 @@ function scanNegativeGrepCommentEcho(content) {
|
|
|
376
369
|
// command, not the whole line) so a pasted verify command does not self-flag
|
|
377
370
|
// while a prose echo on the same line is still caught.
|
|
378
371
|
const cmdSpanRe = /grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n]*?(?:==|-eq|=)\s*0\b/g;
|
|
372
|
+
// #4541: the other spellings of a negative gate — `! grep ...` / `if ! grep ...` (the shell
|
|
373
|
+
// negates the exit status) and `grep ...; test $? -ne 0`. A pasted command in an <action> is
|
|
374
|
+
// likewise not an echo, so these spans are removed from the scanned action text too.
|
|
375
|
+
// The `!` is a command word: it follows a line start, whitespace, `;`, `(` or the `>` closing the
|
|
376
|
+
// `<automated>` tag, and is itself followed by whitespace (so `!=` and `!==` never match).
|
|
377
|
+
const negatedGrepRe = /(?:^|[\s;(>])!\s+grep((?:\s+-{1,2}[A-Za-z][A-Za-z-]*)*)\s+(?:'([^']*)'|"([^"]*)"|([^\s'"|>&;]+))/g;
|
|
378
|
+
const negatedSpanRe = /(?:^|[\s;(>])!\s+grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)*\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n;&|<]*/g;
|
|
379
|
+
const statusNegatedTest = new RegExp(String.raw `\$\?\s*${STATUS_NEGATED_ALTERNATION}(?![\w.])`);
|
|
380
|
+
// The span between the grep literal and the status test is bounded (`{0,1000}`), like the other
|
|
381
|
+
// scans in this file, rather than a lazy unanchored run to the end of the line.
|
|
382
|
+
const statusNegatedSpanRe = new RegExp(String.raw `grep(?:\s+-{1,2}[A-Za-z][A-Za-z-]*)*\s+(?:'[^']*'|"[^"]*"|[^\s'"|>&;]+)[^\n]{0,1000}?\$\?\s*${STATUS_NEGATED_ALTERNATION}(?![\w.])`, 'g');
|
|
383
|
+
const anyGrepRe = /grep((?:\s+-{1,2}[A-Za-z][A-Za-z-]*)*)\s+(?:'([^']*)'|"([^"]*)"|([^\s'"|>&;]+))/g;
|
|
379
384
|
// Security scan: must see the FULL text up to the first </action> — including a
|
|
380
385
|
// malformed inner <action> — so a grep-echo-0 trick cannot hide behind a
|
|
381
386
|
// deliberately-unclosed tag. Use a bounded to-first-close scan (ReDoS-safe via
|
|
@@ -386,7 +391,9 @@ function scanNegativeGrepCommentEcho(content) {
|
|
|
386
391
|
let acm;
|
|
387
392
|
while ((acm = actionRe.exec(text)) !== null)
|
|
388
393
|
actionZones.push(acm[1]);
|
|
389
|
-
const scannableActionText = actionZones
|
|
394
|
+
const scannableActionText = actionZones
|
|
395
|
+
.map((zone) => zone.replace(cmdSpanRe, ' ').replace(negatedSpanRe, ' ').replace(statusNegatedSpanRe, ' '))
|
|
396
|
+
.join('\n');
|
|
390
397
|
// 3. Per shell SEGMENT (split lines on && / ||) extract count-grep literals and
|
|
391
398
|
// check echoes. Per-segment splitting keeps a positive gate (`== 1`) from
|
|
392
399
|
// poisoning a negative gate (`== 0`) sharing the same physical line.
|
|
@@ -394,28 +401,52 @@ function scanNegativeGrepCommentEcho(content) {
|
|
|
394
401
|
const seenWarn = new Set();
|
|
395
402
|
const segments = text.split('\n').flatMap(splitShellSegments);
|
|
396
403
|
for (const seg of segments) {
|
|
397
|
-
if (!/grep
|
|
404
|
+
if (!/grep\b/.test(seg))
|
|
398
405
|
continue;
|
|
399
|
-
|
|
406
|
+
const countForm = /grep(?:\s+-{1,2}[A-Za-z])/.test(seg) && zeroCmp(seg);
|
|
407
|
+
const statusForm = statusNegatedTest.test(seg);
|
|
400
408
|
const quotedLits = [];
|
|
401
409
|
const bareLits = [];
|
|
402
|
-
|
|
403
|
-
while ((m = countGrepRe.exec(seg)) !== null) {
|
|
404
|
-
if (!optsHaveCount(m[1]) || optsHaveInvert(m[1]))
|
|
405
|
-
continue; // need count, not invert (-cv is positive)
|
|
410
|
+
const harvest = (m) => {
|
|
406
411
|
if (m[2] !== undefined)
|
|
407
412
|
quotedLits.push(m[2]);
|
|
408
413
|
else if (m[3] !== undefined)
|
|
409
414
|
quotedLits.push(m[3]);
|
|
410
415
|
else if (m[4] !== undefined && plausibleBare(m[4]))
|
|
411
416
|
bareLits.push(m[4]);
|
|
417
|
+
};
|
|
418
|
+
let m;
|
|
419
|
+
if (countForm) {
|
|
420
|
+
countGrepRe.lastIndex = 0;
|
|
421
|
+
while ((m = countGrepRe.exec(seg)) !== null) {
|
|
422
|
+
if (!optsHaveCount(m[1]) || optsHaveInvert(m[1]))
|
|
423
|
+
continue; // need count, not invert (-cv is positive)
|
|
424
|
+
harvest(m);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// `! grep -q 'LIT' f` / `if ! grep ...`: the negation is the shell's own, so any grep it
|
|
428
|
+
// negates is a negative gate whether or not it counts (an inverted grep is the positive one).
|
|
429
|
+
negatedGrepRe.lastIndex = 0;
|
|
430
|
+
while ((m = negatedGrepRe.exec(seg)) !== null) {
|
|
431
|
+
if (!optsHaveInvert(m[1]))
|
|
432
|
+
harvest(m);
|
|
433
|
+
}
|
|
434
|
+
// `grep -q 'LIT' f; test $? -ne 0`: the same gate with the status tested on the next command.
|
|
435
|
+
if (statusForm) {
|
|
436
|
+
anyGrepRe.lastIndex = 0;
|
|
437
|
+
while ((m = anyGrepRe.exec(seg)) !== null) {
|
|
438
|
+
if (!optsHaveInvert(m[1]))
|
|
439
|
+
harvest(m);
|
|
440
|
+
}
|
|
412
441
|
}
|
|
442
|
+
if (quotedLits.length === 0 && bareLits.length === 0)
|
|
443
|
+
continue;
|
|
413
444
|
for (const quoted of quotedLits) {
|
|
414
445
|
if (!quoted || allow.has(quoted) || seenErr.has(quoted))
|
|
415
446
|
continue;
|
|
416
447
|
if (scannableActionText.includes(quoted)) {
|
|
417
448
|
seenErr.add(quoted);
|
|
418
|
-
errors.push(`Plan body contains forbidden literal "${quoted}" in an <action> block, but an acceptance criterion negative-greps for it (grep -c ... == 0). Rephrase the literal by concept, remove it from the plan body, or add <!-- planner-discipline-allow: ${quoted} --> if it must legitimately appear.`);
|
|
449
|
+
errors.push(`Plan body contains forbidden literal "${quoted}" in an <action> block, but an acceptance criterion negative-greps for it (grep -c ... == 0, or a negated gate such as ! grep -q ...). Rephrase the literal by concept, remove it from the plan body, or add <!-- planner-discipline-allow: ${quoted} --> if it must legitimately appear.`);
|
|
419
450
|
}
|
|
420
451
|
}
|
|
421
452
|
if (quotedLits.length === 0) {
|
|
@@ -1077,6 +1108,45 @@ function validatePlanTaskStructure(task) {
|
|
|
1077
1108
|
}
|
|
1078
1109
|
return { errors, warnings };
|
|
1079
1110
|
}
|
|
1111
|
+
// ─── Gate verb exit status (#5170, ADR-5057 §4) ──────────────────────────────────────────────────
|
|
1112
|
+
//
|
|
1113
|
+
// A verb that returns a verdict is evaluated by callers that branch on the exit status: exit 0 and
|
|
1114
|
+
// exit 1 are verdicts (the JSON on stdout is authoritative), any other status means the verb could
|
|
1115
|
+
// not look. Each verb builds a real GateVerdict and declares its exit from `gateExitOutcome` — no
|
|
1116
|
+
// verb picks its own code.
|
|
1117
|
+
/**
|
|
1118
|
+
* A verb's target file as evidence. An absent file is `none`; an EMPTY file is `found ''` (a plan
|
|
1119
|
+
* that exists but is empty is not "File not found"); any other failure is `unreadable`.
|
|
1120
|
+
*/
|
|
1121
|
+
function readVerbFile(fullPath) {
|
|
1122
|
+
try {
|
|
1123
|
+
const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
|
|
1124
|
+
return content === null ? (0, gate_evidence_cjs_1.evidenceNone)() : (0, gate_evidence_cjs_1.evidenceFound)(content);
|
|
1125
|
+
}
|
|
1126
|
+
catch (err) {
|
|
1127
|
+
return (0, gate_evidence_cjs_1.evidenceFromError)(err, fullPath);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/** Print a verb's verdict payload, then declare its exit. Declared after output(), which rewrites the pending-outcome cell. */
|
|
1131
|
+
function emitVerbVerdict(verdict, mode, raw, rawLabel) {
|
|
1132
|
+
output(verdict.payload, raw, rawLabel);
|
|
1133
|
+
(0, gate_exit_cjs_1.declareGateExit)(verdict, mode);
|
|
1134
|
+
}
|
|
1135
|
+
/** A verb whose result is valid/invalid: invalid is a negative verdict (exit 1 in status mode). */
|
|
1136
|
+
function validityVerdict(valid, payload) {
|
|
1137
|
+
return (0, gate_verdict_cjs_1.gateVerdict)(valid ? 'pass' : 'block', !valid, payload);
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* The "could not evaluate" verdict for a verb's target file that is not `found` (outcome `unreadable`,
|
|
1141
|
+
* exit UNAVAILABLE — an absent file and an unreadable one alike). The caller emits it through
|
|
1142
|
+
* `emitVerbVerdict` on its own return path, so every path out of a verb visibly declares its exit.
|
|
1143
|
+
*/
|
|
1144
|
+
function unreadableFileVerdict(evidence, displayPath) {
|
|
1145
|
+
const payload = evidence.kind === 'none'
|
|
1146
|
+
? { error: 'File not found', path: displayPath }
|
|
1147
|
+
: { error: 'File unreadable', path: displayPath, read_error: evidence.reason };
|
|
1148
|
+
return (0, gate_verdict_cjs_1.gateUnreadable)(false, payload);
|
|
1149
|
+
}
|
|
1080
1150
|
function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
1081
1151
|
if (!filePath) {
|
|
1082
1152
|
error('file path required');
|
|
@@ -1085,17 +1155,18 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
1085
1155
|
error('file path contains null bytes');
|
|
1086
1156
|
}
|
|
1087
1157
|
const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
|
|
1088
|
-
const
|
|
1089
|
-
if (
|
|
1090
|
-
|
|
1158
|
+
const read = readVerbFile(fullPath);
|
|
1159
|
+
if (read.kind !== 'found') {
|
|
1160
|
+
emitVerbVerdict(unreadableFileVerdict(read, filePath), 'status', raw);
|
|
1091
1161
|
return;
|
|
1092
1162
|
}
|
|
1163
|
+
const content = read.value;
|
|
1093
1164
|
// #2701: fail loud on NUL/binary corruption before structure checks. A
|
|
1094
1165
|
// structurally intact-but-NUL-corrupted plan otherwise passes as valid and is
|
|
1095
1166
|
// silently skipped by recursive/binary-skipping searchers downstream.
|
|
1096
1167
|
const encErr = (0, validate_cjs_1.textEncodingError)(content, filePath);
|
|
1097
1168
|
if (encErr) {
|
|
1098
|
-
|
|
1169
|
+
emitVerbVerdict(validityVerdict(false, { valid: false, errors: [encErr] }), 'status', raw);
|
|
1099
1170
|
return;
|
|
1100
1171
|
}
|
|
1101
1172
|
const fm = extractFrontmatter(content, fullPath);
|
|
@@ -1163,14 +1234,14 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
1163
1234
|
const quantScan = scanQuantitativeCriteria(content);
|
|
1164
1235
|
errors.push(...quantScan.errors);
|
|
1165
1236
|
warnings.push(...quantScan.warnings);
|
|
1166
|
-
|
|
1237
|
+
emitVerbVerdict(validityVerdict(errors.length === 0, {
|
|
1167
1238
|
valid: errors.length === 0,
|
|
1168
1239
|
errors,
|
|
1169
1240
|
warnings,
|
|
1170
1241
|
task_count: tasks.length,
|
|
1171
1242
|
tasks,
|
|
1172
1243
|
frontmatter_fields: Object.keys(fm),
|
|
1173
|
-
}, raw, errors.length === 0 ? 'valid' : 'invalid');
|
|
1244
|
+
}), 'status', raw, errors.length === 0 ? 'valid' : 'invalid');
|
|
1174
1245
|
}
|
|
1175
1246
|
function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
|
|
1176
1247
|
if (!phase) {
|
|
@@ -1178,7 +1249,8 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
|
|
|
1178
1249
|
}
|
|
1179
1250
|
const phaseInfoRaw = findPhaseInternal(cwd, phase);
|
|
1180
1251
|
if (!phaseInfoRaw || !phaseInfoRaw['found']) {
|
|
1181
|
-
|
|
1252
|
+
// An unresolvable phase is "could not look" (#5170): there is no phase to evaluate.
|
|
1253
|
+
emitVerbVerdict((0, gate_verdict_cjs_1.gateUnreadable)(false, { error: 'Phase not found', phase }), 'status', raw);
|
|
1182
1254
|
return;
|
|
1183
1255
|
}
|
|
1184
1256
|
const phaseInfo = phaseInfoRaw;
|
|
@@ -1194,12 +1266,14 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
|
|
|
1194
1266
|
// recognized naming forms — producing false "Plans without summaries" /
|
|
1195
1267
|
// "Summaries without plans" for names it could not recognize as paired
|
|
1196
1268
|
// (the same failure class #1988/#2648 fixed for the owner's own callers).
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1269
|
+
// Only SCOPE.COMPLETE is a real answer: an existing nested plans/ that could not be read (TRUNCATED)
|
|
1270
|
+
// would hand the check a short plan set and report the unseen plans as nothing (#5170).
|
|
1271
|
+
const scan = (0, gate_evidence_cjs_1.readPlanScanEvidence)(phaseDir);
|
|
1272
|
+
if (scan.kind === 'unreadable') {
|
|
1273
|
+
emitVerbVerdict((0, gate_verdict_cjs_1.gateUnreadable)(false, { error: 'Cannot read phase directory' }), 'status', raw);
|
|
1200
1274
|
return;
|
|
1201
1275
|
}
|
|
1202
|
-
const { planFiles, summaryFiles } = scan;
|
|
1276
|
+
const { planFiles, summaryFiles } = scan.value;
|
|
1203
1277
|
const incompletePlans = findUnsummarizedPlans(planFiles, summaryFiles);
|
|
1204
1278
|
if (incompletePlans.length > 0) {
|
|
1205
1279
|
errors.push(`Plans without summaries: ${incompletePlans.join(', ')}`);
|
|
@@ -1208,7 +1282,7 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
|
|
|
1208
1282
|
if (orphanSummaries.length > 0) {
|
|
1209
1283
|
warnings.push(`Summaries without plans: ${orphanSummaries.join(', ')}`);
|
|
1210
1284
|
}
|
|
1211
|
-
|
|
1285
|
+
emitVerbVerdict(validityVerdict(errors.length === 0, {
|
|
1212
1286
|
complete: errors.length === 0,
|
|
1213
1287
|
phase: phaseInfo['phase_number'],
|
|
1214
1288
|
plan_count: planFiles.length,
|
|
@@ -1217,7 +1291,7 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
|
|
|
1217
1291
|
orphan_summaries: orphanSummaries,
|
|
1218
1292
|
errors,
|
|
1219
1293
|
warnings,
|
|
1220
|
-
}, raw, errors.length === 0 ? 'complete' : 'incomplete');
|
|
1294
|
+
}), 'status', raw, errors.length === 0 ? 'complete' : 'incomplete');
|
|
1221
1295
|
}
|
|
1222
1296
|
// #4678: citations may carry a trailing line suffix (":42", ":1-20") that
|
|
1223
1297
|
// describes a location inside the file, not part of the path itself.
|
|
@@ -1229,13 +1303,25 @@ function cmdVerifyReferences(cwd, filePath, raw) {
|
|
|
1229
1303
|
error('file path required');
|
|
1230
1304
|
}
|
|
1231
1305
|
const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
|
|
1232
|
-
const
|
|
1233
|
-
if (
|
|
1234
|
-
|
|
1306
|
+
const read = readVerbFile(fullPath);
|
|
1307
|
+
if (read.kind !== 'found') {
|
|
1308
|
+
emitVerbVerdict(unreadableFileVerdict(read, filePath), 'status', raw);
|
|
1235
1309
|
return;
|
|
1236
1310
|
}
|
|
1311
|
+
const content = read.value;
|
|
1237
1312
|
const found = [];
|
|
1238
1313
|
const missing = [];
|
|
1314
|
+
// A reference whose path could not be examined (EACCES on a parent ...) is neither found nor missing.
|
|
1315
|
+
const unexaminable = [];
|
|
1316
|
+
const probeRef = (cleanRef, resolved) => {
|
|
1317
|
+
const probe = (0, gate_evidence_cjs_1.statEvidence)(resolved);
|
|
1318
|
+
if (probe.kind === 'found')
|
|
1319
|
+
found.push(cleanRef);
|
|
1320
|
+
else if (probe.kind === 'none')
|
|
1321
|
+
missing.push(cleanRef);
|
|
1322
|
+
else
|
|
1323
|
+
unexaminable.push({ ref: cleanRef, reason: probe.reason });
|
|
1324
|
+
};
|
|
1239
1325
|
const atRefs = content.match(/@([^\s\n,)]+\/[^\s\n,)]+)/g) || [];
|
|
1240
1326
|
for (const ref of atRefs) {
|
|
1241
1327
|
const cleanRef = ref.slice(1);
|
|
@@ -1243,39 +1329,40 @@ function cmdVerifyReferences(cwd, filePath, raw) {
|
|
|
1243
1329
|
const resolved = fsRef.startsWith('~/')
|
|
1244
1330
|
? node_path_1.default.join(process.env['HOME'] || '', fsRef.slice(2))
|
|
1245
1331
|
: node_path_1.default.join(cwd, fsRef);
|
|
1246
|
-
|
|
1247
|
-
found.push(cleanRef);
|
|
1248
|
-
}
|
|
1249
|
-
else {
|
|
1250
|
-
missing.push(cleanRef);
|
|
1251
|
-
}
|
|
1332
|
+
probeRef(cleanRef, resolved);
|
|
1252
1333
|
}
|
|
1253
1334
|
const backtickRefs = content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10}(?::\d+(?:-\d+)?)?)`/g) || [];
|
|
1254
1335
|
for (const ref of backtickRefs) {
|
|
1255
1336
|
const cleanRef = ref.slice(1, -1);
|
|
1256
1337
|
if (cleanRef.startsWith('http') || cleanRef.includes('${') || cleanRef.includes('{{'))
|
|
1257
1338
|
continue;
|
|
1258
|
-
if (found.includes(cleanRef) || missing.includes(cleanRef))
|
|
1339
|
+
if (found.includes(cleanRef) || missing.includes(cleanRef) || unexaminable.some((u) => u.ref === cleanRef))
|
|
1259
1340
|
continue;
|
|
1260
|
-
|
|
1261
|
-
if (node_fs_1.default.existsSync(resolved)) {
|
|
1262
|
-
found.push(cleanRef);
|
|
1263
|
-
}
|
|
1264
|
-
else {
|
|
1265
|
-
missing.push(cleanRef);
|
|
1266
|
-
}
|
|
1341
|
+
probeRef(cleanRef, node_path_1.default.join(cwd, stripLineSuffix(cleanRef)));
|
|
1267
1342
|
}
|
|
1268
|
-
|
|
1343
|
+
// Unexaminable references only matter when they could flip the verdict: with a reference already
|
|
1344
|
+
// missing the verdict is negative whatever they are; with none missing, "valid" cannot be claimed.
|
|
1345
|
+
if (missing.length === 0 && unexaminable.length > 0) {
|
|
1346
|
+
emitVerbVerdict((0, gate_verdict_cjs_1.gateUnreadable)(false, { error: 'Could not examine references', path: filePath, unreadable: unexaminable }), 'status', raw);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
emitVerbVerdict(validityVerdict(missing.length === 0, {
|
|
1269
1350
|
valid: missing.length === 0,
|
|
1270
1351
|
found: found.length,
|
|
1271
1352
|
missing,
|
|
1272
1353
|
total: found.length + missing.length,
|
|
1273
|
-
}, raw, missing.length === 0 ? 'valid' : 'invalid');
|
|
1354
|
+
}), 'status', raw, missing.length === 0 ? 'valid' : 'invalid');
|
|
1274
1355
|
}
|
|
1275
1356
|
function cmdVerifyCommits(cwd, hashes, raw) {
|
|
1276
1357
|
if (!hashes || hashes.length === 0) {
|
|
1277
1358
|
error('At least one commit hash required');
|
|
1278
1359
|
}
|
|
1360
|
+
// A directory that is not a git work tree cannot answer "is this a commit": could not look, not "invalid".
|
|
1361
|
+
const inWorkTree = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--is-inside-work-tree'], { cwd });
|
|
1362
|
+
if (inWorkTree.exitCode !== 0) {
|
|
1363
|
+
emitVerbVerdict((0, gate_verdict_cjs_1.gateUnreadable)(false, { error: 'Not a git repository' }), 'status', raw);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1279
1366
|
const valid = [];
|
|
1280
1367
|
const invalid = [];
|
|
1281
1368
|
for (const hash of hashes) {
|
|
@@ -1287,26 +1374,30 @@ function cmdVerifyCommits(cwd, hashes, raw) {
|
|
|
1287
1374
|
invalid.push(hash);
|
|
1288
1375
|
}
|
|
1289
1376
|
}
|
|
1290
|
-
|
|
1377
|
+
emitVerbVerdict(validityVerdict(invalid.length === 0, {
|
|
1291
1378
|
all_valid: invalid.length === 0,
|
|
1292
1379
|
valid,
|
|
1293
1380
|
invalid,
|
|
1294
1381
|
total: hashes.length,
|
|
1295
|
-
}, raw, invalid.length === 0 ? 'valid' : 'invalid');
|
|
1382
|
+
}), 'status', raw, invalid.length === 0 ? 'valid' : 'invalid');
|
|
1296
1383
|
}
|
|
1297
1384
|
function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
1298
1385
|
if (!planFilePath) {
|
|
1299
1386
|
error('plan file path required');
|
|
1300
1387
|
}
|
|
1301
1388
|
const fullPath = node_path_1.default.isAbsolute(planFilePath) ? planFilePath : node_path_1.default.join(cwd, planFilePath);
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1389
|
+
// #5170: no plan to read is "could not look" (UNAVAILABLE), never a pass. A plan that exists but is
|
|
1390
|
+
// empty WAS read: it has no artifacts block, which is a genuinely empty scope (below).
|
|
1391
|
+
const read = readVerbFile(fullPath);
|
|
1392
|
+
if (read.kind !== 'found') {
|
|
1393
|
+
emitVerbVerdict(unreadableFileVerdict(read, planFilePath), 'status', raw);
|
|
1305
1394
|
return;
|
|
1306
1395
|
}
|
|
1396
|
+
const content = read.value;
|
|
1307
1397
|
const artifacts = parseMustHavesBlock(content, 'artifacts');
|
|
1308
1398
|
if (artifacts.length === 0) {
|
|
1309
|
-
|
|
1399
|
+
// The plan was read and declares nothing to verify: NO_INPUT (ran, zero units in scope, genuinely).
|
|
1400
|
+
emitVerbVerdict((0, gate_verdict_cjs_1.gateVerdict)('empty', false, { error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }), 'status', raw);
|
|
1310
1401
|
return;
|
|
1311
1402
|
}
|
|
1312
1403
|
const results = [];
|
|
@@ -1317,7 +1408,10 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
|
1317
1408
|
if (!artPath)
|
|
1318
1409
|
continue;
|
|
1319
1410
|
const artFullPath = node_path_1.default.join(cwd, artPath);
|
|
1320
|
-
|
|
1411
|
+
// Typed evidence, not `fs.existsSync`: a path that cannot be examined (EACCES on a parent) is an
|
|
1412
|
+
// unreadable entry, not a missing one.
|
|
1413
|
+
const artStat = (0, gate_evidence_cjs_1.statEvidence)(artFullPath);
|
|
1414
|
+
const exists = artStat.kind === 'found';
|
|
1321
1415
|
const check = { path: artPath, exists, issues: [], passed: false };
|
|
1322
1416
|
// #4685: one artifact's I/O problem is that artifact's failure, never the
|
|
1323
1417
|
// whole plan's. `safeReadFile` rethrows every errno except ENOENT, so before
|
|
@@ -1327,67 +1421,56 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
|
1327
1421
|
// artifacts either. A check that disappears is worse than a check that fails,
|
|
1328
1422
|
// because a failure is visible.
|
|
1329
1423
|
//
|
|
1330
|
-
//
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1333
|
-
//
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1424
|
+
// #5170: the stat and the read are typed evidence (`statEvidence` / `readVerbFile`), so no `try`
|
|
1425
|
+
// is needed: an artifact that could not be examined or read is that artifact's failure, recorded
|
|
1426
|
+
// with its errno (an operator seeing EACCES acts differently from one seeing EIO) and left
|
|
1427
|
+
// `passed: false` — never a pass, so the verdict is negative (exit 1).
|
|
1428
|
+
if (exists) {
|
|
1429
|
+
// A directory is reported as its own kind of failure, distinct from
|
|
1430
|
+
// `File not found`: the path resolved, it simply is not the thing an
|
|
1431
|
+
// artifact entry can be checked against. Verifying a directory (matching
|
|
1432
|
+
// `contains:`/`min_lines:`/`exports:` across the files inside it) is a
|
|
1433
|
+
// feature decision, deliberately not made here.
|
|
1434
|
+
if (artStat.kind === 'found' && artStat.value.isDirectory()) {
|
|
1435
|
+
check['issues'].push('Not a file: path is a directory');
|
|
1436
|
+
}
|
|
1437
|
+
else {
|
|
1438
|
+
// `safeReadFile` returns null on ENOENT, and `|| ''` would turn that
|
|
1439
|
+
// into an empty file — which an entry carrying only `path`/`provides`
|
|
1440
|
+
// would then PASS, having checked nothing. `statSync` just succeeded, so
|
|
1441
|
+
// `none` here means the artifact went away mid-check. Report that rather
|
|
1442
|
+
// than inheriting a pass from it.
|
|
1443
|
+
const artRead = readVerbFile(artFullPath);
|
|
1444
|
+
if (artRead.kind !== 'found') {
|
|
1445
|
+
check['issues'].push(artRead.kind === 'none' ? 'Unreadable: disappeared during check' : `Unreadable: ${artRead.reason}`);
|
|
1446
|
+
results.push(check);
|
|
1447
|
+
continue;
|
|
1346
1448
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
if (artifact['min_lines'] && lineCount < artifact['min_lines']) {
|
|
1363
|
-
check['issues'].push(`Only ${lineCount} lines, need ${artifact['min_lines']}`);
|
|
1364
|
-
}
|
|
1365
|
-
if (artifact['contains'] && !fileContent.includes(artifact['contains'])) {
|
|
1366
|
-
check['issues'].push(`Missing pattern: ${artifact['contains']}`);
|
|
1367
|
-
}
|
|
1368
|
-
if (artifact['exports']) {
|
|
1369
|
-
const exports = Array.isArray(artifact['exports'])
|
|
1370
|
-
? artifact['exports']
|
|
1371
|
-
: [artifact['exports']];
|
|
1372
|
-
for (const exp of exports) {
|
|
1373
|
-
if (!fileContent.includes(exp))
|
|
1374
|
-
check['issues'].push(`Missing export: ${exp}`);
|
|
1375
|
-
}
|
|
1449
|
+
const fileContent = artRead.value;
|
|
1450
|
+
const lineCount = fileContent.split('\n').length;
|
|
1451
|
+
if (artifact['min_lines'] && lineCount < artifact['min_lines']) {
|
|
1452
|
+
check['issues'].push(`Only ${lineCount} lines, need ${artifact['min_lines']}`);
|
|
1453
|
+
}
|
|
1454
|
+
if (artifact['contains'] && !fileContent.includes(artifact['contains'])) {
|
|
1455
|
+
check['issues'].push(`Missing pattern: ${artifact['contains']}`);
|
|
1456
|
+
}
|
|
1457
|
+
if (artifact['exports']) {
|
|
1458
|
+
const exports = Array.isArray(artifact['exports'])
|
|
1459
|
+
? artifact['exports']
|
|
1460
|
+
: [artifact['exports']];
|
|
1461
|
+
for (const exp of exports) {
|
|
1462
|
+
if (!fileContent.includes(exp))
|
|
1463
|
+
check['issues'].push(`Missing export: ${exp}`);
|
|
1376
1464
|
}
|
|
1377
|
-
check['passed'] = check['issues'].length === 0;
|
|
1378
1465
|
}
|
|
1379
|
-
|
|
1380
|
-
else {
|
|
1381
|
-
check['issues'].push('File not found');
|
|
1466
|
+
check['passed'] = check['issues'].length === 0;
|
|
1382
1467
|
}
|
|
1383
1468
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
check['issues'].push(`Unreadable: ${e.code || (e.message ?? String(err))}`);
|
|
1390
|
-
check['passed'] = false;
|
|
1469
|
+
else if (artStat.kind === 'unreadable') {
|
|
1470
|
+
check['issues'].push(`Unreadable: ${artStat.reason}`);
|
|
1471
|
+
}
|
|
1472
|
+
else {
|
|
1473
|
+
check['issues'].push('File not found');
|
|
1391
1474
|
}
|
|
1392
1475
|
results.push(check);
|
|
1393
1476
|
}
|
|
@@ -1399,12 +1482,13 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
|
1399
1482
|
// no-vacuous-pass rule at src/uat-predicate.cts. The fully-empty block is still
|
|
1400
1483
|
// caught earlier by the `artifacts.length === 0` guard and returns its error.
|
|
1401
1484
|
const allPassed = results.length > 0 && passed === results.length;
|
|
1402
|
-
|
|
1485
|
+
// #5170: the exit status follows the verdict — a negative verdict is exit 1 (JSON unchanged).
|
|
1486
|
+
emitVerbVerdict(validityVerdict(allPassed, {
|
|
1403
1487
|
all_passed: allPassed,
|
|
1404
1488
|
passed,
|
|
1405
1489
|
total: results.length,
|
|
1406
1490
|
artifacts: results,
|
|
1407
|
-
}, raw, allPassed ? 'valid' : 'invalid');
|
|
1491
|
+
}), 'status', raw, allPassed ? 'valid' : 'invalid');
|
|
1408
1492
|
}
|
|
1409
1493
|
/**
|
|
1410
1494
|
* Returns a Set of file paths (relative to cwd) that are promised by plans in
|
|
@@ -1415,12 +1499,23 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
|
1415
1499
|
*/
|
|
1416
1500
|
function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
|
|
1417
1501
|
const promised = new Set();
|
|
1418
|
-
|
|
1419
|
-
|
|
1502
|
+
// #5170: a plan set the scan did not fully see (an existing nested plans/ that could not be read), or
|
|
1503
|
+
// a plan that cannot be read, is "could not look" — it may be the plan that promises the file — and
|
|
1504
|
+
// is reported to the caller, never treated as "no plan promises it".
|
|
1505
|
+
let unreadable = null;
|
|
1506
|
+
const planSet = (0, gate_evidence_cjs_1.readPlanSetEvidence)(phaseDir);
|
|
1507
|
+
if (planSet.kind === 'unreadable')
|
|
1508
|
+
return { files: promised, unreadable: planSet.reason };
|
|
1509
|
+
for (const planFile of planSet.value) {
|
|
1420
1510
|
const planFullPath = node_path_1.default.join(phaseDir, planFile);
|
|
1421
|
-
const
|
|
1422
|
-
if (
|
|
1511
|
+
const planRead = readVerbFile(planFullPath);
|
|
1512
|
+
if (planRead.kind === 'unreadable') {
|
|
1513
|
+
unreadable = unreadable ?? planRead.reason;
|
|
1423
1514
|
continue;
|
|
1515
|
+
}
|
|
1516
|
+
if (planRead.kind === 'none' || !planRead.value)
|
|
1517
|
+
continue;
|
|
1518
|
+
const planContent = planRead.value;
|
|
1424
1519
|
const fm = extractFrontmatter(planContent, planFullPath);
|
|
1425
1520
|
const waveRaw = fm['wave'];
|
|
1426
1521
|
const wave = typeof waveRaw === 'string' ? parseInt(waveRaw, 10) : (typeof waveRaw === 'number' ? waveRaw : NaN);
|
|
@@ -1437,21 +1532,23 @@ function collectPromisedFilesAtOrAfterWave(phaseDir, minWave) {
|
|
|
1437
1532
|
promised.add(f.trim());
|
|
1438
1533
|
}
|
|
1439
1534
|
}
|
|
1440
|
-
return promised;
|
|
1535
|
+
return { files: promised, unreadable };
|
|
1441
1536
|
}
|
|
1442
1537
|
function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
1443
1538
|
if (!planFilePath) {
|
|
1444
1539
|
error('plan file path required');
|
|
1445
1540
|
}
|
|
1446
1541
|
const fullPath = node_path_1.default.isAbsolute(planFilePath) ? planFilePath : node_path_1.default.join(cwd, planFilePath);
|
|
1447
|
-
const
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
1542
|
+
const read = readVerbFile(fullPath);
|
|
1543
|
+
if (read.kind !== 'found') {
|
|
1544
|
+
emitVerbVerdict(unreadableFileVerdict(read, planFilePath), 'status', raw);
|
|
1450
1545
|
return;
|
|
1451
1546
|
}
|
|
1547
|
+
const content = read.value;
|
|
1452
1548
|
const keyLinks = parseMustHavesBlock(content, 'key_links');
|
|
1453
1549
|
if (keyLinks.length === 0) {
|
|
1454
|
-
|
|
1550
|
+
// The plan was read and declares no key links: NO_INPUT, the same genuinely-empty scope as `verify artifacts`.
|
|
1551
|
+
emitVerbVerdict((0, gate_verdict_cjs_1.gateVerdict)('empty', false, { error: 'No must_haves.key_links found in frontmatter', path: planFilePath }), 'status', raw);
|
|
1455
1552
|
return;
|
|
1456
1553
|
}
|
|
1457
1554
|
// Derive the current plan's wave number and phase directory for wave-aware
|
|
@@ -1504,28 +1601,30 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
1504
1601
|
results.push(check);
|
|
1505
1602
|
continue;
|
|
1506
1603
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
// can trigger EISDIR/EACCES, which platformReadSync re-throws for any
|
|
1513
|
-
// non-ENOENT errno). A single bad link must not abort the whole command.
|
|
1514
|
-
const code = err?.code ?? 'unknown';
|
|
1515
|
-
check['detail'] = `Source read failed: ${code}`;
|
|
1604
|
+
// Report the errno only — never the message or path (untrusted `from:` can trigger
|
|
1605
|
+
// EISDIR/EACCES). A single bad link must not abort the whole command.
|
|
1606
|
+
const sourceRead = readVerbFile(fromContained);
|
|
1607
|
+
if (sourceRead.kind === 'unreadable') {
|
|
1608
|
+
check['detail'] = `Source read failed: ${sourceRead.reason}`;
|
|
1516
1609
|
results.push(check);
|
|
1517
1610
|
continue;
|
|
1518
1611
|
}
|
|
1612
|
+
sourceContent = sourceRead.kind === 'found' ? sourceRead.value : null;
|
|
1519
1613
|
}
|
|
1520
1614
|
if (!sourceContent) {
|
|
1521
1615
|
// Check if the missing file is promised by a plan at the same or later wave.
|
|
1522
1616
|
const promised = getPromisedFiles();
|
|
1523
|
-
const isPromised = fromPath.trim() !== '' && promised.has(fromPath.trim());
|
|
1617
|
+
const isPromised = fromPath.trim() !== '' && promised.files.has(fromPath.trim());
|
|
1524
1618
|
if (isPromised) {
|
|
1525
1619
|
check['pending'] = true;
|
|
1526
1620
|
check['detail'] = 'Source file not yet created — declared in files_modified of a same-or-later-wave plan';
|
|
1527
1621
|
pendingCount++;
|
|
1528
1622
|
}
|
|
1623
|
+
else if (promised.unreadable !== null) {
|
|
1624
|
+
// The plans that could declare this file could not all be read: it is not known to be missing,
|
|
1625
|
+
// and the link is not verified — a failed link, never a pending or passing one.
|
|
1626
|
+
check['detail'] = `Source file not found, and the plans that could declare it could not be read: ${promised.unreadable}`;
|
|
1627
|
+
}
|
|
1529
1628
|
else {
|
|
1530
1629
|
check['detail'] = 'Source file not found (from: must be a relative file path; describe components/endpoints in via:)';
|
|
1531
1630
|
}
|
|
@@ -1560,46 +1659,47 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
1560
1659
|
check['detail'] = `Pattern not verified (${reason})`;
|
|
1561
1660
|
}
|
|
1562
1661
|
else {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
}
|
|
1584
|
-
else {
|
|
1585
|
-
targetContent = (0, shell_command_projection_cjs_1.platformReadSync)(toContained);
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
if (targetContent && pat.test(targetContent)) {
|
|
1589
|
-
check['verified'] = true;
|
|
1590
|
-
check['detail'] = 'Pattern found in target';
|
|
1662
|
+
if (pat.test(sourceContent)) {
|
|
1663
|
+
check['verified'] = true;
|
|
1664
|
+
check['detail'] = 'Pattern found in source';
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
const toRaw = link['to'];
|
|
1668
|
+
const toPath = typeof toRaw === 'string' ? toRaw : '';
|
|
1669
|
+
let targetContent = null;
|
|
1670
|
+
let targetUnreadable = null;
|
|
1671
|
+
if (toPath !== '') {
|
|
1672
|
+
// An empty/missing `to:` is a malformed plan, not a
|
|
1673
|
+
// path-confinement violation — only a non-empty path that
|
|
1674
|
+
// actually resolves outside the project is path_rejected.
|
|
1675
|
+
const toContained = (0, security_cjs_2.tryWithinRoot)(toPath, cwd);
|
|
1676
|
+
if (toContained === null) {
|
|
1677
|
+
// Do not read a rejected `to:` — treat as no target content
|
|
1678
|
+
// and do not echo result.error, which embeds absolute host
|
|
1679
|
+
// paths.
|
|
1680
|
+
check['path_rejected'] = 'to';
|
|
1681
|
+
check['detail'] = `Pattern "${link['pattern']}" not found in source; target path rejected — resolves outside the project directory`;
|
|
1591
1682
|
}
|
|
1592
|
-
else
|
|
1593
|
-
|
|
1683
|
+
else {
|
|
1684
|
+
// Report the errno only — never the message, which for an untrusted `to:` like "../.."
|
|
1685
|
+
// (EISDIR) would embed an absolute filesystem path.
|
|
1686
|
+
const targetRead = readVerbFile(toContained);
|
|
1687
|
+
if (targetRead.kind === 'unreadable')
|
|
1688
|
+
targetUnreadable = targetRead.reason;
|
|
1689
|
+
else if (targetRead.kind === 'found')
|
|
1690
|
+
targetContent = targetRead.value;
|
|
1594
1691
|
}
|
|
1595
1692
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1693
|
+
if (targetUnreadable !== null) {
|
|
1694
|
+
check['detail'] = `Pattern check failed: ${targetUnreadable}`;
|
|
1695
|
+
}
|
|
1696
|
+
else if (targetContent && pat.test(targetContent)) {
|
|
1697
|
+
check['verified'] = true;
|
|
1698
|
+
check['detail'] = 'Pattern found in target';
|
|
1699
|
+
}
|
|
1700
|
+
else if (!check['path_rejected']) {
|
|
1701
|
+
check['detail'] = `Pattern "${link['pattern']}" not found in source or target`;
|
|
1702
|
+
}
|
|
1603
1703
|
}
|
|
1604
1704
|
}
|
|
1605
1705
|
}
|
|
@@ -1629,13 +1729,13 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
1629
1729
|
// block still satisfies results.length > 0 and its #1202 non-hard-failing
|
|
1630
1730
|
// semantics are unchanged — the floor only rejects the zero-result case.
|
|
1631
1731
|
const allVerified = results.length > 0 && hardFailed === 0;
|
|
1632
|
-
|
|
1732
|
+
emitVerbVerdict(validityVerdict(allVerified, {
|
|
1633
1733
|
all_verified: allVerified,
|
|
1634
1734
|
verified,
|
|
1635
1735
|
pending: pendingCount,
|
|
1636
1736
|
total: results.length,
|
|
1637
1737
|
links: results,
|
|
1638
|
-
}, raw, allVerified ? 'valid' : 'invalid');
|
|
1738
|
+
}), 'status', raw, allVerified ? 'valid' : 'invalid');
|
|
1639
1739
|
}
|
|
1640
1740
|
/**
|
|
1641
1741
|
* Wrapper-level fix-text table for `cmdValidateHealth`'s migrated
|
|
@@ -1799,6 +1899,9 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1799
1899
|
// Diagnostic -> IssueEntry mapping contract this reproduces.
|
|
1800
1900
|
const snapshot = buildPlanningSnapshot(cwd);
|
|
1801
1901
|
const diagnostics = evaluateRules(snapshot);
|
|
1902
|
+
// #5118: an out-of-set verification report status is carried by the
|
|
1903
|
+
// snapshot (`verificationStatusError`) and reported as the W030 finding — a
|
|
1904
|
+
// diagnostics surface survives the defect it diagnoses, so this run exits 0.
|
|
1802
1905
|
const errors = [];
|
|
1803
1906
|
const warnings = [];
|
|
1804
1907
|
const info = [];
|
|
@@ -1922,192 +2025,13 @@ function cmdValidateAgents(cwd, raw) {
|
|
|
1922
2025
|
sandbox_posture: sandboxPosture,
|
|
1923
2026
|
}, raw);
|
|
1924
2027
|
}
|
|
1925
|
-
// ─── Context drift (#3348) ───────────────────────────────────────────────────
|
|
1926
|
-
/**
|
|
1927
|
-
* Resolve a phase directory under `phasesDir` from a user-supplied `phaseArg`,
|
|
1928
|
-
* via the canonical phase-directory matcher (phase-id.cjs::matchPhaseDirs) rather
|
|
1929
|
-
* than a naive substring test — a bare `.includes(phaseArg)` lets a non-existent
|
|
1930
|
-
* phase silently match a different phase whose directory name merely contains the
|
|
1931
|
-
* requested token (e.g. "1" matching "11-expansion"). Falls back to an exact
|
|
1932
|
-
* directory-name match. Returns null if neither resolves. (#1571, #2528)
|
|
1933
|
-
*/
|
|
1934
|
-
function resolvePhaseDirByToken(phasesDir, phaseArg) {
|
|
1935
|
-
const normalizedPhase = normalizePhaseName(phaseArg);
|
|
1936
|
-
const dirEntries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
1937
|
-
const dirNames = dirEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1938
|
-
const matched = matchPhaseDirs(dirNames, normalizedPhase).matches[0];
|
|
1939
|
-
if (matched)
|
|
1940
|
-
return node_path_1.default.join(phasesDir, matched);
|
|
1941
|
-
const contained = (0, security_cjs_2.tryWithinRoot)(phaseArg, phasesDir);
|
|
1942
|
-
if (contained !== null && node_fs_1.default.existsSync(contained))
|
|
1943
|
-
return contained;
|
|
1944
|
-
return null;
|
|
1945
|
-
}
|
|
1946
|
-
/**
|
|
1947
|
-
* Pure comparator: which of `entries` have an effective last-changed time
|
|
1948
|
-
* STRICTLY BEFORE `contextEffectiveMs` (CONTEXT.md's own effective time)? Strict
|
|
1949
|
-
* `<` is "stale" (matches findStaleVerificationSummary's own strict `>` convention
|
|
1950
|
-
* for "newer than" elsewhere in this codebase — an artifact committed in the SAME
|
|
1951
|
-
* commit/second as CONTEXT.md is in sync, not stale).
|
|
1952
|
-
*/
|
|
1953
|
-
function computeContextDrift(contextEffectiveMs, entries) {
|
|
1954
|
-
return entries.filter((e) => e.effectiveMs < contextEffectiveMs).map((e) => e.file);
|
|
1955
|
-
}
|
|
1956
|
-
function buildContextDriftMessage(staleArtifacts, phaseArg) {
|
|
1957
|
-
const parts = [`CONTEXT.md decisions are newer than: ${staleArtifacts.join(', ')}.`];
|
|
1958
|
-
if (staleArtifacts.some((f) => f.endsWith('-RESEARCH.md'))) {
|
|
1959
|
-
parts.push(`Regenerate research: /gsd:plan-phase ${phaseArg} --research.`);
|
|
1960
|
-
}
|
|
1961
|
-
if (staleArtifacts.some((f) => f.endsWith('-PATTERNS.md'))) {
|
|
1962
|
-
parts.push('Regenerate patterns: delete the PATTERNS.md file, then re-run /gsd:plan-phase.');
|
|
1963
|
-
}
|
|
1964
|
-
if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || (f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md')))) {
|
|
1965
|
-
parts.push('Regenerate or manually reconcile VALIDATION.md / SPEC.md against the current decisions.');
|
|
1966
|
-
}
|
|
1967
|
-
parts.push('Do not hand-inject the newer decisions into a prompt as a substitute for regenerating — that carries the staleness forward.');
|
|
1968
|
-
return parts.join(' ');
|
|
1969
|
-
}
|
|
1970
|
-
function cmdVerifyContextDrift(cwd, phaseArg, raw) {
|
|
1971
|
-
if (!phaseArg) {
|
|
1972
|
-
error('Usage: verify context-drift <phase>');
|
|
1973
|
-
return;
|
|
1974
|
-
}
|
|
1975
|
-
const pDir = planningDir(cwd);
|
|
1976
|
-
const phasesDir = node_path_1.default.join(pDir, 'phases');
|
|
1977
|
-
const emitSkip = (reason, message = '') => {
|
|
1978
|
-
output({ block: false, skipped: true, reason, stale_artifacts: [], message }, raw);
|
|
1979
|
-
};
|
|
1980
|
-
if (!node_fs_1.default.existsSync(phasesDir)) {
|
|
1981
|
-
emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
|
|
1982
|
-
return;
|
|
1983
|
-
}
|
|
1984
|
-
// Same phase-directory resolution rule cmdVerifySchemaDrift uses (#1571, #2528):
|
|
1985
|
-
// matchPhaseDirs, never a naive substring test.
|
|
1986
|
-
const phaseDir = resolvePhaseDirByToken(phasesDir, phaseArg);
|
|
1987
|
-
if (!phaseDir) {
|
|
1988
|
-
emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
|
|
1989
|
-
return;
|
|
1990
|
-
}
|
|
1991
|
-
let phaseFiles;
|
|
1992
|
-
try {
|
|
1993
|
-
phaseFiles = node_fs_1.default.readdirSync(phaseDir).slice().sort();
|
|
1994
|
-
}
|
|
1995
|
-
catch {
|
|
1996
|
-
emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
|
|
1997
|
-
return;
|
|
1998
|
-
}
|
|
1999
|
-
const contextFile = phaseFiles.find((f) => f.endsWith('-CONTEXT.md'));
|
|
2000
|
-
if (!contextFile) {
|
|
2001
|
-
emitSkip('no-context-md');
|
|
2002
|
-
return;
|
|
2003
|
-
}
|
|
2004
|
-
const researchFile = phaseFiles.find((f) => f.endsWith('-RESEARCH.md'));
|
|
2005
|
-
const patternsFile = phaseFiles.find((f) => f.endsWith('-PATTERNS.md'));
|
|
2006
|
-
const validationFile = phaseFiles.find((f) => f.endsWith('-VALIDATION.md'));
|
|
2007
|
-
const specFile = phaseFiles.find((f) => f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md'));
|
|
2008
|
-
const upstreamFiles = [researchFile, patternsFile, validationFile, specFile].filter((f) => !!f);
|
|
2009
|
-
if (upstreamFiles.length === 0) {
|
|
2010
|
-
emitSkip('no-upstream-artifacts');
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
const allFiles = [contextFile, ...upstreamFiles];
|
|
2014
|
-
const cleanCommitMs = defaultPhaseCleanCommitTimesMs(phaseDir, allFiles);
|
|
2015
|
-
const effectiveTimeMs = (file) => cleanCommitMs.has(file)
|
|
2016
|
-
? cleanCommitMs.get(file)
|
|
2017
|
-
: node_fs_1.default.statSync(node_path_1.default.join(phaseDir, file)).mtimeMs;
|
|
2018
|
-
const contextMs = effectiveTimeMs(contextFile);
|
|
2019
|
-
const driftEntries = upstreamFiles.map((f) => ({ file: f, effectiveMs: effectiveTimeMs(f) }));
|
|
2020
|
-
const staleArtifacts = computeContextDrift(contextMs, driftEntries);
|
|
2021
|
-
let wf;
|
|
2022
|
-
try {
|
|
2023
|
-
const rawCfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(pDir, 'config.json'), 'utf-8'));
|
|
2024
|
-
wf = rawCfg['workflow'];
|
|
2025
|
-
}
|
|
2026
|
-
catch {
|
|
2027
|
-
wf = undefined;
|
|
2028
|
-
}
|
|
2029
|
-
const action = wf?.context_drift_action === 'block' ? 'block' : 'warn';
|
|
2030
|
-
const block = staleArtifacts.length > 0 && action === 'block';
|
|
2031
|
-
const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg) : '';
|
|
2032
|
-
output({
|
|
2033
|
-
block,
|
|
2034
|
-
skipped: false,
|
|
2035
|
-
stale_artifacts: staleArtifacts,
|
|
2036
|
-
action,
|
|
2037
|
-
message,
|
|
2038
|
-
}, raw);
|
|
2039
|
-
}
|
|
2040
|
-
function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
2041
|
-
if (!phaseArg) {
|
|
2042
|
-
error('Usage: verify schema-drift <phase> [--skip]');
|
|
2043
|
-
return;
|
|
2044
|
-
}
|
|
2045
|
-
const pDir = planningDir(cwd);
|
|
2046
|
-
const phasesDir = node_path_1.default.join(pDir, 'phases');
|
|
2047
|
-
if (!node_fs_1.default.existsSync(phasesDir)) {
|
|
2048
|
-
output({ block: false, drift_detected: false, blocking: false, message: 'No phases directory' }, raw);
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
2051
|
-
// Resolve the phase directory with the canonical phase-directory matcher
|
|
2052
|
-
// (phase-id.cjs::matchPhaseDirs), not a naive substring test. A bare
|
|
2053
|
-
// `.includes(phaseArg)` lets a non-existent phase silently match a different
|
|
2054
|
-
// phase whose directory name merely contains the requested token (e.g. "1"
|
|
2055
|
-
// matching "11-expansion"), making the drift gate inspect the wrong phase.
|
|
2056
|
-
// This shares the one selection rule with find-phase / verify
|
|
2057
|
-
// phase-completeness rather than restating it. (#1571, #2528)
|
|
2058
|
-
const phaseDir = resolvePhaseDirByToken(phasesDir, phaseArg);
|
|
2059
|
-
if (!phaseDir) {
|
|
2060
|
-
output({ block: false, drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
|
|
2061
|
-
return;
|
|
2062
|
-
}
|
|
2063
|
-
// #3183: canonical LIVE plan/summary sets (root+nested,
|
|
2064
|
-
// status: superseded EXCLUDED) from the single owner, rather than a
|
|
2065
|
-
// root-only readdirSync filter — a superseded plan's claimed
|
|
2066
|
-
// files_modified is no longer treated as an expected drift target, and
|
|
2067
|
-
// nested (#3139 layout) plans/summaries are no longer invisible to the
|
|
2068
|
-
// drift check.
|
|
2069
|
-
const { planFiles, summaryFiles } = planScanMod.scanPhasePlans(phaseDir);
|
|
2070
|
-
const allFiles = [];
|
|
2071
|
-
for (const pf of planFiles) {
|
|
2072
|
-
const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, pf), 'utf-8');
|
|
2073
|
-
const fmMatch = content.match(/files_modified:\s*\[([^\]]{0,8000})\]/);
|
|
2074
|
-
if (fmMatch) {
|
|
2075
|
-
const files = fmMatch[1].split(',').map((f) => f.trim()).filter(Boolean);
|
|
2076
|
-
allFiles.push(...files);
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
let executionLog = '';
|
|
2080
|
-
for (const sf of summaryFiles) {
|
|
2081
|
-
executionLog += node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, sf), 'utf-8') + '\n';
|
|
2082
|
-
}
|
|
2083
|
-
const gitLog = (0, shell_command_projection_cjs_1.execGit)(['log', '--oneline', '--all', '-50'], { cwd });
|
|
2084
|
-
if (gitLog.exitCode === 0) {
|
|
2085
|
-
executionLog += '\n' + gitLog.stdout;
|
|
2086
|
-
}
|
|
2087
|
-
const result = (0, schema_detect_cjs_1.checkSchemaDrift)(allFiles, executionLog, { skipCheck: !!skipFlag });
|
|
2088
|
-
const isSkipped = !!result['skipped'];
|
|
2089
|
-
output({
|
|
2090
|
-
// Uniform gate contract: `block` = true means "this gate's bad condition is met".
|
|
2091
|
-
// When skipCheck is true (GSD_SKIP_SCHEMA_CHECK=true), the gate is bypassed —
|
|
2092
|
-
// block must be false regardless of whether drift was detected.
|
|
2093
|
-
// drift_detected and blocking are kept for compatibility.
|
|
2094
|
-
block: isSkipped ? false : !!result['driftDetected'],
|
|
2095
|
-
drift_detected: result['driftDetected'],
|
|
2096
|
-
blocking: result['blocking'],
|
|
2097
|
-
schema_files: result['schemaFiles'],
|
|
2098
|
-
orms: result['orms'],
|
|
2099
|
-
unpushed_orms: result['unpushedOrms'],
|
|
2100
|
-
message: result['message'],
|
|
2101
|
-
skipped: isSkipped,
|
|
2102
|
-
}, raw);
|
|
2103
|
-
}
|
|
2104
2028
|
/**
|
|
2105
2029
|
* Stamp `last_mapped_commit` (plus `last_mapped_at`) into the frontmatter of
|
|
2106
2030
|
* every codebase-map document that exists on disk, using the current HEAD sha.
|
|
2107
2031
|
*
|
|
2108
2032
|
* #3418: `drift.cjs` shipped a correct `writeMappedCommit` with no production
|
|
2109
2033
|
* caller, so no full `/gsd:map-codebase` run ever wrote the machine-readable
|
|
2110
|
-
* baseline that `
|
|
2034
|
+
* baseline that the codebase-drift gate (`gate-codebase-drift.cts`) reads. The stamp lives in CODE rather
|
|
2111
2035
|
* than in a prose instruction to the mapper agent on purpose: an agent that
|
|
2112
2036
|
* decides its work is already done skips a prose step silently, which is the
|
|
2113
2037
|
* exact class of failure the stamp exists to detect.
|
|
@@ -2205,207 +2129,6 @@ function cmdStampCodebaseMap(cwd, raw, only) {
|
|
|
2205
2129
|
emit(skip('exception: ' + (err instanceof Error ? err.message : String(err))));
|
|
2206
2130
|
}
|
|
2207
2131
|
}
|
|
2208
|
-
function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
2209
|
-
// Non-hoisted: load-order matters for circular dep guard
|
|
2210
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports -- drift.cjs is an export= CommonJS module
|
|
2211
|
-
const drift = require('./drift.cjs');
|
|
2212
|
-
const emit = (payload) => output(payload, raw);
|
|
2213
|
-
try {
|
|
2214
|
-
const codebaseDir = node_path_1.default.join(planningDir(cwd), 'codebase');
|
|
2215
|
-
const structurePath = node_path_1.default.join(codebaseDir, 'STRUCTURE.md');
|
|
2216
|
-
if (!node_fs_1.default.existsSync(structurePath)) {
|
|
2217
|
-
emit({
|
|
2218
|
-
// Uniform gate contract: block = action_required (false when skipped).
|
|
2219
|
-
block: false,
|
|
2220
|
-
skipped: true,
|
|
2221
|
-
reason: 'no-structure-md',
|
|
2222
|
-
action_required: false,
|
|
2223
|
-
directive: 'none',
|
|
2224
|
-
elements: [],
|
|
2225
|
-
});
|
|
2226
|
-
return;
|
|
2227
|
-
}
|
|
2228
|
-
let structureMd;
|
|
2229
|
-
try {
|
|
2230
|
-
structureMd = node_fs_1.default.readFileSync(structurePath, 'utf-8');
|
|
2231
|
-
}
|
|
2232
|
-
catch (err) {
|
|
2233
|
-
emit({
|
|
2234
|
-
block: false,
|
|
2235
|
-
skipped: true,
|
|
2236
|
-
reason: 'cannot-read-structure-md: ' + (err instanceof Error ? err.message : String(err)),
|
|
2237
|
-
action_required: false,
|
|
2238
|
-
directive: 'none',
|
|
2239
|
-
elements: [],
|
|
2240
|
-
});
|
|
2241
|
-
return;
|
|
2242
|
-
}
|
|
2243
|
-
const lastMapped = drift['readMappedCommit'](structurePath);
|
|
2244
|
-
const revProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', 'HEAD'], { cwd });
|
|
2245
|
-
if (revProbe.exitCode !== 0) {
|
|
2246
|
-
emit({
|
|
2247
|
-
block: false,
|
|
2248
|
-
skipped: true,
|
|
2249
|
-
reason: 'not-a-git-repo',
|
|
2250
|
-
action_required: false,
|
|
2251
|
-
directive: 'none',
|
|
2252
|
-
elements: [],
|
|
2253
|
-
});
|
|
2254
|
-
return;
|
|
2255
|
-
}
|
|
2256
|
-
// #3418: an absent or unresolvable baseline means NO COMPARISON IS POSSIBLE.
|
|
2257
|
-
// It is neither zero drift nor total drift, and reporting it as either is a
|
|
2258
|
-
// lie the consumer cannot detect. The former fallback diffed HEAD against
|
|
2259
|
-
// the empty tree, so every tracked file read as newly added and the gate
|
|
2260
|
-
// reported maximum drift identically on every run -- which made a genuinely
|
|
2261
|
-
// stale map indistinguishable from a fresh one, and let `spawn_mapper` fire
|
|
2262
|
-
// a whole-repo remap while presenting itself as an incremental one.
|
|
2263
|
-
if (!lastMapped) {
|
|
2264
|
-
emit({
|
|
2265
|
-
block: false,
|
|
2266
|
-
skipped: true,
|
|
2267
|
-
reason: 'no-mapped-commit',
|
|
2268
|
-
action_required: false,
|
|
2269
|
-
directive: 'none',
|
|
2270
|
-
elements: [],
|
|
2271
|
-
last_mapped_commit: null,
|
|
2272
|
-
});
|
|
2273
|
-
return;
|
|
2274
|
-
}
|
|
2275
|
-
const baseProbe = (0, shell_command_projection_cjs_1.execGit)(['cat-file', '-t', lastMapped], { cwd });
|
|
2276
|
-
if (baseProbe.exitCode !== 0 || baseProbe.stdout.trim() !== 'commit') {
|
|
2277
|
-
// A stamp git cannot resolve: history rewrite, GC, or a shallow clone.
|
|
2278
|
-
// Distinct reason from 'no-mapped-commit' -- the map claims a baseline,
|
|
2279
|
-
// this repository just cannot see it, which is an operator-actionable
|
|
2280
|
-
// difference (re-map vs. unshallow). A resolvable non-commit (a tree or
|
|
2281
|
-
// blob sha, a ref name) is the same class of bad baseline: git would
|
|
2282
|
-
// happily diff against it and report drift against the wrong object.
|
|
2283
|
-
emit({
|
|
2284
|
-
block: false,
|
|
2285
|
-
skipped: true,
|
|
2286
|
-
reason: 'unresolvable-mapped-commit',
|
|
2287
|
-
action_required: false,
|
|
2288
|
-
directive: 'none',
|
|
2289
|
-
elements: [],
|
|
2290
|
-
last_mapped_commit: lastMapped,
|
|
2291
|
-
});
|
|
2292
|
-
return;
|
|
2293
|
-
}
|
|
2294
|
-
const base = lastMapped;
|
|
2295
|
-
const diff = (0, shell_command_projection_cjs_1.execGit)(['diff', '--name-status', base, 'HEAD'], { cwd });
|
|
2296
|
-
if (diff.exitCode !== 0) {
|
|
2297
|
-
emit({
|
|
2298
|
-
block: false,
|
|
2299
|
-
skipped: true,
|
|
2300
|
-
reason: 'git-diff-failed',
|
|
2301
|
-
action_required: false,
|
|
2302
|
-
directive: 'none',
|
|
2303
|
-
elements: [],
|
|
2304
|
-
});
|
|
2305
|
-
return;
|
|
2306
|
-
}
|
|
2307
|
-
// #3418: GSD's own planning artifacts are not codebase structure. A
|
|
2308
|
-
// map-codebase run commits `.planning/codebase/*.md`, so a correctly
|
|
2309
|
-
// stamped baseline would be re-poisoned by the very commit that carries
|
|
2310
|
-
// the stamp -- the next gate invocation would report the map's own seven
|
|
2311
|
-
// documents as seven new directories, back over the default threshold of
|
|
2312
|
-
// three. Derived from planningRoot() rather than a hardcoded literal so a
|
|
2313
|
-
// repoint of the planning root cannot leave this filter behind.
|
|
2314
|
-
//
|
|
2315
|
-
// `git diff --name-status` always prints repo-root-relative paths, so a cwd
|
|
2316
|
-
// below the root needs the `sub/` prefix or the filter matches nothing.
|
|
2317
|
-
// That prefix comes from git (`--show-prefix`: root-relative, forward
|
|
2318
|
-
// slashes, trailing slash, empty at the root). The rejected alternative was
|
|
2319
|
-
// path.relative(`--show-toplevel`, cwd), which mixes two path producers: on
|
|
2320
|
-
// Windows os.tmpdir() hands back the 8.3 short form while git resolves the
|
|
2321
|
-
// long one, so relative() between them yields a `../..` chain that matches
|
|
2322
|
-
// nothing. The `.planning` half below is safe to compute with relative()
|
|
2323
|
-
// because both of its sides are the same cwd string.
|
|
2324
|
-
const prefixProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--show-prefix'], { cwd });
|
|
2325
|
-
const repoPrefix = prefixProbe.exitCode === 0 ? prefixProbe.stdout.trim() : '';
|
|
2326
|
-
const planningPrefix = repoPrefix + node_path_1.default.relative(cwd, planningRoot(cwd)).split(node_path_1.default.sep).join('/') + '/';
|
|
2327
|
-
const isPlanningArtifact = (file) => file.split('\\').join('/').startsWith(planningPrefix);
|
|
2328
|
-
const added = [];
|
|
2329
|
-
const modified = [];
|
|
2330
|
-
const deleted = [];
|
|
2331
|
-
for (const line of diff.stdout.split(/\r?\n/)) {
|
|
2332
|
-
if (!line.trim())
|
|
2333
|
-
continue;
|
|
2334
|
-
const m = line.match(/^([A-Z])\d*\t(.+?)(?:\t(.+))?$/);
|
|
2335
|
-
if (!m)
|
|
2336
|
-
continue;
|
|
2337
|
-
const status = m[1];
|
|
2338
|
-
// execGit sets no core.quotepath config, so git's default `true` applies:
|
|
2339
|
-
// any path containing non-ASCII bytes (or `"`, `\`, control bytes) is
|
|
2340
|
-
// C-quoted — `"docs/\350\256\276…/overview.md"`. Capturing that verbatim
|
|
2341
|
-
// garbles affected_paths/elements and makes isPathMapped compare the
|
|
2342
|
-
// quoted prefix (`"docs`) against STRUCTURE.md, misclassifying DOCUMENTED
|
|
2343
|
-
// directories as new_dir (#4081). Decode with the single owner of the
|
|
2344
|
-
// git C-quote seam (worktree-safety.cjs); a non-quoted value — the plain
|
|
2345
|
-
// ASCII common case — passes through untouched. Both capture groups are
|
|
2346
|
-
// decoded: R/C lines carry old AND new paths, either may be quoted.
|
|
2347
|
-
const file = decodeGitQuotedPath(m[3] || m[2]);
|
|
2348
|
-
if (isPlanningArtifact(file))
|
|
2349
|
-
continue;
|
|
2350
|
-
if (status === 'A' || status === 'R' || status === 'C')
|
|
2351
|
-
added.push(file);
|
|
2352
|
-
else if (status === 'M')
|
|
2353
|
-
modified.push(file);
|
|
2354
|
-
else if (status === 'D')
|
|
2355
|
-
deleted.push(file);
|
|
2356
|
-
}
|
|
2357
|
-
// loadConfig() returns a flattened object — there is no nested `workflow`
|
|
2358
|
-
// key. Read the raw config.json directly to access workflow-scoped keys,
|
|
2359
|
-
// matching the pattern used in check-command-router.cts:readWorkflowConfig.
|
|
2360
|
-
let wf;
|
|
2361
|
-
try {
|
|
2362
|
-
const rawCfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'config.json'), 'utf-8'));
|
|
2363
|
-
wf = rawCfg['workflow'];
|
|
2364
|
-
}
|
|
2365
|
-
catch {
|
|
2366
|
-
wf = undefined;
|
|
2367
|
-
}
|
|
2368
|
-
const threshold = Number.isInteger(wf?.drift_threshold) && wf?.drift_threshold >= 1
|
|
2369
|
-
? wf?.drift_threshold
|
|
2370
|
-
: 3;
|
|
2371
|
-
const action = wf?.drift_action === 'auto-remap' ? 'auto-remap' : 'warn';
|
|
2372
|
-
const driftResult = drift['detectDrift']({
|
|
2373
|
-
addedFiles: added,
|
|
2374
|
-
modifiedFiles: modified,
|
|
2375
|
-
deletedFiles: deleted,
|
|
2376
|
-
structureMd,
|
|
2377
|
-
threshold,
|
|
2378
|
-
action,
|
|
2379
|
-
runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd),
|
|
2380
|
-
});
|
|
2381
|
-
const actionRequired = !!driftResult['actionRequired'];
|
|
2382
|
-
emit({
|
|
2383
|
-
// Uniform gate contract: block = action_required.
|
|
2384
|
-
block: actionRequired,
|
|
2385
|
-
skipped: !!driftResult['skipped'],
|
|
2386
|
-
reason: driftResult['reason'] || null,
|
|
2387
|
-
action_required: actionRequired,
|
|
2388
|
-
directive: driftResult['directive'],
|
|
2389
|
-
spawn_mapper: !!driftResult['spawnMapper'],
|
|
2390
|
-
affected_paths: driftResult['affectedPaths'] || [],
|
|
2391
|
-
elements: driftResult['elements'] || [],
|
|
2392
|
-
threshold,
|
|
2393
|
-
action,
|
|
2394
|
-
last_mapped_commit: lastMapped,
|
|
2395
|
-
message: driftResult['message'] || '',
|
|
2396
|
-
});
|
|
2397
|
-
}
|
|
2398
|
-
catch (err) {
|
|
2399
|
-
emit({
|
|
2400
|
-
block: false,
|
|
2401
|
-
skipped: true,
|
|
2402
|
-
reason: 'exception: ' + (err && err instanceof Error ? err.message : String(err)),
|
|
2403
|
-
action_required: false,
|
|
2404
|
-
directive: 'none',
|
|
2405
|
-
elements: [],
|
|
2406
|
-
});
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
2132
|
module.exports = {
|
|
2410
2133
|
scanNegativeGrepCommentEcho,
|
|
2411
2134
|
scanFileWideNegativeGateConflict,
|
|
@@ -2421,10 +2144,6 @@ module.exports = {
|
|
|
2421
2144
|
cmdValidateConsistency,
|
|
2422
2145
|
cmdValidateHealth,
|
|
2423
2146
|
cmdValidateAgents,
|
|
2424
|
-
cmdVerifySchemaDrift,
|
|
2425
|
-
cmdVerifyCodebaseDrift,
|
|
2426
|
-
computeContextDrift,
|
|
2427
|
-
cmdVerifyContextDrift,
|
|
2428
2147
|
cmdStampCodebaseMap,
|
|
2429
2148
|
STATE_HEAD_ADVISORY_COMMITS,
|
|
2430
2149
|
};
|