@opengsd/gsd-core 1.11.0 → 1.13.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 +12 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +78 -42
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +91 -112
- package/agents/gsd-planner.md +20 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +82 -7
- package/agents/gsd-ui-researcher.md +70 -3
- package/agents/gsd-verifier.md +24 -2
- package/bin/install.js +847 -200
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/quick.md +8 -4
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +761 -100
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +785 -144
- package/gsd-core/bin/lib/capability-state.cjs +25 -4
- package/gsd-core/bin/lib/capability-validator.cjs +321 -18
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +229 -6
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +877 -54
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +121 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +118 -14
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +23 -2
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +975 -326
- package/gsd-core/bin/lib/gap-checker.cjs +41 -8
- package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +96 -11
- package/gsd-core/bin/lib/init-command-router.cjs +132 -21
- package/gsd-core/bin/lib/init.cjs +252 -56
- package/gsd-core/bin/lib/install-engine.cjs +252 -15
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
- package/gsd-core/bin/lib/install-profiles.cjs +100 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +195 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +175 -4
- package/gsd-core/bin/lib/milestone.cjs +112 -7
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +321 -13
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +1175 -115
- package/gsd-core/bin/lib/plan-document.cjs +273 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
- package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
- package/gsd-core/bin/lib/roadmap.cjs +248 -64
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -31
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +216 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
- package/gsd-core/bin/lib/state-transition.cjs +850 -145
- package/gsd-core/bin/lib/state.cjs +1629 -287
- package/gsd-core/bin/lib/surface.cjs +33 -10
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +2542 -387
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -13
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +441 -56
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +6 -5
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +17 -5
- package/gsd-core/references/execute-mvp-tdd.md +18 -18
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +76 -3
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +39 -9
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +15 -12
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +7 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +3 -1
- package/gsd-core/workflows/add-phase.md +5 -3
- package/gsd-core/workflows/add-tests.md +4 -9
- package/gsd-core/workflows/add-todo.md +2 -2
- package/gsd-core/workflows/ai-integration-phase.md +5 -10
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +14 -3
- package/gsd-core/workflows/audit-milestone.md +11 -9
- package/gsd-core/workflows/audit-uat.md +19 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +12 -26
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +3 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
- package/gsd-core/workflows/code-review-fix.md +3 -1
- package/gsd-core/workflows/code-review.md +192 -69
- package/gsd-core/workflows/complete-milestone.md +28 -14
- package/gsd-core/workflows/debug.md +6 -4
- package/gsd-core/workflows/diagnose-issues.md +17 -7
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +46 -19
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +3 -1
- package/gsd-core/workflows/eval-review.md +5 -10
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- 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 +29 -5
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +68 -66
- package/gsd-core/workflows/execute-plan.md +25 -20
- package/gsd-core/workflows/explore.md +3 -1
- package/gsd-core/workflows/extract-learnings.md +3 -1
- package/gsd-core/workflows/fast.md +8 -2
- package/gsd-core/workflows/forensics.md +3 -1
- package/gsd-core/workflows/graduation.md +6 -6
- package/gsd-core/workflows/health.md +4 -7
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +17 -14
- package/gsd-core/workflows/inbox.md +5 -6
- package/gsd-core/workflows/ingest-docs.md +45 -12
- package/gsd-core/workflows/insert-phase.md +7 -5
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +7 -3
- package/gsd-core/workflows/list-workspaces.md +3 -1
- package/gsd-core/workflows/manager.md +15 -26
- package/gsd-core/workflows/map-codebase.md +3 -1
- package/gsd-core/workflows/milestone-summary.md +3 -1
- package/gsd-core/workflows/mvp-phase.md +3 -3
- package/gsd-core/workflows/new-milestone.md +10 -22
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +17 -29
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +4 -2
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +20 -5
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
- package/gsd-core/workflows/plan-phase.md +251 -54
- package/gsd-core/workflows/plan-review-convergence.md +148 -19
- package/gsd-core/workflows/plant-seed.md +3 -3
- package/gsd-core/workflows/pr-branch.md +195 -51
- package/gsd-core/workflows/profile-user.md +17 -15
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +52 -15
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +33 -32
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +6 -4
- package/gsd-core/workflows/remove-workspace.md +3 -3
- package/gsd-core/workflows/resume-project.md +14 -14
- package/gsd-core/workflows/review.md +404 -21
- package/gsd-core/workflows/scan.md +3 -1
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +9 -9
- package/gsd-core/workflows/settings-integrations.md +66 -32
- package/gsd-core/workflows/settings.md +4 -6
- package/gsd-core/workflows/ship.md +22 -16
- package/gsd-core/workflows/sketch-wrap-up.md +13 -17
- package/gsd-core/workflows/sketch.md +13 -19
- package/gsd-core/workflows/smart-entry.md +4 -6
- package/gsd-core/workflows/spec-phase.md +31 -4
- package/gsd-core/workflows/spike-wrap-up.md +9 -11
- package/gsd-core/workflows/spike.md +21 -32
- package/gsd-core/workflows/stats.md +4 -2
- package/gsd-core/workflows/sync-skills.md +13 -5
- package/gsd-core/workflows/thread.md +13 -7
- package/gsd-core/workflows/transition.md +7 -5
- package/gsd-core/workflows/ui-phase.md +36 -21
- package/gsd-core/workflows/ui-review.md +7 -11
- package/gsd-core/workflows/ultraplan-phase.md +7 -13
- package/gsd-core/workflows/undo.md +9 -17
- package/gsd-core/workflows/update.md +47 -48
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +106 -21
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +302 -22
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +77 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +46 -12
- package/hooks/dist/gsd-read-guard.js +18 -7
- package/hooks/dist/gsd-read-injection-scanner.js +22 -13
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +222 -29
- package/hooks/dist/gsd-validate-commit.sh +523 -12
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +36 -17
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +302 -22
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +77 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +46 -12
- package/hooks/gsd-read-guard.js +18 -7
- package/hooks/gsd-read-injection-scanner.js +22 -13
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +222 -29
- package/hooks/gsd-validate-commit.sh +523 -12
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +36 -17
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/hooks.json +6 -0
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +14 -9
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +12 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +52 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +406 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +189 -4
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- 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-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +45 -14
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +124 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +83 -14
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Verify-command grounding probe (#2401).
|
|
4
|
+
*
|
|
5
|
+
* #2401: a planner authored `<automated>cd ../../frontend && npm run lint</automated>`
|
|
6
|
+
* whose target did not resolve from the executor's cwd, and the plan-checker —
|
|
7
|
+
* lacking a deterministic probe — hand-reasoned the filesystem and prescribed two
|
|
8
|
+
* successively-wrong replacement paths.
|
|
9
|
+
*
|
|
10
|
+
* This module answers "can this `<automated>` verify command's target directory be
|
|
11
|
+
* grounded?" WITHOUT ever executing the command. PLAN.md is LLM-authored untrusted
|
|
12
|
+
* text, so this module never `exec`s, `spawn`s, or otherwise shells out — it reads
|
|
13
|
+
* only via `fs.statSync`, `fs.existsSync`, `fs.readFileSync`, `fs.readdirSync`.
|
|
14
|
+
*
|
|
15
|
+
* It is a RECOGNIZER, not a shell interpreter (deliberate, per Greenspun's Tenth
|
|
16
|
+
* Rule — the fix for #2401 is refusing to guess, not writing a bigger shell
|
|
17
|
+
* parser): exactly two forms are grounded (`cd <literal>` and
|
|
18
|
+
* `npm --prefix <literal>`), and anything this probe cannot ground returns
|
|
19
|
+
* `unresolvable`, which is a warning and never a blocker.
|
|
20
|
+
*
|
|
21
|
+
* A bare ancestor climb (`cd ../..`, no trailing named segment) is genuinely
|
|
22
|
+
* ambiguous under parallel-worktree execution — the checker's root and the
|
|
23
|
+
* executor's root differ, so "my grandparent directory" cannot be asserted
|
|
24
|
+
* about, and it is reported `outside_root` without touching the filesystem.
|
|
25
|
+
* A climb that names a concrete sibling (`cd ../../frontend`, the exact #2401
|
|
26
|
+
* shape) still names something checkable, so it is resolved and probed like any
|
|
27
|
+
* other target.
|
|
28
|
+
*/
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
|
+
};
|
|
32
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
33
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
34
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
36
|
+
const phaseIdMod = require("./phase-id.cjs");
|
|
37
|
+
const { stripProjectCodePrefix, extractPhaseToken, comparePhaseNum } = phaseIdMod;
|
|
38
|
+
// ─── Extraction ───────────────────────────────────────────────────────────────
|
|
39
|
+
const TASK_NAME_RE = /<name>([\s\S]*?)<\/name>/;
|
|
40
|
+
/**
|
|
41
|
+
* The body cannot cross a subsequent `<automated>` opener (negative lookahead
|
|
42
|
+
* `(?!<automated\b)` gates each consumed char), mirroring the stop-at-next-open
|
|
43
|
+
* shape `extractTaggedBlocks`/`stripTaggedBlocks` already use. This is what
|
|
44
|
+
* makes a scan over an unclosed opener LINEAR instead of quadratic: the lazy
|
|
45
|
+
* `[\s\S]*?` body used to scan all the way to EOF hunting a closing tag that
|
|
46
|
+
* never comes (measured: k=40000 unclosed openers, ~1.5s). `MAX_BLOCK_WALK`
|
|
47
|
+
* below bounds matched-block COUNT, not scan cost — it does nothing for a scan
|
|
48
|
+
* that matches nothing, which is exactly the case this shape fixes.
|
|
49
|
+
*/
|
|
50
|
+
const AUTOMATED_BLOCK_RE = /<automated[^>]{0,200}>((?:(?!<automated\b)[\s\S])*?)<\/automated>/g;
|
|
51
|
+
/**
|
|
52
|
+
* Bounds the NUMBER of matched `<automated>` blocks walked (e.g. hundreds of
|
|
53
|
+
* legitimately closed blocks in one document), not the cost of scanning
|
|
54
|
+
* pathological unclosed input — that cost is now bounded by the non-crossing
|
|
55
|
+
* body in `AUTOMATED_BLOCK_RE`/`VERIFY_TOKEN_RE` themselves (a failed scan is
|
|
56
|
+
* linear, so no separate iteration cap is needed to keep it cheap).
|
|
57
|
+
* `extractTaggedBlocks`/`stripTaggedBlocks` (task-block grammar owner,
|
|
58
|
+
* `./markdown-sectionizer.cjs`) already use the same non-crossing shape with
|
|
59
|
+
* no iteration cap of their own. This guard only bounds the `<automated>`
|
|
60
|
+
* scan this module still runs directly, matching the pre-existing behavior.
|
|
61
|
+
*/
|
|
62
|
+
const MAX_BLOCK_WALK = 20000;
|
|
63
|
+
/**
|
|
64
|
+
* Run `AUTOMATED_BLOCK_RE` over `text`, pushing `{plan: '', task, command}`
|
|
65
|
+
* for each non-empty trimmed block onto `out`, in order. Shares `guard`
|
|
66
|
+
* across every caller in one `extractAutomatedCommands` invocation so the
|
|
67
|
+
* MAX_BLOCK_WALK bound applies to the WHOLE document's `<automated>` count,
|
|
68
|
+
* not per task body. Returns `false` when the bound was hit (caller stops
|
|
69
|
+
* walking further task bodies immediately); `true` otherwise.
|
|
70
|
+
*/
|
|
71
|
+
function extractAutomatedFromText(text, task, out, guard) {
|
|
72
|
+
AUTOMATED_BLOCK_RE.lastIndex = 0;
|
|
73
|
+
let aMatch;
|
|
74
|
+
while ((aMatch = AUTOMATED_BLOCK_RE.exec(text)) !== null) {
|
|
75
|
+
const raw = (aMatch[1] ?? '').trim();
|
|
76
|
+
if (raw !== '')
|
|
77
|
+
out.push({ plan: '', task, command: raw });
|
|
78
|
+
if (aMatch.index === AUTOMATED_BLOCK_RE.lastIndex)
|
|
79
|
+
AUTOMATED_BLOCK_RE.lastIndex += 1;
|
|
80
|
+
guard.n += 1;
|
|
81
|
+
if (guard.n > MAX_BLOCK_WALK)
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract every `<automated>…</automated>` command from PLAN.md text,
|
|
88
|
+
* attaching the owning `<task><name>` when the block sits inside a
|
|
89
|
+
* `<task>…</task>`. Never throws; a non-string or blank plan yields `[]`.
|
|
90
|
+
* `plan` is left `''` — callers (`probePhaseVerifyCommands`,
|
|
91
|
+
* `harvestPriorVerifyCommands`) set it from the filename they read.
|
|
92
|
+
*
|
|
93
|
+
* Task-block bodies are obtained from the canonical sectionizer
|
|
94
|
+
* (`extractTaggedBlocks`/`stripTaggedBlocks`, `./markdown-sectionizer.cjs`)
|
|
95
|
+
* rather than a fourth hand-rolled `<task>` grammar copy (review finding,
|
|
96
|
+
* generative fix divergence class). This module only needs each task's
|
|
97
|
+
* `<name>` and its `<automated>` blocks — never the opening tag's `type=`
|
|
98
|
+
* attribute — so, unlike `src/verify.cts`'s `PLAN_TASK_BLOCK_RE` (which keeps
|
|
99
|
+
* its own copy specifically to read `type=`), it can share the owner
|
|
100
|
+
* outright. Ordering: every in-task command is emitted first, task by task
|
|
101
|
+
* in document order (`extractTaggedBlocks` returns bodies in document
|
|
102
|
+
* order); every command sitting OUTSIDE any `<task>` is emitted after, in
|
|
103
|
+
* the order it appears in the task-stripped remainder. No existing caller or
|
|
104
|
+
* test depends on interleaving an outside-task block between two in-task
|
|
105
|
+
* blocks that surround it in the raw document.
|
|
106
|
+
*/
|
|
107
|
+
function extractAutomatedCommands(planText) {
|
|
108
|
+
if (typeof planText !== 'string' || planText.length === 0)
|
|
109
|
+
return [];
|
|
110
|
+
const out = [];
|
|
111
|
+
const guard = { n: 0 };
|
|
112
|
+
const taskBodies = (0, markdown_sectionizer_cjs_1.extractTaggedBlocks)(planText, 'task', true);
|
|
113
|
+
for (const body of taskBodies) {
|
|
114
|
+
const nameMatch = TASK_NAME_RE.exec(body);
|
|
115
|
+
const task = nameMatch ? nameMatch[1].trim() : '';
|
|
116
|
+
if (!extractAutomatedFromText(body, task, out, guard))
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
const remainder = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(planText, 'task', true);
|
|
120
|
+
extractAutomatedFromText(remainder, '', out, guard);
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Ordered alternation with a backreference so `<automated>` and `<fails_when>`
|
|
125
|
+
* are recovered in a SINGLE document-order pass — the pairing walk needs
|
|
126
|
+
* their relative positions, which two independent scans would discard.
|
|
127
|
+
*
|
|
128
|
+
* The body cannot cross a subsequent `<automated>` or `<fails_when>` opener
|
|
129
|
+
* (negative lookahead `(?!<(?:automated|fails_when)\b)`), the same
|
|
130
|
+
* non-crossing shape as `AUTOMATED_BLOCK_RE` above. This is what keeps a scan
|
|
131
|
+
* over an unclosed opener LINEAR (verified: k=10000/20000/40000 unclosed
|
|
132
|
+
* openers all complete in ~0ms); `MAX_BLOCK_WALK` bounds matched-block COUNT,
|
|
133
|
+
* not scan cost.
|
|
134
|
+
*/
|
|
135
|
+
const VERIFY_TOKEN_RE = /<(automated|fails_when)[^>]{0,200}>((?:(?!<(?:automated|fails_when)\b)[\s\S])*?)<\/\1>/g;
|
|
136
|
+
const PLACEHOLDER_STATEMENTS = new Set(['tbd', 'todo', 'n/a', 'na', 'none', 'unknown', 'tba', '?', '-']);
|
|
137
|
+
/**
|
|
138
|
+
* Judge one `<automated>` command's stated failing direction. Pure, total,
|
|
139
|
+
* and never throws for any input shape — order of checks is load-bearing
|
|
140
|
+
* (#3172 review): the sentinel exemption (step 3) must run BEFORE the
|
|
141
|
+
* statement-presence check (step 4), because the Nyquist Wave-0 sentinel is
|
|
142
|
+
* not a runnable command and so has no failure mode to state, even when a
|
|
143
|
+
* statement happens to follow it in the plan text.
|
|
144
|
+
*/
|
|
145
|
+
function resolveFailingDirection(command, statement) {
|
|
146
|
+
const cmd = typeof command === 'string' ? command.trim() : '';
|
|
147
|
+
if (cmd === '') {
|
|
148
|
+
return {
|
|
149
|
+
command: '',
|
|
150
|
+
statement: typeof statement === 'string' ? statement : null,
|
|
151
|
+
status: 'orphan',
|
|
152
|
+
severity: 'warning',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (MISSING_SENTINEL_RE.test(cmd)) {
|
|
156
|
+
return {
|
|
157
|
+
command: cmd,
|
|
158
|
+
statement: typeof statement === 'string' ? statement.trim() : null,
|
|
159
|
+
status: 'sentinel',
|
|
160
|
+
severity: 'none',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (typeof statement !== 'string') {
|
|
164
|
+
return { command: cmd, statement: null, status: 'missing', severity: 'blocker' };
|
|
165
|
+
}
|
|
166
|
+
const trimmed = statement.trim();
|
|
167
|
+
if (trimmed === '') {
|
|
168
|
+
return { command: cmd, statement, status: 'empty', severity: 'blocker' };
|
|
169
|
+
}
|
|
170
|
+
// Whole-value match only — never a substring test. "TBD in the harness
|
|
171
|
+
// output" is real prose and must pass (#3172 review Finding).
|
|
172
|
+
if (PLACEHOLDER_STATEMENTS.has(trimmed.toLowerCase())) {
|
|
173
|
+
return { command: cmd, statement: trimmed, status: 'placeholder', severity: 'blocker' };
|
|
174
|
+
}
|
|
175
|
+
return { command: cmd, statement: trimmed, status: 'ok', severity: 'none' };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Scan one text unit's `<automated>`/`<fails_when>` tokens in document order,
|
|
179
|
+
* binding each `<fails_when>` to the nearest PRECEDING `<automated>` (first
|
|
180
|
+
* statement wins; a redundant trailing statement adds no row) and emitting an
|
|
181
|
+
* `orphan` row for a `<fails_when>` with no pending command. Shares `guard`
|
|
182
|
+
* across every text unit in one `extractFailingDirections` call, mirroring
|
|
183
|
+
* `extractAutomatedFromText`'s MAX_BLOCK_WALK bound and zero-length-match
|
|
184
|
+
* advance. Returns `false` when the bound was hit; `true` otherwise.
|
|
185
|
+
*/
|
|
186
|
+
function extractFailingFromText(text, task, out, guard) {
|
|
187
|
+
VERIFY_TOKEN_RE.lastIndex = 0;
|
|
188
|
+
let pending = null;
|
|
189
|
+
let bound = false;
|
|
190
|
+
let match;
|
|
191
|
+
while ((match = VERIFY_TOKEN_RE.exec(text)) !== null) {
|
|
192
|
+
const kind = match[1];
|
|
193
|
+
const body = match[2] ?? '';
|
|
194
|
+
if (kind === 'automated') {
|
|
195
|
+
const trimmedCommand = body.trim();
|
|
196
|
+
if (trimmedCommand !== '') {
|
|
197
|
+
const entry = { ...resolveFailingDirection(trimmedCommand, undefined), plan: '', task };
|
|
198
|
+
out.push(entry);
|
|
199
|
+
pending = entry;
|
|
200
|
+
bound = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// kind === 'fails_when'
|
|
205
|
+
if (pending === null) {
|
|
206
|
+
const entry = { ...resolveFailingDirection('', body), plan: '', task };
|
|
207
|
+
out.push(entry);
|
|
208
|
+
}
|
|
209
|
+
else if (!bound) {
|
|
210
|
+
const resolved = resolveFailingDirection(pending.command, body);
|
|
211
|
+
pending.statement = resolved.statement;
|
|
212
|
+
pending.status = resolved.status;
|
|
213
|
+
pending.severity = resolved.severity;
|
|
214
|
+
bound = true;
|
|
215
|
+
}
|
|
216
|
+
// already bound → redundant trailing statement, ignored (first-wins).
|
|
217
|
+
}
|
|
218
|
+
if (match.index === VERIFY_TOKEN_RE.lastIndex)
|
|
219
|
+
VERIFY_TOKEN_RE.lastIndex += 1;
|
|
220
|
+
guard.n += 1;
|
|
221
|
+
if (guard.n > MAX_BLOCK_WALK)
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Extract every `<automated>`/`<fails_when>` pairing verdict from PLAN.md
|
|
228
|
+
* text. Never throws; a non-string or blank plan yields `[]`. `plan` is left
|
|
229
|
+
* `''`, mirroring `extractAutomatedCommands` — callers set it from the
|
|
230
|
+
* filename they read.
|
|
231
|
+
*
|
|
232
|
+
* Walks the SAME text units, in the SAME order, as `extractAutomatedCommands`
|
|
233
|
+
* (every `<task>` body first in document order, then the task-stripped
|
|
234
|
+
* remainder as a final unit with task `''`) so pairing never crosses a task
|
|
235
|
+
* boundary, and shares ONE `{n:0}` guard across every unit so
|
|
236
|
+
* `MAX_BLOCK_WALK` bounds the whole document.
|
|
237
|
+
*/
|
|
238
|
+
function extractFailingDirections(planText) {
|
|
239
|
+
if (typeof planText !== 'string' || planText.length === 0)
|
|
240
|
+
return [];
|
|
241
|
+
const out = [];
|
|
242
|
+
const guard = { n: 0 };
|
|
243
|
+
const taskBodies = (0, markdown_sectionizer_cjs_1.extractTaggedBlocks)(planText, 'task', true);
|
|
244
|
+
for (const body of taskBodies) {
|
|
245
|
+
const nameMatch = TASK_NAME_RE.exec(body);
|
|
246
|
+
const task = nameMatch ? nameMatch[1].trim() : '';
|
|
247
|
+
if (!extractFailingFromText(body, task, out, guard))
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
const remainder = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(planText, 'task', true);
|
|
251
|
+
extractFailingFromText(remainder, '', out, guard);
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Probe every `<automated>` command's stated failing direction across a
|
|
256
|
+
* phase directory's `-PLAN.md` files. Never throws — a read failure degrades
|
|
257
|
+
* to `readError` with the offending file skipped, and an empty result with a
|
|
258
|
+
* populated `readError` means "could not look", which must never render as
|
|
259
|
+
* "nothing to report".
|
|
260
|
+
*/
|
|
261
|
+
function probePhaseFailingDirections(options) {
|
|
262
|
+
const { phaseDir } = options;
|
|
263
|
+
let entries;
|
|
264
|
+
try {
|
|
265
|
+
entries = node_fs_1.default.readdirSync(phaseDir);
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
return {
|
|
269
|
+
status: 'unresolvable',
|
|
270
|
+
commands: [],
|
|
271
|
+
counts: { blocker: 0, warning: 0, total: 0 },
|
|
272
|
+
readError: toMessage(err),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const planFiles = entries.filter(f => PLAN_FILE_RE.test(f)).sort();
|
|
276
|
+
const commands = [];
|
|
277
|
+
const readErrors = [];
|
|
278
|
+
for (const file of planFiles) {
|
|
279
|
+
let text;
|
|
280
|
+
try {
|
|
281
|
+
text = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, file), 'utf-8');
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
readErrors.push(toMessage(err));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
for (const entry of extractFailingDirections(text)) {
|
|
288
|
+
commands.push({ ...entry, plan: file });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const counts = { blocker: 0, warning: 0, total: commands.length };
|
|
292
|
+
for (const c of commands) {
|
|
293
|
+
if (c.severity === 'blocker')
|
|
294
|
+
counts.blocker += 1;
|
|
295
|
+
else if (c.severity === 'warning')
|
|
296
|
+
counts.warning += 1;
|
|
297
|
+
}
|
|
298
|
+
const readError = readErrors.length > 0 ? readErrors.join('; ') : null;
|
|
299
|
+
// A populated readError can never yield 'ok' — "could not look" must never
|
|
300
|
+
// render as "nothing to report" (#3172 review self-finding: with the old
|
|
301
|
+
// `commands.length === 0 && readError !== null` precedence, a phase where
|
|
302
|
+
// one plan read fine with zero blockers and a SECOND plan failed to read
|
|
303
|
+
// fell through to 'ok' because commands.length > 0). 'blocked' keeps
|
|
304
|
+
// precedence over 'unresolvable' because a proven blocker is the stronger
|
|
305
|
+
// signal.
|
|
306
|
+
let status;
|
|
307
|
+
if (counts.blocker > 0) {
|
|
308
|
+
status = 'blocked';
|
|
309
|
+
}
|
|
310
|
+
else if (readError !== null) {
|
|
311
|
+
status = 'unresolvable';
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
status = 'ok';
|
|
315
|
+
}
|
|
316
|
+
return { status, commands, counts, readError };
|
|
317
|
+
}
|
|
318
|
+
// ─── Resolution ───────────────────────────────────────────────────────────────
|
|
319
|
+
/**
|
|
320
|
+
* `$`, backtick, `*`, `?`, or a newline — a path this recognizer refuses to
|
|
321
|
+
* guess at anywhere in the string. `$`/backtick are substitution, `*`/`?` are
|
|
322
|
+
* shell globs (and illegal in Windows path components regardless), and a
|
|
323
|
+
* newline is never a valid single path.
|
|
324
|
+
*/
|
|
325
|
+
const DYNAMIC_PATH_ANYWHERE_RE = /[$`*?\n]/;
|
|
326
|
+
/**
|
|
327
|
+
* A LEADING `~` is shell home-expansion (`~/web`, `~user/web`) and is refused
|
|
328
|
+
* as dynamic; the dynamic check runs BEFORE quote stripping (so `cd
|
|
329
|
+
* "$FRONTEND"` is still caught), so this tolerates one leading quote char
|
|
330
|
+
* before the `~`. A `~` anywhere else in a path is an ordinary literal
|
|
331
|
+
* character — e.g. Windows 8.3 short names like `RUNNER~1` — and must resolve
|
|
332
|
+
* normally rather than being refused (#2401 CI regression).
|
|
333
|
+
*/
|
|
334
|
+
const DYNAMIC_PATH_LEADING_TILDE_RE = /^["']?~/;
|
|
335
|
+
const CD_SEGMENT_RE = /^cd\s+(.+)$/;
|
|
336
|
+
/**
|
|
337
|
+
* Quote-aware `--prefix` value capture (#2401 review Finding 2): a bare
|
|
338
|
+
* `\S+` capture truncates a quoted path containing a space (`--prefix "my
|
|
339
|
+
* dir"` → `"my`). Alternation order is double-quoted, single-quoted,
|
|
340
|
+
* unquoted — the surrounding quote pair (if any) is removed downstream by
|
|
341
|
+
* the existing `stripQuotes`.
|
|
342
|
+
*/
|
|
343
|
+
const PREFIX_FLAG_RE = /(?:^|\s)--prefix(?:=|\s+)("[^"]*"|'[^']*'|\S+)/;
|
|
344
|
+
/** Same value grammar as `PREFIX_FLAG_RE`, `g`-flagged for stripping (Finding 1). */
|
|
345
|
+
const PREFIX_FLAG_STRIP_RE = /(?:^|\s)--prefix(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)/g;
|
|
346
|
+
const NEEDS_NPM_RE = /^(npm|npx|pnpm|yarn|bun)\b/;
|
|
347
|
+
const NEEDS_MAKE_RE = /^make\b/;
|
|
348
|
+
const NPM_RUN_SCRIPT_RE = /\bnpm\s+run\s+([\w:@./-]+)/;
|
|
349
|
+
/**
|
|
350
|
+
* The `\s|$` anchor (not `\b`) is load-bearing: `\b` also matches an env-var
|
|
351
|
+
* assignment prefix such as `MISSING=1 cmd` (`=` is a non-word char, so `\b`
|
|
352
|
+
* sits happily before it) — and that IS a real runnable command, not the
|
|
353
|
+
* Nyquist Wave-0 sentinel. #3172 wires this constant into a BLOCKING gate
|
|
354
|
+
* (`resolveFailingDirection`), so a benign #2401 false-positive on the `\b`
|
|
355
|
+
* form becomes a real gate bypass: `MISSING=true npm test` would be reported
|
|
356
|
+
* `status:'sentinel', severity:'none'` instead of `missing`/`blocker`
|
|
357
|
+
* (#3172 review).
|
|
358
|
+
*/
|
|
359
|
+
const MISSING_SENTINEL_RE = /^MISSING(?:\s|$)/;
|
|
360
|
+
const MAX_MANIFEST_BYTES = 512 * 1024;
|
|
361
|
+
function emptyResult(base) {
|
|
362
|
+
return {
|
|
363
|
+
command: '',
|
|
364
|
+
status: 'not_applicable',
|
|
365
|
+
severity: 'none',
|
|
366
|
+
reason: null,
|
|
367
|
+
form: null,
|
|
368
|
+
rawTarget: null,
|
|
369
|
+
target: null,
|
|
370
|
+
manifest: null,
|
|
371
|
+
script: null,
|
|
372
|
+
sentinel: false,
|
|
373
|
+
base,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/** Split on `&&`, `||`, `;`, and newlines; trim each segment; drop empties. No quote-awareness. */
|
|
377
|
+
function splitSegments(cmd) {
|
|
378
|
+
return cmd
|
|
379
|
+
.split(/&&|\|\||;|\n/)
|
|
380
|
+
.map(s => s.trim())
|
|
381
|
+
.filter(s => s.length > 0);
|
|
382
|
+
}
|
|
383
|
+
/** Strip a single matching pair of surrounding quotes, if present. */
|
|
384
|
+
function stripQuotes(s) {
|
|
385
|
+
if (s.length >= 2) {
|
|
386
|
+
const first = s[0];
|
|
387
|
+
const last = s[s.length - 1];
|
|
388
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
389
|
+
return s.slice(1, -1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return s;
|
|
393
|
+
}
|
|
394
|
+
/** Backslash → forward-slash, applied unconditionally (backslash paths arrive on Linux too). */
|
|
395
|
+
function toSlash(s) {
|
|
396
|
+
return s.replace(/\\/g, '/');
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* `NPM_RUN_SCRIPT_RE` requires `npm` and `run` adjacent, so `npm --prefix
|
|
400
|
+
* ./web run lint` (the form this feature's own docs prefer) never matches
|
|
401
|
+
* (#2401 review Finding 1). Strip the `--prefix <value>` / `--prefix=<value>`
|
|
402
|
+
* flag (either ordering, quote-aware) before running the script-name match.
|
|
403
|
+
*/
|
|
404
|
+
function stripPrefixFlag(s) {
|
|
405
|
+
return s.replace(PREFIX_FLAG_STRIP_RE, ' ');
|
|
406
|
+
}
|
|
407
|
+
/** Is `raw` (after the same quote-stripping/slash-normalization used elsewhere) an absolute path? */
|
|
408
|
+
function isAbsoluteCdSegment(raw) {
|
|
409
|
+
return node_path_1.default.isAbsolute(toSlash(stripQuotes(raw)));
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Fold chained `cd` segments left-to-right with absolute-reset semantics
|
|
413
|
+
* (#2401 review Finding 3): a relative segment appends onto the
|
|
414
|
+
* accumulator; an absolute segment discards everything accumulated so far
|
|
415
|
+
* and becomes the new accumulator (matching real shell `cd` semantics —
|
|
416
|
+
* `cd sub && cd /abs/path` ends up at `/abs/path`, not `sub//abs/path`).
|
|
417
|
+
* A single segment (the overwhelmingly common case) always returns that
|
|
418
|
+
* segment verbatim, byte-identical to the pre-fix `cdArgs[0]` behavior.
|
|
419
|
+
*/
|
|
420
|
+
function foldCdArgs(args) {
|
|
421
|
+
let acc = '';
|
|
422
|
+
for (const raw of args) {
|
|
423
|
+
if (acc === '' || isAbsoluteCdSegment(raw)) {
|
|
424
|
+
acc = raw;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
acc = `${acc}/${raw}`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return acc;
|
|
431
|
+
}
|
|
432
|
+
function stripLeadingDotSlash(s) {
|
|
433
|
+
return s.replace(/^\.\//, '');
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* A relative escape whose every segment is `..` (a bare ancestor climb, e.g.
|
|
437
|
+
* `cd ../..`) is inherently ambiguous across worktrees — flagged `outside_root`
|
|
438
|
+
* without touching the filesystem. A climb that names a concrete sibling (e.g.
|
|
439
|
+
* `cd ../../frontend`, the exact #2401 shape) still names something checkable
|
|
440
|
+
* and falls through to the normal filesystem probe below.
|
|
441
|
+
*/
|
|
442
|
+
function isPureAncestorClimb(rel) {
|
|
443
|
+
if (rel.length === 0)
|
|
444
|
+
return false;
|
|
445
|
+
const segs = rel.split(/[\\/]/).filter(s => s.length > 0);
|
|
446
|
+
return segs.length > 0 && segs.every(s => s === '..');
|
|
447
|
+
}
|
|
448
|
+
function declaredPathCovers(declaredPaths, norm) {
|
|
449
|
+
if (!Array.isArray(declaredPaths))
|
|
450
|
+
return false;
|
|
451
|
+
const target = stripLeadingDotSlash(norm);
|
|
452
|
+
return declaredPaths.some(p => {
|
|
453
|
+
if (typeof p !== 'string')
|
|
454
|
+
return false;
|
|
455
|
+
const dp = stripLeadingDotSlash(toSlash(p));
|
|
456
|
+
return dp === target || dp.startsWith(target + '/');
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Read a package.json manifest bounded by size and guarded end-to-end; never
|
|
461
|
+
* throws. Returns `null` when the manifest cannot be read/parsed/shaped, or
|
|
462
|
+
* the parsed value when it is usable.
|
|
463
|
+
*/
|
|
464
|
+
function readManifestObject(manifestPath) {
|
|
465
|
+
let size = 0;
|
|
466
|
+
try {
|
|
467
|
+
size = node_fs_1.default.statSync(manifestPath).size;
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
if (size > MAX_MANIFEST_BYTES)
|
|
473
|
+
return null;
|
|
474
|
+
let raw;
|
|
475
|
+
try {
|
|
476
|
+
raw = node_fs_1.default.readFileSync(manifestPath, 'utf-8');
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
let parsed;
|
|
482
|
+
try {
|
|
483
|
+
parsed = JSON.parse(raw);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
489
|
+
return null;
|
|
490
|
+
return parsed;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Resolve a single `<automated>` command's target directory WITHOUT ever
|
|
494
|
+
* executing it. Every `fs` call is individually guarded, so this function
|
|
495
|
+
* never throws for any input — including a bare call with no options.
|
|
496
|
+
*/
|
|
497
|
+
function resolveVerifyCommandTarget(command, options) {
|
|
498
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
499
|
+
const base = typeof opts.projectRoot === 'string' && opts.projectRoot !== '' ? opts.projectRoot : process.cwd();
|
|
500
|
+
const result = emptyResult(base);
|
|
501
|
+
if (typeof command !== 'string')
|
|
502
|
+
return result;
|
|
503
|
+
result.command = command;
|
|
504
|
+
const trimmed = command.trim();
|
|
505
|
+
if (trimmed === '')
|
|
506
|
+
return result;
|
|
507
|
+
// Nyquist "MISSING — Wave 0 must create …" sentinel; Dimension 8 owns it.
|
|
508
|
+
if (MISSING_SENTINEL_RE.test(trimmed)) {
|
|
509
|
+
result.sentinel = true;
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
const segments = splitSegments(trimmed);
|
|
513
|
+
let form = null;
|
|
514
|
+
let rawTarget = null;
|
|
515
|
+
let rest;
|
|
516
|
+
const cdArgs = [];
|
|
517
|
+
let i = 0;
|
|
518
|
+
while (i < segments.length) {
|
|
519
|
+
const m = CD_SEGMENT_RE.exec(segments[i]);
|
|
520
|
+
if (!m)
|
|
521
|
+
break;
|
|
522
|
+
cdArgs.push(m[1].trim());
|
|
523
|
+
i += 1;
|
|
524
|
+
}
|
|
525
|
+
if (cdArgs.length > 0) {
|
|
526
|
+
form = 'cd';
|
|
527
|
+
rawTarget = foldCdArgs(cdArgs);
|
|
528
|
+
rest = segments.slice(i).join(' && ');
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const prefixMatch = PREFIX_FLAG_RE.exec(trimmed);
|
|
532
|
+
if (!prefixMatch)
|
|
533
|
+
return result; // not_applicable — neither form matched
|
|
534
|
+
form = 'prefix';
|
|
535
|
+
rawTarget = prefixMatch[1];
|
|
536
|
+
rest = trimmed;
|
|
537
|
+
}
|
|
538
|
+
// Dynamic-path refusal, tested against rawTarget BEFORE any unquoting.
|
|
539
|
+
if (DYNAMIC_PATH_ANYWHERE_RE.test(rawTarget) || DYNAMIC_PATH_LEADING_TILDE_RE.test(rawTarget)) {
|
|
540
|
+
result.status = 'unresolvable';
|
|
541
|
+
result.reason = 'dynamic_path';
|
|
542
|
+
result.severity = 'warning';
|
|
543
|
+
result.form = form;
|
|
544
|
+
result.rawTarget = rawTarget;
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
const norm = toSlash(stripQuotes(rawTarget));
|
|
548
|
+
const isAbs = node_path_1.default.isAbsolute(norm);
|
|
549
|
+
const target = isAbs ? node_path_1.default.normalize(norm) : node_path_1.default.resolve(base, norm);
|
|
550
|
+
result.form = form;
|
|
551
|
+
result.rawTarget = rawTarget;
|
|
552
|
+
result.target = target;
|
|
553
|
+
if (!isAbs) {
|
|
554
|
+
const rel = node_path_1.default.relative(base, target);
|
|
555
|
+
if (isPureAncestorClimb(rel)) {
|
|
556
|
+
result.status = 'ok';
|
|
557
|
+
result.severity = 'warning';
|
|
558
|
+
result.reason = 'outside_root';
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
let stat;
|
|
563
|
+
try {
|
|
564
|
+
stat = node_fs_1.default.statSync(target, { throwIfNoEntry: false });
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
stat = undefined;
|
|
568
|
+
}
|
|
569
|
+
if (!stat || !stat.isDirectory()) {
|
|
570
|
+
if (declaredPathCovers(opts.declaredPaths, norm)) {
|
|
571
|
+
result.status = 'pending_creation';
|
|
572
|
+
result.severity = 'none';
|
|
573
|
+
result.reason = null;
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
result.status = 'broken';
|
|
577
|
+
result.reason = 'missing_dir';
|
|
578
|
+
result.severity = 'blocker';
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
const restTrimmed = rest.trim();
|
|
583
|
+
let neededManifest = null;
|
|
584
|
+
if (NEEDS_NPM_RE.test(restTrimmed))
|
|
585
|
+
neededManifest = 'package.json';
|
|
586
|
+
else if (NEEDS_MAKE_RE.test(restTrimmed))
|
|
587
|
+
neededManifest = 'Makefile';
|
|
588
|
+
if (!neededManifest) {
|
|
589
|
+
result.status = 'ok';
|
|
590
|
+
result.severity = 'none';
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
const manifestPath = node_path_1.default.join(target, neededManifest);
|
|
594
|
+
let manifestExists = false;
|
|
595
|
+
try {
|
|
596
|
+
manifestExists = node_fs_1.default.existsSync(manifestPath);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
manifestExists = false;
|
|
600
|
+
}
|
|
601
|
+
if (!manifestExists) {
|
|
602
|
+
result.status = 'broken';
|
|
603
|
+
result.reason = 'no_manifest';
|
|
604
|
+
result.severity = 'blocker';
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
result.manifest = neededManifest;
|
|
608
|
+
if (neededManifest === 'Makefile') {
|
|
609
|
+
result.status = 'ok';
|
|
610
|
+
result.severity = 'none';
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
const parsed = readManifestObject(manifestPath);
|
|
614
|
+
if (!parsed) {
|
|
615
|
+
result.status = 'ok';
|
|
616
|
+
result.reason = 'manifest_unreadable';
|
|
617
|
+
result.severity = 'warning';
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
const scriptMatch = NPM_RUN_SCRIPT_RE.exec(stripPrefixFlag(restTrimmed));
|
|
621
|
+
if (scriptMatch) {
|
|
622
|
+
const scriptName = scriptMatch[1];
|
|
623
|
+
result.script = scriptName;
|
|
624
|
+
const scripts = parsed['scripts'];
|
|
625
|
+
const hasScript = scripts !== null &&
|
|
626
|
+
typeof scripts === 'object' &&
|
|
627
|
+
!Array.isArray(scripts) &&
|
|
628
|
+
Object.prototype.hasOwnProperty.call(scripts, scriptName);
|
|
629
|
+
if (!hasScript) {
|
|
630
|
+
result.status = 'ok';
|
|
631
|
+
result.reason = 'script_missing';
|
|
632
|
+
result.severity = 'warning';
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
result.status = 'ok';
|
|
637
|
+
result.severity = 'none';
|
|
638
|
+
return result;
|
|
639
|
+
}
|
|
640
|
+
// ─── Phase probing ────────────────────────────────────────────────────────────
|
|
641
|
+
const PLAN_FILE_RE = /-PLAN\.md$/i;
|
|
642
|
+
const ARTIFACTS_HEADING_RE = /^##[ \t]+Artifacts this phase produces\s*$/m;
|
|
643
|
+
const NEXT_HEADING_RE = /^##[ \t]+/m;
|
|
644
|
+
const FILES_BLOCK_RE = /<files>([\s\S]*?)<\/files>/g;
|
|
645
|
+
const ARTIFACT_BULLET_RE = /^-[ \t]+(.+)$/gm;
|
|
646
|
+
function toMessage(err) {
|
|
647
|
+
return err instanceof Error ? err.message : String(err);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Every `<files>…</files>` body (split on commas/whitespace/newlines) plus
|
|
651
|
+
* every `- ` bullet under a `## Artifacts this phase produces` heading (up to
|
|
652
|
+
* the next `## ` heading), scanned across the WHOLE plan text.
|
|
653
|
+
*/
|
|
654
|
+
function extractDeclaredPaths(text) {
|
|
655
|
+
const out = [];
|
|
656
|
+
FILES_BLOCK_RE.lastIndex = 0;
|
|
657
|
+
let fMatch;
|
|
658
|
+
while ((fMatch = FILES_BLOCK_RE.exec(text)) !== null) {
|
|
659
|
+
const body = fMatch[1] ?? '';
|
|
660
|
+
for (const tok of body.split(/[,\s]+/)) {
|
|
661
|
+
const t = tok.trim();
|
|
662
|
+
if (t)
|
|
663
|
+
out.push(t);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const headingMatch = ARTIFACTS_HEADING_RE.exec(text);
|
|
667
|
+
if (headingMatch) {
|
|
668
|
+
const after = text.slice(headingMatch.index + headingMatch[0].length);
|
|
669
|
+
const nextIdx = after.search(NEXT_HEADING_RE);
|
|
670
|
+
const section = nextIdx === -1 ? after : after.slice(0, nextIdx);
|
|
671
|
+
ARTIFACT_BULLET_RE.lastIndex = 0;
|
|
672
|
+
let bMatch;
|
|
673
|
+
while ((bMatch = ARTIFACT_BULLET_RE.exec(section)) !== null) {
|
|
674
|
+
const t = (bMatch[1] ?? '').trim();
|
|
675
|
+
if (t)
|
|
676
|
+
out.push(t);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
const STATUS_PRIORITY = ['broken', 'unresolvable', 'pending_creation', 'ok', 'not_applicable'];
|
|
682
|
+
/**
|
|
683
|
+
* Probe every `<automated>` command in a phase directory's `-PLAN.md` files
|
|
684
|
+
* against the filesystem. Never throws — a read failure degrades to
|
|
685
|
+
* `readError` with the offending file skipped.
|
|
686
|
+
*/
|
|
687
|
+
function probePhaseVerifyCommands(options) {
|
|
688
|
+
const { phaseDir, projectRoot } = options;
|
|
689
|
+
let entries;
|
|
690
|
+
try {
|
|
691
|
+
entries = node_fs_1.default.readdirSync(phaseDir);
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
return {
|
|
695
|
+
status: 'unresolvable',
|
|
696
|
+
commands: [],
|
|
697
|
+
counts: { blocker: 0, warning: 0, total: 0 },
|
|
698
|
+
readError: toMessage(err),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const planFiles = entries.filter(f => PLAN_FILE_RE.test(f)).sort();
|
|
702
|
+
const commands = [];
|
|
703
|
+
const readErrors = [];
|
|
704
|
+
for (const file of planFiles) {
|
|
705
|
+
let text;
|
|
706
|
+
try {
|
|
707
|
+
text = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, file), 'utf-8');
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
readErrors.push(toMessage(err));
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const declaredPaths = extractDeclaredPaths(text);
|
|
714
|
+
const extracted = extractAutomatedCommands(text);
|
|
715
|
+
for (const cmd of extracted) {
|
|
716
|
+
const resolved = resolveVerifyCommandTarget(cmd.command, { projectRoot, declaredPaths });
|
|
717
|
+
commands.push({ ...resolved, plan: file, task: cmd.task });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const counts = { blocker: 0, warning: 0, total: commands.length };
|
|
721
|
+
for (const c of commands) {
|
|
722
|
+
if (c.severity === 'blocker')
|
|
723
|
+
counts.blocker += 1;
|
|
724
|
+
else if (c.severity === 'warning')
|
|
725
|
+
counts.warning += 1;
|
|
726
|
+
}
|
|
727
|
+
let status = 'ok';
|
|
728
|
+
if (commands.length > 0) {
|
|
729
|
+
const present = new Set(commands.map(c => c.status));
|
|
730
|
+
status = STATUS_PRIORITY.find(s => present.has(s)) ?? 'ok';
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
status,
|
|
734
|
+
commands,
|
|
735
|
+
counts,
|
|
736
|
+
readError: readErrors.length > 0 ? readErrors.join('; ') : null,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
// ─── Prior-phase harvesting ───────────────────────────────────────────────────
|
|
740
|
+
const DEFAULT_LIMIT = 20;
|
|
741
|
+
const DEFAULT_LOOKBACK = 3;
|
|
742
|
+
/**
|
|
743
|
+
* Walk backward from `beforePhase` (descending, at most `lookback` phase
|
|
744
|
+
* directories) looking for the nearest prior phase whose plans carry any
|
|
745
|
+
* `<automated>` command. Returns that phase's commands, deduped by command
|
|
746
|
+
* text (first-seen order) and capped at `limit`. Never throws.
|
|
747
|
+
*
|
|
748
|
+
* Phase directories are enumerated and ordered via the canonical grammar in
|
|
749
|
+
* `phase-id.cjs` (#2401 review fix) rather than a bespoke `phase-N-slug`
|
|
750
|
+
* regex — real GSD phase directories are `01-foundation`, `3-thing`,
|
|
751
|
+
* `2.1-thing`, `12A-thing`, or project-code-prefixed (`CK-01-name`), never
|
|
752
|
+
* `phase-N-slug`. A directory name is treated as a phase directory only when
|
|
753
|
+
* it (after stripping an optional project-code prefix) starts with a digit;
|
|
754
|
+
* `notes`, `archive`, etc. are ignored. `extractPhaseToken` reads each
|
|
755
|
+
* directory's phase token and `comparePhaseNum` both filters (`< beforePhase`)
|
|
756
|
+
* and orders (descending) so decimal/lettered/sentinel tokens (`2.1`, `12A`,
|
|
757
|
+
* `999.1`) compare correctly instead of via lossy `Number()` parsing.
|
|
758
|
+
*/
|
|
759
|
+
function harvestPriorVerifyCommands(options) {
|
|
760
|
+
const { planningDir, beforePhase, limit = DEFAULT_LIMIT, lookback = DEFAULT_LOOKBACK } = options;
|
|
761
|
+
let entries;
|
|
762
|
+
try {
|
|
763
|
+
entries = node_fs_1.default.readdirSync(planningDir, { withFileTypes: true });
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
return { commands: [], readError: toMessage(err) };
|
|
767
|
+
}
|
|
768
|
+
const beforePhaseStr = String(beforePhase);
|
|
769
|
+
const candidates = [];
|
|
770
|
+
for (const ent of entries) {
|
|
771
|
+
let isDir = false;
|
|
772
|
+
try {
|
|
773
|
+
isDir = ent.isDirectory();
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
isDir = false;
|
|
777
|
+
}
|
|
778
|
+
if (!isDir)
|
|
779
|
+
continue;
|
|
780
|
+
// Not a phase directory at all (e.g. 'notes', 'archive') — no reliable
|
|
781
|
+
// phase token can be read from a name that doesn't start with a digit
|
|
782
|
+
// once any project-code prefix ('CK-', 'PROJ-') is stripped.
|
|
783
|
+
if (!/^\d/.test(stripProjectCodePrefix(ent.name)))
|
|
784
|
+
continue;
|
|
785
|
+
const token = extractPhaseToken(ent.name);
|
|
786
|
+
if (comparePhaseNum(token, beforePhaseStr) < 0) {
|
|
787
|
+
candidates.push({ token, dir: ent.name });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
candidates.sort((a, b) => comparePhaseNum(b.token, a.token));
|
|
791
|
+
const readErrors = [];
|
|
792
|
+
let examined = 0;
|
|
793
|
+
for (const candidate of candidates) {
|
|
794
|
+
if (examined >= lookback)
|
|
795
|
+
break;
|
|
796
|
+
examined += 1;
|
|
797
|
+
const phaseDirPath = node_path_1.default.join(planningDir, candidate.dir);
|
|
798
|
+
let planFiles;
|
|
799
|
+
try {
|
|
800
|
+
planFiles = node_fs_1.default.readdirSync(phaseDirPath).filter(f => PLAN_FILE_RE.test(f)).sort();
|
|
801
|
+
}
|
|
802
|
+
catch (err) {
|
|
803
|
+
readErrors.push(toMessage(err));
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const seen = new Set();
|
|
807
|
+
const found = [];
|
|
808
|
+
for (const file of planFiles) {
|
|
809
|
+
let text;
|
|
810
|
+
try {
|
|
811
|
+
text = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDirPath, file), 'utf-8');
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
readErrors.push(toMessage(err));
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
for (const cmd of extractAutomatedCommands(text)) {
|
|
818
|
+
if (seen.has(cmd.command))
|
|
819
|
+
continue;
|
|
820
|
+
seen.add(cmd.command);
|
|
821
|
+
found.push({
|
|
822
|
+
phase: candidate.token,
|
|
823
|
+
plan: node_path_1.default.join(phaseDirPath, file),
|
|
824
|
+
task: cmd.task,
|
|
825
|
+
command: cmd.command,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (found.length > 0) {
|
|
830
|
+
return {
|
|
831
|
+
commands: found.slice(0, limit),
|
|
832
|
+
readError: readErrors.length > 0 ? readErrors.join('; ') : null,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return { commands: [], readError: readErrors.length > 0 ? readErrors.join('; ') : null };
|
|
837
|
+
}
|
|
838
|
+
module.exports = {
|
|
839
|
+
extractAutomatedCommands,
|
|
840
|
+
resolveVerifyCommandTarget,
|
|
841
|
+
probePhaseVerifyCommands,
|
|
842
|
+
harvestPriorVerifyCommands,
|
|
843
|
+
resolveFailingDirection,
|
|
844
|
+
extractFailingDirections,
|
|
845
|
+
probePhaseFailingDirections,
|
|
846
|
+
};
|