@opengsd/gsd-core 1.9.1 → 1.11.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 +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +12 -10
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +46 -0
- package/agents/gsd-planner.md +49 -54
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +26 -73
- package/bin/install.js +1272 -1238
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +2 -2
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/plan-phase.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1009 -115
- package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
- package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
- package/gsd-core/bin/lib/api-coverage.cjs +123 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +926 -202
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- 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-registry.cjs +608 -148
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +507 -24
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +114 -38
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +94 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +665 -99
- package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +76 -0
- package/gsd-core/bin/lib/config.cjs +22 -2
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +217 -40
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +239 -32
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
- 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 +145 -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 +265 -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 +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1325 -169
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +805 -264
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +160 -57
- 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-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
- package/gsd-core/bin/lib/io.cjs +38 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +821 -109
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +507 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +258 -58
- package/gsd-core/bin/lib/phase.cjs +891 -156
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +405 -84
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
- package/gsd-core/bin/lib/smart-entry.cjs +154 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +424 -105
- package/gsd-core/bin/lib/state.cjs +1927 -401
- package/gsd-core/bin/lib/surface.cjs +35 -10
- 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 +20 -4
- package/gsd-core/bin/lib/uat.cjs +706 -64
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +33 -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.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -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 +287 -20
- package/gsd-core/bin/lib/verify.cjs +368 -880
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
- package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
- package/gsd-core/bin/lib/workstream.cjs +8 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- 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 +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +8 -7
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +33 -70
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +74 -166
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +160 -95
- package/gsd-core/workflows/debug.md +16 -17
- package/gsd-core/workflows/diagnose-issues.md +56 -8
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- 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 +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +103 -187
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +7 -6
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +35 -47
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +27 -240
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +89 -209
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +45 -159
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +55 -405
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +41 -13
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +41 -20
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +106 -13
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +53 -31
- package/gsd-core/workflows/ui-phase.md +13 -12
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +19 -8
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +17 -65
- package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
- package/hooks/dist/gsd-check-update-worker.js +64 -12
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +45 -24
- package/hooks/dist/gsd-statusline.js +90 -6
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- 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 +277 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +517 -0
- package/hooks/gsd-check-update-worker.js +64 -12
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +607 -26
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +45 -24
- package/hooks/gsd-statusline.js +90 -6
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +277 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +31 -10
- package/pi/gsd.cjs +71 -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 +9 -0
- package/scripts/changeset/lint.cjs +68 -6
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +439 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +150 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
- 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-compiled-artifact-sync.cjs +6 -1
- 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-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -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 +434 -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 +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +40 -3
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +52 -6
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +2 -2
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +2 -2
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +1 -2
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- 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/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 -577
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/gen-emitted-baseline.cjs +0 -145
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1050
|
@@ -14,6 +14,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
14
14
|
const node_os_1 = __importDefault(require("node:os"));
|
|
15
15
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
16
|
const clock_cjs_1 = require("./clock.cjs");
|
|
17
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
17
18
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
|
|
18
19
|
const io = require("./io.cjs");
|
|
19
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
|
|
@@ -38,6 +39,7 @@ const secrets_cjs_1 = require("./secrets.cjs");
|
|
|
38
39
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
39
40
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
40
41
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
42
|
+
const host_runtime_detection_cjs_1 = require("./host-runtime-detection.cjs");
|
|
41
43
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- commands.cjs is an export= CommonJS module
|
|
42
44
|
const commandsMod = require("./commands.cjs");
|
|
43
45
|
const security_cjs_1 = require("./security.cjs");
|
|
@@ -50,6 +52,20 @@ const verificationMod = require("./verification.cjs");
|
|
|
50
52
|
const uatPredicateMod = require("./uat-predicate.cjs");
|
|
51
53
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
|
|
52
54
|
const agentInstallCheck = require("./agent-install-check.cjs");
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- section-manifest.cjs is compiled from section-manifest.cts's named exports; imported as a namespace to read selectSections/SelectableSection/InvocationFacts off module.exports directly (#2932).
|
|
56
|
+
const sectionManifest = require("./section-manifest.cjs");
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- loop-resolver.cjs is an export= CommonJS module
|
|
58
|
+
const loopResolverMod = require("./loop-resolver.cjs");
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- capability-loader.cjs is compiled from capability-loader.cts's named exports; imported as a namespace to read loadRegistry off module.exports directly.
|
|
60
|
+
const capabilityLoaderMod = require("./capability-loader.cjs");
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- capability-state.cjs is an export= CommonJS module
|
|
62
|
+
const capabilityStateMod = require("./capability-state.cjs");
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- docs.cjs is an export= CommonJS module
|
|
64
|
+
const docsMod = require("./docs.cjs");
|
|
65
|
+
const { detectMonorepoWorkspaces } = docsMod;
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- workstream-inventory.cjs is an export= CommonJS module
|
|
67
|
+
const workstreamInventoryMod = require("./workstream-inventory.cjs");
|
|
68
|
+
const { getOtherActiveWorkstreamInventories } = workstreamInventoryMod;
|
|
53
69
|
const { checkAgentsInstalled } = agentInstallCheck;
|
|
54
70
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- git-base-branch.cjs is an export= CommonJS module
|
|
55
71
|
const gitBaseBranch = require("./git-base-branch.cjs");
|
|
@@ -58,19 +74,22 @@ const resolution_cjs_1 = require("./resolution.cjs");
|
|
|
58
74
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- onboard-projection.cjs is an export= CommonJS module
|
|
59
75
|
const onboardProjection = require("./onboard-projection.cjs");
|
|
60
76
|
const { REQUIRED_CODEBASE_MAP_FILES, buildOnboardProjection, hasCodeFilesInternal, hasPackageFileInternal, listCodebaseMapFiles, } = onboardProjection;
|
|
61
|
-
const { output, error } = io;
|
|
77
|
+
const { output, error, ERROR_REASON } = io;
|
|
62
78
|
const { loadConfig, loadConfigResolved } = configLoader;
|
|
63
79
|
const { resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride } = modelResolver;
|
|
64
|
-
const { findPhaseInternal } = phaseLocator;
|
|
65
|
-
const { getRoadmapPhaseInternal, getMilestoneInfo,
|
|
80
|
+
const { findPhaseInternal, listMilestonePhaseDirs } = phaseLocator;
|
|
81
|
+
const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
|
|
66
82
|
const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
|
|
67
|
-
const {
|
|
83
|
+
const { normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
|
|
68
84
|
const { pruneOrphanedWorktrees } = worktreeSafety;
|
|
69
|
-
const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams,
|
|
85
|
+
const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
|
|
70
86
|
const { determinePhaseStatus } = commandsMod;
|
|
71
87
|
const { extractFrontmatter } = frontmatterMod;
|
|
72
|
-
const {
|
|
88
|
+
const { isPhaseComplete, resolveVerificationFile, resolveUatFile } = verificationMod;
|
|
73
89
|
const { evaluateUatPassed } = uatPredicateMod;
|
|
90
|
+
const { resolveLoopHooks } = loopResolverMod;
|
|
91
|
+
const { loadRegistry } = capabilityLoaderMod;
|
|
92
|
+
const { resolveCapabilityRuntimeState } = capabilityStateMod;
|
|
74
93
|
// Unused but imported for structural parity
|
|
75
94
|
void stripShippedMilestones;
|
|
76
95
|
// Accept all bold/colon variants of the Requirements header (#2769)
|
|
@@ -87,7 +106,7 @@ function phaseInfoMatchesExactPrefix(phaseInfo, phase) {
|
|
|
87
106
|
function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
|
|
88
107
|
const sectionRaw = roadmapPhase?.['section'];
|
|
89
108
|
const section = typeof sectionRaw === 'string' ? sectionRaw : '';
|
|
90
|
-
return new RegExp(`^#{2,4}\\s*Phase\\s+${escapeRegex(phase)}(?:\\b|\\s|:)`, 'i').test(section);
|
|
109
|
+
return new RegExp(`^#{2,4}\\s*Phase\\s+${(0, pattern_cjs_1.escapeRegex)(phase)}(?:\\b|\\s|:)`, 'i').test(section);
|
|
91
110
|
}
|
|
92
111
|
// #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
|
|
93
112
|
// with the #2056 foreign-prefix guard, so every init command gets the same
|
|
@@ -106,43 +125,98 @@ function guardedGetRoadmapPhase(cwd, phase, projectCode) {
|
|
|
106
125
|
}
|
|
107
126
|
return roadmapPhase;
|
|
108
127
|
}
|
|
128
|
+
// #2994: `phase_slug` is re-derived from a roadmap-only `phase_name` (no disk
|
|
129
|
+
// directory exists yet) identically at every synthetic-fallback call site
|
|
130
|
+
// below — factored out once so the slugification formula itself cannot drift.
|
|
131
|
+
function slugifyPhaseName(phaseName) {
|
|
132
|
+
return phaseName
|
|
133
|
+
? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
134
|
+
: null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* #2994 (review finding, DEFECT.GENERATIVE-FIX): shared archived/not-found
|
|
138
|
+
* fallback applied identically by `cmdInitExecutePhase`, `cmdInitPlanPhase`,
|
|
139
|
+
* `cmdInitVerifyWork`, `cmdInitCodeReview`, `cmdInitReview`, and
|
|
140
|
+
* `cmdInitDiscussPhaseAssumptions` — 6 call sites previously reproducing the
|
|
141
|
+
* exact same two-branch control flow verbatim (only the synthetic
|
|
142
|
+
* replacement object's field set differs per caller, supplied here via
|
|
143
|
+
* `buildFallback`). `cmdInitPhaseOp` is deliberately left untouched (CRITICAL
|
|
144
|
+
* blast radius, 179 dependents) even though it follows the same shape, since
|
|
145
|
+
* its own fallback object differs by one field (`has_reviews` absent) and is
|
|
146
|
+
* not a byte-identical copy.
|
|
147
|
+
*
|
|
148
|
+
* Behavior-preserving by construction: every original call site either (a)
|
|
149
|
+
* unconditionally computed `roadmapPhase` once up front and then applied
|
|
150
|
+
* `phaseInfo?.archived && roadmapPhase?.found -> null` followed by
|
|
151
|
+
* `!phaseInfo && roadmapPhase?.found -> fallback`, or (b) computed
|
|
152
|
+
* `roadmapPhase` lazily inside each of those same two conditions. Because
|
|
153
|
+
* `guardedGetRoadmapPhase` is a pure, side-effect-free read for a given
|
|
154
|
+
* `(cwd, phase, projectCode)` within one command invocation, both shapes
|
|
155
|
+
* return identical results for identical inputs — so passing one
|
|
156
|
+
* unconditionally-resolved `roadmapPhase` in here (mirroring shape (a))
|
|
157
|
+
* reproduces shape (b)'s output exactly, just without the redundant second
|
|
158
|
+
* disk read shape (b) performed when the first branch already resolved it.
|
|
159
|
+
*/
|
|
160
|
+
function applyRoadmapFallback(phaseInfo, roadmapPhase, buildFallback) {
|
|
161
|
+
if (phaseInfo?.['archived'] && roadmapPhase?.['found']) {
|
|
162
|
+
phaseInfo = null;
|
|
163
|
+
}
|
|
164
|
+
if (!phaseInfo && roadmapPhase?.['found']) {
|
|
165
|
+
phaseInfo = buildFallback(roadmapPhase);
|
|
166
|
+
}
|
|
167
|
+
return phaseInfo;
|
|
168
|
+
}
|
|
109
169
|
function listPhaseSummaryFiles(phaseDir) {
|
|
110
170
|
return scanPhasePlans(phaseDir)['summaryFiles'];
|
|
111
171
|
}
|
|
112
172
|
function listPhasePlanFiles(phaseDir) {
|
|
113
173
|
return scanPhasePlans(phaseDir)['planFiles'];
|
|
114
174
|
}
|
|
115
|
-
function projectCompletionStatus(implementationComplete,
|
|
116
|
-
if (
|
|
175
|
+
function projectCompletionStatus(implementationComplete, phaseComplete) {
|
|
176
|
+
if (phaseComplete)
|
|
117
177
|
return 'complete';
|
|
118
178
|
if (implementationComplete)
|
|
119
179
|
return 'executed';
|
|
120
180
|
return 'incomplete';
|
|
121
181
|
}
|
|
122
182
|
function buildPhaseCompletionProjection(cwd, phaseNumber, phaseDir, planCount, summaryCount, slashRuntime) {
|
|
183
|
+
// ADR-3180 §7.4 (issue #3186) / DO-NOT-MIGRATE exemption
|
|
184
|
+
// (scripts/lint-completion-predicate-drift.cjs FUNCTION_SCOPED_EXEMPTIONS,
|
|
185
|
+
// declared deviation): `implementation_complete` answers "are the plans
|
|
186
|
+
// done" (a `scanPhasePlans`-shaped different question, per the design's
|
|
187
|
+
// 0.x-split), NOT "is the phase complete" — it is kept for the
|
|
188
|
+
// 'executed'-vs-'planned' disk_status distinction downstream consumers
|
|
189
|
+
// still rely on, which `isPhaseComplete`'s locked `{ complete, verification
|
|
190
|
+
// }` return shape does not carry.
|
|
123
191
|
const implementationComplete = planCount > 0 && summaryCount >= planCount;
|
|
124
192
|
const phaseFullDir = phaseDir ? node_path_1.default.join(cwd, phaseDir) : '';
|
|
125
|
-
// #
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
193
|
+
// #3168 / ADR-3180 §7.4 (disk-strict, #2957): route through the canonical
|
|
194
|
+
// owner (`src/verification.cts` · `isPhaseComplete`), which calls
|
|
195
|
+
// readVerificationStatus UNCONDITIONALLY — plan count is NOT a
|
|
196
|
+
// precondition. A zero-plan phase with a passing `*-VERIFICATION.md` is
|
|
197
|
+
// complete; init used to gate the read on `implementationComplete` and
|
|
198
|
+
// synthesize a `not_required` sentinel instead, which is the #3168 defect.
|
|
199
|
+
// #2617: the router still owns both the message content and the runtime
|
|
200
|
+
// projection; init passes the phase number it already knows (its phaseDir
|
|
130
201
|
// is unresolved in some branches, where the router could not derive one).
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
: { status: 'not_required', next_action: '', next_command: '' };
|
|
202
|
+
const completionResult = isPhaseComplete(phaseFullDir, { runtime: slashRuntime, phaseNumber });
|
|
203
|
+
const verificationStatus = completionResult.value.verification;
|
|
134
204
|
const projectedVerificationStatus = verificationStatus.status;
|
|
135
205
|
const projectedVerificationAction = verificationStatus.next_action;
|
|
136
206
|
const verificationPassed = projectedVerificationStatus === 'passed';
|
|
137
|
-
const phaseComplete =
|
|
207
|
+
const phaseComplete = completionResult.value.complete;
|
|
138
208
|
return {
|
|
139
209
|
implementation_complete: implementationComplete,
|
|
140
210
|
verification_status: projectedVerificationStatus,
|
|
141
211
|
verification_passed: verificationPassed,
|
|
142
212
|
phase_complete: phaseComplete,
|
|
143
|
-
completion_status: projectCompletionStatus(implementationComplete,
|
|
213
|
+
completion_status: projectCompletionStatus(implementationComplete, phaseComplete),
|
|
144
214
|
verification_next_action: projectedVerificationAction,
|
|
145
215
|
verification_next_command: verificationStatus.next_command,
|
|
216
|
+
// #3057 B3: readVerificationStatus's result carries this flag when its
|
|
217
|
+
// internal staleness check could not run to completion.
|
|
218
|
+
verification_stale_check_indeterminate: 'staleCheckIndeterminate' in verificationStatus
|
|
219
|
+
&& verificationStatus.staleCheckIndeterminate === true,
|
|
146
220
|
};
|
|
147
221
|
}
|
|
148
222
|
function getLatestCompletedMilestone(cwd) {
|
|
@@ -160,8 +234,9 @@ function getLatestCompletedMilestone(cwd) {
|
|
|
160
234
|
}
|
|
161
235
|
function withProjectRoot(cwd, result) {
|
|
162
236
|
result['project_root'] = cwd;
|
|
163
|
-
|
|
164
|
-
const
|
|
237
|
+
// #3245: the reported agent_runtime gets a host-detection rung below the two explicit sources; every other resolveRuntime caller keeps the old ladder (ADR-2313 scope boundary).
|
|
238
|
+
const activeRuntime = (0, host_runtime_detection_cjs_1.resolveReportedRuntime)(cwd);
|
|
239
|
+
const agentStatus = checkAgentsInstalled(activeRuntime, cwd);
|
|
165
240
|
result['agents_installed'] = agentStatus.agents_installed;
|
|
166
241
|
result['missing_agents'] = agentStatus.missing_agents;
|
|
167
242
|
result['agents_dir'] = agentStatus.agents_dir;
|
|
@@ -250,42 +325,454 @@ function getInitGitState(cwd) {
|
|
|
250
325
|
in_nested_subdir: inNestedSubdir,
|
|
251
326
|
};
|
|
252
327
|
}
|
|
328
|
+
// #2932 (Phase 5, ADR-1671): shipped, generated artifact — see
|
|
329
|
+
// scripts/gen-section-manifest.cjs and gsd-core/workflows/section-manifest.json.
|
|
330
|
+
// Resolved the same way model-catalog.cts resolves model-catalog.json: relative
|
|
331
|
+
// to the compiled module's own directory (gsd-core/bin/lib -> gsd-core/workflows),
|
|
332
|
+
// with a GSD_SECTION_MANIFEST env override so tests can point at a temp fixture
|
|
333
|
+
// (missing/malformed-JSON degraded-path coverage) without mutating the shipped
|
|
334
|
+
// artifact — the shipped file is a shared, concurrently-read resource across
|
|
335
|
+
// parallel test runs and must never be moved/corrupted in place.
|
|
336
|
+
const _sectionManifestCandidatePath = () => process.env['GSD_SECTION_MANIFEST']
|
|
337
|
+
? node_path_1.default.resolve(process.env['GSD_SECTION_MANIFEST'])
|
|
338
|
+
: node_path_1.default.resolve(__dirname, '..', '..', 'workflows', 'section-manifest.json');
|
|
339
|
+
/**
|
|
340
|
+
* Defense-in-depth shape check for a manifest entry's `read` field, which is
|
|
341
|
+
* documented as a POSIX-normalized, repo-root-RELATIVE path (never a
|
|
342
|
+
* filesystem escape). Rejects any absolute path (POSIX leading `/`, a
|
|
343
|
+
* Windows drive prefix like `C:\`/`C:/`, or a Windows UNC/rooted path
|
|
344
|
+
* starting with `\`) and any path containing a `..` segment (checked on
|
|
345
|
+
* BOTH separators — the artifact is generated as POSIX-normalized, but this
|
|
346
|
+
* validates the raw field defensively rather than trusting that invariant).
|
|
347
|
+
* `false` here is the only accept path in {@link loadSectionManifestSections};
|
|
348
|
+
* a `true` degrades the WHOLE load to `null`, same as every other shape
|
|
349
|
+
* violation — never throws, never partially loads.
|
|
350
|
+
*/
|
|
351
|
+
function isUnsafeManifestReadPath(readPath) {
|
|
352
|
+
if (readPath.startsWith('/') || readPath.startsWith('\\'))
|
|
353
|
+
return true;
|
|
354
|
+
if (/^[a-zA-Z]:[\\/]/.test(readPath))
|
|
355
|
+
return true;
|
|
356
|
+
return readPath.split(/[\\/]/).includes('..');
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Loads and shape-validates the generated section manifest, then returns the
|
|
360
|
+
* document-order section array for exactly one named `workflow` (#2992 Phase
|
|
361
|
+
* 6.1: the artifact is now `{ workflows: { <name>: [...] } }`, keyed by
|
|
362
|
+
* `.md` basename — see `scripts/gen-section-manifest.cjs`). Returns `null`
|
|
363
|
+
* — never throws — when the artifact is missing, unreadable, malformed
|
|
364
|
+
* JSON, valid JSON of the wrong shape (INCLUDING the pre-6.1 flat
|
|
365
|
+
* `{sections:[...]}` shape, which must never be mis-attributed to any
|
|
366
|
+
* workflow — design row C4), or when `workflow` has no key in `workflows`.
|
|
367
|
+
* `Object.hasOwn` guards the key lookup so a hostile workflow name
|
|
368
|
+
* (`constructor`, `toString`, `__proto__`) can never resolve via the
|
|
369
|
+
* prototype chain instead of a genuine own key. Each entry's `read` field is
|
|
370
|
+
* additionally validated by {@link isUnsafeManifestReadPath} (rejects an
|
|
371
|
+
* absolute path or a `..` segment) — a single unsafe entry degrades the
|
|
372
|
+
* WHOLE load to `null`, all-or-nothing like every other shape violation.
|
|
373
|
+
*/
|
|
374
|
+
function loadSectionManifestSections(workflow) {
|
|
375
|
+
try {
|
|
376
|
+
const raw = node_fs_1.default.readFileSync(_sectionManifestCandidatePath(), 'utf8');
|
|
377
|
+
const parsed = JSON.parse(raw);
|
|
378
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
379
|
+
return null;
|
|
380
|
+
const workflows = parsed['workflows'];
|
|
381
|
+
if (workflows === null || typeof workflows !== 'object' || Array.isArray(workflows))
|
|
382
|
+
return null;
|
|
383
|
+
if (!Object.hasOwn(workflows, workflow))
|
|
384
|
+
return null;
|
|
385
|
+
const sections = workflows[workflow];
|
|
386
|
+
if (!Array.isArray(sections))
|
|
387
|
+
return null;
|
|
388
|
+
for (const section of sections) {
|
|
389
|
+
const readValue = section?.['read'];
|
|
390
|
+
if (!section ||
|
|
391
|
+
typeof section !== 'object' ||
|
|
392
|
+
typeof section['id'] !== 'string' ||
|
|
393
|
+
typeof section['when'] !== 'string' ||
|
|
394
|
+
typeof readValue !== 'string' ||
|
|
395
|
+
isUnsafeManifestReadPath(readValue)) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return sections;
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* `state:has-prior-phases` ground truth (design doc §Behavior table, regression-gate
|
|
407
|
+
* body: "Skip if: this is the first phase (no prior phases)"): TRUE when at least
|
|
408
|
+
* one OTHER phase directory under `.planning/phases/` contains a `*-VERIFICATION.md`
|
|
409
|
+
* file. Bounded, non-throwing — an unreadable phases directory degrades to `false`
|
|
410
|
+
* rather than surfacing an error from an init query.
|
|
411
|
+
*/
|
|
412
|
+
function detectHasPriorPhases(cwd, phaseInfo) {
|
|
413
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
414
|
+
const currentDirName = phaseInfo?.['directory']
|
|
415
|
+
? node_path_1.default.basename(phaseInfo['directory'])
|
|
416
|
+
: null;
|
|
417
|
+
try {
|
|
418
|
+
if (!node_fs_1.default.existsSync(phasesDir))
|
|
419
|
+
return false;
|
|
420
|
+
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
421
|
+
for (const entry of entries) {
|
|
422
|
+
if (!entry.isDirectory() || entry.name === currentDirName)
|
|
423
|
+
continue;
|
|
424
|
+
let files;
|
|
425
|
+
try {
|
|
426
|
+
files = node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, entry.name));
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
// #3511-class: scope the raw listing to THIS entry's own phase artifacts
|
|
432
|
+
// before the bare `.some()` predicate runs, so a stray `07-VERIFICATION.md`
|
|
433
|
+
// physically sitting in another phase's directory cannot make that
|
|
434
|
+
// directory appear to have its own verification report.
|
|
435
|
+
const scopedFiles = scopeToPhase(files, entry.name);
|
|
436
|
+
if (scopedFiles.some((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md')) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Strict-boolean, bounded, non-throwing read of a dotted key path from
|
|
448
|
+
* `.planning/config.json` (design rows D7-D10): absent file, unreadable
|
|
449
|
+
* file (fs error), malformed JSON, a non-object intermediate segment, or a
|
|
450
|
+
* present-but-non-boolean value (e.g. the string `"true"`) all degrade to
|
|
451
|
+
* `false` — strict `=== true`, never coerced, mirrors `detectHasPriorPhases`'s
|
|
452
|
+
* degrade-to-false discipline. `keyPath` is always a fixed literal supplied
|
|
453
|
+
* by this module, never attacker/user input, so a plain bracket traversal
|
|
454
|
+
* carries no prototype hazard here.
|
|
455
|
+
*/
|
|
456
|
+
function readConfigJsonBoolean(cwd, keyPath) {
|
|
457
|
+
try {
|
|
458
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'config.json'), 'utf8');
|
|
459
|
+
let cursor = JSON.parse(raw);
|
|
460
|
+
for (const segment of keyPath) {
|
|
461
|
+
if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
462
|
+
return false;
|
|
463
|
+
cursor = cursor[segment];
|
|
464
|
+
}
|
|
465
|
+
return cursor === true;
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Bounded, non-throwing read of a dotted key path from `.planning/config.json`,
|
|
473
|
+
* returning the raw resolved value (any JSON type) or `undefined` on any
|
|
474
|
+
* degraded condition (absent file, unreadable file, malformed JSON, or a
|
|
475
|
+
* non-object intermediate segment) — the generic sibling of
|
|
476
|
+
* {@link readConfigJsonBoolean} for callers that need the actual value
|
|
477
|
+
* (a string like `code_quality.fallow.profile`) rather than a strict
|
|
478
|
+
* boolean coercion. `keyPath` is always a fixed literal supplied by this
|
|
479
|
+
* module, never attacker/user input, so a plain bracket traversal carries
|
|
480
|
+
* no prototype hazard here (same discipline as `readConfigJsonBoolean`).
|
|
481
|
+
*/
|
|
482
|
+
function readConfigJsonValue(cwd, keyPath) {
|
|
483
|
+
try {
|
|
484
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'config.json'), 'utf8');
|
|
485
|
+
let cursor = JSON.parse(raw);
|
|
486
|
+
for (const segment of keyPath) {
|
|
487
|
+
if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
488
|
+
return undefined;
|
|
489
|
+
cursor = cursor[segment];
|
|
490
|
+
}
|
|
491
|
+
return cursor;
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* `state:fallow-enabled` ground truth (#2994): resolves `code-review.md`'s
|
|
499
|
+
* `structural_pre_pass` fallow config gate — previously re-derived INSIDE the
|
|
500
|
+
* gated section body itself (`gsd_run query config-get code_quality.fallow.*`),
|
|
501
|
+
* which is circular/self-disabling the moment a section is gated on a fact
|
|
502
|
+
* its own body computes (the same hazard `state:chunked-mode` /
|
|
503
|
+
* `state:ui-phase-active` document for a compound condition). Fail-closed
|
|
504
|
+
* default `false` for `enabled`/`mcp`, matching the pre-hoist bash resolver's
|
|
505
|
+
* `2>/dev/null || echo "false"` fallback; `scope`/`profile` default to
|
|
506
|
+
* `"phase"`/`"standard"` matching that same resolver's `|| echo` fallbacks.
|
|
507
|
+
* `maxCrap` mirrors the step body's profile->threshold mapping (minimal=50,
|
|
508
|
+
* strict=15, else standard=30) so the step file never has to re-derive it.
|
|
509
|
+
*/
|
|
510
|
+
function detectFallowConfig(cwd) {
|
|
511
|
+
const enabled = readConfigJsonValue(cwd, ['code_quality', 'fallow', 'enabled']) === true;
|
|
512
|
+
const rawScope = readConfigJsonValue(cwd, ['code_quality', 'fallow', 'scope']);
|
|
513
|
+
const scope = typeof rawScope === 'string' && rawScope ? rawScope : 'phase';
|
|
514
|
+
const rawProfile = readConfigJsonValue(cwd, ['code_quality', 'fallow', 'profile']);
|
|
515
|
+
const profile = typeof rawProfile === 'string' && rawProfile ? rawProfile : 'standard';
|
|
516
|
+
const mcp = readConfigJsonValue(cwd, ['code_quality', 'fallow', 'mcp']) === true;
|
|
517
|
+
const maxCrap = profile === 'minimal' ? 50 : profile === 'strict' ? 15 : 30;
|
|
518
|
+
return { enabled, scope, profile, mcp, maxCrap };
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* `state:git-create-tag` ground truth (#2994): resolves `complete-milestone.md`'s
|
|
522
|
+
* `git_tag` step config gate — previously re-derived INSIDE a `<config-check>`
|
|
523
|
+
* sub-tag at the top of the step itself (`gsd-tools.cjs query config-get
|
|
524
|
+
* git.create_tag 2>/dev/null || echo "true"`), gating the step's OWN inclusion
|
|
525
|
+
* on a fact only that same step computed. Fail-OPEN default `true` (an unset
|
|
526
|
+
* or missing `git.create_tag` key means "create the tag"), matching the
|
|
527
|
+
* pre-hoist resolver's `|| echo "true"` fallback exactly — this is
|
|
528
|
+
* deliberately the inverse polarity of `detectFallowConfig`'s fail-closed
|
|
529
|
+
* default, mirroring the two source resolvers' own opposite defaults.
|
|
530
|
+
*/
|
|
531
|
+
function detectGitCreateTag(cwd) {
|
|
532
|
+
return readConfigJsonValue(cwd, ['git', 'create_tag']) !== false;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* `state:phase-mvp-mode` ground truth (design doc §Behavior table: ROADMAP.md
|
|
536
|
+
* `**Mode:** mvp` for the CURRENT phase). Bounded, non-throwing — an absent
|
|
537
|
+
* `phaseNumber`, an absent ROADMAP.md, an absent phase heading, or a phase
|
|
538
|
+
* section with no `**Mode:**` line (or a `**Mode:**` value other than the
|
|
539
|
+
* literal `mvp` token, case-insensitively) all degrade to `false` (D11; "a
|
|
540
|
+
* phase with no `**Mode:**` line and an absent ROADMAP are both false, but
|
|
541
|
+
* neither may throw"). Self-contained rather than reusing `phase.cts`'s
|
|
542
|
+
* private `getRoadmapModeForPhase` (unexported, and importing it here would
|
|
543
|
+
* be a cross-module surface change outside this task's scope) — but derived
|
|
544
|
+
* from the SAME extraction primitives (`extractCurrentMilestone`,
|
|
545
|
+
* `PHASE_NUMBER_TOKEN_SOURCE`-adjacent `escapeRegex`) already used by this
|
|
546
|
+
* file's own `cmdInitProgress` MVP-heading scan, so it is not a second
|
|
547
|
+
* ROADMAP-heading parser invented from scratch.
|
|
548
|
+
*/
|
|
549
|
+
function detectPhaseMvpMode(cwd, phaseNumber) {
|
|
550
|
+
if (!phaseNumber)
|
|
551
|
+
return false;
|
|
552
|
+
try {
|
|
553
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
554
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
555
|
+
return false;
|
|
556
|
+
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
557
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
558
|
+
const escapedPhase = (0, pattern_cjs_1.escapeRegex)(phaseNumber);
|
|
559
|
+
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
|
|
560
|
+
const headerMatch = content.match(phaseHeader);
|
|
561
|
+
if (!headerMatch || headerMatch.index === undefined)
|
|
562
|
+
return false;
|
|
563
|
+
const sectionStart = headerMatch.index;
|
|
564
|
+
const rest = content.slice(sectionStart + headerMatch[0].length);
|
|
565
|
+
const nextHeaderMatch = rest.match(/\n#{2,4}\s+Phase\s+\S/i);
|
|
566
|
+
const sectionEnd = nextHeaderMatch
|
|
567
|
+
? sectionStart + headerMatch[0].length + nextHeaderMatch.index
|
|
568
|
+
: content.length;
|
|
569
|
+
const section = content.slice(sectionStart, sectionEnd);
|
|
570
|
+
const modeMatch = section.match(/\*\*Mode:\*\*\s*([^\n]+)/i);
|
|
571
|
+
return modeMatch ? modeMatch[1].trim().toLowerCase() === 'mvp' : false;
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* `state:ui-phase-active` ground truth (#2994): whether the phase's active
|
|
579
|
+
* `plan:pre` loop hooks include the `ui-phase` step (`capabilities/ui/
|
|
580
|
+
* capability.json`'s `plan:pre` step, `ref.skill: "ui-phase"`, gated on
|
|
581
|
+
* config `workflow.ui_phase`), OR the phase directory already contains a
|
|
582
|
+
* `*-UI-SPEC.md` file. The disjunction is resolved to ONE boolean here —
|
|
583
|
+
* same discipline as `chunkedMode` above — so the `when=` grammar never
|
|
584
|
+
* sees an OR. Mirrors `cmdLoopRenderHooks`'s own registry/capability-state
|
|
585
|
+
* setup (`src/loop-resolver.cts`) rather than reinventing a second loop-hook
|
|
586
|
+
* resolution path. Bounded, non-throwing: any failure in loop-hook /
|
|
587
|
+
* registry / capability-state resolution degrades that half of the OR to
|
|
588
|
+
* `false`, never throws; the UI-SPEC file check is independently bounded.
|
|
589
|
+
*/
|
|
590
|
+
function detectUiPhaseActive(cwd, phaseInfo) {
|
|
591
|
+
let hasActiveUiStep = false;
|
|
592
|
+
try {
|
|
593
|
+
const config = loadConfig(cwd);
|
|
594
|
+
const state = resolveCapabilityRuntimeState(cwd, undefined, config);
|
|
595
|
+
const registry = loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] });
|
|
596
|
+
const capabilityStatesById = new Map();
|
|
597
|
+
for (const cap of state.capabilities || []) {
|
|
598
|
+
capabilityStatesById.set(cap.id, cap);
|
|
599
|
+
}
|
|
600
|
+
const resolved = resolveLoopHooks({ point: 'plan:pre', registry, config, cwd, capabilityStatesById });
|
|
601
|
+
hasActiveUiStep = resolved.activeHooks.some((h) => h.kind === 'step' && h.ref?.skill === 'ui-phase');
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
hasActiveUiStep = false;
|
|
605
|
+
}
|
|
606
|
+
let hasUiSpecFile = false;
|
|
607
|
+
const rawDir = phaseInfo?.['directory'];
|
|
608
|
+
if (typeof rawDir === 'string' && rawDir) {
|
|
609
|
+
try {
|
|
610
|
+
// Re-derive under planningDir(cwd)/phases/<basename> rather than trusting
|
|
611
|
+
// rawDir's own absolute/relative-ness (callers mix both — see the #2376
|
|
612
|
+
// comments elsewhere in this file), same technique as detectHasPriorPhases above.
|
|
613
|
+
const dirName = node_path_1.default.basename(rawDir);
|
|
614
|
+
const files = node_fs_1.default.readdirSync(node_path_1.default.join(planningDir(cwd), 'phases', dirName));
|
|
615
|
+
// #3511-class: scope the raw listing to this phase dir before the
|
|
616
|
+
// phase-numbered -UI-SPEC.md predicate, so a stray cross-phase
|
|
617
|
+
// UI-SPEC file cannot flip this phase's ui-phase-active flag.
|
|
618
|
+
const scopedFiles = scopeToPhase(files, dirName);
|
|
619
|
+
hasUiSpecFile = scopedFiles.some((f) => f.endsWith('-UI-SPEC.md') || f === 'UI-SPEC.md');
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
hasUiSpecFile = false;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return hasActiveUiStep || hasUiSpecFile;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Builds the `section_manifest` init-bundle field (#2932 Deliverable 2): resolves
|
|
629
|
+
* {@link sectionManifest.InvocationFacts} from this invocation, loads the generated
|
|
630
|
+
* manifest, and partitions it via the pure {@link sectionManifest.selectSections}
|
|
631
|
+
* evaluator. Returns `null` on any degraded condition (missing/malformed artifact,
|
|
632
|
+
* or an unexpected throw from the evaluator itself) — this field is additive and
|
|
633
|
+
* optional, never load-bearing for dispatch (Hyrum's Law: 22 direct init-bundle
|
|
634
|
+
* dependents must be unaffected by its absence).
|
|
635
|
+
*
|
|
636
|
+
* `flags` (D1-D5): built from `options`'s OWN keys, gated on VALUE TRUTHINESS
|
|
637
|
+
* — not merely `!== undefined`. `parseNamedArgs` (src/command-arg-projection.cts)
|
|
638
|
+
* never yields `undefined` for an absent flag of either kind: a value-flag's
|
|
639
|
+
* absence is `null`, a booleanFlag's absence is `false`. An `undefined`-only
|
|
640
|
+
* absence check therefore lets BOTH kinds of absent flag leak into `flags` as
|
|
641
|
+
* present. A present value-flag is always a non-empty string, and a present
|
|
642
|
+
* booleanFlag is always `true` — so skipping any falsy value (`undefined`,
|
|
643
|
+
* `null`, `false`, `''`, `0`) is a safe, single-rule absence test for both
|
|
644
|
+
* flag kinds; `--wave 0` still resolves to `true` via `booleanFlags`, so
|
|
645
|
+
* truthiness never misclassifies a real invocation as absent. `Object.keys`
|
|
646
|
+
* + a plain `new Set()` so a hostile option key (e.g. `constructor`) can
|
|
647
|
+
* never leak via the prototype chain.
|
|
648
|
+
*
|
|
649
|
+
* `needsCodebaseMap` is not computed in this shared facts-assembly scope —
|
|
650
|
+
* `isBrownfield && !hasCodebaseMap` is only meaningful for `new-project`
|
|
651
|
+
* (`cmdInitNewProject` already computes both operands for its own result
|
|
652
|
+
* object). Rather than recomputing it here (a second, divergence-prone
|
|
653
|
+
* codebase-map scan) or widening every call site's positional signature,
|
|
654
|
+
* callers that HAVE the fact pass it via the optional `overrides` param;
|
|
655
|
+
* every other caller passes nothing and gets `undefined` (falsy per
|
|
656
|
+
* `WHEN_PREDICATES`, never invented, never throws).
|
|
657
|
+
*/
|
|
658
|
+
function buildSectionManifestField(cwd, phaseInfo, options, workflow, overrides = {}) {
|
|
659
|
+
const sections = loadSectionManifestSections(workflow);
|
|
660
|
+
if (!sections)
|
|
661
|
+
return null;
|
|
662
|
+
const rawPhaseNumber = phaseInfo?.['phase_number'];
|
|
663
|
+
const phaseNumber = typeof rawPhaseNumber === 'string'
|
|
664
|
+
? rawPhaseNumber
|
|
665
|
+
: typeof rawPhaseNumber === 'number'
|
|
666
|
+
? String(rawPhaseNumber)
|
|
667
|
+
: null;
|
|
668
|
+
const flags = new Set();
|
|
669
|
+
for (const key of Object.keys(options)) {
|
|
670
|
+
if (!options[key])
|
|
671
|
+
continue;
|
|
672
|
+
flags.add(`--${key}`);
|
|
673
|
+
}
|
|
674
|
+
// `state:chunked-mode` (#2993) is a disjunction — `--chunked` flag OR
|
|
675
|
+
// `.planning/config.json` `workflow.plan_chunked` — resolved to ONE
|
|
676
|
+
// boolean HERE, in fact computation, never in the `when=` grammar itself
|
|
677
|
+
// (WHEN_PREDICATES['state:chunked-mode'] reads only `facts.chunkedMode`).
|
|
678
|
+
// That separation is what keeps ADR-1671:69's Greenspun guard intact: the
|
|
679
|
+
// grammar still sees exactly one atom with no operator.
|
|
680
|
+
const chunkedMode = flags.has('--chunked') || readConfigJsonBoolean(cwd, ['workflow', 'plan_chunked']);
|
|
681
|
+
const facts = {
|
|
682
|
+
flags,
|
|
683
|
+
phaseNumber,
|
|
684
|
+
hasPriorPhases: detectHasPriorPhases(cwd, phaseInfo),
|
|
685
|
+
worktreesEnabled: readConfigJsonBoolean(cwd, ['workflow', 'use_worktrees']),
|
|
686
|
+
phaseMvpMode: detectPhaseMvpMode(cwd, phaseNumber),
|
|
687
|
+
needsCodebaseMap: overrides.needsCodebaseMap,
|
|
688
|
+
chunkedMode,
|
|
689
|
+
uiPhaseActive: overrides.uiPhaseActive,
|
|
690
|
+
fallowEnabled: overrides.fallowEnabled,
|
|
691
|
+
gitCreateTag: overrides.gitCreateTag,
|
|
692
|
+
planStrategyConverge: overrides.planStrategyConverge,
|
|
693
|
+
reviewerInstancesConfigured: overrides.reviewerInstancesConfigured,
|
|
694
|
+
autoAdvanceActive: overrides.autoAdvanceActive,
|
|
695
|
+
isMonorepo: overrides.isMonorepo,
|
|
696
|
+
nextChannel: overrides.nextChannel,
|
|
697
|
+
workstreamActive: overrides.workstreamActive,
|
|
698
|
+
flatMode: overrides.flatMode,
|
|
699
|
+
};
|
|
700
|
+
try {
|
|
701
|
+
const selection = sectionManifest.selectSections(sections, facts);
|
|
702
|
+
const readById = new Map(sections.map((s) => [s.id, s.read]));
|
|
703
|
+
return {
|
|
704
|
+
workflow,
|
|
705
|
+
included: selection.included,
|
|
706
|
+
excluded: selection.excluded,
|
|
707
|
+
read: selection.included
|
|
708
|
+
.map((id) => readById.get(id))
|
|
709
|
+
.filter((p) => typeof p === 'string'),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* #3216 review Finding 1: `getMilestoneInfo(cwd).value` unwrap-and-cast was
|
|
718
|
+
* repeated identically (comment included) at five init call sites — factored
|
|
719
|
+
* out once so the cast and its `?? {}` "no milestone resolved" fallback live
|
|
720
|
+
* in exactly one place. Behavior-preserving: same call, same fallback, same
|
|
721
|
+
* cast, for every caller.
|
|
722
|
+
*/
|
|
723
|
+
function milestoneRecord(cwd) {
|
|
724
|
+
return (getMilestoneInfo(cwd).value ?? {});
|
|
725
|
+
}
|
|
253
726
|
function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
254
727
|
if (!phase) {
|
|
255
728
|
error('phase required for init execute-phase');
|
|
256
729
|
}
|
|
257
730
|
const config = loadConfig(cwd);
|
|
258
731
|
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
259
|
-
|
|
732
|
+
// #3216: getMilestoneInfo now returns a ScopedResult — `.value` carries the
|
|
733
|
+
// MilestoneInfo (or null on any non-COMPLETE scope). NOT display-only: when
|
|
734
|
+
// `branching_strategy === 'milestone'`, `milestone['version']`/`['name']`
|
|
735
|
+
// below feed `branch_name` construction (see the milestone_branch_template
|
|
736
|
+
// branch below), so an unresolved milestone changes the constructed branch
|
|
737
|
+
// name, not merely what gets printed. bracket-access below naturally reads
|
|
738
|
+
// `undefined` when unresolved; the `milestone_version`/`milestone_name`
|
|
739
|
+
// output fields below coerce that to an explicit `null` (#3216 review
|
|
740
|
+
// Finding 2) so the key is never silently omitted from the JSON bundle.
|
|
741
|
+
const milestone = milestoneRecord(cwd);
|
|
260
742
|
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (!phaseInfo && roadmapPhase?.['found']) {
|
|
265
|
-
const phaseName = roadmapPhase['phase_name'];
|
|
266
|
-
phaseInfo = {
|
|
743
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
744
|
+
const phaseName = rp['phase_name'];
|
|
745
|
+
return {
|
|
267
746
|
found: true,
|
|
268
747
|
directory: null,
|
|
269
|
-
phase_number:
|
|
748
|
+
phase_number: rp['phase_number'],
|
|
270
749
|
phase_name: phaseName,
|
|
271
|
-
phase_slug: phaseName
|
|
272
|
-
? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
273
|
-
: null,
|
|
750
|
+
phase_slug: slugifyPhaseName(phaseName),
|
|
274
751
|
plans: [],
|
|
275
752
|
summaries: [],
|
|
276
753
|
incomplete_plans: [],
|
|
754
|
+
halted_plans: [],
|
|
755
|
+
blocked_by: {},
|
|
756
|
+
runnable_plans: [],
|
|
277
757
|
has_research: false,
|
|
278
758
|
has_context: false,
|
|
279
759
|
has_verification: false,
|
|
280
760
|
has_reviews: false,
|
|
281
761
|
};
|
|
282
|
-
}
|
|
762
|
+
});
|
|
283
763
|
const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE);
|
|
284
764
|
const reqExtracted = reqMatch
|
|
285
765
|
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
|
286
766
|
: null;
|
|
287
767
|
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
|
288
768
|
const wf = (config.workflow ?? {});
|
|
769
|
+
// #3188: these paths are null when the file is absent, matching the contract
|
|
770
|
+
// the conditional sibling fields (context_path, patterns_path, ...) already
|
|
771
|
+
// honour and that ultraplan-phase.md / execute-phase.md gate on. Hoisted so
|
|
772
|
+
// the existence check and the emitted path share one source of truth.
|
|
773
|
+
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
774
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
775
|
+
const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
|
|
289
776
|
const result = {
|
|
290
777
|
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
|
|
291
778
|
verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
|
|
@@ -305,7 +792,17 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
305
792
|
? toPosixPath(node_path_1.default.join(cwd, phaseInfo['directory']))
|
|
306
793
|
: null,
|
|
307
794
|
phase_number: phaseInfo?.['phase_number'] || null,
|
|
308
|
-
|
|
795
|
+
// #3171: prefer the ROADMAP's curated display name for `phase_name`. When
|
|
796
|
+
// the phase directory already exists on disk, the disk-lookup path
|
|
797
|
+
// (searchPhaseInDir) derives phase_name from the directory-name remainder
|
|
798
|
+
// — itself an already-slugified value (`phase.add` writes `${num}-${slug}`
|
|
799
|
+
// dirs), so phase_name and phase_slug come out byte-identical. An
|
|
800
|
+
// orchestrator wiring this field into `state begin-phase --name` then
|
|
801
|
+
// lands a raw slug in STATE.md's current_phase_name. The ROADMAP carries
|
|
802
|
+
// the human-curated display name (`### Phase N: <Name>`); prefer it,
|
|
803
|
+
// matching the no-disk fallback above. phase_slug stays disk-derived — it
|
|
804
|
+
// correctly feeds branch-name construction below and is unchanged here.
|
|
805
|
+
phase_name: (roadmapPhase?.['phase_name']) || (phaseInfo?.['phase_name']) || null,
|
|
309
806
|
phase_slug: phaseInfo?.['phase_slug'] || null,
|
|
310
807
|
phase_req_ids,
|
|
311
808
|
plans: phaseInfo?.['plans'] || [],
|
|
@@ -313,6 +810,15 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
313
810
|
incomplete_plans: phaseInfo?.['incomplete_plans'] || [],
|
|
314
811
|
plan_count: phaseInfo?.['plans']?.length || 0,
|
|
315
812
|
incomplete_count: phaseInfo?.['incomplete_plans']?.length || 0,
|
|
813
|
+
// #2830: the halt-aware view, forwarded from the shared computation in
|
|
814
|
+
// phase-locator. Additive — `incomplete_plans`/`incomplete_count` above keep
|
|
815
|
+
// their exact name, type and semantics. Without this passthrough the shared
|
|
816
|
+
// truth is computed and then dropped at this consumer, which is the path the
|
|
817
|
+
// issue reports as regressed.
|
|
818
|
+
halted_plans: phaseInfo?.['halted_plans'] || [],
|
|
819
|
+
blocked_by: phaseInfo?.['blocked_by'] || {},
|
|
820
|
+
runnable_plans: phaseInfo?.['runnable_plans'] || [],
|
|
821
|
+
runnable_count: phaseInfo?.['runnable_plans']?.length || 0,
|
|
316
822
|
branch_name: config.branching_strategy === 'phase' && phaseInfo
|
|
317
823
|
? config.phase_branch_template
|
|
318
824
|
.replace('{project}', config.project_code || '')
|
|
@@ -320,22 +826,23 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
320
826
|
.replace('{slug}', phaseInfo['phase_slug'] || 'phase')
|
|
321
827
|
: config.branching_strategy === 'milestone'
|
|
322
828
|
? config.milestone_branch_template
|
|
323
|
-
.replace('{milestone}', milestone['version'])
|
|
829
|
+
.replace('{milestone}', milestone['version'] ?? '')
|
|
324
830
|
.replace('{slug}', generateSlugInternal(milestone['name']) || 'milestone')
|
|
325
831
|
: null,
|
|
326
|
-
milestone_version: milestone['version'],
|
|
327
|
-
milestone_name: milestone['name'],
|
|
832
|
+
milestone_version: milestone['version'] ?? null,
|
|
833
|
+
milestone_name: milestone['name'] ?? null,
|
|
328
834
|
milestone_slug: generateSlugInternal(milestone['name']),
|
|
329
835
|
state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
330
836
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
331
837
|
config_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
332
838
|
// #2376: emit absolute paths — see comment above on phase_dir.
|
|
333
|
-
|
|
334
|
-
|
|
839
|
+
// #3188: null when the file is absent (parity with patterns_path/context_path).
|
|
840
|
+
state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
|
|
841
|
+
roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
|
|
335
842
|
config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
336
843
|
// #2376: execute-phase.md's verify_phase_goal step reads this instead of
|
|
337
844
|
// hardcoding '.planning/REQUIREMENTS.md' into the gsd-verifier spawn prompt.
|
|
338
|
-
requirements_path:
|
|
845
|
+
requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
|
|
339
846
|
};
|
|
340
847
|
if (options['validate']) {
|
|
341
848
|
try {
|
|
@@ -359,6 +866,8 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
359
866
|
/* intentionally empty */
|
|
360
867
|
}
|
|
361
868
|
}
|
|
869
|
+
// #2932/#2992 (Phase 5/6.1): additive, optional field — degrades to null, never throws.
|
|
870
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, options, 'execute-phase');
|
|
362
871
|
output(withProjectRoot(cwd, result), raw);
|
|
363
872
|
}
|
|
364
873
|
function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
@@ -369,19 +878,14 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
369
878
|
// #2056/#2104: foreign-prefixed queries must not collapse to numeric phases.
|
|
370
879
|
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
371
880
|
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (!phaseInfo && roadmapPhase?.['found']) {
|
|
376
|
-
const phaseName = roadmapPhase['phase_name'];
|
|
377
|
-
phaseInfo = {
|
|
881
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
882
|
+
const phaseName = rp['phase_name'];
|
|
883
|
+
return {
|
|
378
884
|
found: true,
|
|
379
885
|
directory: null,
|
|
380
|
-
phase_number:
|
|
886
|
+
phase_number: rp['phase_number'],
|
|
381
887
|
phase_name: phaseName,
|
|
382
|
-
phase_slug: phaseName
|
|
383
|
-
? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
384
|
-
: null,
|
|
888
|
+
phase_slug: slugifyPhaseName(phaseName),
|
|
385
889
|
plans: [],
|
|
386
890
|
summaries: [],
|
|
387
891
|
incomplete_plans: [],
|
|
@@ -390,7 +894,7 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
390
894
|
has_verification: false,
|
|
391
895
|
has_reviews: false,
|
|
392
896
|
};
|
|
393
|
-
}
|
|
897
|
+
});
|
|
394
898
|
const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE);
|
|
395
899
|
const reqExtracted = reqMatch
|
|
396
900
|
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
|
@@ -415,6 +919,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
415
919
|
assertValidGranularityOverride(granularityOverride, error);
|
|
416
920
|
const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
|
|
417
921
|
const wf = (config.workflow ?? {});
|
|
922
|
+
// #3188: see cmdInitExecutePhase — null when absent, parity with the
|
|
923
|
+
// conditional sibling fields in this same result object.
|
|
924
|
+
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
925
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
926
|
+
const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
|
|
418
927
|
const result = {
|
|
419
928
|
researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
|
|
420
929
|
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
|
|
@@ -451,36 +960,70 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
451
960
|
planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
|
|
452
961
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
453
962
|
// #2376: absolute — see comment on phase_dir above.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
963
|
+
// #3188: null when the file is absent (parity with patterns_path below).
|
|
964
|
+
state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
|
|
965
|
+
roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
|
|
966
|
+
requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
|
|
457
967
|
patterns_path: null,
|
|
458
968
|
};
|
|
459
969
|
if (phaseInfo?.['directory']) {
|
|
460
970
|
const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
461
971
|
try {
|
|
462
972
|
const files = node_fs_1.default.readdirSync(phaseDirFull);
|
|
463
|
-
const
|
|
973
|
+
const phaseDirName = node_path_1.default.basename(phaseDirFull);
|
|
974
|
+
// #3511 BLOCKER-3: scope the raw listing to THIS phase's own artifacts
|
|
975
|
+
// before any bare `.find()` predicate runs, so a `04-UAT.md` (or
|
|
976
|
+
// `04-RESEARCH.md`/`04-REVIEWS.md`/`04-PATTERNS.md`) sitting in phase
|
|
977
|
+
// 03's directory cannot win a phase-03 lookup — the same
|
|
978
|
+
// `isPhaseArtifact` membership rule `resolveVerificationFile` already
|
|
979
|
+
// applies via `phaseDirName` below. `findContextMdIn` is passed the
|
|
980
|
+
// scoped array (rather than the raw directory path) so this call site
|
|
981
|
+
// alone is scoped; its other call sites are unaffected.
|
|
982
|
+
const scopedFiles = scopeToPhase(files, phaseDirName);
|
|
983
|
+
const contextFile = findContextMdIn(scopedFiles);
|
|
464
984
|
if (contextFile) {
|
|
465
985
|
result['context_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, contextFile));
|
|
466
986
|
}
|
|
467
|
-
const researchFile =
|
|
987
|
+
const researchFile = scopedFiles.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
468
988
|
if (researchFile) {
|
|
469
989
|
result['research_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, researchFile));
|
|
470
990
|
}
|
|
471
|
-
|
|
991
|
+
// #3473 F2: routed through the shared resolver — readdir order is
|
|
992
|
+
// filesystem-dependent, so the prior hand-rolled `.find()` could pick
|
|
993
|
+
// either file when a phase held both a canonical report and an ad-hoc
|
|
994
|
+
// `-CORRECTION-VERIFICATION.md` worksheet (#3357).
|
|
995
|
+
// #3492: pin selection to THIS phase's own token so a stray cross-phase
|
|
996
|
+
// or sentinel-numbered canonically-shaped file cannot outrank this
|
|
997
|
+
// phase's own (possibly non-canonical) report.
|
|
998
|
+
const phaseToken = extractPhaseToken(phaseDirName);
|
|
999
|
+
const verificationFile = resolveVerificationFile(files, {
|
|
1000
|
+
allowBare: true,
|
|
1001
|
+
phaseToken,
|
|
1002
|
+
phaseDirName,
|
|
1003
|
+
});
|
|
472
1004
|
if (verificationFile) {
|
|
473
1005
|
result['verification_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, verificationFile));
|
|
474
1006
|
}
|
|
475
|
-
|
|
1007
|
+
// #3518: routed through the shared UAT resolver — the prior hand-rolled
|
|
1008
|
+
// `.find()` over unsorted readdir order had no phase check and no
|
|
1009
|
+
// ordering, so a stray cross-phase 02-UAT.md could become this phase's
|
|
1010
|
+
// uat_path, filesystem-dependently. Pinned to this phase's own token
|
|
1011
|
+
// (same rule as verification_path above), and phase-scoped via
|
|
1012
|
+
// phaseDirName (#3511) so the alphabetically-first fallback tier also
|
|
1013
|
+
// excludes cross-phase strays.
|
|
1014
|
+
const uatFile = resolveUatFile(files, {
|
|
1015
|
+
allowBare: true,
|
|
1016
|
+
phaseToken,
|
|
1017
|
+
phaseDirName,
|
|
1018
|
+
});
|
|
476
1019
|
if (uatFile) {
|
|
477
1020
|
result['uat_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, uatFile));
|
|
478
1021
|
}
|
|
479
|
-
const reviewsFile =
|
|
1022
|
+
const reviewsFile = scopedFiles.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
|
|
480
1023
|
if (reviewsFile) {
|
|
481
1024
|
result['reviews_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, reviewsFile));
|
|
482
1025
|
}
|
|
483
|
-
const patternsFile =
|
|
1026
|
+
const patternsFile = scopedFiles.find((f) => f.endsWith('-PATTERNS.md') || f === 'PATTERNS.md');
|
|
484
1027
|
if (patternsFile) {
|
|
485
1028
|
result['patterns_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, patternsFile));
|
|
486
1029
|
}
|
|
@@ -511,9 +1054,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
511
1054
|
/* intentionally empty */
|
|
512
1055
|
}
|
|
513
1056
|
}
|
|
1057
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
1058
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, options, 'plan-phase');
|
|
514
1059
|
output(withProjectRoot(cwd, result), raw);
|
|
515
1060
|
}
|
|
516
|
-
function cmdInitNewProject(cwd, raw) {
|
|
1061
|
+
function cmdInitNewProject(cwd, raw, options = {}) {
|
|
517
1062
|
const config = loadConfig(cwd);
|
|
518
1063
|
const homedir = node_os_1.default.homedir();
|
|
519
1064
|
const braveKeyFile = node_path_1.default.join(homedir, '.gsd', 'brave_api_key');
|
|
@@ -552,26 +1097,25 @@ function cmdInitNewProject(cwd, raw) {
|
|
|
552
1097
|
config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
553
1098
|
research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
|
|
554
1099
|
};
|
|
1100
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
1101
|
+
// needsCodebaseMap is threaded from this scope's own isBrownfield/hasCodebaseMap
|
|
1102
|
+
// computation (see `needs_codebase_map` above) so `state:needs-codebase-map` is
|
|
1103
|
+
// genuinely computed for this workflow, not left permanently false.
|
|
1104
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'new-project', {
|
|
1105
|
+
needsCodebaseMap: isBrownfield && !hasCodebaseMap,
|
|
1106
|
+
});
|
|
555
1107
|
output(withProjectRoot(cwd, result), raw);
|
|
556
1108
|
}
|
|
557
|
-
function cmdInitNewMilestone(cwd, raw) {
|
|
1109
|
+
function cmdInitNewMilestone(cwd, raw, options = {}) {
|
|
558
1110
|
const config = loadConfig(cwd);
|
|
559
|
-
const milestone =
|
|
1111
|
+
const milestone = milestoneRecord(cwd);
|
|
560
1112
|
const latestCompleted = getLatestCompletedMilestone(cwd);
|
|
561
1113
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
.readdirSync(phasesDir, { withFileTypes: true })
|
|
568
|
-
.filter((entry) => entry.isDirectory() && isDirInMilestone(entry.name))
|
|
569
|
-
.length;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
catch {
|
|
573
|
-
/* intentionally empty */
|
|
574
|
-
}
|
|
1114
|
+
// #3185 (ADR-3180 Decision 1): "how many phase directories belong to the
|
|
1115
|
+
// CURRENT milestone" is exactly the scoped question listMilestonePhaseDirs
|
|
1116
|
+
// owns — routed through it instead of a local readdirSync + hand-rolled
|
|
1117
|
+
// window filter (which also never excluded sentinels, unlike the owner).
|
|
1118
|
+
const phaseDirCount = listMilestonePhaseDirs(phasesDir, { cwd }).value.length;
|
|
575
1119
|
const wf = (config.workflow ?? {});
|
|
576
1120
|
const result = {
|
|
577
1121
|
researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
|
|
@@ -579,8 +1123,12 @@ function cmdInitNewMilestone(cwd, raw) {
|
|
|
579
1123
|
roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
|
|
580
1124
|
commit_docs: config.commit_docs,
|
|
581
1125
|
research_enabled: wf['research'],
|
|
582
|
-
|
|
583
|
-
|
|
1126
|
+
// #3216 review Finding 2: `?? null` so an unresolved milestone still emits
|
|
1127
|
+
// the key with an explicit `null` rather than letting JSON.stringify drop
|
|
1128
|
+
// it — an omitted key reaches the prompt layer's `{current_milestone}`
|
|
1129
|
+
// placeholder as literal, un-substituted text.
|
|
1130
|
+
current_milestone: milestone['version'] ?? null,
|
|
1131
|
+
current_milestone_name: milestone['name'] ?? null,
|
|
584
1132
|
latest_completed_milestone: latestCompleted?.version || null,
|
|
585
1133
|
latest_completed_milestone_name: latestCompleted?.name || null,
|
|
586
1134
|
phase_dir_count: phaseDirCount,
|
|
@@ -601,11 +1149,40 @@ function cmdInitNewMilestone(cwd, raw) {
|
|
|
601
1149
|
research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
|
|
602
1150
|
milestones_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'MILESTONES.md')),
|
|
603
1151
|
};
|
|
1152
|
+
// `state:flat-mode` (#2994): whether NO workstream is active — the inverse
|
|
1153
|
+
// of `state:workstream-active` (introduced for `cmdInitTransition` below).
|
|
1154
|
+
// `new-milestone.md`'s Step 4 Part A (milestone-state write) runs ONLY in
|
|
1155
|
+
// flat mode; a workstream's own `.planning/workstreams/<name>/STATE.md`/
|
|
1156
|
+
// `ROADMAP.md`/`REQUIREMENTS.md` already carry the milestone state, so
|
|
1157
|
+
// writing the shared `## Current Milestone` heading here would clobber it
|
|
1158
|
+
// (#2308). The `when=` grammar has no negation operator (ADR-1671:69), so
|
|
1159
|
+
// Part A's condition — "skip when a workstream IS active" — cannot be
|
|
1160
|
+
// expressed by negating `state:workstream-active` in the marker; a
|
|
1161
|
+
// SEPARATE, positively-phrased atom whose fact is the inverse is the
|
|
1162
|
+
// sanctioned resolution (same discipline as `state:chunked-mode` folding
|
|
1163
|
+
// an OR — never an operator in the grammar itself). Same authoritative
|
|
1164
|
+
// source as `cmdInitTransition`: `GSD_WORKSTREAM` env, falling back to the
|
|
1165
|
+
// stored active-workstream pointer (mirrors `cmdInitProgress`'s own
|
|
1166
|
+
// resolution above).
|
|
1167
|
+
//
|
|
1168
|
+
// #3579 root-cause fix: this is a read-only informational field (no write
|
|
1169
|
+
// follows), so use the non-mutating peek — getActiveWorkstream's self-heal
|
|
1170
|
+
// would otherwise silently delete a stale/invalid pointer as a side effect
|
|
1171
|
+
// of building a JSON report field, and (per #3579) could change what a
|
|
1172
|
+
// LATER resolution in the same process observes.
|
|
1173
|
+
const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
|
|
1174
|
+
const workstreamActive = !!resolvedWorkstream;
|
|
1175
|
+
const flatMode = !workstreamActive;
|
|
1176
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
1177
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'new-milestone', {
|
|
1178
|
+
workstreamActive,
|
|
1179
|
+
flatMode,
|
|
1180
|
+
});
|
|
604
1181
|
output(withProjectRoot(cwd, result), raw);
|
|
605
1182
|
}
|
|
606
|
-
function cmdInitQuick(cwd, description, raw) {
|
|
1183
|
+
function cmdInitQuick(cwd, description, raw, options = {}) {
|
|
607
1184
|
const config = loadConfig(cwd);
|
|
608
|
-
const now = new Date();
|
|
1185
|
+
const now = new Date(clock_cjs_1.realClock.now());
|
|
609
1186
|
const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
|
|
610
1187
|
const yy = String(now.getFullYear()).slice(-2);
|
|
611
1188
|
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
@@ -645,6 +1222,21 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
645
1222
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
646
1223
|
planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
|
|
647
1224
|
};
|
|
1225
|
+
// #2994: `--full` IMPLIES `--discuss`/`--research`/`--validate` — resolved to
|
|
1226
|
+
// ONE set of facts HERE, in fact computation, never in the `when=` grammar
|
|
1227
|
+
// itself (mirrors `state:chunked-mode`'s disjunction fold at
|
|
1228
|
+
// `buildSectionManifestField`'s `chunkedMode` computation above). The three
|
|
1229
|
+
// implied tokens are folded into the flags BEFORE `buildSectionManifestField`
|
|
1230
|
+
// builds its `InvocationFacts.flags` Set, so `discussion-phase`/`research-phase`/
|
|
1231
|
+
// `plan-checker-loop`/`quick-verification` (all gated on their own single
|
|
1232
|
+
// `flag:--discuss`/`flag:--research`/`flag:--validate` atom) include correctly
|
|
1233
|
+
// for a bare `/gsd:quick --full` invocation that never passed the individual
|
|
1234
|
+
// tokens — the grammar still sees exactly one atom per marker, no OR.
|
|
1235
|
+
const sectionManifestOptions = options['full']
|
|
1236
|
+
? { ...options, discuss: true, research: true, validate: true }
|
|
1237
|
+
: options;
|
|
1238
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
1239
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, sectionManifestOptions, 'quick');
|
|
648
1240
|
output(withProjectRoot(cwd, result), raw);
|
|
649
1241
|
}
|
|
650
1242
|
function cmdInitIngestDocs(cwd, raw) {
|
|
@@ -710,33 +1302,23 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
710
1302
|
const config = loadConfig(cwd);
|
|
711
1303
|
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
712
1304
|
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
: null,
|
|
731
|
-
plans: [],
|
|
732
|
-
summaries: [],
|
|
733
|
-
incomplete_plans: [],
|
|
734
|
-
has_research: false,
|
|
735
|
-
has_context: false,
|
|
736
|
-
has_verification: false,
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
}
|
|
1305
|
+
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1306
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1307
|
+
const phaseName = rp['phase_name'];
|
|
1308
|
+
return {
|
|
1309
|
+
found: true,
|
|
1310
|
+
directory: null,
|
|
1311
|
+
phase_number: rp['phase_number'],
|
|
1312
|
+
phase_name: phaseName,
|
|
1313
|
+
phase_slug: slugifyPhaseName(phaseName),
|
|
1314
|
+
plans: [],
|
|
1315
|
+
summaries: [],
|
|
1316
|
+
incomplete_plans: [],
|
|
1317
|
+
has_research: false,
|
|
1318
|
+
has_context: false,
|
|
1319
|
+
has_verification: false,
|
|
1320
|
+
};
|
|
1321
|
+
});
|
|
740
1322
|
const phaseDir = phaseInfo?.['directory'] || null;
|
|
741
1323
|
const planCount = phaseInfo?.['plans']?.length || 0;
|
|
742
1324
|
const summaryCount = phaseInfo?.['summaries']?.length || 0;
|
|
@@ -746,6 +1328,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
746
1328
|
policy: { requireVerification: true },
|
|
747
1329
|
})
|
|
748
1330
|
: null;
|
|
1331
|
+
const uiPhaseActive = detectUiPhaseActive(cwd, phaseInfo);
|
|
749
1332
|
const result = {
|
|
750
1333
|
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
|
|
751
1334
|
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
|
|
@@ -767,7 +1350,227 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
767
1350
|
uat_blockers: uatReport?.blockers ?? [],
|
|
768
1351
|
ready_to_transition: completion.phase_complete && (uatReport?.passed ?? false),
|
|
769
1352
|
},
|
|
1353
|
+
// #2994 (resolver-hoist-guard G5): hoisted `state:ui-phase-active` ground
|
|
1354
|
+
// truth (previously re-derived inline inside the automated_ui_verification
|
|
1355
|
+
// step body via its own `gsd_run loop render-hooks plan:pre --raw` call —
|
|
1356
|
+
// a circular, self-disabling resolver, since the section is only read
|
|
1357
|
+
// when this same fact is already true). Resolved once here, exposed so
|
|
1358
|
+
// the step body can consume it directly instead of recomputing it.
|
|
1359
|
+
ui_phase_active: uiPhaseActive,
|
|
1360
|
+
};
|
|
1361
|
+
// #2994 (Phase 6.3): additive, optional field — degrades to null, never throws.
|
|
1362
|
+
// phaseInfo is passed through directly (mirrors cmdInitExecutePhase / cmdInitPlanPhase)
|
|
1363
|
+
// so buildSectionManifestField's internal detectPhaseMvpMode call gets a real
|
|
1364
|
+
// phase_number/directory rather than permanently-false facts. uiPhaseActive is
|
|
1365
|
+
// computed once above (not re-derived here) and threaded through via overrides,
|
|
1366
|
+
// mirroring the fallow/git-create-tag hoist pattern.
|
|
1367
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, {}, 'verify-work', {
|
|
1368
|
+
uiPhaseActive,
|
|
1369
|
+
});
|
|
1370
|
+
output(withProjectRoot(cwd, result), raw);
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* `code-review.md`'s dedicated init entry point (#2994, epic #1671 Phase
|
|
1374
|
+
* 6.3). `code-review.md` previously routed through the shared, 20+-caller
|
|
1375
|
+
* `init.phase-op` (`cmdInitPhaseOp` below), reading only 6 of its fields
|
|
1376
|
+
* (`phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`,
|
|
1377
|
+
* `commit_docs` — verified against the workflow's own "Parse from init
|
|
1378
|
+
* JSON" line). `cmdInitPhaseOp` is CRITICAL blast radius (179 dependents
|
|
1379
|
+
* across 24 processes per the #2994 dispatch) and is never modified for
|
|
1380
|
+
* this — this function resolves phase info itself via the SAME shared
|
|
1381
|
+
* primitives `cmdInitPhaseOp` calls (`guardedFindPhase`/
|
|
1382
|
+
* `guardedGetRoadmapPhase`, plus the shared `applyRoadmapFallback` archived/
|
|
1383
|
+
* not-found fallback also used by execute-phase, plan-phase, verify-work
|
|
1384
|
+
* and review — see `applyRoadmapFallback`'s own doc comment; `cmdInitPhaseOp`
|
|
1385
|
+
* is deliberately excluded from that shared helper), producing the identical
|
|
1386
|
+
* 6-field shape rather than a second, hand-maintained copy of
|
|
1387
|
+
* `cmdInitPhaseOp`'s full ~60-field bundle. See
|
|
1388
|
+
* `tests/init-code-review-parity.test.cjs` for the DEFECT.GENERATIVE-FIX
|
|
1389
|
+
* parity guard between the two.
|
|
1390
|
+
*
|
|
1391
|
+
* Two further facts are resolved and exposed here that `init.phase-op`
|
|
1392
|
+
* never carried: the fallow structural-pre-pass config gate
|
|
1393
|
+
* (`detectFallowConfig`, `state:fallow-enabled`) and the `--fix` flag
|
|
1394
|
+
* (folded into `options` so `buildSectionManifestField` picks it up as
|
|
1395
|
+
* `flag:--fix`).
|
|
1396
|
+
*/
|
|
1397
|
+
function cmdInitCodeReview(cwd, phase, raw, options = {}) {
|
|
1398
|
+
const config = loadConfig(cwd);
|
|
1399
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1400
|
+
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1401
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1402
|
+
const rpName = rp['phase_name'];
|
|
1403
|
+
return {
|
|
1404
|
+
found: true,
|
|
1405
|
+
directory: null,
|
|
1406
|
+
phase_number: rp['phase_number'],
|
|
1407
|
+
phase_name: rpName,
|
|
1408
|
+
phase_slug: slugifyPhaseName(rpName),
|
|
1409
|
+
};
|
|
1410
|
+
});
|
|
1411
|
+
const phaseDir = phaseInfo?.['directory'] || null;
|
|
1412
|
+
const phaseNumber = phaseInfo?.['phase_number'] || null;
|
|
1413
|
+
const phaseName = phaseInfo?.['phase_name'] || null;
|
|
1414
|
+
const fallow = detectFallowConfig(cwd);
|
|
1415
|
+
const result = {
|
|
1416
|
+
commit_docs: config.commit_docs,
|
|
1417
|
+
phase_found: !!phaseInfo,
|
|
1418
|
+
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1419
|
+
phase_dir: phaseDir ? toPosixPath(node_path_1.default.join(cwd, phaseDir)) : null,
|
|
1420
|
+
phase_number: phaseNumber,
|
|
1421
|
+
phase_name: phaseName,
|
|
1422
|
+
padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
|
|
1423
|
+
// #2994: hoisted fallow config-gate resolution (previously re-derived
|
|
1424
|
+
// inline inside code-review.md's structural_pre_pass step body — a
|
|
1425
|
+
// circular self-disabling gate now resolved once here at init time).
|
|
1426
|
+
fallow_enabled: fallow.enabled,
|
|
1427
|
+
fallow_scope: fallow.scope,
|
|
1428
|
+
fallow_profile: fallow.profile,
|
|
1429
|
+
fallow_mcp: fallow.mcp,
|
|
1430
|
+
fallow_max_crap: fallow.maxCrap,
|
|
770
1431
|
};
|
|
1432
|
+
// #2994 (Phase 6.3): additive, optional field — degrades to null, never throws.
|
|
1433
|
+
const sectionManifestOptions = {
|
|
1434
|
+
...options,
|
|
1435
|
+
fix: options['fix'] || undefined,
|
|
1436
|
+
};
|
|
1437
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, sectionManifestOptions, 'code-review', {
|
|
1438
|
+
fallowEnabled: fallow.enabled,
|
|
1439
|
+
});
|
|
1440
|
+
output(withProjectRoot(cwd, result), raw);
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* `review.md`'s dedicated init entry point (#2994, epic #1671 Phase 6.3
|
|
1444
|
+
* amendment). `review.md` previously routed through the shared, 20+-caller
|
|
1445
|
+
* `init.phase-op` (`cmdInitPhaseOp` below), reading only 3 of its fields
|
|
1446
|
+
* (`phase_dir`, `phase_number`, `padded_phase` — verified against the
|
|
1447
|
+
* workflow's own "Read from init" line in `gather_context`). `cmdInitPhaseOp`
|
|
1448
|
+
* is CRITICAL blast radius (179 dependents across 24 processes) and is never
|
|
1449
|
+
* modified for this — this function resolves phase info itself via the SAME
|
|
1450
|
+
* shared primitives `cmdInitPhaseOp` calls (`guardedFindPhase`/
|
|
1451
|
+
* `guardedGetRoadmapPhase`), plus the shared `applyRoadmapFallback`
|
|
1452
|
+
* archived/not-found fallback (see its own doc comment), producing the
|
|
1453
|
+
* identical 3-field shape rather than a second, hand-maintained copy of
|
|
1454
|
+
* `cmdInitPhaseOp`'s full ~60-field bundle.
|
|
1455
|
+
*
|
|
1456
|
+
* One further fact is resolved and exposed here that `init.phase-op` never
|
|
1457
|
+
* carried: whether reviewer instances are configured
|
|
1458
|
+
* (`.planning/config.json`'s `review.reviewer_instances`, present AND
|
|
1459
|
+
* non-empty — `state:reviewer-instances-configured`), reusing
|
|
1460
|
+
* `readConfigJsonValue` (added for `detectFallowConfig`) rather than a
|
|
1461
|
+
* second, divergence-prone config reader (DEFECT.GENERATIVE-FIX).
|
|
1462
|
+
*/
|
|
1463
|
+
function cmdInitReview(cwd, phase, raw, options = {}) {
|
|
1464
|
+
const config = loadConfig(cwd);
|
|
1465
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1466
|
+
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1467
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => ({
|
|
1468
|
+
found: true,
|
|
1469
|
+
directory: null,
|
|
1470
|
+
phase_number: rp['phase_number'],
|
|
1471
|
+
phase_name: rp['phase_name'],
|
|
1472
|
+
}));
|
|
1473
|
+
const phaseDir = phaseInfo?.['directory'] || null;
|
|
1474
|
+
const phaseNumber = phaseInfo?.['phase_number'] || null;
|
|
1475
|
+
// #2994: `state:reviewer-instances-configured` ground truth — present AND
|
|
1476
|
+
// non-empty `review.reviewer_instances` object. A missing key, a non-object
|
|
1477
|
+
// value, or an empty object all resolve to `false` (fail-closed, matching
|
|
1478
|
+
// the workflow's own pre-hoist prose gate — "Unconfigured -> default path
|
|
1479
|
+
// unchanged").
|
|
1480
|
+
const rawReviewerInstances = readConfigJsonValue(cwd, ['review', 'reviewer_instances']);
|
|
1481
|
+
const reviewerInstancesConfigured = rawReviewerInstances !== null &&
|
|
1482
|
+
typeof rawReviewerInstances === 'object' &&
|
|
1483
|
+
!Array.isArray(rawReviewerInstances) &&
|
|
1484
|
+
Object.keys(rawReviewerInstances).length > 0;
|
|
1485
|
+
const result = {
|
|
1486
|
+
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1487
|
+
phase_dir: phaseDir ? toPosixPath(node_path_1.default.join(cwd, phaseDir)) : null,
|
|
1488
|
+
phase_number: phaseNumber,
|
|
1489
|
+
padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
|
|
1490
|
+
};
|
|
1491
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, options, 'review', {
|
|
1492
|
+
reviewerInstancesConfigured,
|
|
1493
|
+
});
|
|
1494
|
+
output(withProjectRoot(cwd, result), raw);
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* `discuss-phase-assumptions.md`'s dedicated init entry point (#2994, epic
|
|
1498
|
+
* #1671 Phase 6.3 amendment). Previously routed through the shared,
|
|
1499
|
+
* 20+-caller `init.phase-op` (`cmdInitPhaseOp` below), reading 14 of its
|
|
1500
|
+
* fields (`commit_docs`, `phase_found`, `phase_dir`, `phase_number`,
|
|
1501
|
+
* `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`,
|
|
1502
|
+
* `has_plans`, `has_verification`, `plan_count`, `roadmap_exists`,
|
|
1503
|
+
* `planning_exists` — verified against the workflow's own "Parse JSON for"
|
|
1504
|
+
* line). `cmdInitPhaseOp` is CRITICAL blast radius (179 dependents across 24
|
|
1505
|
+
* processes) and is never modified for this — this function resolves phase
|
|
1506
|
+
* info itself via the SAME shared primitives `cmdInitPhaseOp` calls
|
|
1507
|
+
* (`guardedFindPhase`/`guardedGetRoadmapPhase`), plus the shared
|
|
1508
|
+
* `applyRoadmapFallback` archived/not-found fallback (see its own doc
|
|
1509
|
+
* comment) producing the identical fallback shape (`plans: []`,
|
|
1510
|
+
* `has_research: false`, `has_context: false`, `has_verification: false`)
|
|
1511
|
+
* rather than a second, hand-maintained copy of `cmdInitPhaseOp`'s full
|
|
1512
|
+
* ~60-field bundle.
|
|
1513
|
+
*
|
|
1514
|
+
* One further fact is resolved and exposed here that `init.phase-op` never
|
|
1515
|
+
* carried: `state:auto-advance-active` — the workflow's own `auto_advance`
|
|
1516
|
+
* step resolves `--auto` OR a consolidated `check auto-mode --pick active`
|
|
1517
|
+
* fact (itself `workflow._auto_chain_active` OR `workflow.auto_advance`) via
|
|
1518
|
+
* a runtime `gsd_run` call; that identical disjunction is folded into ONE
|
|
1519
|
+
* boolean FACT here (same discipline as `state:chunked-mode` /
|
|
1520
|
+
* `state:plan-strategy-converge`), exposed as `auto_advance_active`.
|
|
1521
|
+
*/
|
|
1522
|
+
function cmdInitDiscussPhaseAssumptions(cwd, phase, raw, options = {}) {
|
|
1523
|
+
const config = loadConfig(cwd);
|
|
1524
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1525
|
+
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1526
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1527
|
+
const rpName = rp['phase_name'];
|
|
1528
|
+
return {
|
|
1529
|
+
found: true,
|
|
1530
|
+
directory: null,
|
|
1531
|
+
phase_number: rp['phase_number'],
|
|
1532
|
+
phase_name: rpName,
|
|
1533
|
+
phase_slug: slugifyPhaseName(rpName),
|
|
1534
|
+
plans: [],
|
|
1535
|
+
has_research: false,
|
|
1536
|
+
has_context: false,
|
|
1537
|
+
has_verification: false,
|
|
1538
|
+
};
|
|
1539
|
+
});
|
|
1540
|
+
const phaseDir = phaseInfo?.['directory'] || null;
|
|
1541
|
+
const phaseNumber = phaseInfo?.['phase_number'] || null;
|
|
1542
|
+
const phaseName = phaseInfo?.['phase_name'] || null;
|
|
1543
|
+
// #2994: mirrors discuss-phase-assumptions.md's own auto_advance step
|
|
1544
|
+
// resolver — `--auto` flag OR the consolidated `check auto-mode --pick
|
|
1545
|
+
// active` fact (workflow._auto_chain_active OR workflow.auto_advance).
|
|
1546
|
+
const autoAdvanceActive = options['auto'] === true ||
|
|
1547
|
+
readConfigJsonBoolean(cwd, ['workflow', '_auto_chain_active']) ||
|
|
1548
|
+
readConfigJsonBoolean(cwd, ['workflow', 'auto_advance']);
|
|
1549
|
+
const result = {
|
|
1550
|
+
commit_docs: config.commit_docs,
|
|
1551
|
+
phase_found: !!phaseInfo,
|
|
1552
|
+
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1553
|
+
phase_dir: phaseDir ? toPosixPath(node_path_1.default.join(cwd, phaseDir)) : null,
|
|
1554
|
+
phase_number: phaseNumber,
|
|
1555
|
+
phase_name: phaseName,
|
|
1556
|
+
phase_slug: phaseInfo?.['phase_slug'] || null,
|
|
1557
|
+
padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
|
|
1558
|
+
has_research: phaseInfo?.['has_research'] || false,
|
|
1559
|
+
has_context: phaseInfo?.['has_context'] || false,
|
|
1560
|
+
has_plans: (phaseInfo?.['plans']?.length || 0) > 0,
|
|
1561
|
+
has_verification: phaseInfo?.['has_verification'] || false,
|
|
1562
|
+
plan_count: phaseInfo?.['plans']?.length || 0,
|
|
1563
|
+
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1564
|
+
planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
|
|
1565
|
+
};
|
|
1566
|
+
// #2994 (Phase 6.3): additive, optional field — degrades to null, never throws.
|
|
1567
|
+
const sectionManifestOptions = {
|
|
1568
|
+
...options,
|
|
1569
|
+
auto: options['auto'] || undefined,
|
|
1570
|
+
};
|
|
1571
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, sectionManifestOptions, 'discuss-phase-assumptions', {
|
|
1572
|
+
autoAdvanceActive,
|
|
1573
|
+
});
|
|
771
1574
|
output(withProjectRoot(cwd, result), raw);
|
|
772
1575
|
}
|
|
773
1576
|
function cmdInitPhaseOp(cwd, phase, raw) {
|
|
@@ -845,6 +1648,11 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
845
1648
|
expectedPhaseDir = toPosixPath(node_path_1.default.join(planningPaths(cwd).phases, dirName));
|
|
846
1649
|
}
|
|
847
1650
|
}
|
|
1651
|
+
// #3188: see cmdInitExecutePhase — null when absent, parity with the
|
|
1652
|
+
// conditional sibling fields in this same result object.
|
|
1653
|
+
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
1654
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
1655
|
+
const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
|
|
848
1656
|
const result = {
|
|
849
1657
|
commit_docs: config.commit_docs,
|
|
850
1658
|
brave_search: typeof config.brave_search === 'string'
|
|
@@ -873,31 +1681,59 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
873
1681
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
874
1682
|
planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
|
|
875
1683
|
// #2376: absolute — see comment on phase_dir above.
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1684
|
+
// #3188: null when the file is absent (parity with context_path/research_path).
|
|
1685
|
+
state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
|
|
1686
|
+
roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
|
|
1687
|
+
requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
|
|
879
1688
|
};
|
|
880
1689
|
if (phaseInfo?.['directory']) {
|
|
881
1690
|
const phaseDirFull = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
882
1691
|
try {
|
|
883
1692
|
const files = node_fs_1.default.readdirSync(phaseDirFull);
|
|
884
|
-
const
|
|
1693
|
+
const phaseDirName = node_path_1.default.basename(phaseDirFull);
|
|
1694
|
+
// #3511 BLOCKER-3: see the parallel site above — scope before any bare
|
|
1695
|
+
// `.find()` predicate so a misfiled cross-phase artifact cannot win.
|
|
1696
|
+
const scopedFiles = scopeToPhase(files, phaseDirName);
|
|
1697
|
+
const contextFile = findContextMdIn(scopedFiles);
|
|
885
1698
|
if (contextFile) {
|
|
886
1699
|
result['context_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, contextFile));
|
|
887
1700
|
}
|
|
888
|
-
const researchFile =
|
|
1701
|
+
const researchFile = scopedFiles.find((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
889
1702
|
if (researchFile) {
|
|
890
1703
|
result['research_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, researchFile));
|
|
891
1704
|
}
|
|
892
|
-
|
|
1705
|
+
// #3473 F2: routed through the shared resolver — readdir order is
|
|
1706
|
+
// filesystem-dependent, so the prior hand-rolled `.find()` could pick
|
|
1707
|
+
// either file when a phase held both a canonical report and an ad-hoc
|
|
1708
|
+
// `-CORRECTION-VERIFICATION.md` worksheet (#3357).
|
|
1709
|
+
// #3492: pin selection to THIS phase's own token so a stray cross-phase
|
|
1710
|
+
// or sentinel-numbered canonically-shaped file cannot outrank this
|
|
1711
|
+
// phase's own (possibly non-canonical) report.
|
|
1712
|
+
const phaseToken = extractPhaseToken(phaseDirName);
|
|
1713
|
+
const verificationFile = resolveVerificationFile(files, {
|
|
1714
|
+
allowBare: true,
|
|
1715
|
+
phaseToken,
|
|
1716
|
+
phaseDirName,
|
|
1717
|
+
});
|
|
893
1718
|
if (verificationFile) {
|
|
894
1719
|
result['verification_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, verificationFile));
|
|
895
1720
|
}
|
|
896
|
-
|
|
1721
|
+
// #3518: routed through the shared UAT resolver — the prior hand-rolled
|
|
1722
|
+
// `.find()` over unsorted readdir order had no phase check and no
|
|
1723
|
+
// ordering, so a stray cross-phase 02-UAT.md could become this phase's
|
|
1724
|
+
// uat_path, filesystem-dependently. Pinned to this phase's own token
|
|
1725
|
+
// (same rule as verification_path above), and phase-scoped via
|
|
1726
|
+
// phaseDirName (#3511) so the alphabetically-first fallback tier also
|
|
1727
|
+
// excludes cross-phase strays.
|
|
1728
|
+
const uatFile = resolveUatFile(files, {
|
|
1729
|
+
allowBare: true,
|
|
1730
|
+
phaseToken,
|
|
1731
|
+
phaseDirName,
|
|
1732
|
+
});
|
|
897
1733
|
if (uatFile) {
|
|
898
1734
|
result['uat_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, uatFile));
|
|
899
1735
|
}
|
|
900
|
-
const reviewsFile =
|
|
1736
|
+
const reviewsFile = scopedFiles.find((f) => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
|
|
901
1737
|
if (reviewsFile) {
|
|
902
1738
|
result['reviews_path'] = toPosixPath(node_path_1.default.join(phaseDirFull, reviewsFile));
|
|
903
1739
|
}
|
|
@@ -966,7 +1802,7 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
966
1802
|
}
|
|
967
1803
|
function cmdInitMilestoneOp(cwd, raw) {
|
|
968
1804
|
const config = loadConfig(cwd);
|
|
969
|
-
const milestone =
|
|
1805
|
+
const milestone = milestoneRecord(cwd);
|
|
970
1806
|
let phaseCount = 0;
|
|
971
1807
|
let completedPhases = 0;
|
|
972
1808
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
@@ -979,7 +1815,8 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
979
1815
|
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
|
|
980
1816
|
let m;
|
|
981
1817
|
while ((m = phasePattern.exec(currentSection)) !== null) {
|
|
982
|
-
|
|
1818
|
+
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
1819
|
+
if (isSentinelPhaseId(m[1]))
|
|
983
1820
|
continue;
|
|
984
1821
|
roadmapPhaseNumbers.push(m[1]);
|
|
985
1822
|
}
|
|
@@ -1024,8 +1861,12 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
1024
1861
|
}
|
|
1025
1862
|
else {
|
|
1026
1863
|
try {
|
|
1027
|
-
|
|
1028
|
-
|
|
1864
|
+
// #3185 (ADR-3180 Decision 1): the ROADMAP heading scan above found no
|
|
1865
|
+
// current-milestone phase headings — fall back to asking the canonical
|
|
1866
|
+
// owner "which phase directories belong to the current milestone"
|
|
1867
|
+
// directly, instead of a hand-rolled readdirSync over every directory
|
|
1868
|
+
// on disk (which also never excluded sentinels, unlike the owner).
|
|
1869
|
+
const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
|
|
1029
1870
|
phaseCount = dirs.length;
|
|
1030
1871
|
for (const dir of dirs) {
|
|
1031
1872
|
try {
|
|
@@ -1055,8 +1896,12 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
1055
1896
|
}
|
|
1056
1897
|
const result = {
|
|
1057
1898
|
commit_docs: config.commit_docs,
|
|
1058
|
-
|
|
1059
|
-
|
|
1899
|
+
// #3216 review Finding 2: `?? null` so an unresolved milestone still emits
|
|
1900
|
+
// the key with an explicit `null` rather than letting JSON.stringify drop
|
|
1901
|
+
// it — an omitted key reaches the prompt layer's `{milestone_version}`
|
|
1902
|
+
// placeholder as literal, un-substituted text.
|
|
1903
|
+
milestone_version: milestone['version'] ?? null,
|
|
1904
|
+
milestone_name: milestone['name'] ?? null,
|
|
1060
1905
|
milestone_slug: generateSlugInternal(milestone['name']),
|
|
1061
1906
|
phase_count: phaseCount,
|
|
1062
1907
|
completed_phases: completedPhases,
|
|
@@ -1100,7 +1945,7 @@ function cmdInitMapCodebase(cwd, raw) {
|
|
|
1100
1945
|
}
|
|
1101
1946
|
function cmdInitManager(cwd, raw) {
|
|
1102
1947
|
const config = loadConfig(cwd);
|
|
1103
|
-
const milestone =
|
|
1948
|
+
const milestone = milestoneRecord(cwd);
|
|
1104
1949
|
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
1105
1950
|
const paths = planningPaths(cwd);
|
|
1106
1951
|
if (!node_fs_1.default.existsSync(paths.roadmap)) {
|
|
@@ -1112,18 +1957,12 @@ function cmdInitManager(cwd, raw) {
|
|
|
1112
1957
|
const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8');
|
|
1113
1958
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
1114
1959
|
const phasesDir = paths.phases;
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
.map((e) => e.name);
|
|
1122
|
-
}
|
|
1123
|
-
catch {
|
|
1124
|
-
return [];
|
|
1125
|
-
}
|
|
1126
|
-
})();
|
|
1960
|
+
// #3185 (ADR-3180 Decision 1): "which phase directories belong to the
|
|
1961
|
+
// CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
|
|
1962
|
+
// routed through it instead of a hand-rolled readdirSync + a separate
|
|
1963
|
+
// getMilestonePhaseFilter window check (which also never excluded
|
|
1964
|
+
// sentinels, unlike the owner).
|
|
1965
|
+
const _phaseDirEntries = listMilestonePhaseDirs(phasesDir, { cwd }).value;
|
|
1127
1966
|
const _checkboxStates = new Map();
|
|
1128
1967
|
const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
|
|
1129
1968
|
let _cbMatch;
|
|
@@ -1158,16 +1997,24 @@ function cmdInitManager(cwd, raw) {
|
|
|
1158
1997
|
let isActive = false;
|
|
1159
1998
|
let completion = buildPhaseCompletionProjection(cwd, phaseNum, null, planCount, summaryCount, _slashRuntime);
|
|
1160
1999
|
try {
|
|
1161
|
-
|
|
1162
|
-
|
|
2000
|
+
// #3185 (ADR-3180 Decision 2) moved this lookup off the
|
|
2001
|
+
// milestone-scoped set and onto the physical one; that scope choice is
|
|
2002
|
+
// kept. Only the matcher is this PR's: matchPhaseDirs resolves
|
|
2003
|
+
// digit-leading directory names the token predicate cannot (#2528).
|
|
2004
|
+
const dirMatch = matchPhaseDirs(_phaseDirEntries, normalized).matches[0];
|
|
1163
2005
|
if (dirMatch) {
|
|
1164
2006
|
const fullDir = node_path_1.default.join(phasesDir, dirMatch);
|
|
1165
2007
|
const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, fullDir));
|
|
1166
2008
|
const phaseFiles = node_fs_1.default.readdirSync(fullDir);
|
|
1167
2009
|
planCount = listPhasePlanFiles(fullDir).length;
|
|
1168
2010
|
summaryCount = listPhaseSummaryFiles(fullDir).length;
|
|
1169
|
-
|
|
1170
|
-
|
|
2011
|
+
// #3511-class: scope the raw listing to THIS phase's own artifacts
|
|
2012
|
+
// before the hasContext/hasResearch predicates run, so a stray
|
|
2013
|
+
// cross-phase `-CONTEXT.md`/`-RESEARCH.md` sitting in this directory
|
|
2014
|
+
// cannot win this phase's lookup.
|
|
2015
|
+
const scopedFiles = scopeToPhase(phaseFiles, dirMatch);
|
|
2016
|
+
hasContext = findContextMdIn(scopedFiles) !== null;
|
|
2017
|
+
hasResearch = scopedFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
1171
2018
|
completion = buildPhaseCompletionProjection(cwd, phaseNum, phaseDirRel, planCount, summaryCount, _slashRuntime);
|
|
1172
2019
|
if (completion.phase_complete)
|
|
1173
2020
|
diskStatus = 'complete';
|
|
@@ -1183,7 +2030,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
1183
2030
|
diskStatus = 'discussed';
|
|
1184
2031
|
else
|
|
1185
2032
|
diskStatus = 'empty';
|
|
1186
|
-
const nowMs =
|
|
2033
|
+
const nowMs = clock_cjs_1.realClock.now();
|
|
1187
2034
|
let newestMtime = 0;
|
|
1188
2035
|
for (const f of phaseFiles) {
|
|
1189
2036
|
try {
|
|
@@ -1204,10 +2051,20 @@ function cmdInitManager(cwd, raw) {
|
|
|
1204
2051
|
catch {
|
|
1205
2052
|
/* intentionally empty */
|
|
1206
2053
|
}
|
|
2054
|
+
// ADR-3180 §7.4 (disk-strict, #2957, maintainer decision 2026-08-08):
|
|
2055
|
+
// `roadmapComplete` is reported below as metadata only — it carries NO
|
|
2056
|
+
// machine authority over `diskStatus`. The #3033 checkbox override that
|
|
2057
|
+
// used to live here (treating a zero-plan phase as complete whenever the
|
|
2058
|
+
// ROADMAP checkbox was ticked, layered on top of
|
|
2059
|
+
// buildPhaseCompletionProjection's own output) is DELETED, not
|
|
2060
|
+
// generalized: `diskStatus` now comes entirely from `completion`, which
|
|
2061
|
+
// already routes through the canonical owner (`isPhaseComplete`) and
|
|
2062
|
+
// itself resolves a zero-plan phase as complete whenever a passing
|
|
2063
|
+
// `*-VERIFICATION.md` exists (#3168) — with no dependency on the
|
|
2064
|
+
// checkbox. A zero-plan phase whose completion previously relied SOLELY
|
|
2065
|
+
// on a ticked checkbox (no passing verification) now reports incomplete;
|
|
2066
|
+
// this is the deliberate Tier-2 break (ADR-3180 §7.4 Decision 3).
|
|
1207
2067
|
const roadmapComplete = _checkboxStates.get(phaseNum) || false;
|
|
1208
|
-
if (roadmapComplete && completion.phase_complete && diskStatus !== 'complete') {
|
|
1209
|
-
diskStatus = 'complete';
|
|
1210
|
-
}
|
|
1211
2068
|
phases.push({
|
|
1212
2069
|
number: phaseNum,
|
|
1213
2070
|
name: phaseName,
|
|
@@ -1312,7 +2169,8 @@ function cmdInitManager(cwd, raw) {
|
|
|
1312
2169
|
for (const phase of phases) {
|
|
1313
2170
|
if (phase['disk_status'] === 'complete')
|
|
1314
2171
|
continue;
|
|
1315
|
-
|
|
2172
|
+
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
2173
|
+
if (isSentinelPhaseId(phase['number']))
|
|
1316
2174
|
continue;
|
|
1317
2175
|
if (phase['disk_status'] === 'executed') {
|
|
1318
2176
|
recommendedActions.push({
|
|
@@ -1366,7 +2224,8 @@ function cmdInitManager(cwd, raw) {
|
|
|
1366
2224
|
}
|
|
1367
2225
|
return true;
|
|
1368
2226
|
});
|
|
1369
|
-
|
|
2227
|
+
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
2228
|
+
const nonBacklogPhases = phases.filter((p) => !isSentinelPhaseId(p['number']));
|
|
1370
2229
|
const completedCount = nonBacklogPhases.filter((p) => p['phase_complete'] === true).length;
|
|
1371
2230
|
const sanitizeFlags = (rawVal) => {
|
|
1372
2231
|
const val = typeof rawVal === 'string' ? rawVal : '';
|
|
@@ -1389,8 +2248,12 @@ function cmdInitManager(cwd, raw) {
|
|
|
1389
2248
|
execute: sanitizeFlags(mgrFlags?.['execute']),
|
|
1390
2249
|
};
|
|
1391
2250
|
const result = {
|
|
1392
|
-
|
|
1393
|
-
|
|
2251
|
+
// #3216 review Finding 2: `?? null` so an unresolved milestone still emits
|
|
2252
|
+
// the key with an explicit `null` rather than letting JSON.stringify drop
|
|
2253
|
+
// it — an omitted key reaches the prompt layer's `{milestone_version}`
|
|
2254
|
+
// placeholder as literal, un-substituted text.
|
|
2255
|
+
milestone_version: milestone['version'] ?? null,
|
|
2256
|
+
milestone_name: milestone['name'] ?? null,
|
|
1394
2257
|
phases,
|
|
1395
2258
|
phase_count: phases.length,
|
|
1396
2259
|
completed_count: completedCount,
|
|
@@ -1405,7 +2268,224 @@ function cmdInitManager(cwd, raw) {
|
|
|
1405
2268
|
};
|
|
1406
2269
|
output(withProjectRoot(cwd, result), raw);
|
|
1407
2270
|
}
|
|
1408
|
-
|
|
2271
|
+
/**
|
|
2272
|
+
* `complete-milestone.md`'s dedicated init entry point (#2994, epic #1671
|
|
2273
|
+
* Phase 6.3). Additive alongside the workflow's existing `init.manager`
|
|
2274
|
+
* (readiness/phase-projection, `cmdInitManager` above — CRITICAL blast
|
|
2275
|
+
* radius, never modified) and `init.execute-phase` (branching-strategy
|
|
2276
|
+
* fields) calls; `cmdInitCompleteMilestone` carries NO phase-listing logic
|
|
2277
|
+
* of its own to delegate — its only job is the `git.create_tag` config-gate
|
|
2278
|
+
* fact the `git_tag` step's `<config-check>` sub-tag used to re-derive
|
|
2279
|
+
* inline (gating the step's own inclusion on a fact only that step
|
|
2280
|
+
* computed), now hoisted here and exposed as `git_create_tag`, plus the
|
|
2281
|
+
* `section_manifest` field neither `init.manager` nor `init.execute-phase`
|
|
2282
|
+
* carries.
|
|
2283
|
+
*/
|
|
2284
|
+
function cmdInitCompleteMilestone(cwd, raw, options = {}) {
|
|
2285
|
+
const gitCreateTag = detectGitCreateTag(cwd);
|
|
2286
|
+
const result = {
|
|
2287
|
+
// #2994: hoisted from complete-milestone.md's git_tag step
|
|
2288
|
+
// <config-check> resolver (git.create_tag, fail-open default true).
|
|
2289
|
+
git_create_tag: gitCreateTag,
|
|
2290
|
+
};
|
|
2291
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'complete-milestone', {
|
|
2292
|
+
gitCreateTag,
|
|
2293
|
+
});
|
|
2294
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* `autonomous.md`'s dedicated init entry point (#2994, epic #1671 Phase
|
|
2298
|
+
* 6.3). Additive alongside the workflow's existing `init.milestone-op`
|
|
2299
|
+
* (`cmdInitMilestoneOp`), `init.manager` (`cmdInitManager`), and
|
|
2300
|
+
* `init.phase-op` (`cmdInitPhaseOp`) calls — all three are CRITICAL blast
|
|
2301
|
+
* radius (179 dependents across 24 processes) and are never modified for
|
|
2302
|
+
* this; `autonomous.md` keeps every one of those calls exactly as it had
|
|
2303
|
+
* them. `cmdInitAutonomous` carries NO phase-listing logic of its own to
|
|
2304
|
+
* delegate — its only job is the `PLAN_STRATEGY` disjunction the workflow's
|
|
2305
|
+
* own bash resolver (`PLAN_STRATEGY="converge"` on `--converge` OR
|
|
2306
|
+
* `--cross-ai`) already computes at the top of the `initialize` step, now
|
|
2307
|
+
* mirrored here as a single boolean FACT (same discipline as
|
|
2308
|
+
* `state:chunked-mode`/`state:ui-phase-active`: the disjunction is resolved
|
|
2309
|
+
* ONCE, in fact computation, never in the `when=` grammar), exposed as
|
|
2310
|
+
* `plan_strategy_converge`, plus the `section_manifest` field none of the
|
|
2311
|
+
* three existing calls carries.
|
|
2312
|
+
*/
|
|
2313
|
+
function cmdInitAutonomous(cwd, raw, options = {}) {
|
|
2314
|
+
const planStrategyConverge = options['converge'] === true || options['cross-ai'] === true;
|
|
2315
|
+
const result = {
|
|
2316
|
+
// #2994: mirrors autonomous.md's own PLAN_STRATEGY resolver
|
|
2317
|
+
// (--converge OR its documented alias --cross-ai).
|
|
2318
|
+
plan_strategy_converge: planStrategyConverge,
|
|
2319
|
+
};
|
|
2320
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'autonomous', {
|
|
2321
|
+
planStrategyConverge,
|
|
2322
|
+
});
|
|
2323
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* `docs-update.md`'s dedicated init entry point (#2994, epic #1671 Phase
|
|
2327
|
+
* 6.3 — final slice). `docs-update.md` previously carried NO `gsd_run query
|
|
2328
|
+
* init.*` call at all: its own `docs-init` command (`cmdDocsInit`,
|
|
2329
|
+
* src/docs.cts) is a SEPARATE, pre-existing entry point outside this
|
|
2330
|
+
* `init.*` family and is left untouched here. This function's only job is
|
|
2331
|
+
* the `section_manifest` field neither `docs-init` nor any other call
|
|
2332
|
+
* carries, gating `docs-update.md`'s `dispatch-monorepo-packages` section.
|
|
2333
|
+
*
|
|
2334
|
+
* `state:is-monorepo` ground truth: the project's monorepo workspaces list
|
|
2335
|
+
* is non-empty — reuses `detectMonorepoWorkspaces` (src/docs.cts, exported
|
|
2336
|
+
* for this purpose) rather than a second, divergence-prone workspace-glob
|
|
2337
|
+
* scan (DEFECT.GENERATIVE-FIX dual surface); this is the SAME detector that
|
|
2338
|
+
* already backs `docs-init`'s own `monorepo_workspaces` field.
|
|
2339
|
+
*/
|
|
2340
|
+
function cmdInitDocsUpdate(cwd, raw, options = {}) {
|
|
2341
|
+
const isMonorepo = detectMonorepoWorkspaces(cwd).length > 0;
|
|
2342
|
+
const result = {};
|
|
2343
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'docs-update', {
|
|
2344
|
+
isMonorepo,
|
|
2345
|
+
});
|
|
2346
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* `update.md`'s dedicated init entry point (#2994, epic #1671 Phase 6.3 —
|
|
2350
|
+
* final slice). `update.md` previously carried NO `gsd_run query init.*`
|
|
2351
|
+
* call at all: it resolves `gsd-tools.cjs` itself (its own bespoke
|
|
2352
|
+
* `PREFERRED_CONFIG_DIR`/`PREFERRED_RUNTIME`-aware `$GSD_TOOLS` cascade,
|
|
2353
|
+
* `update.md` ~lines 13-45) because the update workflow must run BEFORE any
|
|
2354
|
+
* install can be assumed resolvable — the canonical launcher preamble's
|
|
2355
|
+
* fixed candidate list is not a substitute for that cascade, and both
|
|
2356
|
+
* resolutions assign the identical `$GSD_TOOLS` shell variable, so copying
|
|
2357
|
+
* the canonical preamble in ADDITION to the existing cascade would silently
|
|
2358
|
+
* clobber the value `backup_custom_files`/`restore_custom_files` (later
|
|
2359
|
+
* steps) still depend on. This function is invoked via that ALREADY
|
|
2360
|
+
* resolved `$GSD_TOOLS`, not a redundant `gsd_run()` shell function.
|
|
2361
|
+
*
|
|
2362
|
+
* `state:next-channel` ground truth: `--next` OR its documented alias
|
|
2363
|
+
* `--rc` (same disjunction-to-one-boolean discipline as
|
|
2364
|
+
* `state:chunked-mode`/`state:plan-strategy-converge`). This DELIBERATELY
|
|
2365
|
+
* does not replace `update.md`'s own `parse_update_channel` case-statement
|
|
2366
|
+
* (`TAG="next"`/`TAG="latest"`) — issue #815's regression test
|
|
2367
|
+
* (`tests/update-workflow.test.cjs`) asserts that literal
|
|
2368
|
+
* case-statement text stays in `update.md` verbatim (the npm dist-tag
|
|
2369
|
+
* selection has to run in the workflow's own shell before any `gsd_run`
|
|
2370
|
+
* round-trip), so `next_channel` exists purely to gate the `channel-banner`
|
|
2371
|
+
* section's admission — a parallel, consistent-but-not-replacing
|
|
2372
|
+
* resolution of the same flags.
|
|
2373
|
+
*/
|
|
2374
|
+
function cmdInitUpdate(cwd, raw, options = {}) {
|
|
2375
|
+
const nextChannel = options['next'] === true || options['rc'] === true;
|
|
2376
|
+
const result = {
|
|
2377
|
+
next_channel: nextChannel,
|
|
2378
|
+
};
|
|
2379
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'update', {
|
|
2380
|
+
nextChannel,
|
|
2381
|
+
});
|
|
2382
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* `transition.md`'s dedicated init entry point (#2994, epic #1671 Phase
|
|
2386
|
+
* 6.3 — final slice). `transition.md` is an internal workflow (no
|
|
2387
|
+
* user-facing `/gsd-transition` command) that previously carried NO
|
|
2388
|
+
* `gsd_run query init.*` call at all; it already establishes `gsd_run()`
|
|
2389
|
+
* via the canonical launcher preamble in its `update_roadmap_and_state`
|
|
2390
|
+
* step (before this call's insertion point in `offer_next_phase`), so no
|
|
2391
|
+
* second preamble copy is needed in the host file.
|
|
2392
|
+
*
|
|
2393
|
+
* `state:workstream-active` ground truth: a workstream is active — resolved
|
|
2394
|
+
* via `GSD_WORKSTREAM` env, falling back to the stored active-workstream
|
|
2395
|
+
* pointer (mirrors `cmdInitProgress`'s own `_resolvedWorkstream` resolution
|
|
2396
|
+
* above, the established authoritative source for "is a workstream active"
|
|
2397
|
+
* in this file).
|
|
2398
|
+
*
|
|
2399
|
+
* `other_active_workstreams` hoists the resolver-in-body hazard out of
|
|
2400
|
+
* `transition.md`'s `workstream-collision-check` step: that step's body
|
|
2401
|
+
* previously re-derived this via an inline `gsd_run query workstream.list
|
|
2402
|
+
* --raw` call gated on the identical `if [ -n "$GSD_WORKSTREAM" ]`
|
|
2403
|
+
* condition that now backs this section's OWN admission — resolving it here
|
|
2404
|
+
* instead reuses `getOtherActiveWorkstreamInventories` (src/workstream-
|
|
2405
|
+
* inventory.cts), the SAME primitive `workstream.list` itself calls
|
|
2406
|
+
* (`cmdWorkstreamList`, src/workstream.cts), pre-filtered exactly as the
|
|
2407
|
+
* step's own prose described (excludes the current workstream and any
|
|
2408
|
+
* workstream whose status contains "milestone complete" or "archived",
|
|
2409
|
+
* case-insensitively — `isCompletedInventory`), so the step body becomes a
|
|
2410
|
+
* pure JSON consumer with no `gsd_run` call of its own.
|
|
2411
|
+
*/
|
|
2412
|
+
function cmdInitTransition(cwd, raw, options = {}) {
|
|
2413
|
+
// #3579 root-cause fix: read-only informational field — peek, don't
|
|
2414
|
+
// self-heal (see cmdInitNewMilestone's identical rationale above).
|
|
2415
|
+
const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
|
|
2416
|
+
const workstreamActive = !!resolvedWorkstream;
|
|
2417
|
+
const result = {
|
|
2418
|
+
other_active_workstreams: workstreamActive
|
|
2419
|
+
? getOtherActiveWorkstreamInventories(cwd, resolvedWorkstream).map((inv) => ({
|
|
2420
|
+
name: inv.name,
|
|
2421
|
+
status: inv.status,
|
|
2422
|
+
}))
|
|
2423
|
+
: [],
|
|
2424
|
+
};
|
|
2425
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'transition', {
|
|
2426
|
+
workstreamActive,
|
|
2427
|
+
});
|
|
2428
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* `debug.md`'s dedicated init entry point (#3149; prerequisite for #3128).
|
|
2432
|
+
* `debug.md` previously carried NO `gsd_run query init.*` call at all — it made
|
|
2433
|
+
* THREE separate round-trips instead: `state.load` (for `commit_docs`,
|
|
2434
|
+
* `config.response_language` and `debug_dir`), `resolve-model gsd-debugger
|
|
2435
|
+
* --pick model`, and `config-get workflow.tdd_mode --raw`. Because no
|
|
2436
|
+
* debug-scoped fact was computed at any entry point, a `when=` atom naming one
|
|
2437
|
+
* would have evaluated FALSE forever — ADR-1671's admission gate (2) and the
|
|
2438
|
+
* silent-exclusion bug it exists to prevent (`docs/adr/1671-…:122-131`).
|
|
2439
|
+
*
|
|
2440
|
+
* Every field is resolved through the SAME primitive the call it replaces used,
|
|
2441
|
+
* never a second hand-maintained copy (DEFECT.GENERATIVE-FIX):
|
|
2442
|
+
*
|
|
2443
|
+
* - `commit_docs` — `loadConfig`, the same loader `cmdStateLoad` calls.
|
|
2444
|
+
* - `response_language` — NOT read here: `withProjectRoot` already injects it
|
|
2445
|
+
* when configured (#2402), which is also the shape sibling init bundles use.
|
|
2446
|
+
* It is absent, not null, when unset.
|
|
2447
|
+
* - `debug_dir` — `planningPaths(cwd).debug`, the SAME expression `cmdStateLoad`
|
|
2448
|
+
* now uses; the `debug` field was added to `PlanningPaths` (#3149) so the
|
|
2449
|
+
* location has one source instead of two kept in sync by hand.
|
|
2450
|
+
* - `debugger_model` — `resolveModelInternal`, which IS what `query
|
|
2451
|
+
* resolve-model --pick model` returns (`cmdResolveModel`, src/commands.cts).
|
|
2452
|
+
* - `tdd_mode` — the `Boolean(wf['tdd_mode'])` idiom `cmdInitExecutePhase` and
|
|
2453
|
+
* `cmdInitPlanPhase` already use. `/gsd:debug` has no `--tdd` flag, so the
|
|
2454
|
+
* sibling handlers' `options['tdd'] ||` disjunct is deliberately omitted
|
|
2455
|
+
* rather than carried as a phantom.
|
|
2456
|
+
*
|
|
2457
|
+
* `state.load` is deliberately NOT narrowed — see the note beside its own
|
|
2458
|
+
* `debug_dir` field. This handler is purely additive alongside it.
|
|
2459
|
+
*
|
|
2460
|
+
* `diagnose` is the one flag `/gsd:debug` already documents. Exposing it as a
|
|
2461
|
+
* top-level fact follows `cmdInitUpdate`'s `next_channel` and
|
|
2462
|
+
* `cmdInitAutonomous`'s `plan_strategy_converge` precedent, and is what makes
|
|
2463
|
+
* the router's flag forwarding observable. No `when=` atom consumes it yet:
|
|
2464
|
+
* admission gate (1) — a consuming section of at least 400 bytes — is #3128's
|
|
2465
|
+
* to satisfy, and shipping the atom before its section is the same
|
|
2466
|
+
* silent-exclusion bug from the other direction.
|
|
2467
|
+
*/
|
|
2468
|
+
function cmdInitDebug(cwd, raw, options = {}) {
|
|
2469
|
+
const config = loadConfig(cwd);
|
|
2470
|
+
const wf = (config.workflow ?? {});
|
|
2471
|
+
const result = {
|
|
2472
|
+
commit_docs: config.commit_docs,
|
|
2473
|
+
// #2376: absolute — debug.md builds `debug_file_path` as
|
|
2474
|
+
// `{debug_dir}/{slug}.md` for its gsd-debug-session-manager spawns, whose
|
|
2475
|
+
// own cwd may differ from the orchestrator's.
|
|
2476
|
+
debug_dir: toPosixPath(planningPaths(cwd).debug),
|
|
2477
|
+
debugger_model: resolveModelInternal(cwd, 'gsd-debugger'),
|
|
2478
|
+
tdd_mode: Boolean(wf['tdd_mode']),
|
|
2479
|
+
diagnose: options['diagnose'] === true,
|
|
2480
|
+
};
|
|
2481
|
+
// Additive, optional field — degrades to null while `debug` has no key in
|
|
2482
|
+
// `gsd-core/workflows/section-manifest.json` (it has no `gsd:section` markers
|
|
2483
|
+
// until #3128). null means "read everything", which is NOT the same as a
|
|
2484
|
+
// computed empty selection.
|
|
2485
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'debug', {});
|
|
2486
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2487
|
+
}
|
|
2488
|
+
function cmdInitProgress(cwd, raw, options = {}) {
|
|
1409
2489
|
try {
|
|
1410
2490
|
pruneOrphanedWorktrees(cwd);
|
|
1411
2491
|
}
|
|
@@ -1413,7 +2493,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1413
2493
|
/* intentionally empty */
|
|
1414
2494
|
}
|
|
1415
2495
|
const config = loadConfig(cwd);
|
|
1416
|
-
const milestone =
|
|
2496
|
+
const milestone = milestoneRecord(cwd);
|
|
1417
2497
|
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
1418
2498
|
// #1912: fail safe in workstream mode with no active workstream. With no active
|
|
1419
2499
|
// workstream and no --ws, planningDir(cwd) resolves to root .planning — silently
|
|
@@ -1421,11 +2501,27 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1421
2501
|
// Mirror planningDir's resolution (GSD_WORKSTREAM env > stored active pointer) so
|
|
1422
2502
|
// an explicit --ws (which sets GSD_WORKSTREAM) satisfies the check.
|
|
1423
2503
|
const _availableWorkstreams = listAvailableWorkstreams(cwd);
|
|
1424
|
-
|
|
2504
|
+
// #3579 root-cause fix: this is a check, not a consuming read — use the
|
|
2505
|
+
// non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
|
|
2506
|
+
// here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
|
|
2507
|
+
// which would misreport a present-but-bad marker as no marker at all.
|
|
2508
|
+
const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
|
|
1425
2509
|
if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
|
|
2510
|
+
// #3579: getActiveWorkstream now inherits a pointer-less session's read
|
|
2511
|
+
// from the shared .planning/active-workstream marker, so reaching this
|
|
2512
|
+
// branch with a marker actually present means the marker EXISTED but
|
|
2513
|
+
// didn't resolve (invalid name, or its workstream dir is gone) — a
|
|
2514
|
+
// materially different situation from "nothing was ever set" and one
|
|
2515
|
+
// that deserves its own diagnostic instead of the generic message below.
|
|
2516
|
+
const _diagnosis = diagnoseUnresolvedActiveWorkstream(cwd);
|
|
2517
|
+
if (_diagnosis.present) {
|
|
2518
|
+
error(`init.progress requires a workstream in workstream mode — the active-workstream marker names '${_diagnosis.value}', but it did not resolve: ${describeUnresolvedWorkstreamReason(_diagnosis.reason)}. Root STATE.md (likely stale) would be reported otherwise. ` +
|
|
2519
|
+
`Pass --ws <name> or run ${(0, runtime_slash_cjs_1.formatGsdSlash)('workstream set', _slashRuntime)} to point it at an existing workstream. ` +
|
|
2520
|
+
`Available workstreams: ${_availableWorkstreams.join(', ')}`, ERROR_REASON.WORKSTREAM_MODE_MARKER_UNRESOLVED, { marker_value: _diagnosis.value, marker_reason: _diagnosis.reason });
|
|
2521
|
+
}
|
|
1426
2522
|
error(`init.progress requires a workstream in workstream mode — no active workstream is set, so root STATE.md (likely stale) would be reported. ` +
|
|
1427
2523
|
`Pass --ws <name> or run ${(0, runtime_slash_cjs_1.formatGsdSlash)('workstream set', _slashRuntime)} first. ` +
|
|
1428
|
-
`Available workstreams: ${_availableWorkstreams.join(', ')}
|
|
2524
|
+
`Available workstreams: ${_availableWorkstreams.join(', ')}`, ERROR_REASON.WORKSTREAM_MODE_NONE_ACTIVE);
|
|
1429
2525
|
}
|
|
1430
2526
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
1431
2527
|
const phases = [];
|
|
@@ -1452,21 +2548,15 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1452
2548
|
catch {
|
|
1453
2549
|
/* intentionally empty */
|
|
1454
2550
|
}
|
|
1455
|
-
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
1456
2551
|
const seenPhaseNums = new Set();
|
|
1457
2552
|
try {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
const pb = b.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
|
|
1466
|
-
if (!pa || !pb)
|
|
1467
|
-
return a.localeCompare(b);
|
|
1468
|
-
return parseInt(pa[1], 10) - parseInt(pb[1], 10);
|
|
1469
|
-
});
|
|
2553
|
+
// #3185 (ADR-3180 Decision 1): "which phase directories belong to the
|
|
2554
|
+
// CURRENT milestone" — routed through the canonical owner instead of a
|
|
2555
|
+
// hand-rolled readdirSync + isDirInMilestone filter + local sort (which
|
|
2556
|
+
// also never excluded sentinels, unlike the owner; the final `phases`
|
|
2557
|
+
// array is re-sorted below anyway, so dropping the local sort here is
|
|
2558
|
+
// behavior-preserving).
|
|
2559
|
+
const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
|
|
1470
2560
|
for (const dir of dirs) {
|
|
1471
2561
|
const dirMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i'));
|
|
1472
2562
|
const phaseNumber = dirMatch ? dirMatch[1] : dir;
|
|
@@ -1476,7 +2566,12 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1476
2566
|
const phaseFiles = node_fs_1.default.readdirSync(phasePath);
|
|
1477
2567
|
const plans = listPhasePlanFiles(phasePath);
|
|
1478
2568
|
const summaries = listPhaseSummaryFiles(phasePath);
|
|
1479
|
-
|
|
2569
|
+
// #3511-class: scope the raw listing to THIS phase's own artifacts
|
|
2570
|
+
// before the hasResearch predicate runs, so a stray cross-phase
|
|
2571
|
+
// `-RESEARCH.md` sitting in this directory cannot win this phase's
|
|
2572
|
+
// lookup.
|
|
2573
|
+
const scopedPhaseFiles = scopeToPhase(phaseFiles, dir);
|
|
2574
|
+
const hasResearch = scopedPhaseFiles.some((f) => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
1480
2575
|
const phaseDirRel = toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'phases', dir)));
|
|
1481
2576
|
const completion = buildPhaseCompletionProjection(cwd, phaseNumber, phaseDirRel, plans.length, summaries.length, _slashRuntime);
|
|
1482
2577
|
const status = completion.phase_complete
|
|
@@ -1538,6 +2633,25 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1538
2633
|
}
|
|
1539
2634
|
}
|
|
1540
2635
|
phases.sort((a, b) => parseInt(a['number'], 10) - parseInt(b['number'], 10));
|
|
2636
|
+
// #3581: the frontier is ROADMAP ORDER, not artifact presence. The disk loop
|
|
2637
|
+
// above could claim nextPhase from a stray out-of-order artifact directory
|
|
2638
|
+
// (a phase-9 UAT evidence dir while roadmap phase 8 was pending and
|
|
2639
|
+
// unscaffolded), silently skipping 8 — and init.progress then disagreed with
|
|
2640
|
+
// roadmap.analyze on the same tree. Re-derive from the sorted union: the
|
|
2641
|
+
// first phase that has not begun ('pending' | 'not_started') and is not
|
|
2642
|
+
// roadmap-complete wins; artifacts still feed each entry's status and
|
|
2643
|
+
// completion (corroborating evidence) but no longer outrank the ordering.
|
|
2644
|
+
// Aligned trees derive the identical frontier as the loops above; an
|
|
2645
|
+
// all-complete milestone finds none and keeps nextPhase null for the
|
|
2646
|
+
// completion flow.
|
|
2647
|
+
{
|
|
2648
|
+
const frontier = phases.find((p) => {
|
|
2649
|
+
const st = p['status'];
|
|
2650
|
+
return (st === 'pending' || st === 'not_started') && p['roadmap_complete'] !== true;
|
|
2651
|
+
});
|
|
2652
|
+
if (frontier)
|
|
2653
|
+
nextPhase = frontier;
|
|
2654
|
+
}
|
|
1541
2655
|
let pausedAt = null;
|
|
1542
2656
|
const state = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'STATE.md'));
|
|
1543
2657
|
if (state !== null) {
|
|
@@ -1545,12 +2659,29 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1545
2659
|
if (pauseMatch)
|
|
1546
2660
|
pausedAt = pauseMatch[1].trim();
|
|
1547
2661
|
}
|
|
2662
|
+
// #2994: the CURRENT phase's number, used both to expose `phase_mvp_mode`
|
|
2663
|
+
// at the top level (so the `mvp-display` step body can consume an
|
|
2664
|
+
// already-resolved fact instead of re-invoking `gsd_run query
|
|
2665
|
+
// phase.mvp-mode` itself — that inline resolver would otherwise gate a
|
|
2666
|
+
// section on a fact the section's own body recomputes, which is circular
|
|
2667
|
+
// and self-disabling) and to thread a real `phase_number` into
|
|
2668
|
+
// `buildSectionManifestField` below so `state:phase-mvp-mode` is genuinely
|
|
2669
|
+
// computed for this workflow rather than permanently false (the previous
|
|
2670
|
+
// `buildSectionManifestField(cwd, null, ...)` call passed no phase info at
|
|
2671
|
+
// all, so `detectPhaseMvpMode` always short-circuited on the `!phaseNumber`
|
|
2672
|
+
// guard).
|
|
2673
|
+
const currentPhaseNumber = currentPhase?.['number'] ?? null;
|
|
2674
|
+
const phaseMvpMode = detectPhaseMvpMode(cwd, currentPhaseNumber);
|
|
1548
2675
|
const result = {
|
|
1549
2676
|
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
|
|
1550
2677
|
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
|
|
1551
2678
|
commit_docs: config.commit_docs,
|
|
1552
|
-
|
|
1553
|
-
|
|
2679
|
+
// #3216 review Finding 2: `?? null` so an unresolved milestone still emits
|
|
2680
|
+
// the key with an explicit `null` rather than letting JSON.stringify drop
|
|
2681
|
+
// it — an omitted key reaches the prompt layer's `{milestone_version}`
|
|
2682
|
+
// placeholder as literal, un-substituted text.
|
|
2683
|
+
milestone_version: milestone['version'] ?? null,
|
|
2684
|
+
milestone_name: milestone['name'] ?? null,
|
|
1554
2685
|
phases,
|
|
1555
2686
|
phase_count: phases.length,
|
|
1556
2687
|
completed_count: phases.filter((p) => p['status'] === 'complete').length,
|
|
@@ -1559,6 +2690,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1559
2690
|
next_phase: nextPhase,
|
|
1560
2691
|
paused_at: pausedAt,
|
|
1561
2692
|
has_work_in_progress: !!currentPhase,
|
|
2693
|
+
phase_mvp_mode: phaseMvpMode,
|
|
1562
2694
|
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
|
1563
2695
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1564
2696
|
state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
@@ -1568,6 +2700,8 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1568
2700
|
project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
|
|
1569
2701
|
config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
1570
2702
|
};
|
|
2703
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
2704
|
+
result['section_manifest'] = buildSectionManifestField(cwd, currentPhaseNumber ? { phase_number: currentPhaseNumber } : null, options, 'progress');
|
|
1571
2705
|
output(withProjectRoot(cwd, result), raw);
|
|
1572
2706
|
}
|
|
1573
2707
|
function detectChildRepos(dir) {
|
|
@@ -1822,7 +2956,20 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
|
|
|
1822
2956
|
}
|
|
1823
2957
|
const skillMdPath = node_path_1.default.join(projectRoot, skillPath, 'SKILL.md');
|
|
1824
2958
|
if (!node_fs_1.default.existsSync(skillMdPath)) {
|
|
1825
|
-
|
|
2959
|
+
// #2941: if the bare name matches a global skill, hint at the global: prefix.
|
|
2960
|
+
// The bare name resolves as project-relative (which doesn't exist), but the
|
|
2961
|
+
// user likely meant to reference a global skill. getGlobalSkillDir is already
|
|
2962
|
+
// imported for the global: branch above; guard on globalSkillsBase being non-null
|
|
2963
|
+
// since runtimes without a skills directory don't support the prefix.
|
|
2964
|
+
let hint = '';
|
|
2965
|
+
if (globalSkillsBase !== null) {
|
|
2966
|
+
const baseName = node_path_1.default.basename(skillPath);
|
|
2967
|
+
const globalDir = (0, runtime_homes_cjs_1.getGlobalSkillDir)(runtime, baseName);
|
|
2968
|
+
if (globalDir && node_fs_1.default.existsSync(node_path_1.default.join(globalDir, 'SKILL.md'))) {
|
|
2969
|
+
hint = ` — a global skill named "${baseName}" exists; use "global:${baseName}" to reference it`;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
warn(`[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md"${hint} — skipping\n`);
|
|
1826
2973
|
continue;
|
|
1827
2974
|
}
|
|
1828
2975
|
validEntries.push({ kind: 'include', ref: `${skillPath}/SKILL.md`, display: skillPath });
|
|
@@ -1866,7 +3013,7 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
|
|
|
1866
3013
|
if (!block) {
|
|
1867
3014
|
const runtime = (config && config['runtime']) || process.env['GSD_RUNTIME'] || 'claude';
|
|
1868
3015
|
if (runtime !== 'claude') {
|
|
1869
|
-
const agentCheck = checkAgentsInstalled(runtime);
|
|
3016
|
+
const agentCheck = checkAgentsInstalled(runtime, projectRoot);
|
|
1870
3017
|
const agentsDir = agentCheck?.agents_dir;
|
|
1871
3018
|
if (typeof agentsDir === 'string' && agentsDir.length > 0) {
|
|
1872
3019
|
const agentFile = node_path_1.default.join(agentsDir, `${agentType}.md`);
|
|
@@ -2088,7 +3235,7 @@ function buildSkillManifest(cwd, skillsDir = null) {
|
|
|
2088
3235
|
seenNamesInRoot.add(name);
|
|
2089
3236
|
const description = frontmatter['description'] || '';
|
|
2090
3237
|
const triggers = [];
|
|
2091
|
-
const bodyMatch = content.match(/^---[\s\S]*?---\s*\n([\s\S]*)$/);
|
|
3238
|
+
const bodyMatch = content.match(/^---[\s\S]*?---\s*\r?\n([\s\S]*)$/);
|
|
2092
3239
|
if (bodyMatch) {
|
|
2093
3240
|
const body = bodyMatch[1];
|
|
2094
3241
|
const triggerLines = body.match(/^TRIGGER\s+when:\s*(.+)$/gmi);
|
|
@@ -2198,11 +3345,20 @@ module.exports = {
|
|
|
2198
3345
|
cmdInitResume,
|
|
2199
3346
|
cmdInitVerifyWork,
|
|
2200
3347
|
cmdInitPhaseOp,
|
|
3348
|
+
cmdInitCodeReview,
|
|
3349
|
+
cmdInitReview,
|
|
3350
|
+
cmdInitDiscussPhaseAssumptions,
|
|
2201
3351
|
cmdInitTodos,
|
|
2202
3352
|
cmdInitMilestoneOp,
|
|
2203
3353
|
cmdInitMapCodebase,
|
|
2204
3354
|
cmdInitProgress,
|
|
2205
3355
|
cmdInitManager,
|
|
3356
|
+
cmdInitCompleteMilestone,
|
|
3357
|
+
cmdInitAutonomous,
|
|
3358
|
+
cmdInitDocsUpdate,
|
|
3359
|
+
cmdInitUpdate,
|
|
3360
|
+
cmdInitTransition,
|
|
3361
|
+
cmdInitDebug,
|
|
2206
3362
|
cmdInitNewWorkspace,
|
|
2207
3363
|
cmdInitListWorkspaces,
|
|
2208
3364
|
cmdInitRemoveWorkspace,
|