@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
|
@@ -12,35 +12,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
14
|
const clock_cjs_1 = require("./clock.cjs");
|
|
15
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
16
|
+
const text_lines_cjs_1 = require("./text-lines.cjs");
|
|
15
17
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
18
|
const ioMod = require("./io.cjs");
|
|
17
|
-
const { output, error } = ioMod;
|
|
19
|
+
const { output, error, formatDiagnosticToken } = ioMod;
|
|
18
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
21
|
const phaseIdMod = require("./phase-id.cjs");
|
|
20
|
-
const {
|
|
22
|
+
const { normalizePhaseName, phaseMarkdownRegexSource, matchPhaseDirs, stripProjectCodePrefix, OPTIONAL_PHASE_TAG_SOURCE, roadmapPhaseLookupSources, isSentinelPhaseId, scopeToPhase } = phaseIdMod;
|
|
21
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
24
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
23
|
-
const { findPhaseInternal } = phaseLocatorMod;
|
|
25
|
+
const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
28
|
+
const { SCOPE } = planningScopeMod;
|
|
24
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
30
|
const roadmapParserModule = require("./roadmap-parser.cjs");
|
|
26
|
-
const { stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = roadmapParserModule;
|
|
31
|
+
const { stripShippedMilestones, extractCurrentMilestone, extractCurrentMilestoneScoped, replaceInCurrentMilestone, listMilestoneHeadings, scanMilestonePhaseIds, collectTablePhaseRows } = roadmapParserModule;
|
|
27
32
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
28
33
|
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
34
|
+
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
29
35
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
30
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
37
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
32
38
|
const { planningPaths, withPlanningLock, findContextMdIn } = planningWorkspace;
|
|
39
|
+
// #3641: milestone-scope's convention resolution reads the project config
|
|
40
|
+
// (no cycle — config-loader does not import this module).
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
+
const configLoaderForScope = require("./config-loader.cjs");
|
|
43
|
+
const { loadConfig: loadConfigForScope } = configLoaderForScope;
|
|
33
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
45
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
35
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
47
|
const coreUtils = require("./core-utils.cjs");
|
|
37
|
-
const { countMatchedSummaries } = coreUtils;
|
|
48
|
+
const { countMatchedSummaries, findUnsummarizedPlans } = coreUtils;
|
|
38
49
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
50
|
const frontmatter = require("./frontmatter.cjs");
|
|
40
51
|
const { extractFrontmatter, parseMustHavesBlock } = frontmatter;
|
|
41
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
53
|
const verificationMod = require("./verification.cjs");
|
|
43
|
-
const {
|
|
54
|
+
const { isPhaseComplete } = verificationMod;
|
|
44
55
|
// ─── coerceTruthToString ──────────────────────────────────────────────────────
|
|
45
56
|
/**
|
|
46
57
|
* Coerce an arbitrary YAML scalar/object into a string for cross-cutting
|
|
@@ -83,27 +94,53 @@ function countPhasePlansAndSummaries(phaseDir) {
|
|
|
83
94
|
// hasContext and hasResearch are not plan-scan concerns — read the directory
|
|
84
95
|
// once and share the listing for all non-plan metadata that cmdRoadmapAnalyze needs.
|
|
85
96
|
let phaseFiles = [];
|
|
97
|
+
// #3885 (ADR-3473 §8.5): distinguish "genuinely absent" (ENOENT) from
|
|
98
|
+
// "could not read" (EACCES/EIO/...) — the collapse of both to an empty
|
|
99
|
+
// listing is exactly the defect class this item closes. Mirrors
|
|
100
|
+
// core-utils.cts's getPhaseFileStats / phase-locator.cts's
|
|
101
|
+
// listMilestonePhaseDirs SCOPE.UNREADABLE discriminator.
|
|
102
|
+
let contextReadError = null;
|
|
86
103
|
try {
|
|
87
104
|
phaseFiles = node_fs_1.default.readdirSync(phaseDir);
|
|
88
105
|
}
|
|
89
|
-
catch {
|
|
106
|
+
catch (err) {
|
|
107
|
+
const code = err?.code;
|
|
108
|
+
if (code !== 'ENOENT') {
|
|
109
|
+
contextReadError = `Could not read phase directory ${formatDiagnosticToken(phaseDir)}: ${formatDiagnosticToken(err?.message ?? String(err))}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// #3511: scope the raw listing to this phase dir before the
|
|
113
|
+
// phase-numbered-artifact predicates (hasContext/hasResearch) — planCount/
|
|
114
|
+
// summaryCount above stay on scanPhasePlans's own unscoped listing since a
|
|
115
|
+
// PLAN/SUMMARY leading number is a plan sequence number, not a phase
|
|
116
|
+
// number. Mirrors core-utils.cts's getPhaseFileStats.
|
|
117
|
+
const scopedFiles = scopeToPhase(phaseFiles, node_path_1.default.basename(phaseDir));
|
|
90
118
|
return {
|
|
91
119
|
planCount,
|
|
92
120
|
summaryCount,
|
|
93
|
-
hasContext: findContextMdIn(
|
|
94
|
-
hasResearch:
|
|
121
|
+
hasContext: findContextMdIn(scopedFiles) !== null,
|
|
122
|
+
hasResearch: scopedFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
|
|
123
|
+
contextReadError,
|
|
95
124
|
};
|
|
96
125
|
}
|
|
97
126
|
// `phaseMarkdownRegexSource` lives in phase-id.cjs (#3537) and is imported above.
|
|
98
127
|
// ─── searchPhaseInContent ─────────────────────────────────────────────────────
|
|
128
|
+
/**
|
|
129
|
+
* Build the phase-heading regex used by `searchPhaseInContent` for a given
|
|
130
|
+
* pre-escaped phase source. Extracted (#3412) so tests can assert against the
|
|
131
|
+
* exact production pattern instead of hand-duplicating it.
|
|
132
|
+
* #1729: OPTIONAL_PHASE_TAG_SOURCE after the number tolerates a pre-colon ( ) tag.
|
|
133
|
+
*/
|
|
134
|
+
function buildPhaseHeadingRegex(escapedPhase) {
|
|
135
|
+
return new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
|
|
136
|
+
}
|
|
99
137
|
/**
|
|
100
138
|
* Search for a phase header (and its section) within the given content string.
|
|
101
139
|
* Returns a result object if found (either a full match or a malformed_roadmap
|
|
102
140
|
* checklist-only match), or null if the phase is not present at all.
|
|
103
141
|
*/
|
|
104
142
|
function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
105
|
-
|
|
106
|
-
const headingPattern = new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}:\\s*(.+)$`, 'i');
|
|
143
|
+
const headingPattern = buildPhaseHeadingRegex(escapedPhase);
|
|
107
144
|
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
108
145
|
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
|
|
109
146
|
const headerMatch = headingIndex === -1 ? null : headings[headingIndex].text.match(headingPattern);
|
|
@@ -173,7 +210,8 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
|
|
|
173
210
|
* phase resolution as `roadmap.get-phase` — not a milestone-only subset.
|
|
174
211
|
*/
|
|
175
212
|
function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
176
|
-
|
|
213
|
+
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
214
|
+
if (isSentinelPhaseId(stripProjectCodePrefix(phaseNum)))
|
|
177
215
|
return null;
|
|
178
216
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
179
217
|
// Read directly rather than gating on fs.existsSync: existsSync returns false
|
|
@@ -206,7 +244,8 @@ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
|
|
|
206
244
|
}
|
|
207
245
|
// ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
|
|
208
246
|
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
209
|
-
|
|
247
|
+
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
248
|
+
if (isSentinelPhaseId(stripProjectCodePrefix(phaseNum))) {
|
|
210
249
|
output({ found: false, phase_number: phaseNum }, raw, '');
|
|
211
250
|
return;
|
|
212
251
|
}
|
|
@@ -242,6 +281,17 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
242
281
|
if (!malformed)
|
|
243
282
|
malformed = (milestoneResult?.error ? milestoneResult : (fullResult?.error ? fullResult : null));
|
|
244
283
|
}
|
|
284
|
+
// #3577: no heading or checklist entry matched — fall back to a
|
|
285
|
+
// markdown-table row declaration (the same last-resort tier
|
|
286
|
+
// getRoadmapPhaseInternal gained). Zero-pad-tolerant id compare (#3572
|
|
287
|
+
// lesson: the declared form may be padded).
|
|
288
|
+
const stripPad = (s) => s.replace(/^0+(?=.)/, '');
|
|
289
|
+
const tableHit = collectTablePhaseRows(milestoneContent).find((tr) => stripPad(tr.id) === stripPad(phaseNum))
|
|
290
|
+
?? collectTablePhaseRows(fullContent).find((tr) => stripPad(tr.id) === stripPad(phaseNum));
|
|
291
|
+
if (tableHit) {
|
|
292
|
+
output({ found: true, phase_number: phaseNum, phase_name: tableHit.name ?? `Phase ${tableHit.id}`, goal: null, section: tableHit.row.trim() }, raw, tableHit.row.trim());
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
245
295
|
if (malformed) {
|
|
246
296
|
output(malformed, raw, '');
|
|
247
297
|
return;
|
|
@@ -252,16 +302,16 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
252
302
|
error('Failed to read ROADMAP.md: ' + e.message);
|
|
253
303
|
}
|
|
254
304
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
305
|
+
/**
|
|
306
|
+
* #3165: scan `content` for phase-detail headings (`##/###/#### Phase N: Name`)
|
|
307
|
+
* and enrich each with its on-disk plan/summary/completion status and ROADMAP
|
|
308
|
+
* checkbox. Pure extraction over `content` + the pre-built `phaseDirNames`
|
|
309
|
+
* lookup index — no milestone windowing of its own; the caller chooses the
|
|
310
|
+
* content (scoped window or fallback). Extracted verbatim from
|
|
311
|
+
* `cmdRoadmapAnalyze`'s former inline loop so the fallback re-runs the EXACT
|
|
312
|
+
* same enrichment, not a second derivation.
|
|
313
|
+
*/
|
|
314
|
+
function collectAnalyzePhases(content, phasesDir, phaseDirNames) {
|
|
265
315
|
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
|
266
316
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
267
317
|
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
@@ -272,29 +322,9 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
272
322
|
const phasePattern = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]{0,200}\))?\s*:\s*([^\n]+)/gi;
|
|
273
323
|
const phases = [];
|
|
274
324
|
let match;
|
|
275
|
-
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
|
|
276
|
-
// phases. They legitimately have no directory and must never be surfaced as
|
|
277
|
-
// current/next phase or counted in phase_count. Mirrors the engine-wide
|
|
278
|
-
// sentinel convention (phase-id getMilestoneFromPhaseId, roadmap-command-router
|
|
279
|
-
// SENTINELS, the #1445 /^999/ progress filters). (#1580)
|
|
280
|
-
const isSentinelPhase = (num) => {
|
|
281
|
-
const major = parseInt(num, 10);
|
|
282
|
-
return major === 0 || major === 999;
|
|
283
|
-
};
|
|
284
|
-
// Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
|
|
285
|
-
const _phaseDirNames = (() => {
|
|
286
|
-
try {
|
|
287
|
-
return node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
|
|
288
|
-
.filter(e => e.isDirectory())
|
|
289
|
-
.map(e => e.name);
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
return [];
|
|
293
|
-
}
|
|
294
|
-
})();
|
|
295
325
|
while ((match = phasePattern.exec(content)) !== null) {
|
|
296
326
|
const phaseNum = match[1];
|
|
297
|
-
if (
|
|
327
|
+
if (isSentinelPhaseId(phaseNum))
|
|
298
328
|
continue;
|
|
299
329
|
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
|
|
300
330
|
// Extract goal from the section
|
|
@@ -319,20 +349,32 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
319
349
|
let summaryCount = 0;
|
|
320
350
|
let hasContext = false;
|
|
321
351
|
let hasResearch = false;
|
|
322
|
-
//
|
|
352
|
+
// #3885 (ADR-3473 §8.5): null unless dirMatch resolves and its readdirSync
|
|
353
|
+
// hit a non-ENOENT error — no directory at all is `disk_status:
|
|
354
|
+
// 'no_directory'`, a real (if uninteresting) answer, not a read error.
|
|
355
|
+
let contextReadError = null;
|
|
356
|
+
// DEAD catch removed (#2245 audit): matchPhaseDirs(...) is a pure
|
|
323
357
|
// array lookup on an already-resolved string array, and
|
|
324
358
|
// countPhasePlansAndSummaries is itself fully defensive (its own
|
|
325
359
|
// readdirSync is self-guarded, and it delegates to scanPhasePlans, which
|
|
326
360
|
// never throws) — nothing in this block can throw, so the try/catch could
|
|
327
361
|
// never be triggered.
|
|
328
|
-
const dirMatch =
|
|
362
|
+
const dirMatch = matchPhaseDirs(phaseDirNames, normalized).matches[0];
|
|
329
363
|
if (dirMatch) {
|
|
330
364
|
const counts = countPhasePlansAndSummaries(node_path_1.default.join(phasesDir, dirMatch));
|
|
331
365
|
planCount = counts.planCount;
|
|
332
366
|
summaryCount = counts.summaryCount;
|
|
333
367
|
hasContext = counts.hasContext;
|
|
334
368
|
hasResearch = counts.hasResearch;
|
|
335
|
-
|
|
369
|
+
contextReadError = counts.contextReadError;
|
|
370
|
+
// ADR-3180 §7.4 (issue #3186, disk-strict, #3168 fix): route "is this
|
|
371
|
+
// phase complete" through the canonical owner (`isPhaseComplete`),
|
|
372
|
+
// which calls readVerificationStatus UNCONDITIONALLY — plan count is
|
|
373
|
+
// NOT a precondition, so a zero-plan phase with a passing
|
|
374
|
+
// `*-VERIFICATION.md` reports complete here too, not just via
|
|
375
|
+
// `phase.complete`.
|
|
376
|
+
const completionResult = isPhaseComplete(node_path_1.default.join(phasesDir, dirMatch));
|
|
377
|
+
if (completionResult.value.complete)
|
|
336
378
|
diskStatus = 'complete';
|
|
337
379
|
else if (summaryCount > 0)
|
|
338
380
|
diskStatus = 'partial';
|
|
@@ -345,19 +387,23 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
345
387
|
else
|
|
346
388
|
diskStatus = 'empty';
|
|
347
389
|
}
|
|
348
|
-
// Check ROADMAP checkbox status.
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
390
|
+
// Check ROADMAP checkbox status. #3537: padding-tolerant fragment — the
|
|
391
|
+
// heading discovered above may use a different padding than the
|
|
392
|
+
// summary-bullet checkbox below it (mixed padding inside one ROADMAP is
|
|
393
|
+
// legal and seen in real projects).
|
|
394
|
+
//
|
|
395
|
+
// ADR-3180 §7.4 (disk-strict, #2957, maintainer decision 2026-08-08):
|
|
396
|
+
// `roadmapComplete` is reported below as metadata ONLY — it carries NO
|
|
397
|
+
// machine authority over `diskStatus`. The override that used to trust a
|
|
398
|
+
// ticked checkbox over disk file structure is DELETED, not generalized
|
|
399
|
+
// (#2957: "a ticked ROADMAP checkbox is a human annotation with no
|
|
400
|
+
// machine authority"). A phase marked complete solely by a ticked
|
|
401
|
+
// checkbox — no passing `*-VERIFICATION.md`, plans outstanding — now
|
|
402
|
+
// reports incomplete; this is the deliberate Tier-2 break (ADR-3180 §7.4
|
|
403
|
+
// Decision 3).
|
|
352
404
|
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseMarkdownRegexSource(phaseNum)}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
353
405
|
const checkboxMatch = content.match(checkboxPattern);
|
|
354
406
|
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
355
|
-
// If roadmap marks phase complete, trust that over disk file structure.
|
|
356
|
-
// Phases completed before GSD tracking (or via external tools) may lack
|
|
357
|
-
// the standard PLAN/SUMMARY pairs but are still done.
|
|
358
|
-
if (roadmapComplete && diskStatus !== 'complete') {
|
|
359
|
-
diskStatus = 'complete';
|
|
360
|
-
}
|
|
361
407
|
phases.push({
|
|
362
408
|
number: phaseNum,
|
|
363
409
|
name: phaseName,
|
|
@@ -370,18 +416,116 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
370
416
|
has_research: hasResearch,
|
|
371
417
|
disk_status: diskStatus,
|
|
372
418
|
roadmap_complete: roadmapComplete,
|
|
419
|
+
context_read_error: contextReadError,
|
|
373
420
|
});
|
|
374
421
|
}
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
422
|
+
// #3577: markdown-table row declarations join the enumeration — same
|
|
423
|
+
// enrichment contract as headings (disk counts when the directory exists),
|
|
424
|
+
// zero-pad-tolerant duplicate guard so an id declared in BOTH a heading and
|
|
425
|
+
// a table counts once.
|
|
426
|
+
const stripPadA = (s) => s.replace(/^0+(?=.)/, '');
|
|
427
|
+
const seen = new Set(phases.map((ph) => stripPadA(ph.number)));
|
|
428
|
+
for (const tr of collectTablePhaseRows(content)) {
|
|
429
|
+
if (seen.has(stripPadA(tr.id)))
|
|
430
|
+
continue;
|
|
431
|
+
const dirMatchA = matchPhaseDirs(phaseDirNames, normalizePhaseName(tr.id)).matches[0];
|
|
432
|
+
let tPlanCount = 0;
|
|
433
|
+
let tSummaryCount = 0;
|
|
434
|
+
let tHasContext = false;
|
|
435
|
+
let tHasResearch = false;
|
|
436
|
+
let tContextReadError = null;
|
|
437
|
+
if (dirMatchA) {
|
|
438
|
+
const counts = countPhasePlansAndSummaries(node_path_1.default.join(phasesDir, dirMatchA));
|
|
439
|
+
tPlanCount = counts.planCount;
|
|
440
|
+
tSummaryCount = counts.summaryCount;
|
|
441
|
+
tHasContext = node_fs_1.default.existsSync(node_path_1.default.join(phasesDir, dirMatchA, 'CONTEXT.md'));
|
|
442
|
+
tHasResearch = node_fs_1.default.existsSync(node_path_1.default.join(phasesDir, dirMatchA, 'RESEARCH.md'));
|
|
443
|
+
// #3885 (ADR-3473 §8.5): reuse the SAME countPhasePlansAndSummaries call's
|
|
444
|
+
// discriminator — this row's hasContext/hasResearch are read via a direct
|
|
445
|
+
// existsSync (which cannot itself distinguish EACCES from absent), but
|
|
446
|
+
// an unreadable phase directory is still surfaced via the sibling call.
|
|
447
|
+
tContextReadError = counts.contextReadError;
|
|
448
|
+
}
|
|
449
|
+
phases.push({
|
|
450
|
+
number: tr.id,
|
|
451
|
+
name: tr.name ?? `Phase ${tr.id}`,
|
|
452
|
+
goal: null,
|
|
453
|
+
mode: null,
|
|
454
|
+
depends_on: null,
|
|
455
|
+
plan_count: tPlanCount,
|
|
456
|
+
summary_count: tSummaryCount,
|
|
457
|
+
has_context: tHasContext,
|
|
458
|
+
has_research: tHasResearch,
|
|
459
|
+
disk_status: dirMatchA ? 'ok' : 'no_directory',
|
|
460
|
+
roadmap_complete: false,
|
|
461
|
+
context_read_error: tContextReadError,
|
|
383
462
|
});
|
|
384
463
|
}
|
|
464
|
+
return phases;
|
|
465
|
+
}
|
|
466
|
+
function cmdRoadmapAnalyze(cwd, raw) {
|
|
467
|
+
const roadmapPath = planningPaths(cwd).roadmap;
|
|
468
|
+
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
469
|
+
output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw, undefined);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
473
|
+
// #3184/#3165: use the scoped variant so a truncated window is a
|
|
474
|
+
// distinguishable signal in the output instead of a silent `phase_count: 0`
|
|
475
|
+
// indistinguishable from a genuinely empty milestone.
|
|
476
|
+
const { value: content, scope } = extractCurrentMilestoneScoped(rawContent, cwd);
|
|
477
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
478
|
+
// Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
|
|
479
|
+
// #3185 exemption reason (ADR-3180 Decision 4a): this is a heading->directory
|
|
480
|
+
// LOOKUP INDEX, not a milestone enumeration. It must see the PHYSICAL set so
|
|
481
|
+
// a heading already scoped by extractCurrentMilestoneScoped above can find
|
|
482
|
+
// its directory; filtering it through listMilestonePhaseDirs would scope
|
|
483
|
+
// the same set twice. #3882 (ADR-3473 §8.2): routed through the named
|
|
484
|
+
// "physical set, sentinels included" axis instead of a hand-rolled
|
|
485
|
+
// readdirSync — every heading matched below already excludes sentinel
|
|
486
|
+
// phase numbers via isSentinelPhaseId before it ever consults this list
|
|
487
|
+
// (collectAnalyzePhases), so a sentinel directory's presence here is
|
|
488
|
+
// output-invariant; this only removes the re-derivation, not the reason.
|
|
489
|
+
const _phaseDirNames = listAllPhaseDirs(phasesDir, { includeSentinels: true }).value;
|
|
490
|
+
// Scan the scoped milestone window for phase-detail headings and enrich each
|
|
491
|
+
// with its on-disk status. Extracted into `collectAnalyzePhases` (#3165) so
|
|
492
|
+
// the SAME enrichment re-runs on the fallback below — not a second copy.
|
|
493
|
+
let phases = collectAnalyzePhases(content, phasesDir, _phaseDirNames);
|
|
494
|
+
// `effectiveContent` is what the downstream checklist scan (missing_details)
|
|
495
|
+
// iterates. Defaults to the scoped window; switched to the fallback document
|
|
496
|
+
// when the recovery path below fires, so a phase found via fallback is not
|
|
497
|
+
// falsely reported as "in checklist but missing a detail section."
|
|
498
|
+
let effectiveContent = content;
|
|
499
|
+
// #3165: recover phase_count when the scoped window came back empty. A
|
|
500
|
+
// CLOSED milestone heading sitting between the active milestone heading and
|
|
501
|
+
// its own phase-detail sections closes `extractCurrentMilestoneScoped`'s
|
|
502
|
+
// window over prose only — `phases` is empty, and the consuming resume gate
|
|
503
|
+
// (`workflows/next.md` Route 0) iterates `.phases[]` so a safety invariant
|
|
504
|
+
// silently never runs. When the window is suspect (non-COMPLETE scope), the
|
|
505
|
+
// scoped scan found nothing, AND phase directories exist on disk (real
|
|
506
|
+
// evidence phases exist), re-scan the shipped-milestone-stripped document so
|
|
507
|
+
// the phase list reflects the real phases instead of a silent zero. The
|
|
508
|
+
// `scope` field retains its non-COMPLETE value downstream so consumers can
|
|
509
|
+
// still tell this is a best-effort count, not a cleanly scoped one. Position
|
|
510
|
+
// alone cannot attribute phases to the active vs the intervening closed
|
|
511
|
+
// milestone, so this never claims COMPLETE — it converts silence into a
|
|
512
|
+
// populated, flagged result.
|
|
513
|
+
if (phases.length === 0 && scope !== SCOPE.COMPLETE && _phaseDirNames.length > 0) {
|
|
514
|
+
const fallbackContent = stripShippedMilestones(rawContent);
|
|
515
|
+
const fallbackPhases = collectAnalyzePhases(fallbackContent, phasesDir, _phaseDirNames);
|
|
516
|
+
if (fallbackPhases.length > 0) {
|
|
517
|
+
phases = fallbackPhases;
|
|
518
|
+
effectiveContent = fallbackContent;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Extract milestone info. #3216: routed through the canonical
|
|
522
|
+
// `listMilestoneHeadings` owner (deleted the inline `##…` regex, which
|
|
523
|
+
// truncated names at a parenthetical and had no phase-heading exclusion)
|
|
524
|
+
// rather than re-deriving the enumeration here.
|
|
525
|
+
const milestones = listMilestoneHeadings(content).map((m) => ({
|
|
526
|
+
heading: m.heading,
|
|
527
|
+
version: m.version,
|
|
528
|
+
}));
|
|
385
529
|
// Find current and next phase
|
|
386
530
|
const currentPhase = phases.find(p => p.disk_status === 'planned' || p.disk_status === 'partial') || null;
|
|
387
531
|
const nextPhase = phases.find(p => p.disk_status === 'empty' || p.disk_status === 'no_directory' || p.disk_status === 'discussed' || p.disk_status === 'researched') || null;
|
|
@@ -399,11 +543,43 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
399
543
|
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)/gi;
|
|
400
544
|
const checklistPhases = new Set();
|
|
401
545
|
let checklistMatch;
|
|
402
|
-
while ((checklistMatch = checklistPattern.exec(
|
|
546
|
+
while ((checklistMatch = checklistPattern.exec(effectiveContent)) !== null) {
|
|
403
547
|
checklistPhases.add(checklistMatch[1]);
|
|
404
548
|
}
|
|
405
549
|
const detailPhases = new Set(phases.map(p => p.number));
|
|
406
|
-
const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p) && !
|
|
550
|
+
const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p) && !isSentinelPhaseId(p));
|
|
551
|
+
// #3217 (ADR-3180 §7.6 rules 3-4): `progress_percent` used to accumulate
|
|
552
|
+
// `totalPlans`/`totalSummaries` above — a heading-matched enumeration
|
|
553
|
+
// (`phasePattern` over the milestone-windowed `content`) paired against
|
|
554
|
+
// `_phaseDirNames`, a DELIBERATELY unscoped physical directory listing
|
|
555
|
+
// (see its own comment above: it is a heading->directory lookup index,
|
|
556
|
+
// not a milestone enumeration). That set is not the same set
|
|
557
|
+
// `listMilestonePhaseDirs` scopes for `query progress` / `stats` (#3185
|
|
558
|
+
// Phase 3), so `progress_percent` could silently diverge from both siblings
|
|
559
|
+
// on the same project (rule 3). Route `progress_percent`'s own
|
|
560
|
+
// numerator/denominator through the single scoped owner instead — mirrors
|
|
561
|
+
// cmdProgressRender/cmdStats's own aggregation — and withhold the
|
|
562
|
+
// percentage entirely when THAT scope is not COMPLETE (rule 4), never
|
|
563
|
+
// returning `0` for "could not compute". This does not touch `total_plans`
|
|
564
|
+
// / `total_summaries` / `phases` / `completed_phases` above — those stay
|
|
565
|
+
// the heading-matched detail view; only `progress_percent`'s own inputs
|
|
566
|
+
// move onto the scoped owner.
|
|
567
|
+
let scopedTotalPlans = 0;
|
|
568
|
+
let scopedTotalSummaries = 0;
|
|
569
|
+
let progressScope = SCOPE.UNREADABLE;
|
|
570
|
+
try {
|
|
571
|
+
const { value: progressDirs, scope: scopedResult } = listMilestonePhaseDirs(phasesDir, { cwd });
|
|
572
|
+
progressScope = scopedResult;
|
|
573
|
+
for (const dir of progressDirs) {
|
|
574
|
+
const scan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
|
|
575
|
+
scopedTotalPlans += scan.planCount;
|
|
576
|
+
scopedTotalSummaries += scan.summaryCount;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch { /* progressScope stays the pessimistic SCOPE.UNREADABLE default */ }
|
|
580
|
+
const progressPercent = progressScope === SCOPE.COMPLETE
|
|
581
|
+
? (0, phase_lifecycle_cjs_1.clampPercent)(scopedTotalSummaries, scopedTotalPlans)
|
|
582
|
+
: null;
|
|
407
583
|
const result = {
|
|
408
584
|
milestones,
|
|
409
585
|
phases,
|
|
@@ -411,13 +587,94 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
411
587
|
completed_phases: completedPhases,
|
|
412
588
|
total_plans: totalPlans,
|
|
413
589
|
total_summaries: totalSummaries,
|
|
414
|
-
progress_percent:
|
|
590
|
+
progress_percent: progressPercent,
|
|
591
|
+
// #3217 finding 2: `progress_percent` is gated by a SECOND, independently
|
|
592
|
+
// computed `listMilestonePhaseDirs` scope (`progressScope` above) — not
|
|
593
|
+
// by the top-level `scope` field, which describes the heading-windowing
|
|
594
|
+
// identity `phases`/`total_plans`/`total_summaries`/`completed_phases`
|
|
595
|
+
// were built from. Those two scopes can legitimately disagree (e.g.
|
|
596
|
+
// `scope: "complete"` alongside a genuinely unreadable phases directory),
|
|
597
|
+
// and per the documented contract "scope tells you whether the counts
|
|
598
|
+
// are trustworthy", a consumer seeing `progress_percent: null` needs a
|
|
599
|
+
// field to tell WHY without reading source. Exposing `progress_scope`
|
|
600
|
+
// (rather than reconciling the two scopes into one, or re-deriving
|
|
601
|
+
// `total_plans`/`phases`/etc. from the scoped set) preserves the
|
|
602
|
+
// deliberate, already-documented choice a few lines up: `phases`/
|
|
603
|
+
// `total_plans`/`total_summaries`/`completed_phases` stay the
|
|
604
|
+
// heading-matched detail view (`_phaseDirNames` is a lookup index, not a
|
|
605
|
+
// milestone enumeration — see its comment); only `progress_percent`'s own
|
|
606
|
+
// inputs move onto the scoped owner.
|
|
607
|
+
progress_scope: progressScope,
|
|
415
608
|
current_phase: currentPhase ? currentPhase.number : null,
|
|
416
609
|
next_phase: nextPhase ? nextPhase.number : null,
|
|
417
610
|
missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
|
|
611
|
+
// #3184/#3165: distinguishes a genuinely empty milestone (`scope:
|
|
612
|
+
// "complete"`, `phase_count: 0`) from a window that could not be fully
|
|
613
|
+
// resolved (`"truncated"` / `"unscoped"` / `"unreadable"`) — those cases
|
|
614
|
+
// were previously output-identical.
|
|
615
|
+
scope,
|
|
418
616
|
};
|
|
419
617
|
output(result, raw, undefined);
|
|
420
618
|
}
|
|
619
|
+
// ─── cmdRoadmapMilestoneScope ────────────────────────────────────────────────
|
|
620
|
+
/**
|
|
621
|
+
* #3262 (write-time milestone-scope guard): read-only probe emitting the
|
|
622
|
+
* current milestone window's IDENTITY — its scope classification and the
|
|
623
|
+
* phase ids it declares — so the edit-phase workflow can capture it before
|
|
624
|
+
* its in-place section write, re-derive it after, and roll back on any
|
|
625
|
+
* change. This is the milestone-scope sibling of the workflow's existing
|
|
626
|
+
* `depends_on` gate, expressed as a command because the workflow's write is
|
|
627
|
+
* assistant-driven free-text surgery, not a code path.
|
|
628
|
+
*
|
|
629
|
+
* Deliberately NOT `cmdRoadmapAnalyze`: analyze's #3165 recovery re-populates
|
|
630
|
+
* `phases` from the shipped-milestone-stripped document when the scoped
|
|
631
|
+
* window is suspect, which is right for a human-facing progress report and
|
|
632
|
+
* wrong for a before/after equality probe — the refill would mask exactly
|
|
633
|
+
* the narrowing this guard exists to detect. This probe reports the RAW
|
|
634
|
+
* window (`extractCurrentMilestoneScoped` + `scanMilestonePhaseIds`), no
|
|
635
|
+
* fallback, so a narrowed window is always visible as a changed phase set.
|
|
636
|
+
*/
|
|
637
|
+
function cmdRoadmapMilestoneScope(cwd, raw) {
|
|
638
|
+
const roadmapPath = planningPaths(cwd).roadmap;
|
|
639
|
+
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
640
|
+
output({ error: 'ROADMAP.md not found', scope: SCOPE.UNREADABLE, phases: [], phase_count: 0 }, raw, undefined);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
644
|
+
// #3641: resolve phase_id_convention and thread it into the scope axis, so
|
|
645
|
+
// this probe and `roadmap validate`'s V005 answer the SAME question the
|
|
646
|
+
// SAME way for a bracket-convention project — a window the classifier
|
|
647
|
+
// calls TRUNCATED in validate must never read COMPLETE here (the #3262
|
|
648
|
+
// capture/compare guard consumes this scope). Resolution mirrors the
|
|
649
|
+
// validate router's: .planning/config.json first, ROADMAP.md frontmatter
|
|
650
|
+
// as fallback.
|
|
651
|
+
let phaseIdConvention;
|
|
652
|
+
try {
|
|
653
|
+
const cfg = loadConfigForScope(cwd);
|
|
654
|
+
phaseIdConvention = cfg['phase_id_convention'];
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
phaseIdConvention = undefined;
|
|
658
|
+
}
|
|
659
|
+
if (phaseIdConvention === undefined || phaseIdConvention === null) {
|
|
660
|
+
// Bounded per local/no-unbounded-quantifier (#2128): frontmatter is a
|
|
661
|
+
// short header block — 4KB is orders of magnitude beyond any real one.
|
|
662
|
+
const fmMatch = rawContent.match(/^---\r?\n([\s\S]{0,4000}?)\r?\n---/);
|
|
663
|
+
if (fmMatch) {
|
|
664
|
+
const kvMatch = fmMatch[1].match(/^phase_id_convention:\s*(.*)$/m);
|
|
665
|
+
if (kvMatch) {
|
|
666
|
+
const val = kvMatch[1].trim();
|
|
667
|
+
if (val !== 'null' && val !== '') {
|
|
668
|
+
phaseIdConvention = val.replace(/^["']|["']$/g, '');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const { value: window, scope } = extractCurrentMilestoneScoped(rawContent, cwd, undefined, phaseIdConvention);
|
|
674
|
+
// Document order (Set insertion order) — deterministic for a given document.
|
|
675
|
+
const phases = [...scanMilestonePhaseIds(window)];
|
|
676
|
+
output({ scope, phases, phase_count: phases.length }, raw, undefined);
|
|
677
|
+
}
|
|
421
678
|
// ─── cmdRoadmapUpdatePlanProgress ─────────────────────────────────────────────
|
|
422
679
|
/**
|
|
423
680
|
* Scope a ROADMAP.md content string down to its "Progress table" writable
|
|
@@ -474,10 +731,34 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
474
731
|
// completion date until the phase's verification status is 'passed', matching
|
|
475
732
|
// cmdPhaseComplete's gate (phase.cts:1436). Previously the checkbox fired the
|
|
476
733
|
// moment the last plan summary landed — before gsd-verifier had verified.
|
|
734
|
+
//
|
|
735
|
+
// ADR-3180 §7.4 (issue #3186, disk-strict): routed through the canonical
|
|
736
|
+
// owner (`isPhaseComplete`) instead of hand-rolling `summaryCount >=
|
|
737
|
+
// planCount && verificationPassed` locally — the owner calls
|
|
738
|
+
// readVerificationStatus UNCONDITIONALLY, so `isComplete` here always
|
|
739
|
+
// agrees with `roadmap analyze` / `init manager` / `phase complete` for
|
|
740
|
+
// the same phase (ADR-3180 §7.4's headline: one predicate for the read
|
|
741
|
+
// path and the write path).
|
|
477
742
|
const phaseDir = node_path_1.default.join(cwd, phaseInfo.directory);
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
-
|
|
743
|
+
const completionResult = isPhaseComplete(phaseDir);
|
|
744
|
+
const verificationResult = completionResult.value.verification;
|
|
745
|
+
// #2648 precedent, applied at this write site (ADR-3180 §7.4 / #3186):
|
|
746
|
+
// `isPhaseComplete` deliberately carries NO plan-count precondition — the
|
|
747
|
+
// owner's `complete` is exactly `verification.status === 'passed'`, and
|
|
748
|
+
// that must stay true (disk-strict: a zero-plan phase with a passing
|
|
749
|
+
// `*-VERIFICATION.md` IS complete, #3168). But `readVerificationStatus`'s
|
|
750
|
+
// staleness check only compares SUMMARY mtimes against the verification
|
|
751
|
+
// file — it has no idea a NEW plan was added after the file was written,
|
|
752
|
+
// so a still-fresh `passed` verification says nothing about a plan added
|
|
753
|
+
// afterward. This command WRITES a checkbox and a completion date into
|
|
754
|
+
// ROADMAP.md, a materially stronger claim than "verification passed" —
|
|
755
|
+
// mirroring cmdPhaseComplete's own fail-closed plan-coverage gate
|
|
756
|
+
// (phase.cts:~1995, #2648: "a coverage gate that passes when it cannot
|
|
757
|
+
// read the plans is no gate at all"), composed explicitly here rather than
|
|
758
|
+
// folded into the predicate: complete AND all plans executed.
|
|
759
|
+
const coverageScan = scanPhasePlans(phaseDir);
|
|
760
|
+
const unsummarizedPlans = findUnsummarizedPlans(coverageScan.planFiles, coverageScan.summaryFiles);
|
|
761
|
+
const isComplete = completionResult.value.complete && unsummarizedPlans.length === 0;
|
|
481
762
|
// #3057 B3: routing above is unchanged (an indeterminate staleness check
|
|
482
763
|
// still routes as if nothing were stale) — this only makes the fact visible
|
|
483
764
|
// to whatever reads this command's JSON output.
|
|
@@ -539,26 +820,65 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
539
820
|
// `**Plans:** N plans` — bold "Plans:" (colon inside bold)
|
|
540
821
|
// `Plans: N plans` — plain text header
|
|
541
822
|
//
|
|
542
|
-
// #2853: the verb owns the count token ONLY — it must not destroy
|
|
543
|
-
// prose a human placed
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
823
|
+
// #2853 / #3584: the verb owns the count token ONLY — it must not destroy
|
|
824
|
+
// hand-written prose a human placed on the line. Group $1 = phase header →
|
|
825
|
+
// `Plans:` label + trailing whitespace (unchanged). Group $2 = the existing
|
|
826
|
+
// count token to replace: matches `N/N plans complete`, `N/N plans executed`,
|
|
827
|
+
// or the bare template `N plan(s)` form — singular is part of the tool's OWN
|
|
828
|
+
// grammar (gsd-core/templates/roadmap.md:62 ships `**Plans**: 1 plan` as the
|
|
829
|
+
// documented one-plan-phase shape), so the `s` is optional there (bug #3584
|
|
830
|
+
// Finding B; pre-fix a bare `1 plan` fell into the drop-everything path and
|
|
831
|
+
// was accidentally overwritten with the correct count — post-fix it must be
|
|
832
|
+
// recognised as a token in its own right or it freezes stale forever). Group
|
|
833
|
+
// $3 = whatever else is on the line (`[^\r\n]*`, so a CRLF `\r` is never part
|
|
834
|
+
// of the match and rides along untouched in the unmatched remainder of the
|
|
835
|
+
// string — never stranded, never duplicated).
|
|
548
836
|
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
|
|
837
|
+
// Three arms, in order:
|
|
838
|
+
// 1. $2 present (a real count token) → rewrite the token, preserve $3
|
|
839
|
+
// verbatim (an annotation a human wrote after a real count; #2853).
|
|
840
|
+
// 2. $2 absent AND $3, trimmed, is the fresh-template PLACEHOLDER shipped
|
|
841
|
+
// by gsd-core/templates/roadmap.md — either
|
|
842
|
+
// `[Number of plans, e.g., "3 plans" or "TBD"]` (line 37) or
|
|
843
|
+
// `[Number of plans]` (lines 51/75/88) → replace it with the computed
|
|
844
|
+
// count. Detected POSITIVELY on the distinctive `Number of plans`
|
|
845
|
+
// wording (anchored, case-insensitive), NEVER on "wholly bracketed" —
|
|
846
|
+
// a bracketed HUMAN annotation such as `[Deferred pending re-scope]`
|
|
847
|
+
// is structurally identical but must be arm-3 preserved (bug #3584
|
|
848
|
+
// Finding A).
|
|
849
|
+
// 3. Anything else (freeform prose, `TBD` / `TBD — annotation`, a
|
|
850
|
+
// bracketed human note, the first line of a wrapped sentence, an
|
|
851
|
+
// empty value) → leave the whole matched line untouched by returning
|
|
852
|
+
// `_match` unchanged. An untouched first line cannot orphan its own
|
|
853
|
+
// continuation on the next line, since the pattern never spans past
|
|
854
|
+
// `\n` in the first place.
|
|
855
|
+
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)(\\d+\\s*\\/\\s*\\d+\\s+plans(?:\\s+(?:complete|executed))?|\\d+\\s+plans?)?([^\\r\\n]*)`, 'i');
|
|
555
856
|
const planCountText = isComplete
|
|
556
857
|
? `${summaryCount}/${planCount} plans complete`
|
|
557
858
|
: `${summaryCount}/${planCount} plans executed`;
|
|
859
|
+
// Positive detector for the fresh-template placeholder ONLY (bug #3584
|
|
860
|
+
// Finding A). Anchored to the distinctive `Number of plans` wording that
|
|
861
|
+
// gsd-core/templates/roadmap.md actually ships, not to "anything in
|
|
862
|
+
// brackets" — a bracketed human annotation like `[Deferred pending
|
|
863
|
+
// re-scope]` is structurally bracketed too but carries none of this
|
|
864
|
+
// wording, so it correctly falls through to arm 3 untouched.
|
|
865
|
+
const isTemplatePlaceholder = (value) => {
|
|
866
|
+
const trimmed = value.trim();
|
|
867
|
+
return /^\[\s*Number of plans\b[\s\S]*\]$/i.test(trimmed);
|
|
868
|
+
};
|
|
558
869
|
roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, (_match, label, existingCount, trailing) => {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
870
|
+
if (existingCount) {
|
|
871
|
+
// Arm 1: real count token — rewrite it, preserve the trailing annotation.
|
|
872
|
+
return `${label}${planCountText}${trailing}`;
|
|
873
|
+
}
|
|
874
|
+
if (isTemplatePlaceholder(trailing)) {
|
|
875
|
+
// Arm 2: fresh-template placeholder — replace with the count.
|
|
876
|
+
return `${label}${planCountText}`;
|
|
877
|
+
}
|
|
878
|
+
// Arm 3: freeform prose, TBD, a bracketed human annotation, a wrapped
|
|
879
|
+
// sentence's first line, or an empty value — leave the line exactly as
|
|
880
|
+
// it was.
|
|
881
|
+
return _match;
|
|
562
882
|
});
|
|
563
883
|
// If complete: check checkbox
|
|
564
884
|
if (isComplete) {
|
|
@@ -570,7 +890,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
570
890
|
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
571
891
|
if (!planId)
|
|
572
892
|
continue;
|
|
573
|
-
const planEscaped = escapeRegex(planId);
|
|
893
|
+
const planEscaped = (0, pattern_cjs_1.escapeRegex)(planId);
|
|
574
894
|
const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
|
|
575
895
|
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
|
576
896
|
}
|
|
@@ -594,7 +914,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
594
914
|
// archived <details> block is still correctly detected as missing from the
|
|
595
915
|
// active milestone section.
|
|
596
916
|
const missingPlans = phaseInfo.plans.filter((planFile) => {
|
|
597
|
-
const planEscaped = escapeRegex(planFile);
|
|
917
|
+
const planEscaped = (0, pattern_cjs_1.escapeRegex)(planFile);
|
|
598
918
|
return !new RegExp(`-\\s*\\[[x ]\\]\\s*(?:\\*\\*)?${planEscaped}`, 'i').test(activeRegion);
|
|
599
919
|
});
|
|
600
920
|
if (missingPlans.length > 0) {
|
|
@@ -642,7 +962,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
642
962
|
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
643
963
|
if (!planId)
|
|
644
964
|
continue;
|
|
645
|
-
const planEscaped = escapeRegex(planId);
|
|
965
|
+
const planEscaped = (0, pattern_cjs_1.escapeRegex)(planId);
|
|
646
966
|
const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
|
|
647
967
|
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
|
648
968
|
}
|
|
@@ -747,6 +1067,11 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|
|
747
1067
|
let updated = false;
|
|
748
1068
|
withPlanningLock(cwd, () => {
|
|
749
1069
|
const content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1070
|
+
// #3413: preserve the file's own EOL style when the checklist block below
|
|
1071
|
+
// is rebuilt and spliced back in — splitLines() cleans each captured line
|
|
1072
|
+
// of any dangling \r, so rejoining with a bare '\n' would silently
|
|
1073
|
+
// downgrade a CRLF ROADMAP.md's rewritten block to LF only.
|
|
1074
|
+
const eol = (0, text_lines_cjs_1.detectEol)(content);
|
|
750
1075
|
// Find the phase section.
|
|
751
1076
|
// #3537: padding-tolerant fragment so the caller's resolved padded id
|
|
752
1077
|
// matches un-padded ROADMAP headings.
|
|
@@ -776,12 +1101,20 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|
|
776
1101
|
// Review fix (F2): `(?:^|\n)` anchors the match to start-of-line so mid-line
|
|
777
1102
|
// occurrences like `***Plans:***` embedded in a sentence or `OpenPlans: foo`
|
|
778
1103
|
// do not trigger a false match. Groups 1 and 2 retain the same semantics.
|
|
779
|
-
|
|
1104
|
+
// #3415: empirically verified linear-time to 10.9MB / 320,000 lines of adversarial
|
|
1105
|
+
// checklist input (0.31ms@1000 lines -> 8.4ms@320,000 lines). The outer `+` group has
|
|
1106
|
+
// no trailing constraint after it in the pattern, so a successful greedy pass never
|
|
1107
|
+
// needs to explore alternate `\r?\n?` boundary partitions to satisfy something later —
|
|
1108
|
+
// it accepts the first complete parse and stops, which rules out the #2128-class
|
|
1109
|
+
// ambiguous-boundary blowup despite the nested-quantifier shape. Non-global match on
|
|
1110
|
+
// already phase-sliced content, not the whole file.
|
|
1111
|
+
// eslint-disable-next-line local/no-unbounded-quantifier -- outer `+` has no trailing constraint to force re-partitioning; measured linear to 10.9MB
|
|
1112
|
+
const plansBlockMatch = phaseSection.match(/(?:^|\r?\n)(\*{0,2}Plans\*{0,2}:[^\r\n]*\r?\n)((?:\s*-\s*\[[ x]\][^\r\n]*\r?\n?)+)/i);
|
|
780
1113
|
if (!plansBlockMatch)
|
|
781
1114
|
return;
|
|
782
1115
|
const plansHeader = plansBlockMatch[1];
|
|
783
1116
|
const existingList = plansBlockMatch[2];
|
|
784
|
-
const listLines =
|
|
1117
|
+
const listLines = (0, text_lines_cjs_1.splitLines)(existingList).filter(l => /^\s*-\s*\[/.test(l));
|
|
785
1118
|
if (listLines.length === 0)
|
|
786
1119
|
return;
|
|
787
1120
|
// #314 perf: build a first-wins Map so per-line lookup is O(1) instead of O(plans).
|
|
@@ -834,11 +1167,22 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|
|
834
1167
|
annotatedLines.push(`- ${t}`);
|
|
835
1168
|
}
|
|
836
1169
|
}
|
|
837
|
-
const newListBlock =
|
|
838
|
-
// #1103: when `(?:^|\n)` consumed a leading
|
|
839
|
-
// so the line preceding the Plans: header is
|
|
840
|
-
|
|
841
|
-
|
|
1170
|
+
const newListBlock = (0, text_lines_cjs_1.joinLines)(annotatedLines, eol) + eol;
|
|
1171
|
+
// #1103: when `(?:^|\r?\n)` consumed a leading terminator (mid-string
|
|
1172
|
+
// match), re-emit it verbatim so the line preceding the Plans: header is
|
|
1173
|
+
// not fused onto it. #3413: the widened `(?:^|\r?\n)` can now consume a
|
|
1174
|
+
// 2-char `\r\n` — re-emit whatever was actually captured (`''`, `'\n'`,
|
|
1175
|
+
// or `'\r\n'`), not a hardcoded `'\n'`, or a CRLF file loses its `\r`.
|
|
1176
|
+
const leadingMatch = /^\r?\n/.exec(plansBlockMatch[0]);
|
|
1177
|
+
const leadingNewline = leadingMatch ? leadingMatch[0] : '';
|
|
1178
|
+
// Review fix (#3413 security): use the FUNCTION-replacement form. The
|
|
1179
|
+
// string-replacement form expands String#replace's special patterns
|
|
1180
|
+
// (`$&`, `` $` ``, `$'`, `$$`, `$1`-`$9`) inside the replacement — and
|
|
1181
|
+
// newListBlock is built from author-controlled truths/plan-file content,
|
|
1182
|
+
// so a line containing a literal `` $` `` (etc.) would splice unrelated
|
|
1183
|
+
// surrounding phaseSection text into the result. A function replacer is
|
|
1184
|
+
// never pattern-interpreted.
|
|
1185
|
+
const newPhaseSection = phaseSection.replace(plansBlockMatch[0], () => leadingNewline + plansHeader + newListBlock);
|
|
842
1186
|
const nextContent = content.slice(0, phaseStart) + newPhaseSection + content.slice(phaseEnd);
|
|
843
1187
|
if (nextContent === content)
|
|
844
1188
|
return;
|
|
@@ -856,6 +1200,8 @@ module.exports = {
|
|
|
856
1200
|
cmdRoadmapGetPhase,
|
|
857
1201
|
getRoadmapPhaseWithFallback,
|
|
858
1202
|
cmdRoadmapAnalyze,
|
|
1203
|
+
cmdRoadmapMilestoneScope,
|
|
859
1204
|
cmdRoadmapUpdatePlanProgress,
|
|
860
1205
|
cmdRoadmapAnnotateDependencies,
|
|
1206
|
+
buildPhaseHeadingRegex,
|
|
861
1207
|
};
|