@opengsd/gsd-core 1.10.0 → 1.12.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/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +12 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +22 -14
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +5 -3
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +23 -2
- package/agents/gsd-plan-checker.md +50 -53
- package/agents/gsd-planner.md +50 -50
- 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 +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +13 -4
- package/bin/install.js +1448 -1103
- package/commands/gsd/code-review.md +1 -1
- 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/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +9 -5
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1035 -138
- package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
- package/gsd-core/bin/lib/api-coverage.cjs +33 -14
- package/gsd-core/bin/lib/artifacts.cjs +5 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +1026 -268
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +845 -130
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +700 -40
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +216 -42
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +1172 -108
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
- package/gsd-core/bin/lib/config-loader.cjs +187 -23
- package/gsd-core/bin/lib/config.cjs +102 -3
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +208 -33
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- 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/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +899 -229
- package/gsd-core/bin/lib/gap-checker.cjs +95 -10
- package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +439 -168
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +811 -259
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
- package/gsd-core/bin/lib/install-profiles.cjs +212 -61
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +170 -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/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +183 -22
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +842 -73
- package/gsd-core/bin/lib/model-catalog.cjs +232 -16
- package/gsd-core/bin/lib/model-resolver.cjs +193 -68
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
- package/gsd-core/bin/lib/phase-id.cjs +514 -40
- package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
- package/gsd-core/bin/lib/phase-locator.cjs +262 -34
- package/gsd-core/bin/lib/phase.cjs +1038 -214
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +98 -3
- 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-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
- package/gsd-core/bin/lib/probe-core.cjs +5 -2
- package/gsd-core/bin/lib/profile-output.cjs +1 -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/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
- package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +442 -96
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
- package/gsd-core/bin/lib/smart-entry.cjs +133 -23
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +52 -19
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +338 -8
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +846 -176
- package/gsd-core/bin/lib/state.cjs +2589 -369
- package/gsd-core/bin/lib/surface.cjs +33 -11
- 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/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
- package/gsd-core/bin/lib/uat.cjs +1761 -167
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
- package/gsd-core/bin/lib/unusable-input.cjs +37 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +75 -0
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +272 -9
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +453 -918
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
- 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/references/agent-contracts.md +44 -26
- 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 +39 -21
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +4 -6
- package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +17 -11
- package/gsd-core/references/failing-direction.md +78 -0
- 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 +5 -4
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +61 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- 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/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +44 -13
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- 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 +5 -5
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -17
- 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/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +5 -4
- package/gsd-core/templates/state.md +11 -4
- package/gsd-core/templates/verification-report.md +9 -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 +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +13 -20
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +11 -27
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +64 -5
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
- package/gsd-core/workflows/code-review-fix.md +38 -11
- package/gsd-core/workflows/code-review.md +159 -52
- package/gsd-core/workflows/complete-milestone.md +151 -23
- package/gsd-core/workflows/debug.md +12 -8
- package/gsd-core/workflows/diagnose-issues.md +47 -15
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +27 -2
- package/gsd-core/workflows/eval-review.md +7 -14
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
- 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 +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -4
- 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 +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +72 -100
- package/gsd-core/workflows/execute-plan.md +52 -15
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +76 -10
- package/gsd-core/workflows/import.md +18 -15
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +49 -16
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +8 -5
- package/gsd-core/workflows/new-milestone.md +22 -29
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +26 -40
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +14 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- 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 +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +162 -59
- package/gsd-core/workflows/plan-review-convergence.md +96 -11
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +61 -18
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
- package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
- package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +55 -44
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +219 -20
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +69 -7
- package/gsd-core/workflows/ship.md +116 -50
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +53 -13
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +64 -9
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +49 -14
- package/gsd-core/workflows/ui-phase.md +15 -21
- package/gsd-core/workflows/ui-review.md +8 -12
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +7 -11
- 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 +66 -25
- package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
- 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 +83 -3
- package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
- 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 +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +37 -27
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +55 -32
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +231 -24
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- 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 +162 -46
- 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 +92 -59
- 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 +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +158 -30
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +2 -3
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +83 -3
- package/hooks/gsd-cursor-subagent-stop.js +6 -3
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +37 -27
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +55 -32
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +231 -24
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +162 -46
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +28 -11
- package/pi/gsd.cjs +19 -5
- package/scripts/base64-scan.sh +74 -12
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/build-hooks.js +5 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -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 +64 -14
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +11 -21
- package/scripts/gen-context-index.cjs +2 -11
- 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-health-docs.cjs +390 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +138 -25
- package/scripts/gen-registry.cjs +3 -14
- 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/alias-drift-families.cjs +46 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +308 -0
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +492 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +471 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +488 -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-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +950 -0
- package/scripts/lint-test-file-count.allowlist.json +137 -8
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +830 -0
- package/scripts/lint-vendored-deps.cjs +297 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +100 -14
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +24 -7
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +9 -5
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -107
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -13,8 +13,6 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
14
|
const node_os_1 = __importDefault(require("node:os"));
|
|
15
15
|
const validate_cjs_1 = require("./validate.cjs");
|
|
16
|
-
const clock_cjs_1 = require("./clock.cjs");
|
|
17
|
-
const validate_cjs_2 = require("./validate.cjs");
|
|
18
16
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
19
17
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
20
18
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
@@ -25,40 +23,52 @@ const stateMod = require("./state.cjs");
|
|
|
25
23
|
const modelProfilesMod = require("./model-profiles.cjs");
|
|
26
24
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
27
25
|
const planScanMod = require("./plan-scan.cjs");
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
|
|
27
|
+
const coreUtilsMod = require("./core-utils.cjs");
|
|
28
|
+
const { findOrphanSummaries, findUnsummarizedPlans } = coreUtilsMod;
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
|
|
30
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
31
|
+
const { SCOPE } = planningScopeMod;
|
|
28
32
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
29
|
-
const
|
|
33
|
+
const security_cjs_1 = require("./security.cjs");
|
|
30
34
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
31
35
|
const schema_detect_cjs_1 = require("./schema-detect.cjs");
|
|
32
|
-
const artifacts_cjs_1 = require("./artifacts.cjs");
|
|
33
36
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
34
|
-
const
|
|
37
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
35
38
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
|
|
36
39
|
const agentInstallCheck = require("./agent-install-check.cjs");
|
|
37
|
-
const { checkAgentsInstalled } = agentInstallCheck;
|
|
40
|
+
const { checkAgentsInstalled, checkCodexModelPosture, checkCodexSandboxPosture } = agentInstallCheck;
|
|
38
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
42
|
const ioMod = require("./io.cjs");
|
|
40
43
|
const { output, error } = ioMod;
|
|
41
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
-
const configLoaderMod = require("./config-loader.cjs");
|
|
43
|
-
const { loadConfig, CONFIG_DEFAULTS } = configLoaderMod;
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
45
|
const phaseIdMod = require("./phase-id.cjs");
|
|
46
|
-
const { normalizePhaseName,
|
|
46
|
+
const { normalizePhaseName, matchPhaseDirs } = phaseIdMod;
|
|
47
47
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
48
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
49
49
|
const { findPhaseInternal } = phaseLocatorMod;
|
|
50
50
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
51
51
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
52
|
-
const {
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
54
|
-
const
|
|
55
|
-
const {
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports --
|
|
57
|
-
const
|
|
58
|
-
const {
|
|
59
|
-
const { planningDir
|
|
52
|
+
const { stripShippedMilestones } = roadmapParserMod;
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- health-diagnostic.cjs is an export= CommonJS module
|
|
54
|
+
const healthDiagnosticMod = require("./health-diagnostic.cjs");
|
|
55
|
+
const { SEVERITY: HEALTH_SEVERITY, REMEDY_ACTION, REMEDY_RISK, evaluateRules, evaluateConsistencyRules, applyRepairs } = healthDiagnosticMod;
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-snapshot.cjs is an export= CommonJS module
|
|
57
|
+
const planningSnapshotMod = require("./planning-snapshot.cjs");
|
|
58
|
+
const { buildPlanningSnapshot } = planningSnapshotMod;
|
|
59
|
+
const { planningDir } = planningWorkspace;
|
|
60
60
|
const { extractFrontmatter, parseMustHavesBlock } = frontmatterMod;
|
|
61
|
-
const {
|
|
61
|
+
const { readStateHeadFreshness } = stateMod;
|
|
62
|
+
/**
|
|
63
|
+
* W024 (#2573) threshold — how many commits STATE.md may lag HEAD before
|
|
64
|
+
* `validate.health` mentions it.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately coarse. `state_head` restamps on every state write, so a small
|
|
67
|
+
* count is normal for any active project; firing near zero would make health
|
|
68
|
+
* noisy for healthy projects without telling anyone anything. This is a
|
|
69
|
+
* freshness proxy, not a drift measurement — see readStateHeadFreshness.
|
|
70
|
+
*/
|
|
71
|
+
const STATE_HEAD_ADVISORY_COMMITS = 20;
|
|
62
72
|
const { MODEL_PROFILES } = modelProfilesMod;
|
|
63
73
|
// Unused but imported for structural parity
|
|
64
74
|
void stripShippedMilestones;
|
|
@@ -252,15 +262,78 @@ function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
|
|
252
262
|
* verify gate fails on the comment echo rather than a real regression. Conservative:
|
|
253
263
|
* errors only on a confidently-extracted QUOTED literal; ambiguous (bareword) → warning.
|
|
254
264
|
*/
|
|
265
|
+
/**
|
|
266
|
+
* Decode entity-escaped ampersands (& → &) — #3611. Planners emit
|
|
267
|
+
* <automated> bodies with `&&` as the chain operator (66 occurrences
|
|
268
|
+
* vs 0 literal in the reporting repo), and the executing agent reads the
|
|
269
|
+
* decoded (rendered) form. Every downstream scan — segment split,
|
|
270
|
+
* zero-comparison, literal harvest, echo matching — must operate on the same
|
|
271
|
+
* decoded text or a negative clause (`= 0`) poisons the literals of a
|
|
272
|
+
* POSITIVE clause (-ge 3) joined to it. Shared by both plan-discipline
|
|
273
|
+
* scanners so the two gates cannot drift apart again.
|
|
274
|
+
*/
|
|
275
|
+
function decodeEntityAmps(s) {
|
|
276
|
+
return s.replace(/&/g, '&');
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Split one shell line into &&/|| segments, QUOTE-AWARE (#3611 adversarial
|
|
280
|
+
* review): an operator inside a quoted literal (`grep -c 'a&&b'`) is part of
|
|
281
|
+
* the pattern, not a chain operator — a quote-blind split destroys the
|
|
282
|
+
* literal and silently disarms the gate for exactly the plans that spell
|
|
283
|
+
* patterns with ampersands. Backslash escapes count inside double quotes
|
|
284
|
+
* (POSIX single quotes have none, and over-staying a single-quoted span can
|
|
285
|
+
* only miss a split, never invent one).
|
|
286
|
+
*/
|
|
287
|
+
function splitShellSegments(line) {
|
|
288
|
+
const segments = [];
|
|
289
|
+
let current = '';
|
|
290
|
+
let quote = null;
|
|
291
|
+
for (let i = 0; i < line.length; i++) {
|
|
292
|
+
const ch = line[i];
|
|
293
|
+
if (quote === "'") {
|
|
294
|
+
if (ch === "'")
|
|
295
|
+
quote = null;
|
|
296
|
+
current += ch;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (quote === '"') {
|
|
300
|
+
if (ch === '\\') {
|
|
301
|
+
current += ch + (line[i + 1] ?? '');
|
|
302
|
+
i++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (ch === '"')
|
|
306
|
+
quote = null;
|
|
307
|
+
current += ch;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (ch === "'" || ch === '"') {
|
|
311
|
+
quote = ch;
|
|
312
|
+
current += ch;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if ((ch === '&' && line[i + 1] === '&') || (ch === '|' && line[i + 1] === '|')) {
|
|
316
|
+
segments.push(current.trim());
|
|
317
|
+
current = '';
|
|
318
|
+
i++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
current += ch;
|
|
322
|
+
}
|
|
323
|
+
segments.push(current.trim());
|
|
324
|
+
return segments.filter((s) => s !== '');
|
|
325
|
+
}
|
|
255
326
|
function scanNegativeGrepCommentEcho(content) {
|
|
256
327
|
const errors = [];
|
|
257
328
|
const warnings = [];
|
|
258
329
|
// Normalize newlines; join backslash line-continuations so a verify command wrapped
|
|
259
330
|
// across lines (grep ... \ <newline> == 0) is still seen as one segment.
|
|
260
|
-
|
|
331
|
+
// #3611: decode entity-escaped ampersands (see decodeEntityAmps) so every
|
|
332
|
+
// downstream scan reads the same decoded text the executing agent reads.
|
|
333
|
+
const text = decodeEntityAmps((content || '')
|
|
261
334
|
.replace(/\r\n/g, '\n')
|
|
262
335
|
.replace(/\r/g, '\n')
|
|
263
|
-
.replace(/\\\n/g, ' ');
|
|
336
|
+
.replace(/\\\n/g, ' '));
|
|
264
337
|
// 1. Allowlisted literals: <!-- planner-discipline-allow: LIT -->
|
|
265
338
|
const allow = new Set();
|
|
266
339
|
const allowRe = /<!--\s*planner-discipline-allow:\s*(.+?)\s*-->/g;
|
|
@@ -303,7 +376,7 @@ function scanNegativeGrepCommentEcho(content) {
|
|
|
303
376
|
// poisoning a negative gate (`== 0`) sharing the same physical line.
|
|
304
377
|
const seenErr = new Set();
|
|
305
378
|
const seenWarn = new Set();
|
|
306
|
-
const segments = text.split('\n').flatMap(
|
|
379
|
+
const segments = text.split('\n').flatMap(splitShellSegments);
|
|
307
380
|
for (const seg of segments) {
|
|
308
381
|
if (!/grep(?:\s+-{1,2}[A-Za-z])/.test(seg) || !zeroCmp(seg))
|
|
309
382
|
continue;
|
|
@@ -352,10 +425,13 @@ function scanNegativeGrepCommentEcho(content) {
|
|
|
352
425
|
function scanFileWideNegativeGateConflict(content) {
|
|
353
426
|
const warnings = [];
|
|
354
427
|
// Normalize newlines; join backslash line-continuations (same as #429).
|
|
355
|
-
|
|
428
|
+
// #3611: the SAME entity decode as the #429 scanner — the two gates share
|
|
429
|
+
// the caller and the input; a decode on one side only let an entity-escaped
|
|
430
|
+
// chain poison this detector's harvest exactly the same way.
|
|
431
|
+
const text = decodeEntityAmps((content || '')
|
|
356
432
|
.replace(/\r\n/g, '\n')
|
|
357
433
|
.replace(/\r/g, '\n')
|
|
358
|
-
.replace(/\\\n/g, ' ');
|
|
434
|
+
.replace(/\\\n/g, ' '));
|
|
359
435
|
// Allowlisted patterns: <!-- planner-region-allow: PAT -->
|
|
360
436
|
const allow = new Set();
|
|
361
437
|
const allowRe = /<!--\s*planner-region-allow:\s*(.+?)\s*-->/g;
|
|
@@ -489,8 +565,8 @@ function scanFileWideNegativeGateConflict(content) {
|
|
|
489
565
|
// For each task A, scan gate text for file-wide negative grep bans.
|
|
490
566
|
for (let ai = 0; ai < tasks.length; ai++) {
|
|
491
567
|
const taskA = tasks[ai];
|
|
492
|
-
// Split gate text into shell segments (
|
|
493
|
-
const segments = taskA.gateText.split('\n').flatMap(
|
|
568
|
+
// Split gate text into shell segments (quote-aware, shared with #429 — #3611).
|
|
569
|
+
const segments = taskA.gateText.split('\n').flatMap(splitShellSegments);
|
|
494
570
|
for (const seg of segments) {
|
|
495
571
|
if (!/grep/.test(seg))
|
|
496
572
|
continue;
|
|
@@ -655,17 +731,22 @@ function extractPlanTaskInfos(content) {
|
|
|
655
731
|
name,
|
|
656
732
|
type,
|
|
657
733
|
hasName,
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
734
|
+
// #3193: child-tag presence uses /<tag[\s>]/ (attribute-tolerant) so an
|
|
735
|
+
// opener like <verify type="auto"> still counts as present, matching how
|
|
736
|
+
// the parent <task type="…"> is read by PLAN_TASK_BLOCK_RE. The [\s>]
|
|
737
|
+
// terminator (not \b) keeps <verify> from satisfying <verification> and
|
|
738
|
+
// prevents a hyphenated sibling like <verify-mode> from masking <verify>.
|
|
739
|
+
hasFiles: /<files[\s>]/.test(body),
|
|
740
|
+
hasAction: /<action[\s>]/.test(body),
|
|
741
|
+
hasVerify: /<verify[\s>]/.test(body),
|
|
742
|
+
hasDone: /<done[\s>]/.test(body),
|
|
743
|
+
hasWhatBuilt: /<what-built[\s>]/.test(body),
|
|
744
|
+
hasHowToVerify: /<how-to-verify[\s>]/.test(body),
|
|
745
|
+
hasDecision: /<decision[\s>]/.test(body),
|
|
746
|
+
hasOptions: /<options[\s>]/.test(body),
|
|
747
|
+
hasInstructions: /<instructions[\s>]/.test(body),
|
|
748
|
+
hasVerification: /<verification[\s>]/.test(body),
|
|
749
|
+
hasResumeSignal: /<resume-signal[\s>]/.test(body),
|
|
669
750
|
});
|
|
670
751
|
// Guard against zero-length matches looping forever.
|
|
671
752
|
if (match.index === PLAN_TASK_BLOCK_RE.lastIndex) {
|
|
@@ -759,7 +840,7 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
759
840
|
// #2701: fail loud on NUL/binary corruption before structure checks. A
|
|
760
841
|
// structurally intact-but-NUL-corrupted plan otherwise passes as valid and is
|
|
761
842
|
// silently skipped by recursive/binary-skipping searchers downstream.
|
|
762
|
-
const encErr = (0,
|
|
843
|
+
const encErr = (0, validate_cjs_1.textEncodingError)(content, filePath);
|
|
763
844
|
if (encErr) {
|
|
764
845
|
output({ valid: false, errors: [encErr] }, raw);
|
|
765
846
|
return;
|
|
@@ -845,31 +926,34 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
|
|
|
845
926
|
const errors = [];
|
|
846
927
|
const warnings = [];
|
|
847
928
|
const phaseDir = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
929
|
+
// #3183 (lint-plan-count-drift / ADR-3180 Decision 2): source plans/
|
|
930
|
+
// summaries and their pairing from the single owner (scanPhasePlans +
|
|
931
|
+
// findUnsummarizedPlans/findOrphanSummaries) instead of a bespoke
|
|
932
|
+
// root-only `-PLAN.md`/`-SUMMARY.md` filter and exact-suffix-stem
|
|
933
|
+
// Set-diff. The prior pairing missed bare PLAN.md/SUMMARY.md, nested
|
|
934
|
+
// (#3139 layout) plans, and any of the canonical pairing's other
|
|
935
|
+
// recognized naming forms — producing false "Plans without summaries" /
|
|
936
|
+
// "Summaries without plans" for names it could not recognize as paired
|
|
937
|
+
// (the same failure class #1988/#2648 fixed for the owner's own callers).
|
|
938
|
+
const scan = planScanMod.scanPhasePlans(phaseDir);
|
|
939
|
+
if (scan.scope === SCOPE.UNREADABLE) {
|
|
853
940
|
output({ error: 'Cannot read phase directory' }, raw);
|
|
854
941
|
return;
|
|
855
942
|
}
|
|
856
|
-
const
|
|
857
|
-
const
|
|
858
|
-
const planIds = new Set(plans.map((p) => p.replace(/-PLAN\.md$/i, '')));
|
|
859
|
-
const summaryIds = new Set(summaries.map((s) => s.replace(/-SUMMARY\.md$/i, '')));
|
|
860
|
-
const incompletePlans = [...planIds].filter((id) => !summaryIds.has(id));
|
|
943
|
+
const { planFiles, summaryFiles } = scan;
|
|
944
|
+
const incompletePlans = findUnsummarizedPlans(planFiles, summaryFiles);
|
|
861
945
|
if (incompletePlans.length > 0) {
|
|
862
946
|
errors.push(`Plans without summaries: ${incompletePlans.join(', ')}`);
|
|
863
947
|
}
|
|
864
|
-
const orphanSummaries =
|
|
948
|
+
const orphanSummaries = findOrphanSummaries(planFiles, summaryFiles);
|
|
865
949
|
if (orphanSummaries.length > 0) {
|
|
866
950
|
warnings.push(`Summaries without plans: ${orphanSummaries.join(', ')}`);
|
|
867
951
|
}
|
|
868
952
|
output({
|
|
869
953
|
complete: errors.length === 0,
|
|
870
954
|
phase: phaseInfo['phase_number'],
|
|
871
|
-
plan_count:
|
|
872
|
-
summary_count:
|
|
955
|
+
plan_count: planFiles.length,
|
|
956
|
+
summary_count: summaryFiles.length,
|
|
873
957
|
incomplete_plans: incompletePlans,
|
|
874
958
|
orphan_summaries: orphanSummaries,
|
|
875
959
|
errors,
|
|
@@ -1080,8 +1164,40 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
1080
1164
|
verified: false,
|
|
1081
1165
|
detail: '',
|
|
1082
1166
|
};
|
|
1083
|
-
const
|
|
1084
|
-
const
|
|
1167
|
+
const fromRaw = link['from'];
|
|
1168
|
+
const fromPath = typeof fromRaw === 'string' ? fromRaw : '';
|
|
1169
|
+
let sourceContent = null;
|
|
1170
|
+
if (fromPath !== '') {
|
|
1171
|
+
// An empty/missing `from:` is a malformed plan, not a path-confinement
|
|
1172
|
+
// violation — validatePath's traversal check (and path_rejected) only
|
|
1173
|
+
// applies to a non-empty path that actually resolves outside the
|
|
1174
|
+
// project. Leave sourceContent as null so the existing not-found /
|
|
1175
|
+
// pending classification below runs unchanged. Note this guard is
|
|
1176
|
+
// narrower than it may look: `from: "."` is a non-empty string, so it
|
|
1177
|
+
// still reaches validatePath and safeReadFile below, and DOES read the
|
|
1178
|
+
// cwd directory (yielding "Source read failed: EISDIR") — this branch
|
|
1179
|
+
// only short-circuits the true empty-string case.
|
|
1180
|
+
const fromCheck = (0, security_cjs_1.validatePath)(fromPath, cwd);
|
|
1181
|
+
if (!fromCheck.safe) {
|
|
1182
|
+
// Do not echo result.error — it embeds absolute host paths.
|
|
1183
|
+
check['path_rejected'] = 'from';
|
|
1184
|
+
check['detail'] = 'Source path rejected — resolves outside the project directory';
|
|
1185
|
+
results.push(check);
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
sourceContent = (0, shell_command_projection_cjs_1.platformReadSync)(fromCheck.resolved);
|
|
1190
|
+
}
|
|
1191
|
+
catch (err) {
|
|
1192
|
+
// Report the errno only — never the message or path (untrusted `from:`
|
|
1193
|
+
// can trigger EISDIR/EACCES, which platformReadSync re-throws for any
|
|
1194
|
+
// non-ENOENT errno). A single bad link must not abort the whole command.
|
|
1195
|
+
const code = err?.code ?? 'unknown';
|
|
1196
|
+
check['detail'] = `Source read failed: ${code}`;
|
|
1197
|
+
results.push(check);
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1085
1201
|
if (!sourceContent) {
|
|
1086
1202
|
// Check if the missing file is promised by a plan at the same or later wave.
|
|
1087
1203
|
const promised = getPromisedFiles();
|
|
@@ -1096,25 +1212,76 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
1096
1212
|
}
|
|
1097
1213
|
}
|
|
1098
1214
|
else if (link['pattern']) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1215
|
+
const pat = (0, pattern_cjs_1.compileUserPattern)(link['pattern']);
|
|
1216
|
+
if (pat.neutralized !== null) {
|
|
1217
|
+
// A neutralized pattern was refused and never compiled — that is NOT
|
|
1218
|
+
// the check the plan author wrote, so it must never report verified
|
|
1219
|
+
// regardless of what an unrelated fallback might otherwise have
|
|
1220
|
+
// matched (#3477 regression: pattern "(" previously neutralized to a
|
|
1221
|
+
// literal-escaped match that matched nearly any source file,
|
|
1222
|
+
// producing a false verified: true / all_verified: true). The engine
|
|
1223
|
+
// itself now guarantees `test()` returns false for a refused
|
|
1224
|
+
// pattern; this explicit branch exists to produce the good message.
|
|
1225
|
+
// The match-and-report path below is skipped entirely rather than
|
|
1226
|
+
// run and then overwritten, which used to leave a misleading
|
|
1227
|
+
// "Pattern found in source" detail alongside the neutralization note.
|
|
1228
|
+
check['pattern_neutralized'] = pat.neutralized;
|
|
1229
|
+
let reason;
|
|
1230
|
+
switch (pat.neutralized) {
|
|
1231
|
+
case 'empty':
|
|
1232
|
+
reason = 'pattern is not a usable string — no match attempted';
|
|
1233
|
+
break;
|
|
1234
|
+
case 'too-long':
|
|
1235
|
+
reason = `pattern exceeded ${pattern_cjs_1.MAX_USER_PATTERN_LEN} chars — not evaluated`;
|
|
1236
|
+
break;
|
|
1237
|
+
case 'unsupported':
|
|
1238
|
+
reason = 'pattern is not valid RE2 syntax — backreferences and look-around are not supported';
|
|
1239
|
+
break;
|
|
1104
1240
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1241
|
+
check['detail'] = `Pattern not verified (${reason})`;
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
try {
|
|
1245
|
+
if (pat.test(sourceContent)) {
|
|
1108
1246
|
check['verified'] = true;
|
|
1109
|
-
check['detail'] = 'Pattern found in
|
|
1247
|
+
check['detail'] = 'Pattern found in source';
|
|
1110
1248
|
}
|
|
1111
1249
|
else {
|
|
1112
|
-
|
|
1250
|
+
const toRaw = link['to'];
|
|
1251
|
+
const toPath = typeof toRaw === 'string' ? toRaw : '';
|
|
1252
|
+
let targetContent = null;
|
|
1253
|
+
if (toPath !== '') {
|
|
1254
|
+
// An empty/missing `to:` is a malformed plan, not a
|
|
1255
|
+
// path-confinement violation — only a non-empty path that
|
|
1256
|
+
// actually resolves outside the project is path_rejected.
|
|
1257
|
+
const toCheck = (0, security_cjs_1.validatePath)(toPath, cwd);
|
|
1258
|
+
if (!toCheck.safe) {
|
|
1259
|
+
// Do not read a rejected `to:` — treat as no target content
|
|
1260
|
+
// and do not echo result.error, which embeds absolute host
|
|
1261
|
+
// paths.
|
|
1262
|
+
check['path_rejected'] = 'to';
|
|
1263
|
+
check['detail'] = `Pattern "${link['pattern']}" not found in source; target path rejected — resolves outside the project directory`;
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
targetContent = (0, shell_command_projection_cjs_1.platformReadSync)(toCheck.resolved);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (targetContent && pat.test(targetContent)) {
|
|
1270
|
+
check['verified'] = true;
|
|
1271
|
+
check['detail'] = 'Pattern found in target';
|
|
1272
|
+
}
|
|
1273
|
+
else if (!check['path_rejected']) {
|
|
1274
|
+
check['detail'] = `Pattern "${link['pattern']}" not found in source or target`;
|
|
1275
|
+
}
|
|
1113
1276
|
}
|
|
1114
1277
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1278
|
+
catch (err) {
|
|
1279
|
+
// Report the errno only — never the full error/message, which for a
|
|
1280
|
+
// re-thrown non-ENOENT platformReadSync failure (e.g. EISDIR from an
|
|
1281
|
+
// untrusted `to:` like "../..") embeds an absolute filesystem path.
|
|
1282
|
+
const code = err?.code ?? 'unknown';
|
|
1283
|
+
check['detail'] = `Pattern check failed: ${code}`;
|
|
1284
|
+
}
|
|
1118
1285
|
}
|
|
1119
1286
|
}
|
|
1120
1287
|
else {
|
|
@@ -1141,209 +1308,115 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
1141
1308
|
links: results,
|
|
1142
1309
|
}, raw, allVerified ? 'valid' : 'invalid');
|
|
1143
1310
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const archiveDirs = listMilestoneArchiveDirs(planBase);
|
|
1184
|
-
if (archiveDirs.length === 0)
|
|
1185
|
-
return null;
|
|
1186
|
-
try {
|
|
1187
|
-
const statePath = node_path_1.default.join(planBase, 'STATE.md');
|
|
1188
|
-
if (node_fs_1.default.existsSync(statePath)) {
|
|
1189
|
-
const state = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
1190
|
-
const m = state.match(/^\s*(?:\*\*)?milestone(?:\*\*)?:\s*\*{0,2}\s*([^\s*\r\n#][^\s\r\n#]*)/mi);
|
|
1191
|
-
if (m && m[1]) {
|
|
1192
|
-
const milestone = m[1].trim();
|
|
1193
|
-
const candidate = node_path_1.default.join(planBase, 'milestones', `${milestone}-phases`);
|
|
1194
|
-
return archiveDirs.includes(candidate) ? candidate : null;
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
catch {
|
|
1199
|
-
/* intentionally empty — fall through to version-sort below */
|
|
1311
|
+
/**
|
|
1312
|
+
* Wrapper-level fix-text table for `cmdValidateHealth`'s migrated
|
|
1313
|
+
* `HealthDiagnostic` -> `IssueEntry` mapping (Phase 11, #3309). Rules cannot
|
|
1314
|
+
* call `slash()` (forbidden ambient I/O, §8.1 rule 1) so every REPAIRABLE
|
|
1315
|
+
* (non-ADVISE) diagnostic's `fix` text — which the pre-migration code always
|
|
1316
|
+
* built via `${slash('health')} --repair|--backfill ...` — is reconstructed
|
|
1317
|
+
* HERE instead, keyed by `remedy.action`. Each real repair action maps 1:1
|
|
1318
|
+
* back to exactly one pre-migration code (confirmed: no `REMEDY_ACTION`
|
|
1319
|
+
* other than `ADVISE` is used by more than one rule in the migrated table),
|
|
1320
|
+
* so this table reproduces the original `fix` text byte-for-byte, including
|
|
1321
|
+
* its `slash()` calls, without the RULE ever needing to know about `slash`.
|
|
1322
|
+
* Source line refs are the exact pre-migration `addIssue(...)` call each
|
|
1323
|
+
* text was copied from:
|
|
1324
|
+
* - createConfig — verify.cts:1782 (W003)
|
|
1325
|
+
* - resetConfig — verify.cts:1830 (E005)
|
|
1326
|
+
* - regenerateState — verify.cts:1702 (E004)
|
|
1327
|
+
* - addNyquistKey — verify.cts:1847 (W008)
|
|
1328
|
+
* - addAiIntegrationPhaseKey — verify.cts:1857 (W016)
|
|
1329
|
+
* - backfillMilestones — verify.cts:2326 (W018)
|
|
1330
|
+
* ADVISE diagnostics do NOT go through this table — their `fix` is
|
|
1331
|
+
* `diagnostic.remedy.args.command` directly (already a complete, final
|
|
1332
|
+
* string baked in by the rule, confirmed via
|
|
1333
|
+
* `src/health-diagnostic-rules/root-existence.cts`/`config-validation.cts`).
|
|
1334
|
+
*/
|
|
1335
|
+
function repairFixText(slash, action) {
|
|
1336
|
+
switch (action) {
|
|
1337
|
+
case REMEDY_ACTION.CREATE_CONFIG:
|
|
1338
|
+
return `Run ${slash('health')} --repair to create with defaults`;
|
|
1339
|
+
case REMEDY_ACTION.RESET_CONFIG:
|
|
1340
|
+
return `Run ${slash('health')} --repair to reset to defaults`;
|
|
1341
|
+
case REMEDY_ACTION.REGENERATE_STATE:
|
|
1342
|
+
return `Run ${slash('health')} --repair to regenerate`;
|
|
1343
|
+
case REMEDY_ACTION.ADD_NYQUIST_KEY:
|
|
1344
|
+
case REMEDY_ACTION.ADD_AI_INTEGRATION_PHASE_KEY:
|
|
1345
|
+
return `Run ${slash('health')} --repair to add key`;
|
|
1346
|
+
case REMEDY_ACTION.BACKFILL_MILESTONES:
|
|
1347
|
+
return `Run ${slash('health')} --backfill to synthesize missing entries from archive snapshots`;
|
|
1348
|
+
default:
|
|
1349
|
+
return '';
|
|
1200
1350
|
}
|
|
1201
|
-
return archiveDirs[archiveDirs.length - 1];
|
|
1202
1351
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1352
|
+
/**
|
|
1353
|
+
* Map one `HealthDiagnostic` (rule-table shape) back onto the legacy
|
|
1354
|
+
* `IssueEntry` shape `cmdValidateHealth` has always returned (design doc,
|
|
1355
|
+
* "Output-shape preservation" section).
|
|
1356
|
+
*
|
|
1357
|
+
* `repairable` for a DESTRUCTIVE-risk remedy (regenerateState/resetConfig)
|
|
1358
|
+
* is `false` here — NOT the pre-migration `true` those two codes always
|
|
1359
|
+
* carried. This is a deliberate, disclosed decision (see this batch's
|
|
1360
|
+
* dispatch report): the design doc's own "`--repair` behavior change"
|
|
1361
|
+
* section establishes that `--repair` never actually applies a DESTRUCTIVE
|
|
1362
|
+
* remedy post-migration, so marking it `repairable: true` would mislead a
|
|
1363
|
+
* caller that uses this field to decide whether re-running with `--repair`
|
|
1364
|
+
* will fix anything. `repairable` now means "an automatic repair will
|
|
1365
|
+
* actually run", not merely "a remedy exists to describe" — the more
|
|
1366
|
+
* conservative of the two readings the brief identified, chosen because the
|
|
1367
|
+
* question was genuinely ambiguous and this reading cannot itself cause a
|
|
1368
|
+
* caller to skip a fix that would have worked.
|
|
1369
|
+
*/
|
|
1370
|
+
function diagnosticToIssueEntry(diagnostic, slash) {
|
|
1371
|
+
const { code, message, remedy } = diagnostic;
|
|
1372
|
+
if (remedy.action === REMEDY_ACTION.ADVISE) {
|
|
1373
|
+
return { code, message, fix: remedy.args['command'], repairable: false };
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
code,
|
|
1377
|
+
message,
|
|
1378
|
+
fix: repairFixText(slash, remedy.action),
|
|
1379
|
+
repairable: remedy.risk !== REMEDY_RISK.DESTRUCTIVE,
|
|
1230
1380
|
};
|
|
1231
|
-
for (const root of phaseRoots)
|
|
1232
|
-
scanDir(root);
|
|
1233
|
-
return diskPhases;
|
|
1234
|
-
}
|
|
1235
|
-
function checkMilestonePrefixMismatches(roadmapContent, { getMilestoneFromPhaseId }) {
|
|
1236
|
-
const mismatches = [];
|
|
1237
|
-
const sections = [];
|
|
1238
|
-
const sectionRx = /^#{1,3}\s+(?:\[[^\]]{1,200}\]\s*)?.*v(\d+\.\d+)/gim;
|
|
1239
|
-
let m;
|
|
1240
|
-
while ((m = sectionRx.exec(roadmapContent)) !== null) {
|
|
1241
|
-
if (sections.length > 0)
|
|
1242
|
-
sections[sections.length - 1].end = m.index;
|
|
1243
|
-
sections.push({ version: `v${m[1]}`, start: m.index, end: roadmapContent.length });
|
|
1244
|
-
}
|
|
1245
|
-
for (const section of sections) {
|
|
1246
|
-
const content = roadmapContent.slice(section.start, section.end);
|
|
1247
|
-
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1248
|
-
const phaseRx = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/gi;
|
|
1249
|
-
let pm;
|
|
1250
|
-
while ((pm = phaseRx.exec(content)) !== null) {
|
|
1251
|
-
const phaseId = pm[1];
|
|
1252
|
-
const expectedMilestone = getMilestoneFromPhaseId(phaseId);
|
|
1253
|
-
if (expectedMilestone !== null && expectedMilestone !== section.version) {
|
|
1254
|
-
mismatches.push({
|
|
1255
|
-
phaseId,
|
|
1256
|
-
foundInMilestone: section.version,
|
|
1257
|
-
expectedMilestone,
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
return mismatches;
|
|
1263
1381
|
}
|
|
1264
1382
|
function cmdValidateConsistency(cwd, raw) {
|
|
1265
1383
|
const planBase = planningDir(cwd);
|
|
1266
1384
|
const roadmapPath = node_path_1.default.join(planBase, 'ROADMAP.md');
|
|
1267
1385
|
const errors = [];
|
|
1268
1386
|
const warnings = [];
|
|
1387
|
+
// Pre-check, stays OUTSIDE the rule table — same shape as `validate.health`'s
|
|
1388
|
+
// E001/E010/I010 (design doc, "Which rules run where"). `.planning/` cannot
|
|
1389
|
+
// build a `PlanningSnapshot` worth evaluating without a ROADMAP.md to read.
|
|
1269
1390
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
1270
1391
|
errors.push('ROADMAP.md not found');
|
|
1271
1392
|
output({ passed: false, errors, warnings }, raw, 'failed');
|
|
1272
1393
|
return;
|
|
1273
1394
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
const phaseRoots = collectPhaseRoots(planBase);
|
|
1303
|
-
for (const phaseRoot of phaseRoots) {
|
|
1304
|
-
try {
|
|
1305
|
-
const entries = node_fs_1.default.readdirSync(phaseRoot, { withFileTypes: true });
|
|
1306
|
-
const dirs = entries
|
|
1307
|
-
.filter((e) => e.isDirectory())
|
|
1308
|
-
.map((e) => e.name)
|
|
1309
|
-
.sort();
|
|
1310
|
-
for (const dir of dirs) {
|
|
1311
|
-
const phasePath = node_path_1.default.join(phaseRoot, dir);
|
|
1312
|
-
const phaseLabel = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.relative(planBase, phasePath));
|
|
1313
|
-
const phaseFiles = node_fs_1.default.readdirSync(phasePath);
|
|
1314
|
-
const plans = phaseFiles.filter((f) => f.endsWith('-PLAN.md')).sort();
|
|
1315
|
-
const planNums = plans
|
|
1316
|
-
.map((p) => {
|
|
1317
|
-
const pm = p.match(/-(\d{2})-PLAN\.md$/);
|
|
1318
|
-
return pm ? parseInt(pm[1], 10) : null;
|
|
1319
|
-
})
|
|
1320
|
-
.filter((n) => n !== null);
|
|
1321
|
-
for (let i = 1; i < planNums.length; i++) {
|
|
1322
|
-
if (planNums[i] !== planNums[i - 1] + 1) {
|
|
1323
|
-
warnings.push(`Gap in plan numbering in ${phaseLabel}: plan ${planNums[i - 1]} → ${planNums[i]}`);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
const summaries = phaseFiles.filter((f) => f.endsWith('-SUMMARY.md'));
|
|
1327
|
-
const planIds = new Set(plans.map((p) => p.replace('-PLAN.md', '')));
|
|
1328
|
-
const summaryIds = new Set(summaries.map((s) => s.replace('-SUMMARY.md', '')));
|
|
1329
|
-
for (const sid of summaryIds) {
|
|
1330
|
-
if (!planIds.has(sid)) {
|
|
1331
|
-
warnings.push(`Summary ${sid}-SUMMARY.md in ${phaseLabel} has no matching PLAN.md`);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
for (const plan of plans) {
|
|
1335
|
-
const planFilePath = node_path_1.default.join(phasePath, plan);
|
|
1336
|
-
const content = node_fs_1.default.readFileSync(planFilePath, 'utf-8');
|
|
1337
|
-
const fmData = extractFrontmatter(content, planFilePath);
|
|
1338
|
-
if (!fmData['wave']) {
|
|
1339
|
-
warnings.push(`${phaseLabel}/${plan}: missing 'wave' in frontmatter`);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
catch {
|
|
1345
|
-
/* intentionally empty */
|
|
1346
|
-
}
|
|
1395
|
+
// ─── Rule-table evaluation (Phase 12, #3310) ───────────────────────────────
|
|
1396
|
+
// Replaces this function's entire hand-rolled disk-vs-roadmap /
|
|
1397
|
+
// numbering-gap / orphan-summary / wave-missing scan — see the design
|
|
1398
|
+
// doc's "Which rules run where" section. `evaluateConsistencyRules` runs
|
|
1399
|
+
// W006/W007 (the SAME `Rule` objects `validate.health` evaluates, reused
|
|
1400
|
+
// verbatim — not a second, independently-drifting copy) plus the four new
|
|
1401
|
+
// C001-C004 rules, against the same `PlanningSnapshot` `validate.health`
|
|
1402
|
+
// builds.
|
|
1403
|
+
const snapshot = buildPlanningSnapshot(cwd);
|
|
1404
|
+
const diagnostics = evaluateConsistencyRules(snapshot);
|
|
1405
|
+
// Every current C0NN/W006/W007 diagnostic is `SEVERITY.WARNING` (confirmed
|
|
1406
|
+
// by direct read of `consistency.cts`/`roadmap-disk-consistency.cts`) —
|
|
1407
|
+
// matching the pre-migration shape, where only the ROADMAP-missing
|
|
1408
|
+
// pre-check above ever populated `errors`. Bucketed defensively by
|
|
1409
|
+
// severity anyway, mirroring `cmdValidateHealth`'s own pattern, so a
|
|
1410
|
+
// future ERROR-severity rule added to `CONSISTENCY_RULES` lands in the
|
|
1411
|
+
// right bucket without another migration.
|
|
1412
|
+
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
1413
|
+
const slash = (name) => (0, runtime_slash_cjs_1.formatGsdSlash)(name, _slashRuntime);
|
|
1414
|
+
for (const diagnostic of diagnostics) {
|
|
1415
|
+
const entry = diagnosticToIssueEntry(diagnostic, slash);
|
|
1416
|
+
if (diagnostic.severity === HEALTH_SEVERITY.ERROR)
|
|
1417
|
+
errors.push(entry.message);
|
|
1418
|
+
else
|
|
1419
|
+
warnings.push(entry);
|
|
1347
1420
|
}
|
|
1348
1421
|
const passed = errors.length === 0;
|
|
1349
1422
|
output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
|
|
@@ -1366,664 +1439,104 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1366
1439
|
}, raw);
|
|
1367
1440
|
return;
|
|
1368
1441
|
}
|
|
1369
|
-
// rootBase
|
|
1370
|
-
//
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1373
|
-
// planBase is kept as an alias for wsBase for all the internal helpers (collectDiskPhases, etc.)
|
|
1374
|
-
// that are already parameterised on the workstream-aware path.
|
|
1375
|
-
const planBase = wsBase;
|
|
1376
|
-
const projectPath = node_path_1.default.join(rootBase, 'PROJECT.md');
|
|
1377
|
-
const roadmapPath = node_path_1.default.join(wsBase, 'ROADMAP.md');
|
|
1378
|
-
const statePath = node_path_1.default.join(wsBase, 'STATE.md');
|
|
1379
|
-
const configPath = node_path_1.default.join(rootBase, 'config.json');
|
|
1380
|
-
const phasesDir = node_path_1.default.join(wsBase, 'phases');
|
|
1442
|
+
// rootBase resolves to the PROJECT-scoped planning root (`.planning[/<project>]`
|
|
1443
|
+
// — PROJECT.md, config.json live here; #3749). Workstream-free by construction:
|
|
1444
|
+
// planningDir(cwd, null) honors GSD_PROJECT and suppresses GSD_WORKSTREAM.
|
|
1445
|
+
const rootBase = planningDir(cwd, null);
|
|
1381
1446
|
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
1382
1447
|
const slash = (name) => (0, runtime_slash_cjs_1.formatGsdSlash)(name, _slashRuntime);
|
|
1448
|
+
// Second (and last) pre-check that stays OUTSIDE the rule table entirely
|
|
1449
|
+
// (design doc, "Two guards that stay OUTSIDE the rule table entirely" —
|
|
1450
|
+
// this one must run BEFORE any snapshot is built, since a flat
|
|
1451
|
+
// `evaluateRules` pass over an entirely-absent `.planning/` would produce
|
|
1452
|
+
// spurious per-rule clutter no one asked for, not a clean E001-only
|
|
1453
|
+
// report).
|
|
1454
|
+
if (!node_fs_1.default.existsSync(rootBase)) {
|
|
1455
|
+
const errors = [
|
|
1456
|
+
{
|
|
1457
|
+
code: 'E001',
|
|
1458
|
+
message: '.planning/ directory not found',
|
|
1459
|
+
fix: `Run ${slash('new-project')} to initialize`,
|
|
1460
|
+
repairable: false,
|
|
1461
|
+
},
|
|
1462
|
+
];
|
|
1463
|
+
output({ status: 'broken', errors, warnings: [], info: [], repairable_count: 0 }, raw);
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
// ─── Rule-table evaluation (Phase 11, #3309) ───────────────────────────────
|
|
1467
|
+
// Replaces the entire hand-rolled addIssue/switch accumulation this
|
|
1468
|
+
// function used to run inline (verify.cts, pre-migration) — see the
|
|
1469
|
+
// design doc's "Output-shape preservation" section for the exact
|
|
1470
|
+
// Diagnostic -> IssueEntry mapping contract this reproduces.
|
|
1471
|
+
const snapshot = buildPlanningSnapshot(cwd);
|
|
1472
|
+
const diagnostics = evaluateRules(snapshot);
|
|
1383
1473
|
const errors = [];
|
|
1384
1474
|
const warnings = [];
|
|
1385
1475
|
const info = [];
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
warnings.push(issue);
|
|
1476
|
+
for (const diagnostic of diagnostics) {
|
|
1477
|
+
const entry = diagnosticToIssueEntry(diagnostic, slash);
|
|
1478
|
+
if (diagnostic.severity === HEALTH_SEVERITY.ERROR)
|
|
1479
|
+
errors.push(entry);
|
|
1480
|
+
else if (diagnostic.severity === HEALTH_SEVERITY.WARNING)
|
|
1481
|
+
warnings.push(entry);
|
|
1393
1482
|
else
|
|
1394
|
-
info.push(
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
addIssue('error', 'E003', 'ROADMAP.md not found', `Run ${slash('new-milestone')} to create roadmap`);
|
|
1415
|
-
}
|
|
1416
|
-
if (!node_fs_1.default.existsSync(statePath)) {
|
|
1417
|
-
addIssue('error', 'E004', 'STATE.md not found', `Run ${slash('health')} --repair to regenerate`, true);
|
|
1418
|
-
repairs.push('regenerateState');
|
|
1419
|
-
}
|
|
1420
|
-
else {
|
|
1421
|
-
const stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
1422
|
-
const phaseRefs = [
|
|
1423
|
-
...stateContent.matchAll(new RegExp(`[Pp]hase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'g')),
|
|
1424
|
-
].map((m) => m[1]);
|
|
1425
|
-
const validPhases = collectDiskPhases(planBase);
|
|
1426
|
-
try {
|
|
1427
|
-
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
1428
|
-
const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1429
|
-
const all = [
|
|
1430
|
-
...roadmapRaw.matchAll(new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'gi')),
|
|
1431
|
-
];
|
|
1432
|
-
for (const m of all)
|
|
1433
|
-
validPhases.add(m[1]);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
catch {
|
|
1437
|
-
/* intentionally empty */
|
|
1438
|
-
}
|
|
1439
|
-
forEachArchivedPhaseToken(planBase, (token) => validPhases.add(token));
|
|
1440
|
-
const normalizedValid = new Set();
|
|
1441
|
-
for (const p of validPhases) {
|
|
1442
|
-
normalizedValid.add(p);
|
|
1443
|
-
const dotIdx = p.indexOf('.');
|
|
1444
|
-
const head = dotIdx === -1 ? p : p.slice(0, dotIdx);
|
|
1445
|
-
const tail = dotIdx === -1 ? '' : p.slice(dotIdx);
|
|
1446
|
-
if (/^\d+$/.test(head)) {
|
|
1447
|
-
normalizedValid.add(head.padStart(2, '0') + tail);
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
for (const ref of phaseRefs) {
|
|
1451
|
-
const dotIdx = ref.indexOf('.');
|
|
1452
|
-
const head = dotIdx === -1 ? ref : ref.slice(0, dotIdx);
|
|
1453
|
-
const tail = dotIdx === -1 ? '' : ref.slice(dotIdx);
|
|
1454
|
-
const padded = /^\d+$/.test(head) ? head.padStart(2, '0') + tail : ref;
|
|
1455
|
-
if (!normalizedValid.has(ref) && !normalizedValid.has(padded)) {
|
|
1456
|
-
if (normalizedValid.size > 0) {
|
|
1457
|
-
addIssue('warning', 'W002', `STATE.md references phase ${ref}, but only phases ${[...validPhases].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).join(', ')} are declared`, `Review STATE.md manually before changing it; ${slash('health')} --repair will not overwrite an existing STATE.md for phase mismatches`);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
if (!node_fs_1.default.existsSync(configPath)) {
|
|
1463
|
-
addIssue('warning', 'W003', 'config.json not found', `Run ${slash('health')} --repair to create with defaults`, true);
|
|
1464
|
-
repairs.push('createConfig');
|
|
1465
|
-
}
|
|
1466
|
-
else {
|
|
1467
|
-
try {
|
|
1468
|
-
const rawCfg = node_fs_1.default.readFileSync(configPath, 'utf-8');
|
|
1469
|
-
const parsed = JSON.parse(rawCfg);
|
|
1470
|
-
if (parsed['model_profile'] && !model_catalog_cjs_1.VALID_PROFILES.includes(parsed['model_profile'])) {
|
|
1471
|
-
addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed['model_profile']}"`, `Valid values: ${model_catalog_cjs_1.VALID_PROFILES.join(', ')}`);
|
|
1472
|
-
}
|
|
1473
|
-
const configModels = parsed['models'];
|
|
1474
|
-
if (configModels && typeof configModels === 'object' && !Array.isArray(configModels)) {
|
|
1475
|
-
for (const [phaseType, tierValue] of Object.entries(configModels)) {
|
|
1476
|
-
if (!model_catalog_cjs_1.VALID_PHASE_TYPES.has(phaseType)) {
|
|
1477
|
-
addIssue('warning', 'W022', `config.json: models has an unknown phase type "${phaseType}" which will be ignored`, `Valid phase types: ${[...model_catalog_cjs_1.VALID_PHASE_TYPES].join(', ')}`);
|
|
1478
|
-
}
|
|
1479
|
-
else if (typeof tierValue !== 'string' || !model_catalog_cjs_1.VALID_TIERS.has(tierValue)) {
|
|
1480
|
-
addIssue('warning', 'W022', `config.json: models.${phaseType} has an invalid tier value ${JSON.stringify(tierValue)} which will be ignored`, `Valid tiers: ${[...model_catalog_cjs_1.VALID_TIERS].join(', ')}`);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
else if (configModels !== undefined && configModels !== null) {
|
|
1485
|
-
addIssue('warning', 'W022', `config.json: models is set to ${JSON.stringify(configModels)}, but must be an object mapping phase types to tiers — this value will be ignored`, `Set models to an object like {"planning": "sonnet"}, or remove the key to use profile defaults`);
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
catch (err) {
|
|
1489
|
-
addIssue('error', 'E005', `config.json: JSON parse error - ${err instanceof Error ? err.message : String(err)}`, `Run ${slash('health')} --repair to reset to defaults`, true);
|
|
1490
|
-
repairs.push('resetConfig');
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
if (node_fs_1.default.existsSync(configPath)) {
|
|
1494
|
-
try {
|
|
1495
|
-
const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
|
|
1496
|
-
const configParsed = JSON.parse(configRaw);
|
|
1497
|
-
const workflow = configParsed['workflow'];
|
|
1498
|
-
if (workflow && workflow['nyquist_validation'] === undefined) {
|
|
1499
|
-
addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', `Run ${slash('health')} --repair to add key`, true);
|
|
1500
|
-
if (!repairs.includes('addNyquistKey'))
|
|
1501
|
-
repairs.push('addNyquistKey');
|
|
1502
|
-
}
|
|
1503
|
-
if (workflow && workflow['ai_integration_phase'] === undefined) {
|
|
1504
|
-
addIssue('warning', 'W016', `config.json: workflow.ai_integration_phase absent (defaults to enabled — run ${slash('ai-integration-phase')} before planning AI system phases)`, `Run ${slash('health')} --repair to add key`, true);
|
|
1505
|
-
if (!repairs.includes('addAiIntegrationPhaseKey'))
|
|
1506
|
-
repairs.push('addAiIntegrationPhaseKey');
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
catch {
|
|
1510
|
-
/* intentionally empty */
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
let phaseDirEntries = [];
|
|
1514
|
-
const phaseDirFiles = new Map();
|
|
1515
|
-
try {
|
|
1516
|
-
phaseDirEntries = node_fs_1.default
|
|
1517
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
1518
|
-
.filter((e) => e.isDirectory());
|
|
1519
|
-
for (const e of phaseDirEntries) {
|
|
1520
|
-
try {
|
|
1521
|
-
phaseDirFiles.set(e.name, node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, e.name)));
|
|
1522
|
-
}
|
|
1523
|
-
catch {
|
|
1524
|
-
phaseDirFiles.set(e.name, []);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
catch {
|
|
1529
|
-
/* intentionally empty */
|
|
1530
|
-
}
|
|
1531
|
-
for (const e of phaseDirEntries) {
|
|
1532
|
-
if (!e.name.match(validate_cjs_2.phaseDirNameRe)) {
|
|
1533
|
-
addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
// W023 (#2408): detect two or more real on-disk phase directories that
|
|
1537
|
-
// normalize to the same phase key (e.g. `05-real/` + `05-real-stray/`).
|
|
1538
|
-
// The collision silently breaks /gsd-stats status accuracy (now folded by
|
|
1539
|
-
// precedence — see commands.cts foldPhaseStatus) and forces an operator
|
|
1540
|
-
// decision. Wording is neutral — never guesses which directory is "real".
|
|
1483
|
+
info.push(entry);
|
|
1484
|
+
}
|
|
1485
|
+
// W024 (#2573): STATE.md commit-age freshness — kept OUTSIDE the rule
|
|
1486
|
+
// table, exactly like E001/E010/I010 above. NOT a preservation nicety: the
|
|
1487
|
+
// committed `RULE_W024` (`src/health-diagnostic-rules/state-consistency.cts`)
|
|
1488
|
+
// is a documented PERMANENT no-op (`check` always returns `[]`) because
|
|
1489
|
+
// `readStateHeadFreshness`'s `git log` shell-out is ambient I/O a
|
|
1490
|
+
// `Rule.check(snapshot)` may never perform (§8.1 rule 1) and no
|
|
1491
|
+
// `PlanningSnapshot` field carries a commits-behind count. Migrating
|
|
1492
|
+
// `cmdValidateHealth` onto the rule table as designed would silently
|
|
1493
|
+
// regress `tests/health-validation.test.cjs`'s "W024 — STATE.md commit-age
|
|
1494
|
+
// freshness advisory" suite (7 currently-passing tests exercising the REAL
|
|
1495
|
+
// git-based check end-to-end) — found while wiring this function to the
|
|
1496
|
+
// rule table, fixed inline per this repo's no-defer policy rather than
|
|
1497
|
+
// silently accepting the loss. `cmdValidateHealth` itself (unlike a Rule)
|
|
1498
|
+
// is licensed to perform its own bounded I/O — the same license
|
|
1499
|
+
// `applyRepairs` already relies on (see `health-diagnostic.cts`'s header
|
|
1500
|
+
// comment) — so this reproduces the exact pre-migration check
|
|
1501
|
+
// (`verify.cts`, W024) verbatim, advisory-only: it only ever appends to
|
|
1502
|
+
// `warnings`, never touches `status`/`errors`/the repair set.
|
|
1541
1503
|
{
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
// falls back to the dir name itself. Two distinct unparseable names
|
|
1546
|
-
// therefore normalize to distinct keys and cannot false-positive here;
|
|
1547
|
-
// only dirs whose tokens collapse to the same key (e.g. `05-real` and
|
|
1548
|
-
// `05-real-stray` → token `05`) produce a collision group.
|
|
1549
|
-
const token = extractPhaseToken(e.name);
|
|
1550
|
-
const key = normalizePhaseName(token);
|
|
1551
|
-
const list = groups.get(key);
|
|
1552
|
-
if (list)
|
|
1553
|
-
list.push(e.name);
|
|
1554
|
-
else
|
|
1555
|
-
groups.set(key, [e.name]);
|
|
1556
|
-
}
|
|
1557
|
-
for (const [key, dirs] of groups) {
|
|
1558
|
-
if (dirs.length < 2)
|
|
1559
|
-
continue;
|
|
1560
|
-
// Compute each dir's status independently so the warning is informative.
|
|
1561
|
-
// Sort by phase id for stable output regardless of readdir order; tie-
|
|
1562
|
-
// break on the dir name itself so two dirs sharing the same phase token
|
|
1563
|
-
// (the collision case itself) still sort deterministically (V8's stable
|
|
1564
|
-
// sort would otherwise fall back to non-portable fs.readdirSync order).
|
|
1565
|
-
const described = dirs
|
|
1566
|
-
.slice()
|
|
1567
|
-
.sort((a, b) => comparePhaseNum(a, b) || String(a).localeCompare(String(b)))
|
|
1568
|
-
.map((d) => {
|
|
1569
|
-
const files = phaseDirFiles.get(d) || [];
|
|
1570
|
-
const plans = files.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
1571
|
-
const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
1572
|
-
const status = determinePhaseStatus(plans, summaries, node_path_1.default.join(phasesDir, d), 'Not Started');
|
|
1573
|
-
return `${d} (${status})`;
|
|
1574
|
-
})
|
|
1575
|
-
.join(', ');
|
|
1576
|
-
addIssue('warning', 'W023', `Phase directories collide on normalized key "${key}": ${described}`, 'Inspect each directory; rename or remove the duplicate so only one directory maps to this phase key');
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
for (const e of phaseDirEntries) {
|
|
1580
|
-
const phaseFiles = phaseDirFiles.get(e.name) || [];
|
|
1581
|
-
const plans = phaseFiles.filter((f) => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
1582
|
-
const summaries = phaseFiles.filter((f) => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
1583
|
-
const summaryBases = new Set();
|
|
1584
|
-
for (const s of summaries) {
|
|
1585
|
-
const summaryBase = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
1586
|
-
summaryBases.add(summaryBase);
|
|
1587
|
-
summaryBases.add((0, validate_cjs_2.canonicalPlanStem)(summaryBase));
|
|
1588
|
-
}
|
|
1589
|
-
for (const plan of plans) {
|
|
1590
|
-
const planBase = plan.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
1591
|
-
const canonicalBase = (0, validate_cjs_2.canonicalPlanStem)(planBase);
|
|
1592
|
-
if (!summaryBases.has(planBase) && !summaryBases.has(canonicalBase)) {
|
|
1593
|
-
addIssue('info', 'I001', `${e.name}/${plan} has no SUMMARY.md`, 'May be in progress');
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
for (const e of phaseDirEntries) {
|
|
1598
|
-
const phaseFiles = phaseDirFiles.get(e.name) || [];
|
|
1599
|
-
const hasResearch = phaseFiles.some((f) => f.endsWith('-RESEARCH.md'));
|
|
1600
|
-
const hasValidation = phaseFiles.some((f) => f.endsWith('-VALIDATION.md'));
|
|
1601
|
-
if (hasResearch && !hasValidation) {
|
|
1602
|
-
const researchFile = phaseFiles.find((f) => f.endsWith('-RESEARCH.md'));
|
|
1504
|
+
const wsBase = planningDir(cwd);
|
|
1505
|
+
const statePath = node_path_1.default.join(wsBase, 'STATE.md');
|
|
1506
|
+
if (node_fs_1.default.existsSync(statePath)) {
|
|
1603
1507
|
try {
|
|
1604
|
-
const
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
const agentStatus = checkAgentsInstalled(_slashRuntime, cwd);
|
|
1616
|
-
if (!agentStatus.agents_installed) {
|
|
1617
|
-
if ((agentStatus.installed_agents).length === 0) {
|
|
1618
|
-
addIssue('warning', 'W010', `No GSD agents found in ${agentStatus.agents_dir} — Task(subagent_type="gsd-*") will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
|
|
1619
|
-
}
|
|
1620
|
-
else if ((agentStatus.incomplete_agents).length > 0 && (agentStatus.missing_agents).length === 0) {
|
|
1621
|
-
addIssue('warning', 'W010', `Incomplete agent installs (missing generated file): ${(agentStatus.incomplete_agents).join(', ')} — affected workflows may fall back to general-purpose`, `Re-run the GSD installer to complete the install: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
|
|
1622
|
-
}
|
|
1623
|
-
else if ((agentStatus.incomplete_agents).length > 0) {
|
|
1624
|
-
addIssue('warning', 'W010', `Missing ${(agentStatus.missing_agents).length} GSD agents: ${(agentStatus.missing_agents).join(', ')}; incomplete agent installs (missing generated file): ${(agentStatus.incomplete_agents).join(', ')} — affected workflows will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
|
|
1625
|
-
}
|
|
1626
|
-
else {
|
|
1627
|
-
addIssue('warning', 'W010', `Missing ${(agentStatus.missing_agents).length} GSD agents: ${(agentStatus.missing_agents).join(', ')} — affected workflows will fall back to general-purpose`, `Run the GSD installer: npx ${package_identity_cjs_1.PACKAGE_NAME}@latest`);
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
catch {
|
|
1632
|
-
/* intentionally empty — agent check is non-blocking */
|
|
1633
|
-
}
|
|
1634
|
-
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
1635
|
-
const roadmapContentRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1636
|
-
const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
|
|
1637
|
-
const { roadmapPhases } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(roadmapContent);
|
|
1638
|
-
const { roadmapPhaseVariants: fullRoadmapPhaseVariants } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(roadmapContentRaw);
|
|
1639
|
-
const diskPhases = collectDiskPhases(planBase);
|
|
1640
|
-
forEachArchivedPhaseToken(planBase, (token) => diskPhases.add(token));
|
|
1641
|
-
const activeDiskPhases = collectDiskPhases(planBase);
|
|
1642
|
-
const notStartedPhases = (0, validate_cjs_1.buildNotStartedPhaseVariants)(roadmapContent);
|
|
1643
|
-
for (const p of roadmapPhases) {
|
|
1644
|
-
const variants = (0, validate_cjs_1.phaseVariants)(p);
|
|
1645
|
-
const existsOnDisk = [...variants].some((v) => diskPhases.has(v));
|
|
1646
|
-
if (!existsOnDisk) {
|
|
1647
|
-
const isNotStarted = [...variants].some((v) => notStartedPhases.has(v));
|
|
1648
|
-
if (isNotStarted)
|
|
1649
|
-
continue;
|
|
1650
|
-
addIssue('warning', 'W006', `Phase ${p} in ROADMAP.md but no directory on disk`, 'Create phase directory or remove from roadmap');
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
for (const p of activeDiskPhases) {
|
|
1654
|
-
const variants = (0, validate_cjs_1.phaseVariants)(p);
|
|
1655
|
-
if (![...variants].some((v) => fullRoadmapPhaseVariants.has(v))) {
|
|
1656
|
-
addIssue('warning', 'W007', `Phase ${p} exists on disk but not in ROADMAP.md`, 'Add to roadmap or remove directory');
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
if (node_fs_1.default.existsSync(statePath) && node_fs_1.default.existsSync(roadmapPath)) {
|
|
1661
|
-
try {
|
|
1662
|
-
const stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
1663
|
-
const roadmapContentFull = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1664
|
-
const currentPhaseMatch = stateContent.match(/\*\*Current Phase:\*\*\s*(\S+)/i) ||
|
|
1665
|
-
stateContent.match(/Current Phase:\s*(\S+)/i);
|
|
1666
|
-
if (currentPhaseMatch) {
|
|
1667
|
-
const statePhase = currentPhaseMatch[1].replace(/^0+/, '');
|
|
1668
|
-
const phaseCheckboxRe = new RegExp(`-\\s*\\[x\\].*Phase\\s+0*${escapeRegex(statePhase)}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
1669
|
-
if (phaseCheckboxRe.test(roadmapContentFull)) {
|
|
1670
|
-
const stateStatus = stateContent.match(/\*\*Status:\*\*\s*(.+)/i);
|
|
1671
|
-
const statusVal = stateStatus ? stateStatus[1].trim().toLowerCase() : '';
|
|
1672
|
-
if (statusVal !== 'complete' && statusVal !== 'done') {
|
|
1673
|
-
addIssue('warning', 'W011', `STATE.md says current phase is ${statePhase} (status: ${statusVal || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`, `Run ${slash('progress')} to re-derive current position, or manually update STATE.md`);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
catch {
|
|
1679
|
-
/* intentionally empty — cross-validation is advisory */
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
if (node_fs_1.default.existsSync(configPath)) {
|
|
1683
|
-
try {
|
|
1684
|
-
const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
|
|
1685
|
-
const configParsed = JSON.parse(configRaw);
|
|
1686
|
-
const validStrategies = ['none', 'phase', 'milestone'];
|
|
1687
|
-
if (configParsed['branching_strategy'] &&
|
|
1688
|
-
!validStrategies.includes(configParsed['branching_strategy'])) {
|
|
1689
|
-
addIssue('warning', 'W012', `config.json: invalid branching_strategy "${configParsed['branching_strategy']}"`, `Valid values: ${validStrategies.join(', ')}`);
|
|
1690
|
-
}
|
|
1691
|
-
if (configParsed['context_window'] !== undefined) {
|
|
1692
|
-
const cw = configParsed['context_window'];
|
|
1693
|
-
if (typeof cw !== 'number' || cw <= 0 || !Number.isInteger(cw)) {
|
|
1694
|
-
addIssue('warning', 'W013', `config.json: context_window should be a positive integer, got "${cw}"`, 'Set to 200000 (default) or 1000000 (for 1M models)');
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
if (configParsed['phase_branch_template'] &&
|
|
1698
|
-
!configParsed['phase_branch_template'].includes('{phase}')) {
|
|
1699
|
-
addIssue('warning', 'W014', 'config.json: phase_branch_template missing {phase} placeholder', 'Template must include {phase} for phase number substitution');
|
|
1700
|
-
}
|
|
1701
|
-
if (configParsed['milestone_branch_template'] &&
|
|
1702
|
-
!configParsed['milestone_branch_template'].includes('{milestone}')) {
|
|
1703
|
-
addIssue('warning', 'W015', 'config.json: milestone_branch_template missing {milestone} placeholder', 'Template must include {milestone} for version substitution');
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
catch {
|
|
1707
|
-
/* parse error already caught in Check 5 */
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
try {
|
|
1711
|
-
const worktreeHealth = inspectWorktreeHealth(cwd, { staleAfterMs: 60 * 60 * 1000 }, { execGit: shell_command_projection_cjs_1.execGit, existsSync: node_fs_1.default.existsSync, statSync: node_fs_1.default.statSync });
|
|
1712
|
-
if (!worktreeHealth['ok']) {
|
|
1713
|
-
if (worktreeHealth['reason'] === 'git_timed_out') {
|
|
1714
|
-
addIssue('warning', 'W020', 'Worktree health check degraded: git worktree list timed out after 10s — orphan/stale worktrees could not be inspected', 'Run: git worktree list --porcelain to diagnose; check for .git/index.lock or a hung git process');
|
|
1715
|
-
}
|
|
1716
|
-
if (worktreeHealth['reason'] === 'git_list_failed') {
|
|
1717
|
-
addIssue('warning', 'W020', 'Worktree health check degraded: git worktree list failed — orphan/stale worktrees could not be inspected', 'Run: git worktree list --porcelain to diagnose; check git repository state and permissions');
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
else {
|
|
1721
|
-
for (const finding of worktreeHealth['findings']) {
|
|
1722
|
-
if (finding['kind'] === 'orphan') {
|
|
1723
|
-
addIssue('warning', 'W017', `Orphan git worktree: ${finding['path']} (path no longer exists on disk)`, 'Run: git worktree prune');
|
|
1724
|
-
continue;
|
|
1725
|
-
}
|
|
1726
|
-
if (finding['kind'] === 'stale') {
|
|
1727
|
-
// Do not flag the active session's worktree — removing it would be harmful.
|
|
1728
|
-
const worktreePath = finding['path'];
|
|
1729
|
-
const activeCwd = process.cwd();
|
|
1730
|
-
const normalizedWorktree = node_path_1.default.resolve(worktreePath);
|
|
1731
|
-
const normalizedCwd = node_path_1.default.resolve(activeCwd);
|
|
1732
|
-
// Skip if the worktree IS the cwd or is an ancestor of it.
|
|
1733
|
-
const isActiveWorktree = normalizedCwd === normalizedWorktree ||
|
|
1734
|
-
normalizedCwd.startsWith(normalizedWorktree + node_path_1.default.sep);
|
|
1735
|
-
if (isActiveWorktree)
|
|
1736
|
-
continue;
|
|
1737
|
-
addIssue('warning', 'W017', `Stale git worktree: ${worktreePath} (last modified ${finding['ageMinutes']} minutes ago)`, `Run: git worktree remove ${worktreePath} --force`);
|
|
1738
|
-
continue;
|
|
1739
|
-
}
|
|
1740
|
-
// #3050/#3057 (B5): a 'unverified' finding means existsSync confirmed
|
|
1741
|
-
// the worktree is present but statSync threw, so orphan/stale status
|
|
1742
|
-
// could not be determined for THIS entry — it must not be silently
|
|
1743
|
-
// dropped (that would be the exact fail-open the row exists to close).
|
|
1744
|
-
if (finding['kind'] === 'unverified') {
|
|
1745
|
-
addIssue('warning', 'W020', `Worktree health check degraded: could not stat ${finding['path']} — presence/staleness could not be verified`, 'Check filesystem permissions on the worktree path, or investigate why statSync failed for it');
|
|
1508
|
+
const stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
1509
|
+
const fm = extractFrontmatter(stateContent);
|
|
1510
|
+
const freshness = readStateHeadFreshness(cwd, fm['state_head']);
|
|
1511
|
+
if (freshness.commits_behind !== null &&
|
|
1512
|
+
freshness.commits_behind >= STATE_HEAD_ADVISORY_COMMITS) {
|
|
1513
|
+
warnings.push({
|
|
1514
|
+
code: 'W024',
|
|
1515
|
+
message: `STATE.md was written ${freshness.commits_behind} commits ago (at ${freshness.state_head}) — treat its contents as approximate`,
|
|
1516
|
+
fix: 'Re-read the current phase artifacts before relying on STATE.md, or run a GSD command that refreshes it',
|
|
1517
|
+
repairable: false,
|
|
1518
|
+
});
|
|
1746
1519
|
}
|
|
1747
1520
|
}
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
catch {
|
|
1751
|
-
/* git worktree not available or not a git repo — skip silently */
|
|
1752
|
-
}
|
|
1753
|
-
try {
|
|
1754
|
-
const phaseConvention = (() => {
|
|
1755
|
-
if (!node_fs_1.default.existsSync(configPath))
|
|
1756
|
-
return null;
|
|
1757
|
-
try {
|
|
1758
|
-
const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
|
|
1759
|
-
const configParsed = JSON.parse(configRaw);
|
|
1760
|
-
return configParsed['phase_id_convention'] || null;
|
|
1761
|
-
}
|
|
1762
1521
|
catch {
|
|
1763
|
-
|
|
1764
|
-
}
|
|
1765
|
-
})();
|
|
1766
|
-
if (phaseConvention === 'milestone-prefixed') {
|
|
1767
|
-
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
1768
|
-
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1769
|
-
const mismatches = checkMilestonePrefixMismatches(roadmapContent, {
|
|
1770
|
-
getMilestoneFromPhaseId: getMilestoneFromPhaseId,
|
|
1771
|
-
});
|
|
1772
|
-
for (const mm of mismatches) {
|
|
1773
|
-
addIssue('warning', 'W021', `Phase ${mm.phaseId}: integer prefix implies ${mm.expectedMilestone} but listed under ${mm.foundInMilestone}`, 'Run `gsd-tools roadmap upgrade --convention milestone-prefixed` to migrate (dry-run by default)');
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
catch {
|
|
1779
|
-
/* W021 check is advisory — skip on error */
|
|
1780
|
-
}
|
|
1781
|
-
const milestonesPath = node_path_1.default.join(rootBase, 'MILESTONES.md');
|
|
1782
|
-
const milestonesArchiveDir = node_path_1.default.join(rootBase, 'milestones');
|
|
1783
|
-
const missingFromRegistry = [];
|
|
1784
|
-
try {
|
|
1785
|
-
if (node_fs_1.default.existsSync(milestonesArchiveDir)) {
|
|
1786
|
-
const archiveFiles = node_fs_1.default.readdirSync(milestonesArchiveDir);
|
|
1787
|
-
const archivedVersions = archiveFiles
|
|
1788
|
-
.map((f) => f.match(/^(v\d+\.\d+(?:\.\d+)?)-ROADMAP\.md$/))
|
|
1789
|
-
.filter(Boolean)
|
|
1790
|
-
.map((m) => m[1]);
|
|
1791
|
-
if (archivedVersions.length > 0) {
|
|
1792
|
-
const registryContent = node_fs_1.default.existsSync(milestonesPath)
|
|
1793
|
-
? node_fs_1.default.readFileSync(milestonesPath, 'utf-8')
|
|
1794
|
-
: '';
|
|
1795
|
-
for (const ver of archivedVersions) {
|
|
1796
|
-
if (!registryContent.includes(`## ${ver}`)) {
|
|
1797
|
-
missingFromRegistry.push(ver);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
if (missingFromRegistry.length > 0) {
|
|
1801
|
-
addIssue('warning', 'W018', `MILESTONES.md missing ${missingFromRegistry.length} archived milestone(s): ${missingFromRegistry.join(', ')}`, `Run ${slash('health')} --backfill to synthesize missing entries from archive snapshots`, true);
|
|
1802
|
-
repairs.push('backfillMilestones');
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
catch {
|
|
1808
|
-
/* intentionally empty — milestone sync check is advisory */
|
|
1809
|
-
}
|
|
1810
|
-
try {
|
|
1811
|
-
const entries = node_fs_1.default.readdirSync(rootBase, { withFileTypes: true });
|
|
1812
|
-
for (const entry of entries) {
|
|
1813
|
-
if (!entry.isFile())
|
|
1814
|
-
continue;
|
|
1815
|
-
if (!entry.name.endsWith('.md'))
|
|
1816
|
-
continue;
|
|
1817
|
-
if (!(0, artifacts_cjs_1.isCanonicalPlanningFile)(entry.name)) {
|
|
1818
|
-
addIssue('warning', 'W019', `Unrecognized .planning/ file: ${entry.name} — not a canonical GSD artifact`, 'Move to .planning/milestones/ archive subdir or delete if stale. See templates/README.md for the canonical artifact list.', false);
|
|
1522
|
+
/* intentionally empty — W024 is advisory */
|
|
1819
1523
|
}
|
|
1820
1524
|
}
|
|
1821
1525
|
}
|
|
1822
|
-
catch {
|
|
1823
|
-
/* artifact check is advisory — skip on error */
|
|
1824
|
-
}
|
|
1825
|
-
try {
|
|
1826
|
-
if (node_fs_1.default.existsSync(statePath) && node_fs_1.default.existsSync(roadmapPath)) {
|
|
1827
|
-
const stateRaw = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
1828
|
-
const statusMatch = stateRaw.match(/^status:\s*(.+)/im);
|
|
1829
|
-
const stateStatus = statusMatch ? statusMatch[1].trim().toLowerCase() : '';
|
|
1830
|
-
const isMarkedComplete = /milestone complete|archived/.test(stateStatus);
|
|
1831
|
-
if (isMarkedComplete) {
|
|
1832
|
-
const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1833
|
-
const scopedContent = extractCurrentMilestone(roadmapRaw, cwd);
|
|
1834
|
-
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1835
|
-
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
1836
|
-
const unstarted = [];
|
|
1837
|
-
let pm;
|
|
1838
|
-
// Non-hoisted: load-order matters (circular dep guard)
|
|
1839
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
1840
|
-
const planningWorkspace2 = require('./planning-workspace.cjs');
|
|
1841
|
-
const phasesDir2 = planningWorkspace2.planningPaths(cwd).phases;
|
|
1842
|
-
const phaseDirNames2 = (() => {
|
|
1843
|
-
try {
|
|
1844
|
-
return node_fs_1.default
|
|
1845
|
-
.readdirSync(phasesDir2, { withFileTypes: true })
|
|
1846
|
-
.filter((e) => e.isDirectory())
|
|
1847
|
-
.map((e) => e.name);
|
|
1848
|
-
}
|
|
1849
|
-
catch {
|
|
1850
|
-
return [];
|
|
1851
|
-
}
|
|
1852
|
-
})();
|
|
1853
|
-
while ((pm = phasePattern.exec(scopedContent)) !== null) {
|
|
1854
|
-
const phaseNum = pm[1];
|
|
1855
|
-
const normalizedPh = normalizePhaseName(phaseNum);
|
|
1856
|
-
const hasDirectory = phaseDirNames2.some((d) => phaseTokenMatches(d, normalizedPh));
|
|
1857
|
-
if (!hasDirectory) {
|
|
1858
|
-
unstarted.push(phaseNum);
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
if (unstarted.length > 0) {
|
|
1862
|
-
addIssue('warning', 'W021', `STATE says milestone complete but ROADMAP lists ${unstarted.length} unstarted phase(s) (e.g. Phase ${unstarted[0]})`, 'Run validate consistency or re-run complete-milestone after verifying all phases are done');
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
catch {
|
|
1868
|
-
/* W021 check is advisory — skip on error */
|
|
1869
|
-
}
|
|
1870
1526
|
// ─── Perform repairs if requested ─────────────────────────────────────────
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
milestone_branch_template: CONFIG_DEFAULTS.milestone_branch_template,
|
|
1885
|
-
quick_branch_template: CONFIG_DEFAULTS.quick_branch_template,
|
|
1886
|
-
workflow: {
|
|
1887
|
-
research: CONFIG_DEFAULTS.research,
|
|
1888
|
-
plan_check: CONFIG_DEFAULTS.plan_checker,
|
|
1889
|
-
verifier: CONFIG_DEFAULTS.verifier,
|
|
1890
|
-
nyquist_validation: CONFIG_DEFAULTS.nyquist_validation,
|
|
1891
|
-
},
|
|
1892
|
-
parallelization: CONFIG_DEFAULTS.parallelization,
|
|
1893
|
-
brave_search: CONFIG_DEFAULTS.brave_search,
|
|
1894
|
-
};
|
|
1895
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(defaults, null, 2));
|
|
1896
|
-
repairActions.push({ action: repair, success: true, path: 'config.json' });
|
|
1897
|
-
break;
|
|
1898
|
-
}
|
|
1899
|
-
case 'regenerateState': {
|
|
1900
|
-
if (node_fs_1.default.existsSync(statePath)) {
|
|
1901
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
1902
|
-
const backupPath = `${statePath}.bak-${timestamp}`;
|
|
1903
|
-
node_fs_1.default.copyFileSync(statePath, backupPath);
|
|
1904
|
-
repairActions.push({ action: 'backupState', success: true, path: backupPath });
|
|
1905
|
-
}
|
|
1906
|
-
const milestone = getMilestoneInfo(cwd);
|
|
1907
|
-
const projectRef = node_path_1.default
|
|
1908
|
-
.relative(cwd, node_path_1.default.join(rootBase, 'PROJECT.md'))
|
|
1909
|
-
.split(node_path_1.default.sep)
|
|
1910
|
-
.join('/');
|
|
1911
|
-
let stateContent = `# Session State\n\n`;
|
|
1912
|
-
stateContent += `## Project Reference\n\n`;
|
|
1913
|
-
stateContent += `See: ${projectRef}\n\n`;
|
|
1914
|
-
stateContent += `## Position\n\n`;
|
|
1915
|
-
stateContent += `**Milestone:** ${milestone.version} ${milestone.name}\n`;
|
|
1916
|
-
stateContent += `**Current phase:** (determining...)\n`;
|
|
1917
|
-
stateContent += `**Status:** Resuming\n\n`;
|
|
1918
|
-
stateContent += `## Session Log\n\n`;
|
|
1919
|
-
stateContent += `- ${clock_cjs_1.realClock.localToday()}: STATE.md regenerated by ${slash('health')} --repair\n`;
|
|
1920
|
-
writeStateMd(statePath, stateContent, cwd);
|
|
1921
|
-
repairActions.push({ action: repair, success: true, path: 'STATE.md' });
|
|
1922
|
-
break;
|
|
1923
|
-
}
|
|
1924
|
-
case 'addNyquistKey': {
|
|
1925
|
-
if (node_fs_1.default.existsSync(configPath)) {
|
|
1926
|
-
try {
|
|
1927
|
-
const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
|
|
1928
|
-
const configParsed = JSON.parse(configRaw);
|
|
1929
|
-
if (!configParsed['workflow'])
|
|
1930
|
-
configParsed['workflow'] = {};
|
|
1931
|
-
const wf = configParsed['workflow'];
|
|
1932
|
-
if (wf['nyquist_validation'] === undefined) {
|
|
1933
|
-
wf['nyquist_validation'] = true;
|
|
1934
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(configParsed, null, 2));
|
|
1935
|
-
}
|
|
1936
|
-
repairActions.push({ action: repair, success: true, path: 'config.json' });
|
|
1937
|
-
}
|
|
1938
|
-
catch (err) {
|
|
1939
|
-
repairActions.push({
|
|
1940
|
-
action: repair,
|
|
1941
|
-
success: false,
|
|
1942
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1943
|
-
});
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
break;
|
|
1947
|
-
}
|
|
1948
|
-
case 'addAiIntegrationPhaseKey': {
|
|
1949
|
-
if (node_fs_1.default.existsSync(configPath)) {
|
|
1950
|
-
try {
|
|
1951
|
-
const configRaw = node_fs_1.default.readFileSync(configPath, 'utf-8');
|
|
1952
|
-
const configParsed = JSON.parse(configRaw);
|
|
1953
|
-
if (!configParsed['workflow'])
|
|
1954
|
-
configParsed['workflow'] = {};
|
|
1955
|
-
const wf = configParsed['workflow'];
|
|
1956
|
-
if (wf['ai_integration_phase'] === undefined) {
|
|
1957
|
-
wf['ai_integration_phase'] = true;
|
|
1958
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(configParsed, null, 2));
|
|
1959
|
-
}
|
|
1960
|
-
repairActions.push({ action: repair, success: true, path: 'config.json' });
|
|
1961
|
-
}
|
|
1962
|
-
catch (err) {
|
|
1963
|
-
repairActions.push({
|
|
1964
|
-
action: repair,
|
|
1965
|
-
success: false,
|
|
1966
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1967
|
-
});
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
break;
|
|
1971
|
-
}
|
|
1972
|
-
case 'backfillMilestones': {
|
|
1973
|
-
if (!options['backfill'] && !options['repair'])
|
|
1974
|
-
break;
|
|
1975
|
-
const today = clock_cjs_1.realClock.localToday();
|
|
1976
|
-
let backfilled = 0;
|
|
1977
|
-
for (const ver of missingFromRegistry) {
|
|
1978
|
-
try {
|
|
1979
|
-
const snapshotPath = node_path_1.default.join(milestonesArchiveDir, `${ver}-ROADMAP.md`);
|
|
1980
|
-
const snapshot = (0, shell_command_projection_cjs_1.platformReadSync)(snapshotPath);
|
|
1981
|
-
const titleMatch = snapshot && snapshot.match(/^#\s+(.+)$/m);
|
|
1982
|
-
const milestoneName = titleMatch
|
|
1983
|
-
? titleMatch[1].replace(/^Milestone\s+/i, '').replace(/^v[\d.]+\s*/, '').trim()
|
|
1984
|
-
: ver;
|
|
1985
|
-
const entry = `## ${ver}${milestoneName && milestoneName !== ver ? ` ${milestoneName}` : ''} (Backfilled: ${today})\n\n**Note:** Synthesized from archive snapshot by \`${slash('health')} --backfill\`. Original completion date unknown.\n\n---\n\n`;
|
|
1986
|
-
const milestonesContent = node_fs_1.default.existsSync(milestonesPath)
|
|
1987
|
-
? node_fs_1.default.readFileSync(milestonesPath, 'utf-8')
|
|
1988
|
-
: '';
|
|
1989
|
-
if (!milestonesContent.trim()) {
|
|
1990
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, `# Milestones\n\n${entry}`);
|
|
1991
|
-
}
|
|
1992
|
-
else {
|
|
1993
|
-
const headerMatch = milestonesContent.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
|
|
1994
|
-
if (headerMatch) {
|
|
1995
|
-
const header = headerMatch[1];
|
|
1996
|
-
const rest = milestonesContent.slice(header.length);
|
|
1997
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, header + entry + rest);
|
|
1998
|
-
}
|
|
1999
|
-
else {
|
|
2000
|
-
(0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, entry + milestonesContent);
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
backfilled++;
|
|
2004
|
-
}
|
|
2005
|
-
catch {
|
|
2006
|
-
/* intentionally empty — partial backfill is acceptable */
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
repairActions.push({
|
|
2010
|
-
action: repair,
|
|
2011
|
-
success: true,
|
|
2012
|
-
detail: `Backfilled ${backfilled} milestone(s) into MILESTONES.md`,
|
|
2013
|
-
});
|
|
2014
|
-
break;
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
catch (err) {
|
|
2019
|
-
repairActions.push({
|
|
2020
|
-
action: repair,
|
|
2021
|
-
success: false,
|
|
2022
|
-
error: err instanceof Error ? err.message : String(err),
|
|
2023
|
-
});
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
1527
|
+
// `applyRepairs` internally no-ops (produces zero `details` rows, no
|
|
1528
|
+
// filesystem writes) when both `repair` and `backfill` are falsy, so this
|
|
1529
|
+
// call is unconditional — mirroring the original's own
|
|
1530
|
+
// `if (options['repair'] && repairs.length > 0)` gate without needing to
|
|
1531
|
+
// duplicate that condition here. `backfill` is threaded through as its own
|
|
1532
|
+
// boolean (not folded into `repair`), which is what makes `--backfill`
|
|
1533
|
+
// alone now actually trigger `backfillMilestones` — the disclosed latent-
|
|
1534
|
+
// bug fix from `verify.cts:2504`'s previously-unreachable inner gate (see
|
|
1535
|
+
// design doc "Known limits").
|
|
1536
|
+
const repairResult = applyRepairs(cwd, diagnostics, Boolean(options['repair']), Boolean(options['backfill']));
|
|
1537
|
+
// The legacy `repairs_performed` shape never carried a `code` field —
|
|
1538
|
+
// strip it before it reaches JSON output.
|
|
1539
|
+
const repairActions = repairResult.details.map(({ code: _code, ...rest }) => rest);
|
|
2027
1540
|
let status;
|
|
2028
1541
|
if (errors.length > 0) {
|
|
2029
1542
|
status = 'broken';
|
|
@@ -2047,8 +1560,28 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
2047
1560
|
return result;
|
|
2048
1561
|
}
|
|
2049
1562
|
function cmdValidateAgents(cwd, raw) {
|
|
2050
|
-
const
|
|
1563
|
+
const runtime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
1564
|
+
const agentStatus = checkAgentsInstalled(runtime, cwd);
|
|
2051
1565
|
const expected = Object.keys(MODEL_PROFILES);
|
|
1566
|
+
// #3242 ADR-2313 D6 — additive: validates posture (never an Anthropic-flavored
|
|
1567
|
+
// model or an orphaned reasoning-effort pin in a Codex agent .toml), not just
|
|
1568
|
+
// presence. checkAgentsInstalled above is untouched.
|
|
1569
|
+
const codexPosture = checkCodexModelPosture(runtime, cwd);
|
|
1570
|
+
// #3897 rung 3 (ADR-3473 §8.3 criterion 3) — sibling posture check, additive
|
|
1571
|
+
// alongside codex_posture (never nested inside it, never replacing it): a
|
|
1572
|
+
// TOML's `sandbox_mode` disagreeing with its role's derived expectation is a
|
|
1573
|
+
// different defect class than an Anthropic-flavored model. Same short-circuits,
|
|
1574
|
+
// same read-only posture as checkCodexModelPosture above; checkAgentsInstalled
|
|
1575
|
+
// and codex_posture are both left untouched.
|
|
1576
|
+
//
|
|
1577
|
+
// Failure semantics: matches the checkCodexModelPosture precedent exactly.
|
|
1578
|
+
// Neither posture check makes `validate agents` exit non-zero on its own —
|
|
1579
|
+
// both are report-only fields inspected by the caller (or a human) via the
|
|
1580
|
+
// JSON payload. A `validate` verb whose two posture checks disagreed on
|
|
1581
|
+
// fatality would be its own defect (#3897 dispatch note); this keeps them
|
|
1582
|
+
// consistent rather than inventing new exit-code behavior for only one of
|
|
1583
|
+
// the two siblings.
|
|
1584
|
+
const sandboxPosture = checkCodexSandboxPosture(runtime, cwd);
|
|
2052
1585
|
output({
|
|
2053
1586
|
agents_dir: agentStatus.agents_dir,
|
|
2054
1587
|
agents_found: agentStatus.agents_installed,
|
|
@@ -2056,6 +1589,8 @@ function cmdValidateAgents(cwd, raw) {
|
|
|
2056
1589
|
missing: agentStatus.missing_agents,
|
|
2057
1590
|
incomplete: agentStatus.incomplete_agents,
|
|
2058
1591
|
expected,
|
|
1592
|
+
codex_posture: codexPosture,
|
|
1593
|
+
sandbox_posture: sandboxPosture,
|
|
2059
1594
|
}, raw);
|
|
2060
1595
|
}
|
|
2061
1596
|
function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
@@ -2069,21 +1604,20 @@ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
|
2069
1604
|
output({ block: false, drift_detected: false, blocking: false, message: 'No phases directory' }, raw);
|
|
2070
1605
|
return;
|
|
2071
1606
|
}
|
|
2072
|
-
// Resolve the phase directory with the canonical phase-
|
|
2073
|
-
// (phase-id.cjs), not a naive substring test. A bare
|
|
2074
|
-
// lets a non-existent phase silently match a different
|
|
2075
|
-
// name merely contains the requested token (e.g. "1"
|
|
2076
|
-
// making the drift gate inspect the wrong phase.
|
|
2077
|
-
//
|
|
1607
|
+
// Resolve the phase directory with the canonical phase-directory matcher
|
|
1608
|
+
// (phase-id.cjs::matchPhaseDirs), not a naive substring test. A bare
|
|
1609
|
+
// `.includes(phaseArg)` lets a non-existent phase silently match a different
|
|
1610
|
+
// phase whose directory name merely contains the requested token (e.g. "1"
|
|
1611
|
+
// matching "11-expansion"), making the drift gate inspect the wrong phase.
|
|
1612
|
+
// This shares the one selection rule with find-phase / verify
|
|
1613
|
+
// phase-completeness rather than restating it. (#1571, #2528)
|
|
2078
1614
|
let phaseDir = null;
|
|
2079
1615
|
const normalizedPhase = normalizePhaseName(phaseArg);
|
|
2080
1616
|
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
1617
|
+
const dirNames = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1618
|
+
const drift = matchPhaseDirs(dirNames, normalizedPhase).matches[0];
|
|
1619
|
+
if (drift)
|
|
1620
|
+
phaseDir = node_path_1.default.join(phasesDir, drift);
|
|
2087
1621
|
if (!phaseDir) {
|
|
2088
1622
|
const exact = node_path_1.default.join(phasesDir, phaseArg);
|
|
2089
1623
|
if (node_fs_1.default.existsSync(exact))
|
|
@@ -2093,8 +1627,14 @@ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
|
2093
1627
|
output({ block: false, drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
|
|
2094
1628
|
return;
|
|
2095
1629
|
}
|
|
1630
|
+
// #3183: canonical LIVE plan/summary sets (root+nested,
|
|
1631
|
+
// status: superseded EXCLUDED) from the single owner, rather than a
|
|
1632
|
+
// root-only readdirSync filter — a superseded plan's claimed
|
|
1633
|
+
// files_modified is no longer treated as an expected drift target, and
|
|
1634
|
+
// nested (#3139 layout) plans/summaries are no longer invisible to the
|
|
1635
|
+
// drift check.
|
|
1636
|
+
const { planFiles, summaryFiles } = planScanMod.scanPhasePlans(phaseDir);
|
|
2096
1637
|
const allFiles = [];
|
|
2097
|
-
const planFiles = node_fs_1.default.readdirSync(phaseDir).filter((f) => f.endsWith('-PLAN.md'));
|
|
2098
1638
|
for (const pf of planFiles) {
|
|
2099
1639
|
const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, pf), 'utf-8');
|
|
2100
1640
|
const fmMatch = content.match(/files_modified:\s*\[([^\]]{0,8000})\]/);
|
|
@@ -2104,7 +1644,6 @@ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
|
2104
1644
|
}
|
|
2105
1645
|
}
|
|
2106
1646
|
let executionLog = '';
|
|
2107
|
-
const summaryFiles = node_fs_1.default.readdirSync(phaseDir).filter((f) => f.endsWith('-SUMMARY.md'));
|
|
2108
1647
|
for (const sf of summaryFiles) {
|
|
2109
1648
|
executionLog += node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, sf), 'utf-8') + '\n';
|
|
2110
1649
|
}
|
|
@@ -2285,9 +1824,5 @@ module.exports = {
|
|
|
2285
1824
|
cmdValidateAgents,
|
|
2286
1825
|
cmdVerifySchemaDrift,
|
|
2287
1826
|
cmdVerifyCodebaseDrift,
|
|
2288
|
-
|
|
2289
|
-
// the validate command, which runs in a subprocess — an fs monkeypatch in the
|
|
2290
|
-
// test process cannot reach it. Exposed under a leading underscore so the
|
|
2291
|
-
// permission-error path can be unit-tested directly (no chmod 0o000).
|
|
2292
|
-
_listMilestoneArchiveDirs: listMilestoneArchiveDirs,
|
|
1827
|
+
STATE_HEAD_ADVISORY_COMMITS,
|
|
2293
1828
|
};
|