@opengsd/gsd-core 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +459 -0
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +360 -0
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +848 -163
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +8 -3
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/review.md +2 -3
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +672 -146
- package/gsd-core/bin/lib/adr-parser.cjs +4 -2
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +119 -34
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +96 -189
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -2
- package/gsd-core/bin/lib/check-command-router.cjs +213 -49
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +823 -112
- package/gsd-core/bin/lib/config-loader.cjs +66 -4
- package/gsd-core/bin/lib/config.cjs +186 -45
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +164 -45
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +614 -86
- package/gsd-core/bin/lib/install-engine.cjs +29 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +37 -13
- package/gsd-core/bin/lib/model-resolver.cjs +253 -53
- package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +268 -27
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +393 -88
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +356 -42
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +181 -30
- package/gsd-core/bin/lib/state.cjs +265 -27
- package/gsd-core/bin/lib/surface.cjs +77 -3
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +9 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +315 -30
- package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
- package/gsd-core/bin/lib/verify.cjs +320 -48
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
- package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +13 -4
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +14 -2
- package/gsd-core/references/tdd.md +30 -4
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +433 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +29 -16
- package/gsd-core/workflows/check-todos.md +6 -4
- package/gsd-core/workflows/cleanup.md +5 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +216 -73
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +41 -264
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +17 -158
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +83 -172
- package/gsd-core/workflows/execute-plan.md +24 -10
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +52 -5
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +56 -14
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +39 -209
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +45 -187
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +132 -20
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +29 -10
- package/gsd-core/workflows/reapply-patches.md +86 -6
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +22 -9
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +14 -12
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +101 -196
- package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +71 -19
- package/hooks/dist/gsd-statusline.js +81 -20
- package/hooks/dist/gsd-validate-commit.sh +97 -8
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +66 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +71 -19
- package/hooks/gsd-statusline.js +81 -20
- package/hooks/gsd-validate-commit.sh +97 -8
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +11 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +131 -12
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +35 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +623 -13
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +29 -9
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
- package/scripts/prompt-injection-scan.sh +18 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +8 -3
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review/SKILL.md +2 -3
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -15,6 +15,7 @@ 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
17
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
18
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
18
19
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
|
|
19
20
|
const io = require("./io.cjs");
|
|
20
21
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
|
|
@@ -40,6 +41,8 @@ const { SCOPE } = planningScopeMod;
|
|
|
40
41
|
const secrets_cjs_1 = require("./secrets.cjs");
|
|
41
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
42
43
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-document.cjs is an export= CommonJS module
|
|
45
|
+
const planDocument = require("./plan-document.cjs");
|
|
43
46
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
44
47
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
45
48
|
const host_runtime_detection_cjs_1 = require("./host-runtime-detection.cjs");
|
|
@@ -86,9 +89,9 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
|
|
|
86
89
|
const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
|
|
87
90
|
const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
|
|
88
91
|
const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
|
|
89
|
-
const { comparePhaseNum, normalizePhaseName,
|
|
92
|
+
const { comparePhaseNum, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
|
|
90
93
|
const { pruneOrphanedWorktrees } = worktreeSafety;
|
|
91
|
-
const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
|
|
94
|
+
const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, resolveEnvWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, resolvePhaseIdConvention, } = planningWorkspace;
|
|
92
95
|
const { determinePhaseStatus } = commandsMod;
|
|
93
96
|
const { extractFrontmatter } = frontmatterMod;
|
|
94
97
|
const { isPhaseComplete, resolveVerificationFile, resolveUatFile } = verificationMod;
|
|
@@ -99,7 +102,6 @@ const { resolveCapabilityRuntimeState } = capabilityStateMod;
|
|
|
99
102
|
// Unused but imported for structural parity
|
|
100
103
|
void stripShippedMilestones;
|
|
101
104
|
// Accept all bold/colon variants of the Requirements header (#2769)
|
|
102
|
-
const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
|
|
103
105
|
// #2056/#2104: isForeignPrefixedPhaseQuery is imported from phase-id.cts
|
|
104
106
|
// (the canonical predicate). parsePhasePrefix is no longer needed locally.
|
|
105
107
|
// phaseInfoMatchesExactPrefix and roadmapPhaseMatchesExactPrefix are local
|
|
@@ -109,9 +111,18 @@ function phaseInfoMatchesExactPrefix(phaseInfo, phase) {
|
|
|
109
111
|
const numStr = typeof num === 'string' ? num : (typeof num === 'number' ? String(num) : '');
|
|
110
112
|
return numStr.toUpperCase() === phase.toUpperCase();
|
|
111
113
|
}
|
|
114
|
+
// #4906 Phase 5 (#4984): NOT migrated onto the heading-baseline selector —
|
|
115
|
+
// this site was never one of the five ADR-4910 §8 call sites this phase owns
|
|
116
|
+
// (init milestone/progress heading scans + milestone.cts's unstarted-phase
|
|
117
|
+
// guard, all via buildPhaseHeadingScanRegex), and adding a NEW direct
|
|
118
|
+
// any-bracket selector consumer here inflated init.cts's
|
|
119
|
+
// tests/adr-612-bracket-heading-selection.test.cjs census past its pinned
|
|
120
|
+
// count. Left bracket-blind deliberately, matching this call's pre-#4984
|
|
121
|
+
// behavior; folding it into the census is Phase 6 work.
|
|
112
122
|
function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
|
|
113
123
|
const sectionRaw = roadmapPhase?.['section'];
|
|
114
124
|
const section = typeof sectionRaw === 'string' ? sectionRaw : '';
|
|
125
|
+
// phase-id-owner: deliberately unmigrated (#4984 revert) — see comment above the function.
|
|
115
126
|
return new RegExp(`^#{2,4}\\s*Phase\\s+${(0, pattern_cjs_1.escapeRegex)(phase)}(?:\\b|\\s|:)`, 'i').test(section);
|
|
116
127
|
}
|
|
117
128
|
// #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
|
|
@@ -257,7 +268,15 @@ function withProjectRoot(cwd, result) {
|
|
|
257
268
|
if (config.project_code) {
|
|
258
269
|
result['project_code'] = config.project_code;
|
|
259
270
|
}
|
|
260
|
-
|
|
271
|
+
// #4455 follow-up (self-discovered): PROJECT.md is shared across a
|
|
272
|
+
// project's own workstreams (never cloned per workstream) but DOES
|
|
273
|
+
// respect the separate GSD_PROJECT multi-project namespace (#3749) — see
|
|
274
|
+
// cmdInitCompleteMilestone's projectPath comment for the full evidence.
|
|
275
|
+
// `ws` explicitly nulled, `project` left to default from GSD_PROJECT.
|
|
276
|
+
// Reading via the workstream-aware planningDir(cwd) meant every init.*
|
|
277
|
+
// call's project_title silently vanished whenever a workstream was
|
|
278
|
+
// active, since no PROJECT.md ever exists at the workstream path.
|
|
279
|
+
const projectMdPath = node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md');
|
|
261
280
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(projectMdPath);
|
|
262
281
|
if (content) {
|
|
263
282
|
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
@@ -477,6 +496,16 @@ function readConfigJsonBoolean(cwd, keyPath) {
|
|
|
477
496
|
return false;
|
|
478
497
|
}
|
|
479
498
|
}
|
|
499
|
+
/** Reads `filePath`; returns its content, or `null` when missing/unreadable/empty. */
|
|
500
|
+
function readNonEmptyFileOrNull(filePath) {
|
|
501
|
+
try {
|
|
502
|
+
const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
|
|
503
|
+
return content && content.length > 0 ? content : null;
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
480
509
|
/**
|
|
481
510
|
* Bounded, non-throwing read of a dotted key path from `.planning/config.json`,
|
|
482
511
|
* returning the raw resolved value (any JSON type) or `undefined` on any
|
|
@@ -565,6 +594,13 @@ function detectPhaseMvpMode(cwd, phaseNumber) {
|
|
|
565
594
|
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
566
595
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
567
596
|
const escapedPhase = (0, pattern_cjs_1.escapeRegex)(phaseNumber);
|
|
597
|
+
// #4906 Phase 5 (#4984): NOT migrated onto phaseHeadingPrefixSrcFor — same
|
|
598
|
+
// out-of-scope reasoning as roadmapPhaseMatchesExactPrefix above (only the
|
|
599
|
+
// five buildPhaseHeadingScanRegex sites in init.cts/milestone.cts are this
|
|
600
|
+
// phase's owned migration); a direct selector call here would have added
|
|
601
|
+
// a fourth uncounted ANY_BRACKET consumer to init.cts's pinned
|
|
602
|
+
// tests/adr-612-bracket-heading-selection.test.cjs census.
|
|
603
|
+
// phase-id-owner: deliberately unmigrated (#4984 revert) — see comment above.
|
|
568
604
|
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
|
|
569
605
|
const headerMatch = content.match(phaseHeader);
|
|
570
606
|
if (!headerMatch || headerMatch.index === undefined)
|
|
@@ -732,6 +768,54 @@ function buildSectionManifestField(cwd, phaseInfo, options, workflow, overrides
|
|
|
732
768
|
function milestoneRecord(cwd) {
|
|
733
769
|
return (getMilestoneInfo(cwd).value ?? {});
|
|
734
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* #4683 — threat IDs claimed by more than one of the phase's live PLAN files.
|
|
773
|
+
* `scanPhasePlans().planFiles` is the right input set twice over: it excludes
|
|
774
|
+
* derivative files (OUTLINE / PLAN-REVIEW / pre-bounce) AND `status: superseded`
|
|
775
|
+
* plans (#2349) — a superseded plan's IDs were deliberately reassigned to its
|
|
776
|
+
* replacement, so they must not hold against it. The per-document row parse is
|
|
777
|
+
* planDocument.extractThreatRegisterIds; only `<threat_model>` register rows
|
|
778
|
+
* count, and the reserved `T-{phase}-SC` shape is excluded there by grammar
|
|
779
|
+
* (every plan keeps that row by design). A missing/unreadable phase directory
|
|
780
|
+
* degrades to "no duplicates" — planning a brand-new phase has nothing to
|
|
781
|
+
* collide with.
|
|
782
|
+
*/
|
|
783
|
+
function findDuplicateThreatIds(cwd, phaseDirRel) {
|
|
784
|
+
if (!phaseDirRel)
|
|
785
|
+
return [];
|
|
786
|
+
const phaseDir = node_path_1.default.join(cwd, phaseDirRel);
|
|
787
|
+
let planFiles;
|
|
788
|
+
try {
|
|
789
|
+
planFiles = scanPhasePlans(phaseDir).planFiles;
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
const owners = new Map();
|
|
795
|
+
for (const planFile of planFiles) {
|
|
796
|
+
let content;
|
|
797
|
+
try {
|
|
798
|
+
content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, planFile), 'utf-8');
|
|
799
|
+
}
|
|
800
|
+
catch {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
for (const id of planDocument.extractThreatRegisterIds(content)) {
|
|
804
|
+
const claimed = owners.get(id);
|
|
805
|
+
if (claimed) {
|
|
806
|
+
if (!claimed.includes(planFile))
|
|
807
|
+
claimed.push(planFile);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
owners.set(id, [planFile]);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return [...owners.entries()]
|
|
815
|
+
.filter(([, claims]) => claims.length > 1)
|
|
816
|
+
.map(([id, plans]) => ({ id, plans: [...plans].sort() }))
|
|
817
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
818
|
+
}
|
|
735
819
|
function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
736
820
|
if (!phase) {
|
|
737
821
|
error('phase required for init execute-phase');
|
|
@@ -769,9 +853,14 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
769
853
|
has_reviews: false,
|
|
770
854
|
};
|
|
771
855
|
});
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
856
|
+
// #4731: multiline-aware — the Requirements field may hard-wrap, so the
|
|
857
|
+
// value is extracted past the line break before the ID scan.
|
|
858
|
+
const phaseSection = roadmapPhase?.['section'];
|
|
859
|
+
const reqLine = phaseSection
|
|
860
|
+
? roadmapParser.extractPhaseFieldMultiline(phaseSection, 'Requirements')
|
|
861
|
+
: null;
|
|
862
|
+
const reqExtracted = reqLine
|
|
863
|
+
? reqLine.replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
|
775
864
|
: null;
|
|
776
865
|
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
|
777
866
|
// #3188: these paths are null when the file is absent, matching the contract
|
|
@@ -781,6 +870,8 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
781
870
|
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
782
871
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
783
872
|
const requirementsPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
|
|
873
|
+
// #4683: computed once — see the threat_id_duplicates fields in the payload.
|
|
874
|
+
const threatIdDuplicates = findDuplicateThreatIds(cwd, phaseInfo?.['directory']);
|
|
784
875
|
const result = {
|
|
785
876
|
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
|
|
786
877
|
verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
|
|
@@ -800,6 +891,13 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
800
891
|
? toPosixPath(node_path_1.default.join(cwd, phaseInfo['directory']))
|
|
801
892
|
: null,
|
|
802
893
|
phase_number: phaseInfo?.['phase_number'] || null,
|
|
894
|
+
// #4748: the disk path hands back the directory's padded number (`03A`)
|
|
895
|
+
// but the ROADMAP fallback above hands back the heading's bare one (`3A`),
|
|
896
|
+
// and execute-phase.md's review lookup needs the padded form for
|
|
897
|
+
// `{PADDED}-REVIEW.md`. It used to re-pad in shell with `printf "%02d"`,
|
|
898
|
+
// which cannot pad a letter id and reads an already-padded `08` as octal.
|
|
899
|
+
// Emit the canonical normalization, as the plan-phase/code-review inits do.
|
|
900
|
+
padded_phase: phaseInfo?.['phase_number'] ? normalizePhaseName(phaseInfo['phase_number']) : null,
|
|
803
901
|
// #3171: prefer the ROADMAP's curated display name for `phase_name`. When
|
|
804
902
|
// the phase directory already exists on disk, the disk-lookup path
|
|
805
903
|
// (searchPhaseInDir) derives phase_name from the directory-name remainder
|
|
@@ -818,6 +916,12 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
818
916
|
incomplete_plans: phaseInfo?.['incomplete_plans'] || [],
|
|
819
917
|
plan_count: phaseInfo?.['plans']?.length || 0,
|
|
820
918
|
incomplete_count: phaseInfo?.['incomplete_plans']?.length || 0,
|
|
919
|
+
// #4683: cross-plan threat-ID collisions (gap-closure plans renumbering
|
|
920
|
+
// from T-{phase}-01 again). execute-phase.md hard-stops on a non-empty
|
|
921
|
+
// list BEFORE any dispatch — SECURITY.md rows and VALIDATION.md's Threat
|
|
922
|
+
// Ref column key on this ID, so a reused ID is ambiguous downstream.
|
|
923
|
+
threat_id_duplicates: threatIdDuplicates,
|
|
924
|
+
threat_id_duplicate_count: threatIdDuplicates.length,
|
|
821
925
|
// #2830: the halt-aware view, forwarded from the shared computation in
|
|
822
926
|
// phase-locator. Additive — `incomplete_plans`/`incomplete_count` above keep
|
|
823
927
|
// their exact name, type and semantics. Without this passthrough the shared
|
|
@@ -828,10 +932,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
828
932
|
runnable_plans: phaseInfo?.['runnable_plans'] || [],
|
|
829
933
|
runnable_count: phaseInfo?.['runnable_plans']?.length || 0,
|
|
830
934
|
branch_name: config.branching_strategy === 'phase' && phaseInfo
|
|
831
|
-
? config.phase_branch_template
|
|
832
|
-
.replace('{project}', config.project_code || '')
|
|
833
|
-
.replace('{phase}', normalizePhaseName(phaseInfo['phase_number']))
|
|
834
|
-
.replace('{slug}', phaseInfo['phase_slug'] || 'phase')
|
|
935
|
+
? renderPhaseBranchName(config.phase_branch_template.replace('{project}', config.project_code || ''), phaseInfo['phase_number'], phaseInfo['phase_slug'])
|
|
835
936
|
: config.branching_strategy === 'milestone'
|
|
836
937
|
? config.milestone_branch_template
|
|
837
938
|
.replace('{milestone}', milestone['version'] ?? '')
|
|
@@ -847,6 +948,17 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
847
948
|
// #3188: null when the file is absent (parity with patterns_path/context_path).
|
|
848
949
|
state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
|
|
849
950
|
roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
|
|
951
|
+
// #4456 correction: an isolated review pass initially "fixed" this to
|
|
952
|
+
// planningDir(cwd, null) on the assumption that config.json is shared
|
|
953
|
+
// like PROJECT.md (workstream-flag.md's directory diagram marks it
|
|
954
|
+
// `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
|
|
955
|
+
// handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
|
|
956
|
+
// for execute-phase/new-project/new-milestone/progress, and gsd-test
|
|
957
|
+
// caught the regression immediately. The diagram is stale for
|
|
958
|
+
// config.json specifically (same class of staleness already found for
|
|
959
|
+
// `milestones/` during the #4455 follow-up) — reverted to the
|
|
960
|
+
// workstream-aware planningDir(cwd), matching the established,
|
|
961
|
+
// ADR-governed, tested contract.
|
|
850
962
|
config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
851
963
|
// #2376: execute-phase.md's verify_phase_goal step reads this instead of
|
|
852
964
|
// hardcoding '.planning/REQUIREMENTS.md' into the gsd-verifier spawn prompt.
|
|
@@ -903,9 +1015,14 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
903
1015
|
has_reviews: false,
|
|
904
1016
|
};
|
|
905
1017
|
});
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1018
|
+
// #4731: multiline-aware — the Requirements field may hard-wrap, so the
|
|
1019
|
+
// value is extracted past the line break before the ID scan.
|
|
1020
|
+
const phaseSection = roadmapPhase?.['section'];
|
|
1021
|
+
const reqLine = phaseSection
|
|
1022
|
+
? roadmapParser.extractPhaseFieldMultiline(phaseSection, 'Requirements')
|
|
1023
|
+
: null;
|
|
1024
|
+
const reqExtracted = reqLine
|
|
1025
|
+
? reqLine.replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
|
909
1026
|
: null;
|
|
910
1027
|
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
|
911
1028
|
const phaseDirPlan = phaseInfo?.['directory'] || null;
|
|
@@ -925,6 +1042,8 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
925
1042
|
}
|
|
926
1043
|
const granularityOverride = options['granularity'];
|
|
927
1044
|
assertValidGranularityOverride(granularityOverride, error);
|
|
1045
|
+
// #4683: computed once — see the threat_id_duplicates fields in the payload.
|
|
1046
|
+
const threatIdDuplicatesPlan = findDuplicateThreatIds(cwd, phaseDirPlan);
|
|
928
1047
|
const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
|
|
929
1048
|
// #3188: see cmdInitExecutePhase — null when absent, parity with the
|
|
930
1049
|
// conditional sibling fields in this same result object.
|
|
@@ -969,6 +1088,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
969
1088
|
has_reviews: phaseInfo?.['has_reviews'] || false,
|
|
970
1089
|
has_plans: (phaseInfo?.['plans']?.length || 0) > 0,
|
|
971
1090
|
plan_count: phaseInfo?.['plans']?.length || 0,
|
|
1091
|
+
// #4683: the same cross-plan threat-ID duplicate list execute-phase gates
|
|
1092
|
+
// on, surfaced at PLAN time so the checker/reviewer catches the collision
|
|
1093
|
+
// before the plans are approved — not just before execution.
|
|
1094
|
+
threat_id_duplicates: threatIdDuplicatesPlan,
|
|
1095
|
+
threat_id_duplicate_count: threatIdDuplicatesPlan.length,
|
|
972
1096
|
planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
|
|
973
1097
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
974
1098
|
// #2376: absolute — see comment on phase_dir above.
|
|
@@ -1130,7 +1254,13 @@ function buildInitCompletenessFields(cwd) {
|
|
|
1130
1254
|
const planningExists = node_fs_1.default.existsSync(dir);
|
|
1131
1255
|
const requirementsExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'REQUIREMENTS.md'));
|
|
1132
1256
|
const milestonesExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'MILESTONES.md'));
|
|
1133
|
-
|
|
1257
|
+
// #4455 follow-up (code-review finding): PROJECT.md is shared across
|
|
1258
|
+
// workstreams (see cmdInitCompleteMilestone's projectPath comment for the
|
|
1259
|
+
// full evidence) — checked at planningRoot(cwd), never the workstream-scoped
|
|
1260
|
+
// `dir`, so a workstream whose own REQUIREMENTS/ROADMAP/STATE are all
|
|
1261
|
+
// present isn't wrongly reported incomplete just because the shared
|
|
1262
|
+
// PROJECT.md isn't ALSO duplicated under its own directory.
|
|
1263
|
+
const coreComplete = node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')) &&
|
|
1134
1264
|
requirementsExists &&
|
|
1135
1265
|
node_fs_1.default.existsSync(node_path_1.default.join(dir, 'ROADMAP.md')) &&
|
|
1136
1266
|
node_fs_1.default.existsSync(node_path_1.default.join(dir, 'STATE.md'));
|
|
@@ -1165,23 +1295,46 @@ function cmdInitNewProject(cwd, raw, options = {}) {
|
|
|
1165
1295
|
// root-scoped (`pathExistsInternal(cwd, '.planning')`) semantics for that
|
|
1166
1296
|
// one key stay byte-identical for existing consumers.
|
|
1167
1297
|
...buildInitCompletenessFields(cwd),
|
|
1168
|
-
|
|
1298
|
+
// #4455 follow-up (code-review finding): PROJECT.md is shared across
|
|
1299
|
+
// workstreams — see cmdInitCompleteMilestone's projectPath comment for
|
|
1300
|
+
// the full evidence.
|
|
1301
|
+
project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
|
|
1169
1302
|
has_codebase_map: hasCodebaseMap,
|
|
1170
1303
|
planning_exists: pathExistsInternal(cwd, '.planning'),
|
|
1171
1304
|
has_existing_code: hasCode,
|
|
1172
1305
|
has_package_file: hasPackageFile,
|
|
1173
1306
|
is_brownfield: isBrownfield,
|
|
1307
|
+
// #4458: new-project.md's Step 5.1 (Sub-Repo Detection) used to run its own
|
|
1308
|
+
// narrower `find ... -exec test -d "{}/.git"` predicate, which requires
|
|
1309
|
+
// .git to be a DIRECTORY and so silently excluded linked git worktree
|
|
1310
|
+
// children (.git is a FILE there). detectSubRepos already handled this
|
|
1311
|
+
// correctly (fs.existsSync, not isDirectory) but had zero callers anywhere
|
|
1312
|
+
// in the codebase — reused here instead of leaving the workflow to
|
|
1313
|
+
// maintain its own duplicate, narrower detection logic.
|
|
1314
|
+
sub_repos_detected: coreUtils.detectSubRepos(cwd),
|
|
1174
1315
|
needs_codebase_map: isBrownfield && !hasCodebaseMap,
|
|
1175
1316
|
...getInitGitState(cwd),
|
|
1176
1317
|
brave_search_available: hasBraveSearch,
|
|
1177
1318
|
firecrawl_available: hasFirecrawl,
|
|
1178
1319
|
exa_search_available: hasExaSearch,
|
|
1179
1320
|
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1180
|
-
|
|
1321
|
+
// #4455 follow-up: PROJECT.md is shared across workstreams.
|
|
1322
|
+
project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
|
|
1181
1323
|
// #2376: new-project.md's research-synthesizer/roadmapper spawn prompts
|
|
1182
1324
|
// read these instead of hardcoding '.planning/...' literals.
|
|
1183
1325
|
requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
|
|
1184
1326
|
roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1327
|
+
// #4456 correction: an isolated review pass initially "fixed" this to
|
|
1328
|
+
// planningDir(cwd, null) on the assumption that config.json is shared
|
|
1329
|
+
// like PROJECT.md (workstream-flag.md's directory diagram marks it
|
|
1330
|
+
// `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
|
|
1331
|
+
// handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
|
|
1332
|
+
// for execute-phase/new-project/new-milestone/progress, and gsd-test
|
|
1333
|
+
// caught the regression immediately. The diagram is stale for
|
|
1334
|
+
// config.json specifically (same class of staleness already found for
|
|
1335
|
+
// `milestones/` during the #4455 follow-up) — reverted to the
|
|
1336
|
+
// workstream-aware planningDir(cwd), matching the established,
|
|
1337
|
+
// ADR-governed, tested contract.
|
|
1185
1338
|
config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
1186
1339
|
research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
|
|
1187
1340
|
};
|
|
@@ -1223,18 +1376,41 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
|
|
|
1223
1376
|
phase_archive_path: latestCompleted
|
|
1224
1377
|
? toPosixPath(node_path_1.default.join(planningRoot(cwd), 'milestones', `${latestCompleted.version}-phases`))
|
|
1225
1378
|
: null,
|
|
1226
|
-
|
|
1379
|
+
// #4455 follow-up (code-review finding): PROJECT.md is shared across
|
|
1380
|
+
// workstreams — see cmdInitCompleteMilestone's projectPath comment for
|
|
1381
|
+
// the full evidence.
|
|
1382
|
+
project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
|
|
1227
1383
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1228
1384
|
state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
1229
|
-
|
|
1385
|
+
// #4455 follow-up: PROJECT.md is shared across workstreams.
|
|
1386
|
+
project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
|
|
1230
1387
|
roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1231
1388
|
state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
1232
1389
|
// #2376: new-milestone.md's research-synthesizer/roadmapper spawn prompts
|
|
1233
1390
|
// read these instead of hardcoding '.planning/...' literals.
|
|
1234
1391
|
requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
|
|
1392
|
+
// #4456 correction: an isolated review pass initially "fixed" this to
|
|
1393
|
+
// planningDir(cwd, null) on the assumption that config.json is shared
|
|
1394
|
+
// like PROJECT.md (workstream-flag.md's directory diagram marks it
|
|
1395
|
+
// `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
|
|
1396
|
+
// handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
|
|
1397
|
+
// for execute-phase/new-project/new-milestone/progress, and gsd-test
|
|
1398
|
+
// caught the regression immediately. The diagram is stale for
|
|
1399
|
+
// config.json specifically (same class of staleness already found for
|
|
1400
|
+
// `milestones/` during the #4455 follow-up) — reverted to the
|
|
1401
|
+
// workstream-aware planningDir(cwd), matching the established,
|
|
1402
|
+
// ADR-governed, tested contract.
|
|
1235
1403
|
config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
1236
1404
|
research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
|
|
1237
1405
|
milestones_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'MILESTONES.md')),
|
|
1406
|
+
// #4456: new-milestone.md's Step 6 stages the phase-archive move
|
|
1407
|
+
// (`git add .planning/milestones/ .planning/phases/`) — both
|
|
1408
|
+
// workstream-scoped (phases_dir mirrors the phasesDir local above;
|
|
1409
|
+
// archive_dir mirrors cmdInitCompleteMilestone's own field of the same
|
|
1410
|
+
// name), so a literal root `git add` misses the actual files
|
|
1411
|
+
// phases.clear just moved under an active workstream.
|
|
1412
|
+
phases_dir: toPosixPath(phasesDir),
|
|
1413
|
+
archive_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'milestones')),
|
|
1238
1414
|
};
|
|
1239
1415
|
// `state:flat-mode` (#2994): whether NO workstream is active — the inverse
|
|
1240
1416
|
// of `state:workstream-active` (introduced for `cmdInitTransition` below).
|
|
@@ -1257,7 +1433,7 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
|
|
|
1257
1433
|
// would otherwise silently delete a stale/invalid pointer as a side effect
|
|
1258
1434
|
// of building a JSON report field, and (per #3579) could change what a
|
|
1259
1435
|
// LATER resolution in the same process observes.
|
|
1260
|
-
const resolvedWorkstream =
|
|
1436
|
+
const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
|
|
1261
1437
|
const workstreamActive = !!resolvedWorkstream;
|
|
1262
1438
|
const flatMode = !workstreamActive;
|
|
1263
1439
|
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
@@ -1372,7 +1548,10 @@ function cmdInitQuickBatch(cwd, raw, options = {}) {
|
|
|
1372
1548
|
function cmdInitIngestDocs(cwd, raw) {
|
|
1373
1549
|
const config = loadConfig(cwd);
|
|
1374
1550
|
const result = {
|
|
1375
|
-
|
|
1551
|
+
// #4455 follow-up (code-review finding): PROJECT.md is shared across
|
|
1552
|
+
// workstreams — see cmdInitCompleteMilestone's projectPath comment for
|
|
1553
|
+
// the full evidence.
|
|
1554
|
+
project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
|
|
1376
1555
|
planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
|
|
1377
1556
|
...getInitGitState(cwd),
|
|
1378
1557
|
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase. The
|
|
@@ -1381,7 +1560,8 @@ function cmdInitIngestDocs(cwd, raw) {
|
|
|
1381
1560
|
// hardcoded bare '.planning/intel/...', '.planning/PROJECT.md', etc.
|
|
1382
1561
|
// literals into their Agent(prompt=...) blocks; those now interpolate
|
|
1383
1562
|
// these fields instead.
|
|
1384
|
-
|
|
1563
|
+
// #4455 follow-up: PROJECT.md is shared across workstreams.
|
|
1564
|
+
project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
|
|
1385
1565
|
requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
|
|
1386
1566
|
roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1387
1567
|
state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
@@ -1417,12 +1597,16 @@ function cmdInitResume(cwd, raw) {
|
|
|
1417
1597
|
...buildInitCompletenessFields(cwd),
|
|
1418
1598
|
state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
1419
1599
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1420
|
-
|
|
1600
|
+
// #4455 follow-up (code-review finding): PROJECT.md is shared across
|
|
1601
|
+
// workstreams — see cmdInitCompleteMilestone's projectPath comment for
|
|
1602
|
+
// the full evidence.
|
|
1603
|
+
project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
|
|
1421
1604
|
planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
|
|
1422
1605
|
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1423
1606
|
state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
1424
1607
|
roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1425
|
-
|
|
1608
|
+
// #4455 follow-up: PROJECT.md is shared across workstreams.
|
|
1609
|
+
project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
|
|
1426
1610
|
has_interrupted_agent: !!interruptedAgentId,
|
|
1427
1611
|
interrupted_agent_id: interruptedAgentId,
|
|
1428
1612
|
commit_docs: config.commit_docs,
|
|
@@ -1898,13 +2082,156 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
1898
2082
|
}
|
|
1899
2083
|
output(withProjectRoot(cwd, result), raw);
|
|
1900
2084
|
}
|
|
2085
|
+
// #2618: bullet-cap and title-floor for renderPendingTodosMarkdown below.
|
|
2086
|
+
// 240 matches the bound already vetted by maintainer review on the prior
|
|
2087
|
+
// attempt at this issue (PR #2662) — re-deriving a different number would be
|
|
2088
|
+
// pure bikeshedding, not a correctness improvement. See
|
|
2089
|
+
// .gsd/phase/feat-2618-compact-todo-pointers/40-design.md.
|
|
2090
|
+
const PENDING_TODO_BULLET_MAX_CHARS = 240;
|
|
2091
|
+
const PENDING_TODO_TITLE_FLOOR = 15;
|
|
2092
|
+
const PENDING_TODO_AREA_FLOOR = 3;
|
|
2093
|
+
function sanitizePendingTodoInline(value) {
|
|
2094
|
+
// Defensive: the regex captures that populate title/area/needs can only
|
|
2095
|
+
// ever match a single line, so this is belt-and-suspenders against any
|
|
2096
|
+
// future non-regex-sourced input, not a reachable case today.
|
|
2097
|
+
return value.replace(/[\r\n]+/g, ' ').trim();
|
|
2098
|
+
}
|
|
2099
|
+
function truncatePendingTodoText(value, maxLen) {
|
|
2100
|
+
if (value.length <= maxLen)
|
|
2101
|
+
return value;
|
|
2102
|
+
if (maxLen <= 1)
|
|
2103
|
+
return value.slice(0, Math.max(0, maxLen));
|
|
2104
|
+
return `${value.slice(0, maxLen - 1)}…`;
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* #2618: pure renderer for STATE.md's "### Pending Todos" section BODY (not
|
|
2108
|
+
* the heading). One bullet per todo, each capped at
|
|
2109
|
+
* PENDING_TODO_BULLET_MAX_CHARS. `gsd-core/workflows/add-todo.md` and
|
|
2110
|
+
* `check-todos.md` splice this string in verbatim instead of free-hand
|
|
2111
|
+
* editing STATE.md — see the design doc for why this is real, unit-tested
|
|
2112
|
+
* code rather than a prose algorithm (DEFECT.GENERATIVE-FIX: a prose
|
|
2113
|
+
* algorithm duplicated as a test oracle is exactly the divergence class
|
|
2114
|
+
* this avoids).
|
|
2115
|
+
*
|
|
2116
|
+
* #4384 regression fix: the optional `projectRoot` makes the bullet's
|
|
2117
|
+
* `[todo file](…)` link repo-relative (see pendingTodoLinkTarget) so the cap
|
|
2118
|
+
* is deterministic w.r.t. where the repo is checked out. Omitting it keeps
|
|
2119
|
+
* the legacy absolute-link behavior for existing direct callers.
|
|
2120
|
+
*/
|
|
2121
|
+
function renderPendingTodosMarkdown(todos, projectRoot) {
|
|
2122
|
+
if (!Array.isArray(todos) || todos.length === 0) {
|
|
2123
|
+
return 'None yet.';
|
|
2124
|
+
}
|
|
2125
|
+
return todos.map((todo) => renderPendingTodoBullet(todo, projectRoot)).join('\n');
|
|
2126
|
+
}
|
|
2127
|
+
function pendingTodoFieldAsString(value, fallback) {
|
|
2128
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* #4384 regression fix: the bullet's markdown link target, rendered
|
|
2132
|
+
* repo-relative when `projectRoot` is given and the todo's `path` is
|
|
2133
|
+
* absolute. The JSON `todos[].path` field stays absolute (#2376 contract);
|
|
2134
|
+
* only the rendered display link changes — embedding the machine-variable
|
|
2135
|
+
* absolute base let macOS's /private/var/folders/… temp paths consume the
|
|
2136
|
+
* 240-char budget and drop the "Needs <solution>" clause on long-path
|
|
2137
|
+
* machines only (next's own macos CI shard went red on exactly this, run
|
|
2138
|
+
* 34038716700). Repo-relative links also resolve correctly from STATE.md at
|
|
2139
|
+
* the repo root and survive repo moves.
|
|
2140
|
+
*/
|
|
2141
|
+
function pendingTodoLinkTarget(todo, projectRoot) {
|
|
2142
|
+
const raw = pendingTodoFieldAsString(todo['path'], '');
|
|
2143
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0 || !node_path_1.default.isAbsolute(raw)) {
|
|
2144
|
+
return raw;
|
|
2145
|
+
}
|
|
2146
|
+
const rel = toPosixPath(node_path_1.default.relative(projectRoot, raw));
|
|
2147
|
+
if (rel.length === 0 || node_path_1.default.isAbsolute(rel)) {
|
|
2148
|
+
// Degenerate (path === projectRoot) or Windows cross-drive fallback:
|
|
2149
|
+
// keep the raw target rather than emitting an empty or incorrect link.
|
|
2150
|
+
return raw;
|
|
2151
|
+
}
|
|
2152
|
+
return rel;
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* #4439: the stored `created:` frontmatter (and the JSON `todos[].created`
|
|
2156
|
+
* field it round-trips through) is always a full ISO-8601 timestamp, by
|
|
2157
|
+
* design — this is the display-only seam that reformats it to the
|
|
2158
|
+
* date-only `[date]` bullet documented in docs/reference/state-md.md and
|
|
2159
|
+
* docs/COMMANDS.md. A value that doesn't start with a well-formed
|
|
2160
|
+
* `YYYY-MM-DD` (the 'unknown' fallback, or any other non-conforming
|
|
2161
|
+
* string) passes through unchanged rather than being mangled.
|
|
2162
|
+
*/
|
|
2163
|
+
function pendingTodoDateOnly(value) {
|
|
2164
|
+
const match = value.match(/^\d{4}-\d{2}-\d{2}/);
|
|
2165
|
+
return match ? match[0] : value;
|
|
2166
|
+
}
|
|
2167
|
+
function renderPendingTodoBullet(todo, projectRoot) {
|
|
2168
|
+
const date = pendingTodoDateOnly(sanitizePendingTodoInline(pendingTodoFieldAsString(todo['created'], 'unknown')));
|
|
2169
|
+
let area = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['area'], 'general'));
|
|
2170
|
+
let title = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['title'], 'Untitled'));
|
|
2171
|
+
// Strip trailing "." so the fixed "Needs ....` template below never
|
|
2172
|
+
// produces a doubled period when the source text already ended in one.
|
|
2173
|
+
let needs = typeof todo['needs'] === 'string'
|
|
2174
|
+
? sanitizePendingTodoInline(todo['needs']).replace(/\.+$/, '')
|
|
2175
|
+
: '';
|
|
2176
|
+
const link = `[todo file](${pendingTodoLinkTarget(todo, projectRoot)})`;
|
|
2177
|
+
const assemble = () => {
|
|
2178
|
+
const needsClause = needs ? ` — Needs ${needs}.` : '';
|
|
2179
|
+
return `- [${date}] [${area}] ${title} — ${link}${needsClause}`;
|
|
2180
|
+
};
|
|
2181
|
+
let line = assemble();
|
|
2182
|
+
if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
|
|
2183
|
+
return line;
|
|
2184
|
+
// 1) Drop the needs clause entirely first — date/area/title/link untouched.
|
|
2185
|
+
needs = '';
|
|
2186
|
+
line = assemble();
|
|
2187
|
+
if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
|
|
2188
|
+
return line;
|
|
2189
|
+
// 2) Shorten the title next, down to a floor — date/area/link untouched.
|
|
2190
|
+
const titleOverage = line.length - PENDING_TODO_BULLET_MAX_CHARS;
|
|
2191
|
+
const targetTitleLen = Math.max(PENDING_TODO_TITLE_FLOOR, title.length - titleOverage);
|
|
2192
|
+
if (targetTitleLen < title.length) {
|
|
2193
|
+
title = truncatePendingTodoText(title, targetTitleLen);
|
|
2194
|
+
line = assemble();
|
|
2195
|
+
}
|
|
2196
|
+
if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
|
|
2197
|
+
return line;
|
|
2198
|
+
// 3) Shorten area as a last resort — date and the markdown link are never
|
|
2199
|
+
// altered (link correctness > strict cap; see design doc "Known limits").
|
|
2200
|
+
const areaOverage = line.length - PENDING_TODO_BULLET_MAX_CHARS;
|
|
2201
|
+
const targetAreaLen = Math.max(PENDING_TODO_AREA_FLOOR, area.length - areaOverage);
|
|
2202
|
+
if (targetAreaLen < area.length) {
|
|
2203
|
+
area = truncatePendingTodoText(area, targetAreaLen);
|
|
2204
|
+
line = assemble();
|
|
2205
|
+
}
|
|
2206
|
+
return line;
|
|
2207
|
+
}
|
|
1901
2208
|
function cmdInitTodos(cwd, area, raw) {
|
|
1902
2209
|
const config = loadConfig(cwd);
|
|
1903
|
-
|
|
2210
|
+
// #4256: todos are root-scoped shared state (migrateToWorkstreams keeps
|
|
2211
|
+
// them at .planning/todos/ and every workflow writer writes that literal
|
|
2212
|
+
// path), so this read resolves via todosDir(cwd) — NOT planningDir(cwd),
|
|
2213
|
+
// which would look in .planning/workstreams/<ws>/todos/ under a workstream
|
|
2214
|
+
// (a directory nothing creates) and report existing todos as absent.
|
|
2215
|
+
const todosRoot = todosDir(cwd);
|
|
2216
|
+
const pendingDir = node_path_1.default.join(todosRoot, 'pending');
|
|
1904
2217
|
let count = 0;
|
|
1905
2218
|
const todos = [];
|
|
2219
|
+
// #2618: distinct from "genuinely zero pending todos" — false only when
|
|
2220
|
+
// readdirSync itself failed for a reason OTHER than the directory simply
|
|
2221
|
+
// not existing yet (ENOENT), mirroring the ENOENT-vs-other-errno split
|
|
2222
|
+
// already used above in this file (#3885, ADR-3473 §8.5). Without this,
|
|
2223
|
+
// a real I/O/permission error on the pending dir would look identical to
|
|
2224
|
+
// "no pending todos" and could wipe an existing, non-empty Pending Todos
|
|
2225
|
+
// section in STATE.md on refresh — the fail-safe requirement for #2618.
|
|
2226
|
+
let pendingReadOk = true;
|
|
1906
2227
|
try {
|
|
1907
|
-
|
|
2228
|
+
// #2618: sorted so pending_todos_markdown's bullet order is stable across
|
|
2229
|
+
// runs — readdirSync's order is filesystem-dependent, not contractually
|
|
2230
|
+
// stable, and an unstable order would reorder every bullet on an
|
|
2231
|
+
// unrelated re-render, turning a one-line git diff into a full-section
|
|
2232
|
+
// rewrite (must-have #3). Filenames are `YYYY-MM-DD-slug.md`, so this
|
|
2233
|
+
// also yields a sensible chronological order as a side effect.
|
|
2234
|
+
const files = node_fs_1.default.readdirSync(pendingDir).filter((f) => f.endsWith('.md')).sort();
|
|
1908
2235
|
for (const file of files) {
|
|
1909
2236
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(pendingDir, file));
|
|
1910
2237
|
if (content === null)
|
|
@@ -1916,6 +2243,21 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
1916
2243
|
// #2337: kept in parity with cmdListTodos — surface severity when
|
|
1917
2244
|
// present, omit the key entirely for todos with no severity line.
|
|
1918
2245
|
const severityMatch = content.match(/^severity:\s*(.+)$/m);
|
|
2246
|
+
// #2618: first non-empty line of the `## Solution` body, used as the
|
|
2247
|
+
// bullet's "Needs ..." clause. "TBD" (the create_file template's own
|
|
2248
|
+
// placeholder for an unresolved solution) renders no clause at all
|
|
2249
|
+
// rather than the useless literal "Needs TBD.".
|
|
2250
|
+
const solutionSection = (0, markdown_sectionizer_cjs_1.collectSection)(content, (h) => h.level === 2 && h.text.trim() === 'Solution');
|
|
2251
|
+
let needs;
|
|
2252
|
+
if (solutionSection) {
|
|
2253
|
+
const firstLine = solutionSection.body
|
|
2254
|
+
.split('\n')
|
|
2255
|
+
.map((l) => l.trim())
|
|
2256
|
+
.find((l) => l.length > 0);
|
|
2257
|
+
if (firstLine && firstLine.toUpperCase() !== 'TBD') {
|
|
2258
|
+
needs = firstLine;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
1919
2261
|
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
|
1920
2262
|
if (area && todoArea !== area)
|
|
1921
2263
|
continue;
|
|
@@ -1926,8 +2268,9 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
1926
2268
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
1927
2269
|
area: todoArea,
|
|
1928
2270
|
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1929
|
-
path: toPosixPath(node_path_1.default.join(
|
|
2271
|
+
path: toPosixPath(node_path_1.default.join(pendingDir, file)),
|
|
1930
2272
|
...(severityMatch ? { severity: severityMatch[1].trim() } : {}),
|
|
2273
|
+
...(needs ? { needs } : {}),
|
|
1931
2274
|
});
|
|
1932
2275
|
}
|
|
1933
2276
|
catch {
|
|
@@ -1935,8 +2278,11 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
1935
2278
|
}
|
|
1936
2279
|
}
|
|
1937
2280
|
}
|
|
1938
|
-
catch {
|
|
1939
|
-
|
|
2281
|
+
catch (err) {
|
|
2282
|
+
const code = err?.code;
|
|
2283
|
+
if (code !== 'ENOENT') {
|
|
2284
|
+
pendingReadOk = false;
|
|
2285
|
+
}
|
|
1940
2286
|
}
|
|
1941
2287
|
const result = {
|
|
1942
2288
|
commit_docs: config.commit_docs,
|
|
@@ -1946,11 +2292,22 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
1946
2292
|
todos,
|
|
1947
2293
|
area_filter: area || null,
|
|
1948
2294
|
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1949
|
-
|
|
1950
|
-
|
|
2295
|
+
// #4256: both dir fields probe the ROOT todos tree via todosDir(cwd).
|
|
2296
|
+
pending_dir: toPosixPath(pendingDir),
|
|
2297
|
+
completed_dir: toPosixPath(node_path_1.default.join(todosRoot, 'completed')),
|
|
2298
|
+
// planning_exists intentionally stays workstream/project-scoped — it
|
|
2299
|
+
// answers "does the ACTIVE planning dir exist", not a todos question.
|
|
1951
2300
|
planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
|
|
1952
|
-
todos_dir_exists: node_fs_1.default.existsSync(
|
|
1953
|
-
pending_dir_exists: node_fs_1.default.existsSync(
|
|
2301
|
+
todos_dir_exists: node_fs_1.default.existsSync(todosRoot),
|
|
2302
|
+
pending_dir_exists: node_fs_1.default.existsSync(pendingDir),
|
|
2303
|
+
// #2618: see PENDING_TODO_BULLET_MAX_CHARS comment / design doc. Consumed
|
|
2304
|
+
// by add-todo.md / check-todos.md's update_state step; omitted entirely
|
|
2305
|
+
// (rather than emitted with possibly-wrong data) when pendingReadOk is
|
|
2306
|
+
// false, so the workflow's fail-safe check can key off field presence.
|
|
2307
|
+
pending_read_ok: pendingReadOk,
|
|
2308
|
+
// #4384 fix: pass cwd as projectRoot so the bullet link renders
|
|
2309
|
+
// repo-relative — see pendingTodoLinkTarget.
|
|
2310
|
+
...(pendingReadOk ? { pending_todos_markdown: renderPendingTodosMarkdown(todos, cwd) } : {}),
|
|
1954
2311
|
};
|
|
1955
2312
|
output(withProjectRoot(cwd, result), raw);
|
|
1956
2313
|
}
|
|
@@ -1965,14 +2322,16 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
1965
2322
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
1966
2323
|
const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1967
2324
|
const currentSection = extractCurrentMilestone(roadmapRaw, cwd);
|
|
1968
|
-
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag
|
|
1969
|
-
|
|
2325
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
|
|
2326
|
+
// buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
|
|
2327
|
+
// bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
|
|
2328
|
+
const { regex: phasePattern, phaseNumGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, resolvePhaseIdConvention(cwd));
|
|
1970
2329
|
let m;
|
|
1971
2330
|
while ((m = phasePattern.exec(currentSection)) !== null) {
|
|
1972
2331
|
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
1973
|
-
if (isSentinelPhaseId(m[
|
|
2332
|
+
if (isSentinelPhaseId(m[phaseNumGroup]))
|
|
1974
2333
|
continue;
|
|
1975
|
-
roadmapPhaseNumbers.push(m[
|
|
2334
|
+
roadmapPhaseNumbers.push(m[phaseNumGroup]);
|
|
1976
2335
|
}
|
|
1977
2336
|
}
|
|
1978
2337
|
catch {
|
|
@@ -2065,7 +2424,10 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
2065
2424
|
all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
|
|
2066
2425
|
archived_milestones: archivedMilestones,
|
|
2067
2426
|
archive_count: archivedMilestones.length,
|
|
2068
|
-
|
|
2427
|
+
// #4455 follow-up (code-review finding): PROJECT.md is shared across
|
|
2428
|
+
// workstreams — see cmdInitCompleteMilestone's projectPath comment for
|
|
2429
|
+
// the full evidence.
|
|
2430
|
+
project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
|
|
2069
2431
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
2070
2432
|
state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
2071
2433
|
archive_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningRoot(cwd), 'archive')),
|
|
@@ -2112,6 +2474,10 @@ function cmdInitManager(cwd, raw) {
|
|
|
2112
2474
|
const config = loadConfig(cwd);
|
|
2113
2475
|
const milestone = milestoneRecord(cwd);
|
|
2114
2476
|
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
2477
|
+
const phaseIdConvention = resolvePhaseIdConvention(cwd);
|
|
2478
|
+
const capturesBracketId = phaseIdConvention === 'bracket';
|
|
2479
|
+
const phaseHeadingPrefix = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, phaseIdConvention, capturesBracketId);
|
|
2480
|
+
const phaseHeadingPrefixNoCapture = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, phaseIdConvention);
|
|
2115
2481
|
const paths = planningPaths(cwd);
|
|
2116
2482
|
if (!node_fs_1.default.existsSync(paths.roadmap)) {
|
|
2117
2483
|
error(`No ROADMAP.md found. Run ${(0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', _slashRuntime)} first.`);
|
|
@@ -2121,38 +2487,48 @@ function cmdInitManager(cwd, raw) {
|
|
|
2121
2487
|
}
|
|
2122
2488
|
const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8');
|
|
2123
2489
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
2124
|
-
const phasesDir = paths.phases;
|
|
2125
2490
|
// #3185 (ADR-3180 Decision 1): "which phase directories belong to the
|
|
2126
2491
|
// CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
|
|
2127
2492
|
// routed through it instead of a hand-rolled readdirSync + a separate
|
|
2128
2493
|
// getMilestonePhaseFilter window check (which also never excluded
|
|
2129
2494
|
// sentinels, unlike the owner).
|
|
2130
|
-
const _phaseDirEntries = listMilestonePhaseDirs(phasesDir, { cwd }).value;
|
|
2131
2495
|
const _checkboxStates = new Map();
|
|
2132
|
-
const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s
|
|
2496
|
+
const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
|
|
2133
2497
|
let _cbMatch;
|
|
2134
2498
|
while ((_cbMatch = _cbPattern.exec(content)) !== null) {
|
|
2135
|
-
|
|
2499
|
+
const phaseGroup = capturesBracketId ? 3 : 2;
|
|
2500
|
+
_checkboxStates.set(_cbMatch[phaseGroup], _cbMatch[1].toLowerCase() === 'x');
|
|
2136
2501
|
}
|
|
2137
2502
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
2138
|
-
const phasePattern = new RegExp(`#{2,4}\\s
|
|
2503
|
+
const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
2139
2504
|
const phases = [];
|
|
2140
2505
|
let match;
|
|
2141
2506
|
while ((match = phasePattern.exec(content)) !== null) {
|
|
2142
|
-
const
|
|
2143
|
-
const
|
|
2507
|
+
const bracketId = capturesBracketId ? match[1] : undefined;
|
|
2508
|
+
const phaseNum = capturesBracketId ? match[2] : match[1];
|
|
2509
|
+
const phaseName = (capturesBracketId ? match[3] : match[2])
|
|
2510
|
+
.replace(/\(INSERTED\)/i, '')
|
|
2511
|
+
.trim();
|
|
2512
|
+
let displayId;
|
|
2513
|
+
if (bracketId) {
|
|
2514
|
+
try {
|
|
2515
|
+
displayId = renderPhaseId(parsePhaseId(`${bracketId}-${phaseNum}`));
|
|
2516
|
+
}
|
|
2517
|
+
catch {
|
|
2518
|
+
// The bracket selector is deliberately read-tolerant. If a heading is
|
|
2519
|
+
// non-canonical, retain the manager row without fabricating display_id.
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2144
2522
|
const sectionStart = match.index;
|
|
2145
2523
|
const restOfContent = content.slice(sectionStart);
|
|
2146
|
-
const nextHeader = restOfContent.match(
|
|
2524
|
+
const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}\\d[\\d.]*`, 'i'));
|
|
2147
2525
|
const sectionEnd = nextHeader
|
|
2148
2526
|
? sectionStart + nextHeader.index
|
|
2149
2527
|
: content.length;
|
|
2150
2528
|
const section = content.slice(sectionStart, sectionEnd);
|
|
2151
|
-
const
|
|
2152
|
-
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
2529
|
+
const goal = roadmapParser.extractPhaseFieldMultiline(section, 'Goal');
|
|
2153
2530
|
const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
2154
2531
|
const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
|
|
2155
|
-
const normalized = normalizePhaseName(phaseNum);
|
|
2156
2532
|
let diskStatus = 'no_directory';
|
|
2157
2533
|
let planCount = 0;
|
|
2158
2534
|
let summaryCount = 0;
|
|
@@ -2165,14 +2541,23 @@ function cmdInitManager(cwd, raw) {
|
|
|
2165
2541
|
let contextScope = SCOPE.COMPLETE;
|
|
2166
2542
|
let completion = buildPhaseCompletionProjection(cwd, phaseNum, null, planCount, summaryCount, _slashRuntime);
|
|
2167
2543
|
try {
|
|
2168
|
-
// #
|
|
2169
|
-
// milestone-
|
|
2170
|
-
//
|
|
2171
|
-
//
|
|
2172
|
-
|
|
2544
|
+
// #4801: resolve through the canonical locator instead of a private
|
|
2545
|
+
// current-milestone-only scan. findPhaseInternal searches the live
|
|
2546
|
+
// .planning/phases directory FIRST (same set the retired
|
|
2547
|
+
// matchPhaseDirs/_phaseDirEntries pair scanned — the #3185 physical-dir
|
|
2548
|
+
// scope choice is inherited by the locator's live arm) and then falls
|
|
2549
|
+
// back through listArchiveVersionDirs (workstream-scoped, #2855), so an
|
|
2550
|
+
// ARCHIVED phase directory with a passing verification resolves instead
|
|
2551
|
+
// of reporting no_directory/phase_complete:false. Five other init
|
|
2552
|
+
// commands already route through this same primitive; this was the
|
|
2553
|
+
// holdout private copy (#4793-family declared-owner drift).
|
|
2554
|
+
const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention);
|
|
2555
|
+
const dirMatch = located && located['found']
|
|
2556
|
+
? node_path_1.default.posix.basename(String(located['directory']))
|
|
2557
|
+
: null;
|
|
2173
2558
|
if (dirMatch) {
|
|
2174
|
-
const fullDir = node_path_1.default.join(
|
|
2175
|
-
const phaseDirRel =
|
|
2559
|
+
const fullDir = node_path_1.default.join(cwd, String(located['directory']));
|
|
2560
|
+
const phaseDirRel = String(located['directory']);
|
|
2176
2561
|
// #4014 (epic #3473 B4-unreadable): this whole block used to swallow
|
|
2177
2562
|
// ANY readdirSync failure below into the bare `catch { /* empty */ }`
|
|
2178
2563
|
// at the bottom — an unreadable phase directory reported the exact
|
|
@@ -2246,6 +2631,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2246
2631
|
const roadmapComplete = _checkboxStates.get(phaseNum) || false;
|
|
2247
2632
|
phases.push({
|
|
2248
2633
|
number: phaseNum,
|
|
2634
|
+
...(displayId ? { display_id: displayId } : {}),
|
|
2249
2635
|
name: phaseName,
|
|
2250
2636
|
goal,
|
|
2251
2637
|
depends_on,
|
|
@@ -2286,7 +2672,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2286
2672
|
.filter((p) => p['phase_complete'] === true)
|
|
2287
2673
|
.map((p) => normalizePhaseNumber(p['number'])));
|
|
2288
2674
|
const phaseMap = new Map(phases.map((p) => [normalizePhaseNumber(p['number']), p]));
|
|
2289
|
-
const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s
|
|
2675
|
+
const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s*.*${phaseHeadingPrefixNoCapture}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
|
|
2290
2676
|
let _allMatch;
|
|
2291
2677
|
while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
|
|
2292
2678
|
const phaseNum = normalizePhaseNumber(_allMatch[1]);
|
|
@@ -2312,13 +2698,47 @@ function cmdInitManager(cwd, raw) {
|
|
|
2312
2698
|
function hasDepRelationship(numA, numB) {
|
|
2313
2699
|
return reaches(numA, numB) || reaches(numB, numA);
|
|
2314
2700
|
}
|
|
2701
|
+
// #4764: a phase reference in depends_on prose is a PHASE-SHAPED token in
|
|
2702
|
+
// context — directly following "Phase"/"Phases" — never a bare digit run.
|
|
2703
|
+
// The previous whole-field scrape matched the token grammar against every
|
|
2704
|
+
// digit run, so calendar dates ("2026-09-14" → 2026, 09, 14), git shas
|
|
2705
|
+
// ("8bf403100d" → 8b, 403100d, …), bracketed ledger ids (WINDOWS #1843) and
|
|
2706
|
+
// the row's OWN number all became "dependencies", and deps_satisfied came
|
|
2707
|
+
// back false for phases whose prose declares none (50 of 92 phases in the
|
|
2708
|
+
// reporter's milestone). The anchored grammar (owned by phase-id.cts as
|
|
2709
|
+
// PHASE_DEP_REF_SOURCE, shared with planning-inspect's dependencies) keeps
|
|
2710
|
+
// lists fully extracted ("Phases 601 and 602", "Phase 601, 602, and 603",
|
|
2711
|
+
// "Phase 1-3") — silently dropping a REAL dependency would clear
|
|
2712
|
+
// deps_satisfied prematurely, the dangerous direction. Negation prose
|
|
2713
|
+
// ("dropped the dependency on Phase 654") is NOT detected: the issue's own
|
|
2714
|
+
// minimum keeps such tokens.
|
|
2715
|
+
const depPhaseRefRe = new RegExp(`${PHASE_DEP_REF_SOURCE}`, 'gi');
|
|
2716
|
+
const depTokenRe = new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi');
|
|
2315
2717
|
for (const phase of phases) {
|
|
2316
2718
|
if (!phase['depends_on'] ||
|
|
2317
2719
|
/^none$/i.test(phase['depends_on'].trim())) {
|
|
2318
2720
|
phase['deps_satisfied'] = true;
|
|
2319
2721
|
}
|
|
2320
2722
|
else {
|
|
2321
|
-
const
|
|
2723
|
+
const prose = phase['depends_on'];
|
|
2724
|
+
const ownNumber = normalizePhaseNumber(phase['number']);
|
|
2725
|
+
const depNums = [];
|
|
2726
|
+
const seen = new Set();
|
|
2727
|
+
let refMatch;
|
|
2728
|
+
depPhaseRefRe.lastIndex = 0;
|
|
2729
|
+
while ((refMatch = depPhaseRefRe.exec(prose)) !== null) {
|
|
2730
|
+
let tok;
|
|
2731
|
+
depTokenRe.lastIndex = 0;
|
|
2732
|
+
while ((tok = depTokenRe.exec(refMatch[1])) !== null) {
|
|
2733
|
+
const normalized = normalizePhaseNumber(tok[0]);
|
|
2734
|
+
if (normalized === ownNumber)
|
|
2735
|
+
continue; // #4764: never the row's own phase
|
|
2736
|
+
if (seen.has(normalized))
|
|
2737
|
+
continue;
|
|
2738
|
+
seen.add(normalized);
|
|
2739
|
+
depNums.push(tok[0]);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2322
2742
|
phase['deps_satisfied'] = depNums.every((n) => completedNums.has(normalizePhaseNumber(n)));
|
|
2323
2743
|
phase['dep_phases'] = depNums;
|
|
2324
2744
|
}
|
|
@@ -2447,10 +2867,23 @@ function cmdInitManager(cwd, raw) {
|
|
|
2447
2867
|
recommended_actions: filteredActions,
|
|
2448
2868
|
waiting_signal: waitingSignal,
|
|
2449
2869
|
all_complete: completedCount === nonBacklogPhases.length && nonBacklogPhases.length > 0,
|
|
2450
|
-
|
|
2870
|
+
// #4455 follow-up (code-review finding): PROJECT.md is shared across
|
|
2871
|
+
// workstreams — see cmdInitCompleteMilestone's projectPath comment for
|
|
2872
|
+
// the full evidence.
|
|
2873
|
+
project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
|
|
2451
2874
|
roadmap_exists: true,
|
|
2452
2875
|
state_exists: true,
|
|
2453
2876
|
manager_flags: managerFlags,
|
|
2877
|
+
// #4455: workstream-scoped STATE/ROADMAP/milestone-archive paths — same
|
|
2878
|
+
// pattern cmdInitPlanPhase already uses (existence-checked, toPosixPath'd,
|
|
2879
|
+
// null when absent) plus the archive dir composition milestone.cts's
|
|
2880
|
+
// `cmdMilestoneComplete` uses (#1911: planningPaths(cwd).planning +
|
|
2881
|
+
// 'milestones', workstream-aware). autonomous.md's discover_phases/
|
|
2882
|
+
// iterate/lifecycle steps consume these instead of hardcoding
|
|
2883
|
+
// `.planning/STATE.md` / `.planning/milestones/...`.
|
|
2884
|
+
state_path: node_fs_1.default.existsSync(paths.state) ? toPosixPath(paths.state) : null,
|
|
2885
|
+
roadmap_path: node_fs_1.default.existsSync(paths.roadmap) ? toPosixPath(paths.roadmap) : null,
|
|
2886
|
+
archive_dir: toPosixPath(node_path_1.default.join(paths.planning, 'milestones')),
|
|
2454
2887
|
};
|
|
2455
2888
|
output(withProjectRoot(cwd, result), raw);
|
|
2456
2889
|
}
|
|
@@ -2469,10 +2902,57 @@ function cmdInitManager(cwd, raw) {
|
|
|
2469
2902
|
*/
|
|
2470
2903
|
function cmdInitCompleteMilestone(cwd, raw, options = {}) {
|
|
2471
2904
|
const gitCreateTag = detectGitCreateTag(cwd);
|
|
2905
|
+
// #4455: workstream-scoped STATE/ROADMAP/milestone-archive paths for
|
|
2906
|
+
// complete-milestone.md's reorganize_roadmap_and_delete_originals step —
|
|
2907
|
+
// same pattern cmdInitPlanPhase already uses, mirrored here since this is
|
|
2908
|
+
// that workflow's own dedicated init entry point.
|
|
2909
|
+
const planningBase = planningDir(cwd);
|
|
2910
|
+
const statePath = node_path_1.default.join(planningBase, 'STATE.md');
|
|
2911
|
+
const roadmapPath = node_path_1.default.join(planningBase, 'ROADMAP.md');
|
|
2912
|
+
const archiveDir = node_path_1.default.join(planningBase, 'milestones');
|
|
2913
|
+
// #4455 follow-up (code-review finding): MILESTONES.md is workstream-scoped
|
|
2914
|
+
// too — cmdMilestoneComplete (src/milestone.cts) writes it via
|
|
2915
|
+
// planningPaths(cwd).planning (the workstream base, not root; #1911). It is
|
|
2916
|
+
// not the deliberately-root-scoped exception `todos` is (#4256) — an
|
|
2917
|
+
// earlier version of this fix wrongly treated it as a shared root file,
|
|
2918
|
+
// which would have made the safety commit below silently miss the actual
|
|
2919
|
+
// file milestone.complete just wrote under an active workstream.
|
|
2920
|
+
const milestonesPath = node_path_1.default.join(planningBase, 'MILESTONES.md');
|
|
2921
|
+
// #4455 follow-up round 2 (self-discovered regression): PROJECT.md, unlike
|
|
2922
|
+
// MILESTONES.md, is genuinely SHARED across a project's own workstreams —
|
|
2923
|
+
// never cloned per workstream. gsd-core/references/workstream-flag.md's
|
|
2924
|
+
// directory diagram marks it `# Shared`; new-milestone.md states it
|
|
2925
|
+
// outright ("PROJECT.md is shared across workstreams") and explicitly
|
|
2926
|
+
// SKIPS writing its `## Current Milestone` heading under an active
|
|
2927
|
+
// workstream specifically to avoid clobbering the one shared file (#2308);
|
|
2928
|
+
// cmdWorkstreamCreate (src/workstream.cts) never creates a PROJECT.md
|
|
2929
|
+
// under a workstream directory. The first version of this #4455 follow-up
|
|
2930
|
+
// wrongly generalized from planningPaths()'s structural shape (which
|
|
2931
|
+
// composes `project` under the workstream base) without checking an
|
|
2932
|
+
// actual PROJECT.md write path — resolved against planningRoot(cwd)
|
|
2933
|
+
// (round 2), but that ALSO ignores the separate GSD_PROJECT dimension
|
|
2934
|
+
// (multi-project namespacing, #3749: PROJECT.md legitimately lives at
|
|
2935
|
+
// `.planning/<project>/PROJECT.md` when GSD_PROJECT is set — a real,
|
|
2936
|
+
// tested, pre-existing feature planningRoot's blanket root-only read
|
|
2937
|
+
// broke), caught by gsd-test on this fix's own first push. `planningDir`
|
|
2938
|
+
// with `ws` explicitly nulled (never read from GSD_WORKSTREAM) but
|
|
2939
|
+
// `project` left to default from GSD_PROJECT is the correct middle
|
|
2940
|
+
// ground: respects project-namespacing, ignores workstream-namespacing.
|
|
2941
|
+
const projectPath = node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md');
|
|
2942
|
+
// REQUIREMENTS.md is workstream-scoped the same way (planningPaths(cwd).requirements,
|
|
2943
|
+
// src/planning-workspace.cts) — the git-rm-after-archive step needs the
|
|
2944
|
+
// resolved path too, not the literal root file.
|
|
2945
|
+
const requirementsPath = node_path_1.default.join(planningBase, 'REQUIREMENTS.md');
|
|
2472
2946
|
const result = {
|
|
2473
2947
|
// #2994: hoisted from complete-milestone.md's git_tag step
|
|
2474
2948
|
// <config-check> resolver (git.create_tag, fail-open default true).
|
|
2475
2949
|
git_create_tag: gitCreateTag,
|
|
2950
|
+
state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
|
|
2951
|
+
roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
|
|
2952
|
+
archive_dir: toPosixPath(archiveDir),
|
|
2953
|
+
milestones_path: node_fs_1.default.existsSync(milestonesPath) ? toPosixPath(milestonesPath) : null,
|
|
2954
|
+
project_path: node_fs_1.default.existsSync(projectPath) ? toPosixPath(projectPath) : null,
|
|
2955
|
+
requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
|
|
2476
2956
|
};
|
|
2477
2957
|
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'complete-milestone', {
|
|
2478
2958
|
gitCreateTag,
|
|
@@ -2598,7 +3078,7 @@ function cmdInitUpdate(cwd, raw, options = {}) {
|
|
|
2598
3078
|
function cmdInitTransition(cwd, raw, options = {}) {
|
|
2599
3079
|
// #3579 root-cause fix: read-only informational field — peek, don't
|
|
2600
3080
|
// self-heal (see cmdInitNewMilestone's identical rationale above).
|
|
2601
|
-
const resolvedWorkstream =
|
|
3081
|
+
const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
|
|
2602
3082
|
const workstreamActive = !!resolvedWorkstream;
|
|
2603
3083
|
const result = {
|
|
2604
3084
|
other_active_workstreams: workstreamActive
|
|
@@ -2690,7 +3170,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
2690
3170
|
// non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
|
|
2691
3171
|
// here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
|
|
2692
3172
|
// which would misreport a present-but-bad marker as no marker at all.
|
|
2693
|
-
const _resolvedWorkstream =
|
|
3173
|
+
const _resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
|
|
2694
3174
|
if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
|
|
2695
3175
|
// #3579: getActiveWorkstream now inherits a pointer-less session's read
|
|
2696
3176
|
// from the shared .planning/active-workstream marker, so reaching this
|
|
@@ -2717,13 +3197,22 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
2717
3197
|
const roadmapCheckboxStates = new Map();
|
|
2718
3198
|
try {
|
|
2719
3199
|
const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
|
|
2720
|
-
|
|
2721
|
-
|
|
3200
|
+
const progressConvention = resolvePhaseIdConvention(cwd);
|
|
3201
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
|
|
3202
|
+
// buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
|
|
3203
|
+
// bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
|
|
3204
|
+
const { regex: headingPattern, phaseNumGroup: hNumGroup, phaseNameGroup: hNameGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, progressConvention);
|
|
2722
3205
|
let hm;
|
|
2723
3206
|
while ((hm = headingPattern.exec(roadmapContent)) !== null) {
|
|
2724
|
-
roadmapPhaseNums.add(hm[
|
|
2725
|
-
roadmapPhaseNames.set(hm[
|
|
2726
|
-
}
|
|
3207
|
+
roadmapPhaseNums.add(hm[hNumGroup]);
|
|
3208
|
+
roadmapPhaseNames.set(hm[hNumGroup], hm[hNameGroup].replace(/\(INSERTED\)/i, '').trim());
|
|
3209
|
+
}
|
|
3210
|
+
// #4906 Phase 5 (#4984): NOT migrated onto phaseHeadingPrefixSrcFor — same
|
|
3211
|
+
// out-of-scope reasoning as roadmapPhaseMatchesExactPrefix/detectPhaseMvpMode
|
|
3212
|
+
// above; a direct LABEL_ONLY selector call here would have added a third
|
|
3213
|
+
// uncounted LABEL_ONLY consumer to init.cts's pinned
|
|
3214
|
+
// tests/adr-612-bracket-heading-selection.test.cjs census.
|
|
3215
|
+
// phase-id-owner: deliberately unmigrated (#4984 revert) — see comment above.
|
|
2727
3216
|
const cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
|
|
2728
3217
|
let cbm;
|
|
2729
3218
|
while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
|
|
@@ -2880,7 +3369,10 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
2880
3369
|
paused_at: pausedAt,
|
|
2881
3370
|
has_work_in_progress: !!currentPhase,
|
|
2882
3371
|
phase_mvp_mode: phaseMvpMode,
|
|
2883
|
-
|
|
3372
|
+
// #4455 follow-up (code-review finding): PROJECT.md is shared across
|
|
3373
|
+
// workstreams — see cmdInitCompleteMilestone's projectPath comment for
|
|
3374
|
+
// the full evidence.
|
|
3375
|
+
project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
|
|
2884
3376
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
2885
3377
|
state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
2886
3378
|
// #4040: partial-init discriminator (see buildInitCompletenessFields) —
|
|
@@ -2890,7 +3382,19 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
2890
3382
|
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
2891
3383
|
state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
2892
3384
|
roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
2893
|
-
|
|
3385
|
+
// #4455 follow-up: PROJECT.md is shared across workstreams.
|
|
3386
|
+
project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
|
|
3387
|
+
// #4456 correction: an isolated review pass initially "fixed" this to
|
|
3388
|
+
// planningDir(cwd, null) on the assumption that config.json is shared
|
|
3389
|
+
// like PROJECT.md (workstream-flag.md's directory diagram marks it
|
|
3390
|
+
// `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
|
|
3391
|
+
// handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
|
|
3392
|
+
// for execute-phase/new-project/new-milestone/progress, and gsd-test
|
|
3393
|
+
// caught the regression immediately. The diagram is stale for
|
|
3394
|
+
// config.json specifically (same class of staleness already found for
|
|
3395
|
+
// `milestones/` during the #4455 follow-up) — reverted to the
|
|
3396
|
+
// workstream-aware planningDir(cwd), matching the established,
|
|
3397
|
+
// ADR-governed, tested contract.
|
|
2894
3398
|
config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
2895
3399
|
};
|
|
2896
3400
|
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
@@ -3125,12 +3629,9 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
|
|
|
3125
3629
|
warn(`[agent-skills] WARNING: Global skill not found at "${displayPath}/SKILL.md" — skipping\n`);
|
|
3126
3630
|
continue;
|
|
3127
3631
|
}
|
|
3128
|
-
const
|
|
3129
|
-
if (
|
|
3130
|
-
const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) =>
|
|
3131
|
-
const rootCheck = (0, security_cjs_1.validatePath)(globalSkillMd, root, { allowAbsolute: true });
|
|
3132
|
-
return Boolean(rootCheck['safe']);
|
|
3133
|
-
});
|
|
3632
|
+
const globalSkillMdContained = (0, security_cjs_1.tryWithinRoot)(globalSkillMd, globalSkillsBase, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
3633
|
+
if (globalSkillMdContained === null) {
|
|
3634
|
+
const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => (0, security_cjs_1.tryWithinRoot)(globalSkillMd, root, security_cjs_1.PathAcceptance.AbsoluteInsideRoot) !== null);
|
|
3134
3635
|
if (!acceptedViaTrustedRoot) {
|
|
3135
3636
|
warn(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
|
|
3136
3637
|
continue;
|
|
@@ -3139,15 +3640,23 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
|
|
|
3139
3640
|
// trace, not a skip, so it must not land in the diagnostics warnings[].
|
|
3140
3641
|
process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
|
|
3141
3642
|
}
|
|
3643
|
+
// `ref` is an emitted display token, not a path anything reads or writes
|
|
3644
|
+
// through — the containment check above is a gate, not a path producer.
|
|
3645
|
+
// Emitting the validated (realpath-resolved, platform-separator) value
|
|
3646
|
+
// instead of this literal broke symlinked skill dirs and Windows output.
|
|
3647
|
+
// The only filesystem read here (existsSync above) already ran on the
|
|
3648
|
+
// lexical path before containment was checked, so ADR-4650's "use the
|
|
3649
|
+
// validated value" rule doesn't apply to this emission.
|
|
3142
3650
|
validEntries.push({ kind: 'include', ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
|
|
3143
3651
|
continue;
|
|
3144
3652
|
}
|
|
3145
|
-
const
|
|
3146
|
-
if (
|
|
3147
|
-
warn(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}":
|
|
3653
|
+
const skillPathContained = (0, security_cjs_1.tryWithinRoot)(skillPath, projectRoot);
|
|
3654
|
+
if (skillPathContained === null) {
|
|
3655
|
+
warn(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": not confined to the project directory\n`);
|
|
3148
3656
|
continue;
|
|
3149
3657
|
}
|
|
3150
|
-
|
|
3658
|
+
// ADR-4650: the validated value is the value used — never re-derive from raw input.
|
|
3659
|
+
const skillMdPath = node_path_1.default.join(skillPathContained, 'SKILL.md');
|
|
3151
3660
|
if (!node_fs_1.default.existsSync(skillMdPath)) {
|
|
3152
3661
|
// #2941: if the bare name matches a global skill, hint at the global: prefix.
|
|
3153
3662
|
// The bare name resolves as project-relative (which doesn't exist), but the
|
|
@@ -3203,20 +3712,37 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
|
|
|
3203
3712
|
// persona fallback. Triggering the fallback for claude would change the
|
|
3204
3713
|
// documented "unconfigured → empty block" contract that agent-skills tests
|
|
3205
3714
|
// pin.
|
|
3715
|
+
//
|
|
3716
|
+
// #4407 (ADR-4139 stream 2): this is the one place GSD's own agent-persona
|
|
3717
|
+
// content is served through a real code seam rather than an eagerly
|
|
3718
|
+
// @-included file, so the compact/canonical choice is made here in code
|
|
3719
|
+
// (a real exit code) instead of a prose config-get gate. Compact is tried
|
|
3720
|
+
// first when requested; a missing compact sibling falls back to canonical
|
|
3721
|
+
// with the fallback disclosed in the payload itself, never a silent switch.
|
|
3722
|
+
let agentPayloadVariant = null;
|
|
3206
3723
|
if (!block) {
|
|
3207
3724
|
const runtime = (config && config['runtime']) || process.env['GSD_RUNTIME'] || 'claude';
|
|
3208
3725
|
if (runtime !== 'claude') {
|
|
3209
3726
|
const agentCheck = checkAgentsInstalled(runtime, projectRoot);
|
|
3210
3727
|
const agentsDir = agentCheck?.agents_dir;
|
|
3211
3728
|
if (typeof agentsDir === 'string' && agentsDir.length > 0) {
|
|
3212
|
-
const
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3729
|
+
const compactRequested = readConfigJsonBoolean(projectRoot, ['workflow', 'compact_content']);
|
|
3730
|
+
const compactContent = compactRequested
|
|
3731
|
+
? readNonEmptyFileOrNull(node_path_1.default.join(agentsDir, `${agentType}.compact.md`))
|
|
3732
|
+
: null;
|
|
3733
|
+
if (compactContent !== null) {
|
|
3734
|
+
block = compactContent;
|
|
3735
|
+
agentPayloadVariant = 'compact';
|
|
3736
|
+
}
|
|
3737
|
+
else {
|
|
3738
|
+
const canonicalContent = readNonEmptyFileOrNull(node_path_1.default.join(agentsDir, `${agentType}.md`));
|
|
3739
|
+
if (canonicalContent !== null) {
|
|
3740
|
+
block = compactRequested
|
|
3741
|
+
? `<!-- gsd: no compact payload registered for ${agentType}; serving canonical -->\n\n${canonicalContent}`
|
|
3742
|
+
: canonicalContent;
|
|
3743
|
+
agentPayloadVariant = 'canonical';
|
|
3217
3744
|
}
|
|
3218
3745
|
}
|
|
3219
|
-
catch { /* agent file not found — fall through to empty block */ }
|
|
3220
3746
|
}
|
|
3221
3747
|
}
|
|
3222
3748
|
}
|
|
@@ -3263,8 +3789,8 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
|
|
|
3263
3789
|
if (jsonMode) {
|
|
3264
3790
|
// Build the Resolution<AgentSkillsValue> envelope and embed .value additively.
|
|
3265
3791
|
// Flat fields are retained unchanged for back-compat; value formalises the
|
|
3266
|
-
// Resolution convention (ADR-1411 P3, #1416). source/degraded
|
|
3267
|
-
// config-provenance extras, outside the Resolution<T> envelope.
|
|
3792
|
+
// Resolution convention (ADR-1411 P3, #1416). source/degraded/agent_payload_variant
|
|
3793
|
+
// remain config-provenance extras, outside the Resolution<T> envelope.
|
|
3268
3794
|
const resolution = (0, resolution_cjs_1.makeResolution)({ block: block || '', skills_count: normalizedPaths.length }, { configured, reason, warnings: diagnostics.warnings });
|
|
3269
3795
|
output({
|
|
3270
3796
|
agent_type: agentType,
|
|
@@ -3275,6 +3801,7 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
|
|
|
3275
3801
|
reason,
|
|
3276
3802
|
source,
|
|
3277
3803
|
degraded,
|
|
3804
|
+
agent_payload_variant: agentPayloadVariant,
|
|
3278
3805
|
value: resolution.value,
|
|
3279
3806
|
}, raw);
|
|
3280
3807
|
return;
|
|
@@ -3563,4 +4090,5 @@ module.exports = {
|
|
|
3563
4090
|
cmdAgentSkills,
|
|
3564
4091
|
buildSkillManifest,
|
|
3565
4092
|
cmdSkillManifest,
|
|
4093
|
+
renderPendingTodosMarkdown,
|
|
3566
4094
|
};
|