@opengsd/gsd-core 1.13.0 → 1.15.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/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +459 -0
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +360 -0
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +848 -163
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +8 -3
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/review.md +2 -3
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +672 -146
- package/gsd-core/bin/lib/adr-parser.cjs +4 -2
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +119 -34
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +96 -189
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -2
- package/gsd-core/bin/lib/check-command-router.cjs +213 -49
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +823 -112
- package/gsd-core/bin/lib/config-loader.cjs +66 -4
- package/gsd-core/bin/lib/config.cjs +186 -45
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +164 -45
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +614 -86
- package/gsd-core/bin/lib/install-engine.cjs +29 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +37 -13
- package/gsd-core/bin/lib/model-resolver.cjs +253 -53
- package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +268 -27
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +393 -88
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +356 -42
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +181 -30
- package/gsd-core/bin/lib/state.cjs +265 -27
- package/gsd-core/bin/lib/surface.cjs +77 -3
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +9 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +315 -30
- package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
- package/gsd-core/bin/lib/verify.cjs +320 -48
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
- package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +13 -4
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +14 -2
- package/gsd-core/references/tdd.md +30 -4
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +433 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +29 -16
- package/gsd-core/workflows/check-todos.md +6 -4
- package/gsd-core/workflows/cleanup.md +5 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +216 -73
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +41 -264
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +17 -158
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +83 -172
- package/gsd-core/workflows/execute-plan.md +24 -10
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +52 -5
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +56 -14
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +39 -209
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +45 -187
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +132 -20
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +29 -10
- package/gsd-core/workflows/reapply-patches.md +86 -6
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +22 -9
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +14 -12
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +101 -196
- package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +71 -19
- package/hooks/dist/gsd-statusline.js +81 -20
- package/hooks/dist/gsd-validate-commit.sh +97 -8
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +66 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +71 -19
- package/hooks/gsd-statusline.js +81 -20
- package/hooks/gsd-validate-commit.sh +97 -8
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +11 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +131 -12
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +35 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +623 -13
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +29 -9
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
- package/scripts/prompt-injection-scan.sh +18 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +8 -3
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review/SKILL.md +2 -3
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -45,10 +45,19 @@ const { normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, matchPhas
|
|
|
45
45
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
46
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
47
47
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
48
|
-
const { findPhaseInternal, getArchivedPhaseDirs, listMilestonePhaseDirs } = phaseLocatorMod;
|
|
48
|
+
const { findPhaseInternal, getArchivedPhaseDirs, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
|
|
50
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
51
|
+
const { SCOPE } = planningScopeMod;
|
|
49
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- roadmap-parser.cjs is an export= CommonJS module
|
|
50
53
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
51
54
|
const { stripShippedMilestones, extractCurrentMilestone, currentMilestoneRawRanges, withPhaseSection, findMilestoneScopeHeadingLines } = roadmapParserMod;
|
|
55
|
+
// #4129: the single owner of "count the ROADMAP's milestone Complete rows"
|
|
56
|
+
// (pure computation, no I/O — no cycle on this path) for the intent-first
|
|
57
|
+
// progress counters the phase-complete transaction passes downstream.
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-lifecycle.cjs is an export= CommonJS module
|
|
59
|
+
const phaseLifecycleMod = require("./phase-lifecycle.cjs");
|
|
60
|
+
const { deriveProgressFromRoadmap: deriveProgressFromRoadmapForIntent, clampPercent: clampPercentForIntent } = phaseLifecycleMod;
|
|
52
61
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
53
62
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
54
63
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
@@ -56,6 +65,7 @@ const frontmatterMod = require("./frontmatter.cjs");
|
|
|
56
65
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- state.cjs is an export= CommonJS module
|
|
57
66
|
const stateMod = require("./state.cjs");
|
|
58
67
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
68
|
+
const planning_document_cjs_1 = require("./planning-document.cjs");
|
|
59
69
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
60
70
|
const clock_cjs_1 = require("./clock.cjs");
|
|
61
71
|
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
@@ -77,7 +87,7 @@ const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
|
|
|
77
87
|
const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSummaryFileBlocked } = planDependencyGraphMod;
|
|
78
88
|
// #612: `resolvePhaseIdConvention` selects the write-time milestone-scope
|
|
79
89
|
// guard's terminator vocabulary (see assertDescriptionPreservesMilestoneScope).
|
|
80
|
-
const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolvePhaseIdConvention, } = planningWorkspace;
|
|
90
|
+
const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, resolveEnvWorkstream, resolvePhaseIdConvention, } = planningWorkspace;
|
|
81
91
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- milestone-lock.cjs is an export= CommonJS module
|
|
82
92
|
const milestoneLockMod = require("./milestone-lock.cjs");
|
|
83
93
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -287,25 +297,26 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
287
297
|
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
288
298
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
289
299
|
baseExists = matchPhaseDirs(dirs, normalized).matches.length > 0;
|
|
290
|
-
const dirPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${(0, pattern_cjs_1.escapeRegex)(normalized)}\\.(\\d+)`);
|
|
291
|
-
for (const dir of dirs) {
|
|
292
|
-
const match = dir.match(dirPattern);
|
|
293
|
-
if (match)
|
|
294
|
-
decimalSet.add(parseInt(match[1], 10));
|
|
295
|
-
}
|
|
296
300
|
}
|
|
297
301
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
298
302
|
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
299
303
|
try {
|
|
300
304
|
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
while ((pm = phasePattern.exec(roadmapContent)) !== null) {
|
|
304
|
-
decimalSet.add(parseInt(pm[1], 10));
|
|
305
|
+
for (const n of scanExistingDecimalPhaseNumbers(phasesDir, roadmapContent, normalized)) {
|
|
306
|
+
decimalSet.add(n);
|
|
305
307
|
}
|
|
306
308
|
}
|
|
307
309
|
catch {
|
|
308
|
-
|
|
310
|
+
// ROADMAP.md read failure is non-fatal — fall back to the directory-only
|
|
311
|
+
// scan (empty rawContent) so on-disk decimal directories are still counted.
|
|
312
|
+
for (const n of scanExistingDecimalPhaseNumbers(phasesDir, '', normalized)) {
|
|
313
|
+
decimalSet.add(n);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
for (const n of scanExistingDecimalPhaseNumbers(phasesDir, '', normalized)) {
|
|
319
|
+
decimalSet.add(n);
|
|
309
320
|
}
|
|
310
321
|
}
|
|
311
322
|
const existingDecimals = Array.from(decimalSet)
|
|
@@ -338,6 +349,7 @@ function getRoadmapModeForPhase(cwd, phaseNum) {
|
|
|
338
349
|
const milestoneContent = extractCurrentMilestone(rawContent, cwd);
|
|
339
350
|
const fullContent = stripShippedMilestones(rawContent);
|
|
340
351
|
const escapedPhase = phaseMarkdownRegexSource(phaseNum);
|
|
352
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
341
353
|
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'i');
|
|
342
354
|
for (const content of [milestoneContent, fullContent]) {
|
|
343
355
|
const headerMatch = content.match(phaseHeader);
|
|
@@ -582,8 +594,16 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
582
594
|
* none left in this file) degrades to the pre-#3897 two-tier behavior rather
|
|
583
595
|
* than throwing on a missing argument.
|
|
584
596
|
*/
|
|
597
|
+
/**
|
|
598
|
+
* The dependency resolver's one comparison normalization. Callers that must
|
|
599
|
+
* predict whether a token names a plan reuse this seam instead of copying its
|
|
600
|
+
* case-folding rule.
|
|
601
|
+
*/
|
|
602
|
+
function normalizeDependencyToken(token) {
|
|
603
|
+
return String(token).toLowerCase();
|
|
604
|
+
}
|
|
585
605
|
function resolveDependencyId(dep, planMap, canonicalToId, shortFormToId) {
|
|
586
|
-
const lower = dep
|
|
606
|
+
const lower = normalizeDependencyToken(dep);
|
|
587
607
|
if (planMap.has(lower))
|
|
588
608
|
return planMap.get(lower).id;
|
|
589
609
|
if (canonicalToId.has(lower))
|
|
@@ -624,7 +644,7 @@ function buildShortFormToId(rawPlans) {
|
|
|
624
644
|
const canonical = extractCanonicalPlanId(p.id);
|
|
625
645
|
const lastDash = canonical.lastIndexOf('-');
|
|
626
646
|
if (lastDash > 0 && lastDash < canonical.length - 1) {
|
|
627
|
-
const shortForm = canonical.slice(lastDash + 1)
|
|
647
|
+
const shortForm = normalizeDependencyToken(canonical.slice(lastDash + 1));
|
|
628
648
|
if (/^\d+$/.test(shortForm) && !shortFormToId.has(shortForm)) {
|
|
629
649
|
shortFormToId.set(shortForm, p.id);
|
|
630
650
|
}
|
|
@@ -734,12 +754,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
734
754
|
phase: normalized,
|
|
735
755
|
error: `Phase ${normalized} is ambiguous: ${ambiguousMatches.length} directories match (${ambiguousMatches.map((m) => `"${m}"`).join(', ')}).`,
|
|
736
756
|
ambiguous_matches: ambiguousMatches,
|
|
737
|
-
plans: [], waves: {}, incomplete: [], has_checkpoints: false,
|
|
757
|
+
plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false,
|
|
738
758
|
}, raw);
|
|
739
759
|
return;
|
|
740
760
|
}
|
|
741
761
|
if (!phaseDir) {
|
|
742
|
-
output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], has_checkpoints: false }, raw);
|
|
762
|
+
output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], runnable: [], ready_plans: [], has_checkpoints: false }, raw);
|
|
743
763
|
return;
|
|
744
764
|
}
|
|
745
765
|
void phaseDirName; // used only to set phaseDir above
|
|
@@ -834,7 +854,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
834
854
|
// ── Pass 2: topological level assignment via depends_on DAG ──────────────
|
|
835
855
|
const seenLower = new Map();
|
|
836
856
|
for (const p of rawPlans) {
|
|
837
|
-
const lower = p.id
|
|
857
|
+
const lower = normalizeDependencyToken(p.id);
|
|
838
858
|
const existing = seenLower.get(lower);
|
|
839
859
|
if (existing !== undefined) {
|
|
840
860
|
error(`depends_on index collision in phase ${normalized}: plan IDs '${existing}' and '${p.id}' are identical when case-folded. Rename one file to avoid ambiguous dependency resolution.`);
|
|
@@ -842,8 +862,8 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
842
862
|
}
|
|
843
863
|
seenLower.set(lower, p.id);
|
|
844
864
|
}
|
|
845
|
-
const planMap = new Map(rawPlans.map((p) => [p.id
|
|
846
|
-
const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id)
|
|
865
|
+
const planMap = new Map(rawPlans.map((p) => [normalizeDependencyToken(p.id), p]));
|
|
866
|
+
const canonicalToId = new Map(rawPlans.map((p) => [normalizeDependencyToken(extractCanonicalPlanId(p.id)), p.id]));
|
|
847
867
|
// #3897 rung 4 (ADR-3473 §8.9) — the third depends_on resolution tier.
|
|
848
868
|
// Resolves a bare in-phase plan-number short form (e.g. "01") to its owning
|
|
849
869
|
// plan id. In-phase only by construction (T49): the map is built from THIS
|
|
@@ -878,6 +898,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
878
898
|
const waves = {};
|
|
879
899
|
const incomplete = [];
|
|
880
900
|
const runnable = [];
|
|
901
|
+
// #4628: DAG-ready view — see the per-plan emission below. Keyed by id so
|
|
902
|
+
// the per-plan pass can resolve each plan's dependency edges.
|
|
903
|
+
const resolvedDepsByPlan = new Map(haltNodes.map((n) => [n.id, n.resolvedDependsOn]));
|
|
904
|
+
const readyPlans = [];
|
|
881
905
|
let hasCheckpoints = false;
|
|
882
906
|
const warnings = [];
|
|
883
907
|
// #3427 / ADR-3473 §8.5: name every dropped depends_on edge (plan AND
|
|
@@ -887,8 +911,12 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
887
911
|
// plan with no dropped edges and a genuinely wrong `wave:` still warns
|
|
888
912
|
// (N3, D6, T25).
|
|
889
913
|
const plansWithUnresolvedTokens = new Set();
|
|
914
|
+
const unresolvedTokensByPlan = new Map();
|
|
890
915
|
for (const { plan, token } of unresolved) {
|
|
891
916
|
plansWithUnresolvedTokens.add(plan);
|
|
917
|
+
const tokens = unresolvedTokensByPlan.get(plan) ?? [];
|
|
918
|
+
tokens.push(formatDiagnosticToken(token));
|
|
919
|
+
unresolvedTokensByPlan.set(plan, tokens);
|
|
892
920
|
warnings.push(`Plan ${plan}: depends_on token ${formatDiagnosticToken(token)} does not resolve to any plan in this phase — edge dropped, wave placement for this plan may be unreliable`);
|
|
893
921
|
}
|
|
894
922
|
for (const rawPlan of rawPlans) {
|
|
@@ -896,14 +924,34 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
896
924
|
hasCheckpoints = true;
|
|
897
925
|
}
|
|
898
926
|
const blockedByIds = blockedBy.get(rawPlan.id) ?? [];
|
|
927
|
+
// #4628: readiness fails closed — every resolved dependency must have
|
|
928
|
+
// completion evidence (has_summary), and a depends_on edge that never
|
|
929
|
+
// resolved (#3427 — a dropped edge) carries no evidence to check. A
|
|
930
|
+
// single evaluation feeds both the ready_plans list and the per-plan
|
|
931
|
+
// `ready` / `unresolved_dependencies` fields below.
|
|
932
|
+
// Case-fold the dep before the planMap lookup: planMap is lowercase-keyed
|
|
933
|
+
// (#2237) while resolveDependencyId returns the raw id — a mixed-case id
|
|
934
|
+
// must not read as 'no evidence' (over-blocking a ready plan).
|
|
935
|
+
const missingEvidence = (resolvedDepsByPlan.get(rawPlan.id) ?? [])
|
|
936
|
+
.filter((dep) => planMap.get(dep.toLowerCase())?.hasSummary !== true);
|
|
937
|
+
if (plansWithUnresolvedTokens.has(rawPlan.id)) {
|
|
938
|
+
missingEvidence.push(...(unresolvedTokensByPlan.get(rawPlan.id) ?? []));
|
|
939
|
+
}
|
|
940
|
+
const isReady = blockedByIds.length === 0 && missingEvidence.length === 0;
|
|
899
941
|
if (!rawPlan.hasSummary) {
|
|
900
942
|
incomplete.push(rawPlan.id);
|
|
901
943
|
// #2830: the runnable-only view — incomplete AND not transitively
|
|
902
944
|
// blocked by a halted upstream plan. Additive alongside `incomplete`,
|
|
903
945
|
// which keeps its existing "no SUMMARY yet" meaning unchanged.
|
|
946
|
+
// #4628: runnable says NOTHING about completion evidence — a runnable
|
|
947
|
+
// plan whose dependencies lack a SUMMARY is not DAG-ready. Consumers
|
|
948
|
+
// dispatch from `ready_plans`.
|
|
904
949
|
if (blockedByIds.length === 0) {
|
|
905
950
|
runnable.push(rawPlan.id);
|
|
906
951
|
}
|
|
952
|
+
if (isReady) {
|
|
953
|
+
readyPlans.push(rawPlan.id);
|
|
954
|
+
}
|
|
907
955
|
}
|
|
908
956
|
const computedWave = (level.get(rawPlan.id) ?? 0) + levelOffset;
|
|
909
957
|
const effectiveWave = computedWave;
|
|
@@ -945,6 +993,15 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
945
993
|
halted: rawPlan.halted,
|
|
946
994
|
blocked_by: blockedByIds,
|
|
947
995
|
};
|
|
996
|
+
if (!rawPlan.hasSummary) {
|
|
997
|
+
// #4628: readiness is a property of INCOMPLETE plans (a summarized plan
|
|
998
|
+
// is filtered by has_summary before readiness is ever consulted). The
|
|
999
|
+
// unresolved_dependencies list names exactly which predecessors lack
|
|
1000
|
+
// completion evidence, for the named-skip report.
|
|
1001
|
+
plan['ready'] = isReady;
|
|
1002
|
+
if (missingEvidence.length > 0)
|
|
1003
|
+
plan['unresolved_dependencies'] = missingEvidence;
|
|
1004
|
+
}
|
|
948
1005
|
plans.push(plan);
|
|
949
1006
|
const waveKey = String(effectiveWave);
|
|
950
1007
|
if (!waves[waveKey]) {
|
|
@@ -958,6 +1015,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
958
1015
|
waves,
|
|
959
1016
|
incomplete,
|
|
960
1017
|
runnable,
|
|
1018
|
+
ready_plans: readyPlans,
|
|
961
1019
|
has_checkpoints: hasCheckpoints,
|
|
962
1020
|
};
|
|
963
1021
|
if (planNamingWarning)
|
|
@@ -1061,12 +1119,24 @@ function assertDescriptionPreservesMilestoneScope(cwd, description, command) {
|
|
|
1061
1119
|
* before any directory does; milestone-scoping is wrong here because a number
|
|
1062
1120
|
* used under any milestone on another branch is still taken).
|
|
1063
1121
|
*
|
|
1122
|
+
* #4225 — the horizon must track the ALLOCATION scope. When the allocation is
|
|
1123
|
+
* workstream-scoped (`--ws`/`GSD_WORKSTREAM`, resolved into the env before
|
|
1124
|
+
* dispatch), the sibling's copy of the SAME workstream is what carries that
|
|
1125
|
+
* scope's independent numbering; the sibling's ROOT roadmap and phases/
|
|
1126
|
+
* belong to a different numbering universe (docs/FEATURES.md §51 REQ-WS-01 —
|
|
1127
|
+
* workstream state is isolated in `.planning/workstreams/{name}/`) and must
|
|
1128
|
+
* not contribute. `planningDir(wt, ws)` reuses the canonical resolver, so the
|
|
1129
|
+
* sibling scope matches the local scope's own resolution (env workstream plus
|
|
1130
|
+
* env project segment) by construction; `ws === null` (no workstream active)
|
|
1131
|
+
* keeps the #3849 root-scope horizon byte-for-byte.
|
|
1132
|
+
*
|
|
1064
1133
|
* Widen, never refuse: a missing `.planning/`, an unreadable sibling, a
|
|
1065
1134
|
* non-git cwd, or an unavailable git binary each leave `used` untouched —
|
|
1066
1135
|
* allocation then behaves exactly as it did before this horizon existed.
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1136
|
+
* A sibling that simply lacks the active workstream's directory is the same
|
|
1137
|
+
* fail-open case: it contributes nothing. Sentinels reuse the canonical
|
|
1138
|
+
* `isSentinelPhaseId`; the dir pattern is the same one the on-disk scan uses,
|
|
1139
|
+
* so decimal sub-phases (`411.1-foo`) are correctly not integers.
|
|
1070
1140
|
*/
|
|
1071
1141
|
function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
1072
1142
|
let porcelain;
|
|
@@ -1084,8 +1154,16 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1084
1154
|
catch {
|
|
1085
1155
|
return; // not a git repo / git unavailable — unchanged behavior
|
|
1086
1156
|
}
|
|
1157
|
+
// #4225: the env workstream, read once with planningDir's own discriminator
|
|
1158
|
+
// (`?? null` = deliberately no workstream — never re-derived per sibling).
|
|
1159
|
+
// A poisoned value would already have thrown at the local `planningDir(cwd)`
|
|
1160
|
+
// call every allocator makes before reaching this horizon; the per-sibling
|
|
1161
|
+
// try/catch below still keeps any resolution failure fail-open.
|
|
1162
|
+
const ws = process.env['GSD_WORKSTREAM'] ?? null;
|
|
1163
|
+
const siblingPlanningDir = (wt) => planningDir(wt, ws);
|
|
1087
1164
|
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
|
1088
1165
|
// Same header shape the allocators scan locally (#1729 tag tolerance).
|
|
1166
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1089
1167
|
const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
|
|
1090
1168
|
for (const line of porcelain.split('\n')) {
|
|
1091
1169
|
if (!line.startsWith('worktree '))
|
|
@@ -1094,7 +1172,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1094
1172
|
if (!wt || node_path_1.default.resolve(wt) === node_path_1.default.resolve(cwd))
|
|
1095
1173
|
continue;
|
|
1096
1174
|
try {
|
|
1097
|
-
for (const entry of node_fs_1.default.readdirSync(node_path_1.default.join(wt, '
|
|
1175
|
+
for (const entry of node_fs_1.default.readdirSync(node_path_1.default.join(siblingPlanningDir(wt), 'phases'))) {
|
|
1098
1176
|
const match = entry.match(dirNumPattern);
|
|
1099
1177
|
if (!match)
|
|
1100
1178
|
continue;
|
|
@@ -1104,10 +1182,10 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1104
1182
|
}
|
|
1105
1183
|
}
|
|
1106
1184
|
catch {
|
|
1107
|
-
/* worktree has no .planning — normal, contributes nothing */
|
|
1185
|
+
/* worktree has no .planning (or no copy of this scope) — normal, contributes nothing */
|
|
1108
1186
|
}
|
|
1109
1187
|
try {
|
|
1110
|
-
const content = node_fs_1.default.readFileSync(node_path_1.default.join(wt, '
|
|
1188
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(siblingPlanningDir(wt), 'ROADMAP.md'), 'utf-8');
|
|
1111
1189
|
let m;
|
|
1112
1190
|
headerPattern.lastIndex = 0;
|
|
1113
1191
|
while ((m = headerPattern.exec(content)) !== null) {
|
|
@@ -1117,7 +1195,7 @@ function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
|
1117
1195
|
}
|
|
1118
1196
|
}
|
|
1119
1197
|
catch {
|
|
1120
|
-
/* no roadmap in that worktree — normal, contributes nothing */
|
|
1198
|
+
/* no roadmap in that worktree (or scope) — normal, contributes nothing */
|
|
1121
1199
|
}
|
|
1122
1200
|
}
|
|
1123
1201
|
}
|
|
@@ -1151,6 +1229,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
1151
1229
|
// (section header, roadmap bullet, or on-disk directory) is counted:
|
|
1152
1230
|
// 1) Section headers: ### Phase N: / ## Phase N: / #### Phase N:
|
|
1153
1231
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1232
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1154
1233
|
const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
|
|
1155
1234
|
// 2) Roadmap bullet entries: - [ ] **Phase N: ...** (all checkbox variants)
|
|
1156
1235
|
// The lookahead accepts colon, decimal-dot, whitespace, bold-close asterisk,
|
|
@@ -1260,6 +1339,7 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1260
1339
|
// bullets, on-disk dirs. The bullet scan was missing here — a bullet-only
|
|
1261
1340
|
// `Phase N` row was invisible to batch allocation (#3849 secondary).
|
|
1262
1341
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1342
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1263
1343
|
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
|
|
1264
1344
|
const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]{0,200}\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
|
|
1265
1345
|
let m;
|
|
@@ -1343,7 +1423,65 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1343
1423
|
// found')` above, which terminates the process and never reaches here.
|
|
1344
1424
|
publishStateContract(cwd);
|
|
1345
1425
|
}
|
|
1346
|
-
|
|
1426
|
+
// #4569: scans all three representations of an existing decimal sub-phase
|
|
1427
|
+
// under `base` — on-disk `phases/` directories, `### Phase BASE.N:` headings,
|
|
1428
|
+
// and `- [ ] Phase BASE.N:` roadmap SUMMARY CHECKLIST bullets. A bullet-only
|
|
1429
|
+
// roadmap with no heading yet and no on-disk directory yet must still be
|
|
1430
|
+
// seen, or an allocator can silently reallocate an already-used decimal
|
|
1431
|
+
// number. Shared by `cmdPhaseInsert`'s normalized-base scan and its
|
|
1432
|
+
// sibling-allocation parent-base scan so the two never drift apart.
|
|
1433
|
+
function scanExistingDecimalPhaseNumbers(phasesDir, rawContent, base) {
|
|
1434
|
+
const decimalSet = new Set();
|
|
1435
|
+
// #2245 audit: existsSync-guarded, mirroring cmdPhaseNextDecimal's identical
|
|
1436
|
+
// scan above — a missing phasesDir (no decimal sub-phases yet) is the
|
|
1437
|
+
// expected, silent case (empty decimalSet). A readdirSync failure once the
|
|
1438
|
+
// dir is confirmed to EXIST is a genuine anomaly; swallowing it used to let
|
|
1439
|
+
// `phase insert` proceed with an incomplete decimalSet and risk writing a
|
|
1440
|
+
// decimal phase number that collides with an existing on-disk directory
|
|
1441
|
+
// the scan simply never saw — surfaced loud instead, like the sibling.
|
|
1442
|
+
//
|
|
1443
|
+
// #4634 (lint-phase-enumeration-drift): routed through the canonical
|
|
1444
|
+
// PHYSICAL-set owner (`listAllPhaseDirs`, phase-locator.cts) instead of a
|
|
1445
|
+
// hand-rolled `readdirSync`. This scan — like its sibling `cmdPhaseNextDecimal`
|
|
1446
|
+
// and its caller `cmdPhaseInsert` (both exempted in the drift guard for the
|
|
1447
|
+
// same reason) — must see EVERY on-disk decimal sub-phase directory
|
|
1448
|
+
// regardless of the current milestone window, so `listMilestonePhaseDirs`
|
|
1449
|
+
// (windowed) is the wrong owner here; `includeSentinels: true` preserves this
|
|
1450
|
+
// function's pre-existing behavior of never sentinel-filtering (the decimal
|
|
1451
|
+
// regex below only ever matches `base.N`-shaped names, so sentinel inclusion
|
|
1452
|
+
// is a no-op either way).
|
|
1453
|
+
if (node_fs_1.default.existsSync(phasesDir)) {
|
|
1454
|
+
const { value: dirs, scope } = listAllPhaseDirs(phasesDir, { includeSentinels: true });
|
|
1455
|
+
if (scope === SCOPE.UNREADABLE) {
|
|
1456
|
+
// The dir EXISTS but could not be read (EACCES/EIO) — a genuine anomaly,
|
|
1457
|
+
// not the expected empty-decimalSet case above. Surfaced loud, matching
|
|
1458
|
+
// this function's pre-migration `readdirSync` catch: swallowing it would
|
|
1459
|
+
// let `phase insert` proceed with an incomplete decimalSet and collide
|
|
1460
|
+
// with an existing on-disk decimal directory the scan never saw.
|
|
1461
|
+
error(`Failed to scan phase directories for existing decimal phases: unable to read ${phasesDir}`);
|
|
1462
|
+
}
|
|
1463
|
+
const decimalPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${(0, pattern_cjs_1.escapeRegex)(base)}\\.(\\d+)`);
|
|
1464
|
+
for (const dir of dirs) {
|
|
1465
|
+
const dm = dir.match(decimalPattern);
|
|
1466
|
+
if (dm)
|
|
1467
|
+
decimalSet.add(parseInt(dm[1], 10));
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
const rmPhasePattern = new RegExp(
|
|
1471
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1472
|
+
`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
|
|
1473
|
+
let rmMatch;
|
|
1474
|
+
while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
|
|
1475
|
+
decimalSet.add(parseInt(rmMatch[1], 10));
|
|
1476
|
+
}
|
|
1477
|
+
const checklistDecimalPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${phaseMarkdownRegexSource(base)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi');
|
|
1478
|
+
let clMatch;
|
|
1479
|
+
while ((clMatch = checklistDecimalPattern.exec(rawContent)) !== null) {
|
|
1480
|
+
decimalSet.add(parseInt(clMatch[1], 10));
|
|
1481
|
+
}
|
|
1482
|
+
return decimalSet;
|
|
1483
|
+
}
|
|
1484
|
+
function cmdPhaseInsert(cwd, afterPhase, description, raw, allocation = 'nested') {
|
|
1347
1485
|
if (!afterPhase || !description) {
|
|
1348
1486
|
error('after-phase and description required for phase insert');
|
|
1349
1487
|
}
|
|
@@ -1358,9 +1496,11 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
1358
1496
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
1359
1497
|
const normalizedAfter = normalizePhaseName(afterPhase);
|
|
1360
1498
|
const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
|
|
1499
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1361
1500
|
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:`, 'i');
|
|
1362
1501
|
const headingMatch = targetPattern.test(content);
|
|
1363
1502
|
const bulletPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'i');
|
|
1503
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1364
1504
|
const anyHeadingPattern = /#{2,4}\s*Phase\s+\d/i;
|
|
1365
1505
|
const roadmapHasHeadingPhases = anyHeadingPattern.test(content);
|
|
1366
1506
|
const isBulletStyle = !headingMatch && bulletPattern.test(content) && !roadmapHasHeadingPhases;
|
|
@@ -1373,43 +1513,20 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
1373
1513
|
}
|
|
1374
1514
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
1375
1515
|
const normalizedBase = normalizePhaseName(afterPhase);
|
|
1376
|
-
const decimalSet =
|
|
1377
|
-
// #2245 audit: existsSync-guarded, mirroring cmdPhaseNextDecimal's identical
|
|
1378
|
-
// scan above — a missing phasesDir (no decimal sub-phases yet) is the
|
|
1379
|
-
// expected, silent case (empty decimalSet). A readdirSync failure once the
|
|
1380
|
-
// dir is confirmed to EXIST is a genuine anomaly; swallowing it used to let
|
|
1381
|
-
// `phase insert` proceed with an incomplete decimalSet and risk writing a
|
|
1382
|
-
// decimal phase number that collides with an existing on-disk directory
|
|
1383
|
-
// the scan simply never saw — surfaced loud instead, like the sibling.
|
|
1384
|
-
if (node_fs_1.default.existsSync(phasesDir)) {
|
|
1385
|
-
// Initialized (not just declared) so TS's definite-assignment check is
|
|
1386
|
-
// satisfied without relying on control-flow narrowing through error()'s
|
|
1387
|
-
// `never` return, which TS does not propagate through a destructured
|
|
1388
|
-
// module-property function reference — error() still halts the process
|
|
1389
|
-
// before `dirs` below is ever computed from this placeholder value.
|
|
1390
|
-
let entries = [];
|
|
1391
|
-
try {
|
|
1392
|
-
entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
1393
|
-
}
|
|
1394
|
-
catch (e) {
|
|
1395
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1396
|
-
error(`Failed to scan phase directories for existing decimal phases: ${msg}`);
|
|
1397
|
-
}
|
|
1398
|
-
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1399
|
-
const decimalPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${(0, pattern_cjs_1.escapeRegex)(normalizedBase)}\\.(\\d+)`);
|
|
1400
|
-
for (const dir of dirs) {
|
|
1401
|
-
const dm = dir.match(decimalPattern);
|
|
1402
|
-
if (dm)
|
|
1403
|
-
decimalSet.add(parseInt(dm[1], 10));
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
const rmPhasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)${OPTIONAL_PHASE_TAG_SOURCE}\\s*:`, 'gi');
|
|
1407
|
-
let rmMatch;
|
|
1408
|
-
while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
|
|
1409
|
-
decimalSet.add(parseInt(rmMatch[1], 10));
|
|
1410
|
-
}
|
|
1516
|
+
const decimalSet = scanExistingDecimalPhaseNumbers(phasesDir, rawContent, normalizedBase);
|
|
1411
1517
|
const nextDecimal = decimalSet.size === 0 ? 1 : Math.max(...decimalSet) + 1;
|
|
1412
|
-
|
|
1518
|
+
let _decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
|
1519
|
+
// #4569: sibling allocation joins afterPhase's PARENT level instead of nesting
|
|
1520
|
+
// one level deeper under afterPhase itself. A top-level phase (no existing
|
|
1521
|
+
// decimal segment) has no sibling level to join; nested is the only sensible
|
|
1522
|
+
// allocation, so we silently fall back for that case.
|
|
1523
|
+
const lastDotIndex = normalizedBase.lastIndexOf('.');
|
|
1524
|
+
if (allocation === 'sibling' && lastDotIndex !== -1) {
|
|
1525
|
+
const parentBase = normalizedBase.slice(0, lastDotIndex);
|
|
1526
|
+
const siblingDecimalSet = scanExistingDecimalPhaseNumbers(phasesDir, rawContent, parentBase);
|
|
1527
|
+
const siblingNextDecimal = siblingDecimalSet.size === 0 ? 1 : Math.max(...siblingDecimalSet) + 1;
|
|
1528
|
+
_decimalPhase = `${parentBase}.${siblingNextDecimal}`;
|
|
1529
|
+
}
|
|
1413
1530
|
const insertConfig = loadConfig(cwd);
|
|
1414
1531
|
const projectCode = insertConfig.project_code || '';
|
|
1415
1532
|
const pfx = projectCode ? `${projectCode}-` : '';
|
|
@@ -1460,7 +1577,9 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
1460
1577
|
}
|
|
1461
1578
|
else {
|
|
1462
1579
|
const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))} ${_decimalPhase} to break down)\n`;
|
|
1463
|
-
const headerPattern = new RegExp(
|
|
1580
|
+
const headerPattern = new RegExp(
|
|
1581
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1582
|
+
`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}:[^\\n]*\\n)`, 'i');
|
|
1464
1583
|
const headerMatch = rawContent.match(headerPattern);
|
|
1465
1584
|
if (!headerMatch) {
|
|
1466
1585
|
error(`Could not find Phase ${afterPhase} header`);
|
|
@@ -1782,7 +1901,9 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
1782
1901
|
if (!isDecimal) {
|
|
1783
1902
|
// #1729: fold an optional pre-colon ( ) tag into the suffix capture so it
|
|
1784
1903
|
// is re-emitted verbatim — a tagged later phase still gets renumbered.
|
|
1785
|
-
content = content.replace(
|
|
1904
|
+
content = content.replace(
|
|
1905
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
1906
|
+
/(#{2,4}\s*Phase\s+)(\d+(?:\.\d+)?)((?:\s*\([^)\r\n]{0,200}\))?\s*:)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}${suffix}`);
|
|
1786
1907
|
content = content.replace(/(-\s*\[[ x]\]\s*.*?Phase\s+)(\d+)(\s*:|\s+)/gi, (_match, prefix, num, suffix) => `${prefix}${decrementRoadmapPhaseNumber(num, removedInt)}${suffix}`);
|
|
1787
1908
|
// ORDINAL-RENUMBER — CELL EDIT (not row-deletion) — migrated onto
|
|
1788
1909
|
// updateTableCell (ADR-2143 §7, sibling of the deleteTableRow scoping
|
|
@@ -2115,6 +2236,7 @@ function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
|
|
|
2115
2236
|
if (!roadmapContent || !phaseNum)
|
|
2116
2237
|
return null;
|
|
2117
2238
|
const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
|
|
2239
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
2118
2240
|
const heading = roadmapContent.match(new RegExp(`^#{2,4}\\s*Phase\\s+${phaseEscaped}${OPTIONAL_PHASE_TAG_SOURCE}\\s*:\\s*([^\\n]+)`, 'im'));
|
|
2119
2241
|
if (!heading)
|
|
2120
2242
|
return null;
|
|
@@ -2776,7 +2898,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2776
2898
|
// non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
|
|
2777
2899
|
// here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
|
|
2778
2900
|
// which would misreport a present-but-bad marker as no marker at all.
|
|
2779
|
-
const resolvedWorkstream =
|
|
2901
|
+
const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
|
|
2780
2902
|
if (availableWorkstreams.length > 0 && !resolvedWorkstream) {
|
|
2781
2903
|
// #3579: getActiveWorkstream now inherits a pointer-less session's read
|
|
2782
2904
|
// from the shared .planning/active-workstream marker, so reaching this
|
|
@@ -3022,7 +3144,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3022
3144
|
// #2617: pass the project's runtime so the blocked-completion error below
|
|
3023
3145
|
// suggests the command surface this runtime actually installs
|
|
3024
3146
|
// ($gsd-… on Codex) rather than a hard-coded Claude-style string.
|
|
3025
|
-
const verificationStatus = readVerificationStatus(phaseFullDir, {
|
|
3147
|
+
const verificationStatus = readVerificationStatus(phaseFullDir, {
|
|
3148
|
+
runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd),
|
|
3149
|
+
convention: resolvePhaseIdConvention(cwd),
|
|
3150
|
+
});
|
|
3026
3151
|
// #3057 B3: the staleness check inside readVerificationStatus can itself
|
|
3027
3152
|
// fail (fs / scanPhasePlans / clock error), in which case `status` above
|
|
3028
3153
|
// was routed as if nothing were stale (unchanged fail-open routing) — but
|
|
@@ -3065,7 +3190,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3065
3190
|
// whole-slice `.replace()` onto the seam. Applied per single physical
|
|
3066
3191
|
// line by updateBullet, so the pattern no longer needs the `m` flag
|
|
3067
3192
|
// (it never sees more than one line at a time); see
|
|
3068
|
-
//
|
|
3193
|
+
// writePlansField below for the sites that were migrated onto
|
|
3069
3194
|
// withPhaseSection instead.
|
|
3070
3195
|
//
|
|
3071
3196
|
// #2245 review Fix 6: this is behaviour-preserving for GSD-GENERATED
|
|
@@ -3118,12 +3243,105 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3118
3243
|
};
|
|
3119
3244
|
// ADR-2143 §4: the plan-count write is now routed through
|
|
3120
3245
|
// withPhaseSection (see mutateMilestonePhase below), which hands this
|
|
3121
|
-
//
|
|
3122
|
-
//
|
|
3123
|
-
//
|
|
3124
|
-
//
|
|
3125
|
-
//
|
|
3126
|
-
|
|
3246
|
+
// seam call ONLY phase N's own detail-section body — the section
|
|
3247
|
+
// boundary itself confines the write (the #2067/#2200 boundary-
|
|
3248
|
+
// crossing class is structurally impossible for this site).
|
|
3249
|
+
//
|
|
3250
|
+
// #4906 Phase 2 (#4917/ADR-4910): migrated off the one-capture-group
|
|
3251
|
+
// regex that replaced to end of line, dropping any hand-written
|
|
3252
|
+
// trailing prose after the count (#4852) — onto the PlanningDoc
|
|
3253
|
+
// `boldField` write seam, whose `valueSpan`/`trailingSpan` split
|
|
3254
|
+
// never touches the trailing annotation.
|
|
3255
|
+
const writePlansField = (body) => {
|
|
3256
|
+
const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(body, 'ROADMAP.md');
|
|
3257
|
+
if (!parsed.ok) {
|
|
3258
|
+
preservationWarnings.push({ field: 'Plans', reason: parsed.reason });
|
|
3259
|
+
return body;
|
|
3260
|
+
}
|
|
3261
|
+
const fieldId = (0, planning_document_cjs_1.findField)(parsed.value, 'Plans');
|
|
3262
|
+
if (!fieldId) {
|
|
3263
|
+
// #4906 regression (#1163 parity, caught by gsd-test against
|
|
3264
|
+
// roadmap.cts's sibling site): a hand-edited or pre-template
|
|
3265
|
+
// ROADMAP.md may carry a PLAIN (non-bold) `Plans:` line rather
|
|
3266
|
+
// than the canonical `**Plans**:`/`**Plans:**` bold field.
|
|
3267
|
+
// BOLD_FIELD_RE stays bold-only (widening it would register
|
|
3268
|
+
// ordinary prose as a spurious field seam-wide) — this fallback
|
|
3269
|
+
// mirrors roadmap.cts's identical one, kept in parity per
|
|
3270
|
+
// Decision 2 rather than letting the two sites diverge on which
|
|
3271
|
+
// legacy shapes they tolerate.
|
|
3272
|
+
const plainMatch = body.match(/^([ \t]*)Plans:([ \t]*)([^\r\n]*)$/m);
|
|
3273
|
+
if (!plainMatch) {
|
|
3274
|
+
// No `**Plans:**`/`**Plans**:`/plain `Plans:` line in this
|
|
3275
|
+
// phase's section — nothing to write; not a failure (mirrors
|
|
3276
|
+
// the old regex's silent no-match no-op).
|
|
3277
|
+
return body;
|
|
3278
|
+
}
|
|
3279
|
+
const [whole, indent, spacing, plainValue] = plainMatch;
|
|
3280
|
+
const plainCountPrefixMatch = plainValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
|
|
3281
|
+
const plainIsTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(plainValue.trim());
|
|
3282
|
+
if (!plainCountPrefixMatch && !plainIsTemplatePlaceholder) {
|
|
3283
|
+
// Arm 3: freeform prose, TBD, a bracketed human annotation, or
|
|
3284
|
+
// an empty value — leave the field exactly as it was.
|
|
3285
|
+
return body;
|
|
3286
|
+
}
|
|
3287
|
+
const plainNewCountText = `${summaryCount}/${planCount} plans complete`;
|
|
3288
|
+
const plainSuffix = plainCountPrefixMatch ? plainValue.slice(plainCountPrefixMatch[0].length) : '';
|
|
3289
|
+
const newPlainLine = `${indent}Plans:${spacing}${plainNewCountText}${plainSuffix}`;
|
|
3290
|
+
const start = plainMatch.index ?? body.indexOf(whole);
|
|
3291
|
+
return body.slice(0, start) + newPlainLine + body.slice(start + whole.length);
|
|
3292
|
+
}
|
|
3293
|
+
// #4906 regression fix: PREFIX-match the existing value's count
|
|
3294
|
+
// token and re-glue whatever follows it VERBATIM — a glued-on
|
|
3295
|
+
// annotation with no ` — ` separator (e.g. a parenthetical like
|
|
3296
|
+
// `0/1 plans executed (11-16 are gap closure from VERIFICATION)`)
|
|
3297
|
+
// lives entirely inside `value` (`TRAILING_SEPARATOR_RE` in
|
|
3298
|
+
// planning-document.cts only splits on ` — `, unchanged/correct),
|
|
3299
|
+
// so overwriting `value` outright previously destroyed it.
|
|
3300
|
+
//
|
|
3301
|
+
// #4906 review finding (isolated adversarial pass): the prior
|
|
3302
|
+
// version of this migration preserved this site's OLD
|
|
3303
|
+
// unconditional-overwrite behavior for the no-count-prefix case,
|
|
3304
|
+
// which clobbers arm 3 (freeform prose / TBD / a bracketed human
|
|
3305
|
+
// annotation like `[Deferred pending re-scope]`) — a real
|
|
3306
|
+
// regression against the design doc's own Behavior table row 4,
|
|
3307
|
+
// not an accepted trade-off. Fixed here by adopting the SAME
|
|
3308
|
+
// template-placeholder / arm-3-untouched classification
|
|
3309
|
+
// roadmap.cts's sibling site already uses (isTemplatePlaceholder +
|
|
3310
|
+
// "no count prefix and not a placeholder => leave untouched"),
|
|
3311
|
+
// rather than letting the two migrated sites diverge on this.
|
|
3312
|
+
const newCountText = `${summaryCount}/${planCount} plans complete`;
|
|
3313
|
+
const current = (0, planning_document_cjs_1.readNode)(parsed.value, fieldId);
|
|
3314
|
+
if (!current.ok) {
|
|
3315
|
+
return body;
|
|
3316
|
+
}
|
|
3317
|
+
const currentValue = current.value;
|
|
3318
|
+
const countPrefixMatch = currentValue.match(/^(?:\d+\s*\/\s*\d+\s+plans(?:\s+(?:complete|executed))?|\d+\s+plans?)/i);
|
|
3319
|
+
const isTemplatePlaceholder = /^\[\s*Number of plans\b[\s\S]*\]$/i.test(currentValue.trim());
|
|
3320
|
+
if (!countPrefixMatch && !isTemplatePlaceholder) {
|
|
3321
|
+
// Arm 3: freeform prose, TBD, a bracketed human annotation, or an
|
|
3322
|
+
// empty value — leave the field exactly as it was.
|
|
3323
|
+
return body;
|
|
3324
|
+
}
|
|
3325
|
+
const newValueToWrite = countPrefixMatch
|
|
3326
|
+
? newCountText + currentValue.slice(countPrefixMatch[0].length)
|
|
3327
|
+
: newCountText;
|
|
3328
|
+
const staged = (0, planning_document_cjs_1.setFieldValue)(parsed.value, fieldId, newValueToWrite);
|
|
3329
|
+
if (!staged.ok) {
|
|
3330
|
+
preservationWarnings.push({ field: 'Plans', reason: staged.reason });
|
|
3331
|
+
return body;
|
|
3332
|
+
}
|
|
3333
|
+
const out = (0, planning_document_cjs_1.serialize)(staged.value);
|
|
3334
|
+
if (!out.ok) {
|
|
3335
|
+
// `hasUnreadableNodes` refusal (ADR-4910 amendment) — a ragged
|
|
3336
|
+
// SIBLING node elsewhere in this same section refuses the whole
|
|
3337
|
+
// splice. Never throw / crash the phase-complete transaction over
|
|
3338
|
+
// a node unrelated to this write; surface it and leave `body`
|
|
3339
|
+
// unchanged, same as any other preservation warning.
|
|
3340
|
+
preservationWarnings.push({ field: 'Plans', reason: out.reason });
|
|
3341
|
+
return body;
|
|
3342
|
+
}
|
|
3343
|
+
return out.value;
|
|
3344
|
+
};
|
|
3127
3345
|
const phaseInfoSummaries = phaseInfo['summaries'];
|
|
3128
3346
|
// #2200: apply the phase-checkbox flip, the plan-count write, and the
|
|
3129
3347
|
// per-plan checkbox flips ONLY within the current milestone's region(s)
|
|
@@ -3160,7 +3378,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3160
3378
|
// section's body, so neither regex can escape into a sibling
|
|
3161
3379
|
// phase's section, a shipped milestone, or a Backlog entry.
|
|
3162
3380
|
s = withPhaseSection(s, phaseNum, (body) => {
|
|
3163
|
-
let b = body
|
|
3381
|
+
let b = writePlansField(body);
|
|
3164
3382
|
for (const summaryFile of phaseInfoSummaries) {
|
|
3165
3383
|
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
3166
3384
|
if (!planId)
|
|
@@ -3205,14 +3423,24 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3205
3423
|
if (node_fs_1.default.existsSync(reqPath)) {
|
|
3206
3424
|
const phaseEsc = phaseMarkdownRegexSource(phaseNum);
|
|
3207
3425
|
const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
3208
|
-
const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(
|
|
3426
|
+
const phaseSectionMatch = currentMilestoneRoadmap.match(new RegExp(
|
|
3427
|
+
// phase-id-owner: pre-existing hand-rolled Phase-heading pattern — grandfathered pending Phase 6 migration (ADR-4910 §8, epic #4906)
|
|
3428
|
+
`(#{2,4}\\s*Phase\\s+${phaseEsc}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i'));
|
|
3209
3429
|
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
|
3210
|
-
|
|
3430
|
+
// #4731: multiline-aware — hard-wrapped Requirements read past the
|
|
3431
|
+
// line break before the ID scan. The shared extractor also stops at
|
|
3432
|
+
// headings and table rows, so a Requirements field followed by the
|
|
3433
|
+
// Traceability table cannot bleed other phases' REQ-IDs into the
|
|
3434
|
+
// citation scan (isolated-review MEDIUM on the inline lookahead,
|
|
3435
|
+
// whose lazy capture swallowed everything to section end).
|
|
3436
|
+
const reqLine = sectionText
|
|
3437
|
+
? roadmapParserMod.extractPhaseFieldMultiline(sectionText, 'Requirements')
|
|
3438
|
+
: null;
|
|
3211
3439
|
const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
|
|
3212
3440
|
let reqContent = originalReqContent;
|
|
3213
3441
|
// #2316: `citedReqIds` — the REQ-IDs ROADMAP's own **Requirements:**
|
|
3214
3442
|
// line for this phase actually cites — is hoisted out of the
|
|
3215
|
-
// `if (
|
|
3443
|
+
// `if (reqLine)` block (previously scoped only inside it) so the
|
|
3216
3444
|
// ghost-ID cross-check below (~#2316-1) can consult it. `TBD` is the
|
|
3217
3445
|
// literal placeholder `phase.add`/`-batch`/`-insert` seed
|
|
3218
3446
|
// (`**Requirements**: TBD`, src/phase.cts:833,920,1078) — never a
|
|
@@ -3224,16 +3452,16 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3224
3452
|
// otherwise) — the `if (reqUpdate.ok)` below previously had no
|
|
3225
3453
|
// `else`, discarding this fact silently instead of surfacing it.
|
|
3226
3454
|
const traceabilityWriteMisses = [];
|
|
3227
|
-
if (
|
|
3455
|
+
if (reqLine) {
|
|
3228
3456
|
// #2334 HIGH 3 + #3697: selection and under-selection detection both
|
|
3229
3457
|
// live in `analyzeRequirementsLine` (module scope, above), extracted in
|
|
3230
3458
|
// round 3 so the parser is directly testable — a closure in here is
|
|
3231
3459
|
// reachable only by spawning the CLI, which no fast-check property test
|
|
3232
3460
|
// can do. `citedReqIds` is byte-identical to the expression that stood
|
|
3233
3461
|
// here; nothing about what phase-complete MARKS has changed.
|
|
3234
|
-
const reqLineAnalysis = analyzeRequirementsLine(
|
|
3462
|
+
const reqLineAnalysis = analyzeRequirementsLine(reqLine);
|
|
3235
3463
|
citedReqIds = reqLineAnalysis.citedReqIds;
|
|
3236
|
-
const reqLineWarning = formatRequirementsLineWarning(phaseNum,
|
|
3464
|
+
const reqLineWarning = formatRequirementsLineWarning(phaseNum, reqLine, reqLineAnalysis);
|
|
3237
3465
|
if (reqLineWarning) {
|
|
3238
3466
|
warnings.push(reqLineWarning.message);
|
|
3239
3467
|
// Carried out to the JSON result as its own field — see
|
|
@@ -3519,6 +3747,34 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3519
3747
|
let diskNextName = null;
|
|
3520
3748
|
let roadmapNextNum = null;
|
|
3521
3749
|
let roadmapNextName = null;
|
|
3750
|
+
// #4699: a phase whose roadmap checkbox is `[x]` is already complete and
|
|
3751
|
+
// must never be selected as next_phase — out-of-order completion (a
|
|
3752
|
+
// reopened phase finished after later phases shipped) otherwise persists
|
|
3753
|
+
// the already-done phase as STATE.md current_phase. Collected from the
|
|
3754
|
+
// same milestone-scoped text the roadmap scan walks; membership is
|
|
3755
|
+
// comparePhaseNum-based so `02` and `2` dedupe. With no ROADMAP.md (or no
|
|
3756
|
+
// parseable rows) the set is empty and the scans behave exactly as
|
|
3757
|
+
// before.
|
|
3758
|
+
const roadmapCompleteNums = [];
|
|
3759
|
+
if (roadmapContent !== null) {
|
|
3760
|
+
try {
|
|
3761
|
+
const milestoneForComplete = extractCurrentMilestone(roadmapContent, cwd);
|
|
3762
|
+
const completePattern = new RegExp(`-\\s*\\[[xX]\\]\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'gi');
|
|
3763
|
+
let cm;
|
|
3764
|
+
while ((cm = completePattern.exec(milestoneForComplete)) !== null) {
|
|
3765
|
+
if (isSentinelPhaseId(cm[1]))
|
|
3766
|
+
continue;
|
|
3767
|
+
if (!roadmapCompleteNums.some((n) => comparePhaseNum(cm[1], n) === 0)) {
|
|
3768
|
+
roadmapCompleteNums.push(cm[1]);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
catch {
|
|
3773
|
+
/* best-effort: an unreadable milestone section leaves the complete
|
|
3774
|
+
* set empty — the scans then behave exactly as they did pre-#4699. */
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
const isCompletePhaseNum = (num) => roadmapCompleteNums.some((n) => comparePhaseNum(num, n) === 0);
|
|
3522
3778
|
try {
|
|
3523
3779
|
// #3185 (ADR-3180 Decision 1): "which phase directories belong to
|
|
3524
3780
|
// the CURRENT milestone" — routed through the canonical owner
|
|
@@ -3534,6 +3790,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3534
3790
|
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
3535
3791
|
if (isSentinelPhaseId(dm[1]))
|
|
3536
3792
|
continue;
|
|
3793
|
+
// #4699: an already-complete phase (roadmap checkbox [x]) is never
|
|
3794
|
+
// a next_phase candidate — out-of-order completion must skip it.
|
|
3795
|
+
if (roadmapContent !== null && isCompletePhaseNum(dm[1]))
|
|
3796
|
+
continue;
|
|
3537
3797
|
// Numeric MINIMUM above N, not "first encountered". `listMilestonePhaseDirs`
|
|
3538
3798
|
// does sort by `comparePhaseNum`, so a `break` on the first hit happens to be
|
|
3539
3799
|
// correct today — but that makes this scan's correctness depend on an
|
|
@@ -3601,6 +3861,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3601
3861
|
// stage 2's heading scan must not advance into backlog headings either.
|
|
3602
3862
|
if (isSentinelPhaseId(pmNum))
|
|
3603
3863
|
continue;
|
|
3864
|
+
// #4699: skip complete phases — a `[x]` checkbox row and the
|
|
3865
|
+
// `## Phase Details` heading of an already-done phase both name a
|
|
3866
|
+
// phase that must never be next_phase.
|
|
3867
|
+
if (roadmapContent !== null && isCompletePhaseNum(pmNum))
|
|
3868
|
+
continue;
|
|
3604
3869
|
// #3701 review: the numeric MINIMUM above N, not the first row above N in
|
|
3605
3870
|
// DOCUMENT order. This scan walks raw roadmap text, and one global regex
|
|
3606
3871
|
// sweeps both the `## Phases` checklist and the `## Phase Details`
|
|
@@ -3767,14 +4032,53 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
3767
4032
|
const fmBody = frontmatterMod.stripFrontmatter(stateContent);
|
|
3768
4033
|
const bodyHasPhaseField = stateExtractField(fmBody, 'Current Phase') != null ||
|
|
3769
4034
|
stateExtractField(fmBody, 'Phase') != null;
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
4035
|
+
// #4129: the POST-completion progress counters, derived from the very
|
|
4036
|
+
// ROADMAP this transaction just mutated (still in memory — it hits disk
|
|
4037
|
+
// only at writePlanningFileSet, AFTER this content was assembled).
|
|
4038
|
+
// buildStateFrontmatter's disk scan inside syncAndPreserveStateMd
|
|
4039
|
+
// reads the PRE-completion ROADMAP (and any stale-dated sibling
|
|
4040
|
+
// verification), so without this intent the persisted counter failed
|
|
4041
|
+
// to increment on the completing phase's own transaction. Routed
|
|
4042
|
+
// through the #2736 authoritativeFm seam's object direction: the
|
|
4043
|
+
// pre-preservation merge makes it the derived truth the ratchet
|
|
4044
|
+
// compares, and the post-preservation re-assert (completedOnlyRaise)
|
|
4045
|
+
// is a floor no preservation branch can drop below. clampPercent is
|
|
4046
|
+
// completePhaseCore's own percent formula (state-transition.cts),
|
|
4047
|
+
// reused so the frontmatter and the body `Progress:` line agree.
|
|
4048
|
+
const postCompletionRoadmapScope = roadmapContent !== null
|
|
4049
|
+
? extractCurrentMilestone(roadmapContent, cwd)
|
|
4050
|
+
: null;
|
|
4051
|
+
const postCompletionRoadmapProgress = postCompletionRoadmapScope !== null
|
|
4052
|
+
? deriveProgressFromRoadmapForIntent(postCompletionRoadmapScope)
|
|
4053
|
+
: null;
|
|
4054
|
+
const authoritativeProgress = postCompletionRoadmapProgress && postCompletionRoadmapProgress.completedPhases !== null
|
|
4055
|
+
? postCompletionRoadmapProgress.totalPhases !== null && postCompletionRoadmapProgress.totalPhases > 0
|
|
4056
|
+
? {
|
|
4057
|
+
completed_phases: postCompletionRoadmapProgress.completedPhases,
|
|
4058
|
+
percent: clampPercentForIntent(postCompletionRoadmapProgress.completedPhases, postCompletionRoadmapProgress.totalPhases),
|
|
3776
4059
|
}
|
|
4060
|
+
: { completed_phases: postCompletionRoadmapProgress.completedPhases }
|
|
3777
4061
|
: undefined;
|
|
4062
|
+
const authoritativeFm = authoritativeProgress
|
|
4063
|
+
? {
|
|
4064
|
+
...(nextPhaseDisplayName
|
|
4065
|
+
? bodyHasPhaseField || !nextPhaseNum
|
|
4066
|
+
? { current_phase_name: nextPhaseDisplayName }
|
|
4067
|
+
: {
|
|
4068
|
+
current_phase: String(nextPhaseNum),
|
|
4069
|
+
current_phase_name: nextPhaseDisplayName,
|
|
4070
|
+
}
|
|
4071
|
+
: {}),
|
|
4072
|
+
progress: authoritativeProgress,
|
|
4073
|
+
}
|
|
4074
|
+
: nextPhaseDisplayName
|
|
4075
|
+
? bodyHasPhaseField || !nextPhaseNum
|
|
4076
|
+
? { current_phase_name: nextPhaseDisplayName }
|
|
4077
|
+
: {
|
|
4078
|
+
current_phase: String(nextPhaseNum),
|
|
4079
|
+
current_phase_name: nextPhaseDisplayName,
|
|
4080
|
+
}
|
|
4081
|
+
: undefined;
|
|
3778
4082
|
// ADR-3408 §8.3 / #3469: this deliberately bypasses
|
|
3779
4083
|
// readModifyWriteStateMd (STATE.md is committed atomically with
|
|
3780
4084
|
// ROADMAP/REQUIREMENTS), so it calls the single write-seam
|
|
@@ -3953,4 +4257,5 @@ module.exports = {
|
|
|
3953
4257
|
cmdPhaseListPlans,
|
|
3954
4258
|
computeDependencyLevels,
|
|
3955
4259
|
buildShortFormToId,
|
|
4260
|
+
normalizeDependencyToken,
|
|
3956
4261
|
};
|