@opengsd/gsd-core 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -50,6 +50,20 @@ const verificationMod = require("./verification.cjs");
|
|
|
50
50
|
const uatPredicateMod = require("./uat-predicate.cjs");
|
|
51
51
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
|
|
52
52
|
const agentInstallCheck = require("./agent-install-check.cjs");
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- section-manifest.cjs is compiled from section-manifest.cts's named exports; imported as a namespace to read selectSections/SelectableSection/InvocationFacts off module.exports directly (#2932).
|
|
54
|
+
const sectionManifest = require("./section-manifest.cjs");
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- loop-resolver.cjs is an export= CommonJS module
|
|
56
|
+
const loopResolverMod = require("./loop-resolver.cjs");
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- capability-loader.cjs is compiled from capability-loader.cts's named exports; imported as a namespace to read loadRegistry off module.exports directly.
|
|
58
|
+
const capabilityLoaderMod = require("./capability-loader.cjs");
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- capability-state.cjs is an export= CommonJS module
|
|
60
|
+
const capabilityStateMod = require("./capability-state.cjs");
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- docs.cjs is an export= CommonJS module
|
|
62
|
+
const docsMod = require("./docs.cjs");
|
|
63
|
+
const { detectMonorepoWorkspaces } = docsMod;
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- workstream-inventory.cjs is an export= CommonJS module
|
|
65
|
+
const workstreamInventoryMod = require("./workstream-inventory.cjs");
|
|
66
|
+
const { getOtherActiveWorkstreamInventories } = workstreamInventoryMod;
|
|
53
67
|
const { checkAgentsInstalled } = agentInstallCheck;
|
|
54
68
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- git-base-branch.cjs is an export= CommonJS module
|
|
55
69
|
const gitBaseBranch = require("./git-base-branch.cjs");
|
|
@@ -71,6 +85,9 @@ const { determinePhaseStatus } = commandsMod;
|
|
|
71
85
|
const { extractFrontmatter } = frontmatterMod;
|
|
72
86
|
const { readVerificationStatus } = verificationMod;
|
|
73
87
|
const { evaluateUatPassed } = uatPredicateMod;
|
|
88
|
+
const { resolveLoopHooks } = loopResolverMod;
|
|
89
|
+
const { loadRegistry } = capabilityLoaderMod;
|
|
90
|
+
const { resolveCapabilityRuntimeState } = capabilityStateMod;
|
|
74
91
|
// Unused but imported for structural parity
|
|
75
92
|
void stripShippedMilestones;
|
|
76
93
|
// Accept all bold/colon variants of the Requirements header (#2769)
|
|
@@ -106,6 +123,47 @@ function guardedGetRoadmapPhase(cwd, phase, projectCode) {
|
|
|
106
123
|
}
|
|
107
124
|
return roadmapPhase;
|
|
108
125
|
}
|
|
126
|
+
// #2994: `phase_slug` is re-derived from a roadmap-only `phase_name` (no disk
|
|
127
|
+
// directory exists yet) identically at every synthetic-fallback call site
|
|
128
|
+
// below — factored out once so the slugification formula itself cannot drift.
|
|
129
|
+
function slugifyPhaseName(phaseName) {
|
|
130
|
+
return phaseName
|
|
131
|
+
? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
132
|
+
: null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* #2994 (review finding, DEFECT.GENERATIVE-FIX): shared archived/not-found
|
|
136
|
+
* fallback applied identically by `cmdInitExecutePhase`, `cmdInitPlanPhase`,
|
|
137
|
+
* `cmdInitVerifyWork`, `cmdInitCodeReview`, `cmdInitReview`, and
|
|
138
|
+
* `cmdInitDiscussPhaseAssumptions` — 6 call sites previously reproducing the
|
|
139
|
+
* exact same two-branch control flow verbatim (only the synthetic
|
|
140
|
+
* replacement object's field set differs per caller, supplied here via
|
|
141
|
+
* `buildFallback`). `cmdInitPhaseOp` is deliberately left untouched (CRITICAL
|
|
142
|
+
* blast radius, 179 dependents) even though it follows the same shape, since
|
|
143
|
+
* its own fallback object differs by one field (`has_reviews` absent) and is
|
|
144
|
+
* not a byte-identical copy.
|
|
145
|
+
*
|
|
146
|
+
* Behavior-preserving by construction: every original call site either (a)
|
|
147
|
+
* unconditionally computed `roadmapPhase` once up front and then applied
|
|
148
|
+
* `phaseInfo?.archived && roadmapPhase?.found -> null` followed by
|
|
149
|
+
* `!phaseInfo && roadmapPhase?.found -> fallback`, or (b) computed
|
|
150
|
+
* `roadmapPhase` lazily inside each of those same two conditions. Because
|
|
151
|
+
* `guardedGetRoadmapPhase` is a pure, side-effect-free read for a given
|
|
152
|
+
* `(cwd, phase, projectCode)` within one command invocation, both shapes
|
|
153
|
+
* return identical results for identical inputs — so passing one
|
|
154
|
+
* unconditionally-resolved `roadmapPhase` in here (mirroring shape (a))
|
|
155
|
+
* reproduces shape (b)'s output exactly, just without the redundant second
|
|
156
|
+
* disk read shape (b) performed when the first branch already resolved it.
|
|
157
|
+
*/
|
|
158
|
+
function applyRoadmapFallback(phaseInfo, roadmapPhase, buildFallback) {
|
|
159
|
+
if (phaseInfo?.['archived'] && roadmapPhase?.['found']) {
|
|
160
|
+
phaseInfo = null;
|
|
161
|
+
}
|
|
162
|
+
if (!phaseInfo && roadmapPhase?.['found']) {
|
|
163
|
+
phaseInfo = buildFallback(roadmapPhase);
|
|
164
|
+
}
|
|
165
|
+
return phaseInfo;
|
|
166
|
+
}
|
|
109
167
|
function listPhaseSummaryFiles(phaseDir) {
|
|
110
168
|
return scanPhasePlans(phaseDir)['summaryFiles'];
|
|
111
169
|
}
|
|
@@ -143,6 +201,10 @@ function buildPhaseCompletionProjection(cwd, phaseNumber, phaseDir, planCount, s
|
|
|
143
201
|
completion_status: projectCompletionStatus(implementationComplete, verificationPassed),
|
|
144
202
|
verification_next_action: projectedVerificationAction,
|
|
145
203
|
verification_next_command: verificationStatus.next_command,
|
|
204
|
+
// #3057 B3: only readVerificationStatus's result ever carries this flag —
|
|
205
|
+
// the `not_required` synthetic object above never does.
|
|
206
|
+
verification_stale_check_indeterminate: 'staleCheckIndeterminate' in verificationStatus
|
|
207
|
+
&& verificationStatus.staleCheckIndeterminate === true,
|
|
146
208
|
};
|
|
147
209
|
}
|
|
148
210
|
function getLatestCompletedMilestone(cwd) {
|
|
@@ -161,7 +223,7 @@ function getLatestCompletedMilestone(cwd) {
|
|
|
161
223
|
function withProjectRoot(cwd, result) {
|
|
162
224
|
result['project_root'] = cwd;
|
|
163
225
|
const activeRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
164
|
-
const agentStatus = checkAgentsInstalled(activeRuntime);
|
|
226
|
+
const agentStatus = checkAgentsInstalled(activeRuntime, cwd);
|
|
165
227
|
result['agents_installed'] = agentStatus.agents_installed;
|
|
166
228
|
result['missing_agents'] = agentStatus.missing_agents;
|
|
167
229
|
result['agents_dir'] = agentStatus.agents_dir;
|
|
@@ -250,6 +312,385 @@ function getInitGitState(cwd) {
|
|
|
250
312
|
in_nested_subdir: inNestedSubdir,
|
|
251
313
|
};
|
|
252
314
|
}
|
|
315
|
+
// #2932 (Phase 5, ADR-1671): shipped, generated artifact — see
|
|
316
|
+
// scripts/gen-section-manifest.cjs and gsd-core/workflows/section-manifest.json.
|
|
317
|
+
// Resolved the same way model-catalog.cts resolves model-catalog.json: relative
|
|
318
|
+
// to the compiled module's own directory (gsd-core/bin/lib -> gsd-core/workflows),
|
|
319
|
+
// with a GSD_SECTION_MANIFEST env override so tests can point at a temp fixture
|
|
320
|
+
// (missing/malformed-JSON degraded-path coverage) without mutating the shipped
|
|
321
|
+
// artifact — the shipped file is a shared, concurrently-read resource across
|
|
322
|
+
// parallel test runs and must never be moved/corrupted in place.
|
|
323
|
+
const _sectionManifestCandidatePath = () => process.env['GSD_SECTION_MANIFEST']
|
|
324
|
+
? node_path_1.default.resolve(process.env['GSD_SECTION_MANIFEST'])
|
|
325
|
+
: node_path_1.default.resolve(__dirname, '..', '..', 'workflows', 'section-manifest.json');
|
|
326
|
+
/**
|
|
327
|
+
* Defense-in-depth shape check for a manifest entry's `read` field, which is
|
|
328
|
+
* documented as a POSIX-normalized, repo-root-RELATIVE path (never a
|
|
329
|
+
* filesystem escape). Rejects any absolute path (POSIX leading `/`, a
|
|
330
|
+
* Windows drive prefix like `C:\`/`C:/`, or a Windows UNC/rooted path
|
|
331
|
+
* starting with `\`) and any path containing a `..` segment (checked on
|
|
332
|
+
* BOTH separators — the artifact is generated as POSIX-normalized, but this
|
|
333
|
+
* validates the raw field defensively rather than trusting that invariant).
|
|
334
|
+
* `false` here is the only accept path in {@link loadSectionManifestSections};
|
|
335
|
+
* a `true` degrades the WHOLE load to `null`, same as every other shape
|
|
336
|
+
* violation — never throws, never partially loads.
|
|
337
|
+
*/
|
|
338
|
+
function isUnsafeManifestReadPath(readPath) {
|
|
339
|
+
if (readPath.startsWith('/') || readPath.startsWith('\\'))
|
|
340
|
+
return true;
|
|
341
|
+
if (/^[a-zA-Z]:[\\/]/.test(readPath))
|
|
342
|
+
return true;
|
|
343
|
+
return readPath.split(/[\\/]/).includes('..');
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Loads and shape-validates the generated section manifest, then returns the
|
|
347
|
+
* document-order section array for exactly one named `workflow` (#2992 Phase
|
|
348
|
+
* 6.1: the artifact is now `{ workflows: { <name>: [...] } }`, keyed by
|
|
349
|
+
* `.md` basename — see `scripts/gen-section-manifest.cjs`). Returns `null`
|
|
350
|
+
* — never throws — when the artifact is missing, unreadable, malformed
|
|
351
|
+
* JSON, valid JSON of the wrong shape (INCLUDING the pre-6.1 flat
|
|
352
|
+
* `{sections:[...]}` shape, which must never be mis-attributed to any
|
|
353
|
+
* workflow — design row C4), or when `workflow` has no key in `workflows`.
|
|
354
|
+
* `Object.hasOwn` guards the key lookup so a hostile workflow name
|
|
355
|
+
* (`constructor`, `toString`, `__proto__`) can never resolve via the
|
|
356
|
+
* prototype chain instead of a genuine own key. Each entry's `read` field is
|
|
357
|
+
* additionally validated by {@link isUnsafeManifestReadPath} (rejects an
|
|
358
|
+
* absolute path or a `..` segment) — a single unsafe entry degrades the
|
|
359
|
+
* WHOLE load to `null`, all-or-nothing like every other shape violation.
|
|
360
|
+
*/
|
|
361
|
+
function loadSectionManifestSections(workflow) {
|
|
362
|
+
try {
|
|
363
|
+
const raw = node_fs_1.default.readFileSync(_sectionManifestCandidatePath(), 'utf8');
|
|
364
|
+
const parsed = JSON.parse(raw);
|
|
365
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
366
|
+
return null;
|
|
367
|
+
const workflows = parsed['workflows'];
|
|
368
|
+
if (workflows === null || typeof workflows !== 'object' || Array.isArray(workflows))
|
|
369
|
+
return null;
|
|
370
|
+
if (!Object.hasOwn(workflows, workflow))
|
|
371
|
+
return null;
|
|
372
|
+
const sections = workflows[workflow];
|
|
373
|
+
if (!Array.isArray(sections))
|
|
374
|
+
return null;
|
|
375
|
+
for (const section of sections) {
|
|
376
|
+
const readValue = section?.['read'];
|
|
377
|
+
if (!section ||
|
|
378
|
+
typeof section !== 'object' ||
|
|
379
|
+
typeof section['id'] !== 'string' ||
|
|
380
|
+
typeof section['when'] !== 'string' ||
|
|
381
|
+
typeof readValue !== 'string' ||
|
|
382
|
+
isUnsafeManifestReadPath(readValue)) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return sections;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* `state:has-prior-phases` ground truth (design doc §Behavior table, regression-gate
|
|
394
|
+
* body: "Skip if: this is the first phase (no prior phases)"): TRUE when at least
|
|
395
|
+
* one OTHER phase directory under `.planning/phases/` contains a `*-VERIFICATION.md`
|
|
396
|
+
* file. Bounded, non-throwing — an unreadable phases directory degrades to `false`
|
|
397
|
+
* rather than surfacing an error from an init query.
|
|
398
|
+
*/
|
|
399
|
+
function detectHasPriorPhases(cwd, phaseInfo) {
|
|
400
|
+
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
401
|
+
const currentDirName = phaseInfo?.['directory']
|
|
402
|
+
? node_path_1.default.basename(phaseInfo['directory'])
|
|
403
|
+
: null;
|
|
404
|
+
try {
|
|
405
|
+
if (!node_fs_1.default.existsSync(phasesDir))
|
|
406
|
+
return false;
|
|
407
|
+
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
408
|
+
for (const entry of entries) {
|
|
409
|
+
if (!entry.isDirectory() || entry.name === currentDirName)
|
|
410
|
+
continue;
|
|
411
|
+
let files;
|
|
412
|
+
try {
|
|
413
|
+
files = node_fs_1.default.readdirSync(node_path_1.default.join(phasesDir, entry.name));
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (files.some((f) => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md')) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Strict-boolean, bounded, non-throwing read of a dotted key path from
|
|
430
|
+
* `.planning/config.json` (design rows D7-D10): absent file, unreadable
|
|
431
|
+
* file (fs error), malformed JSON, a non-object intermediate segment, or a
|
|
432
|
+
* present-but-non-boolean value (e.g. the string `"true"`) all degrade to
|
|
433
|
+
* `false` — strict `=== true`, never coerced, mirrors `detectHasPriorPhases`'s
|
|
434
|
+
* degrade-to-false discipline. `keyPath` is always a fixed literal supplied
|
|
435
|
+
* by this module, never attacker/user input, so a plain bracket traversal
|
|
436
|
+
* carries no prototype hazard here.
|
|
437
|
+
*/
|
|
438
|
+
function readConfigJsonBoolean(cwd, keyPath) {
|
|
439
|
+
try {
|
|
440
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'config.json'), 'utf8');
|
|
441
|
+
let cursor = JSON.parse(raw);
|
|
442
|
+
for (const segment of keyPath) {
|
|
443
|
+
if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
444
|
+
return false;
|
|
445
|
+
cursor = cursor[segment];
|
|
446
|
+
}
|
|
447
|
+
return cursor === true;
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Bounded, non-throwing read of a dotted key path from `.planning/config.json`,
|
|
455
|
+
* returning the raw resolved value (any JSON type) or `undefined` on any
|
|
456
|
+
* degraded condition (absent file, unreadable file, malformed JSON, or a
|
|
457
|
+
* non-object intermediate segment) — the generic sibling of
|
|
458
|
+
* {@link readConfigJsonBoolean} for callers that need the actual value
|
|
459
|
+
* (a string like `code_quality.fallow.profile`) rather than a strict
|
|
460
|
+
* boolean coercion. `keyPath` is always a fixed literal supplied by this
|
|
461
|
+
* module, never attacker/user input, so a plain bracket traversal carries
|
|
462
|
+
* no prototype hazard here (same discipline as `readConfigJsonBoolean`).
|
|
463
|
+
*/
|
|
464
|
+
function readConfigJsonValue(cwd, keyPath) {
|
|
465
|
+
try {
|
|
466
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'config.json'), 'utf8');
|
|
467
|
+
let cursor = JSON.parse(raw);
|
|
468
|
+
for (const segment of keyPath) {
|
|
469
|
+
if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
470
|
+
return undefined;
|
|
471
|
+
cursor = cursor[segment];
|
|
472
|
+
}
|
|
473
|
+
return cursor;
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* `state:fallow-enabled` ground truth (#2994): resolves `code-review.md`'s
|
|
481
|
+
* `structural_pre_pass` fallow config gate — previously re-derived INSIDE the
|
|
482
|
+
* gated section body itself (`gsd_run query config-get code_quality.fallow.*`),
|
|
483
|
+
* which is circular/self-disabling the moment a section is gated on a fact
|
|
484
|
+
* its own body computes (the same hazard `state:chunked-mode` /
|
|
485
|
+
* `state:ui-phase-active` document for a compound condition). Fail-closed
|
|
486
|
+
* default `false` for `enabled`/`mcp`, matching the pre-hoist bash resolver's
|
|
487
|
+
* `2>/dev/null || echo "false"` fallback; `scope`/`profile` default to
|
|
488
|
+
* `"phase"`/`"standard"` matching that same resolver's `|| echo` fallbacks.
|
|
489
|
+
* `maxCrap` mirrors the step body's profile->threshold mapping (minimal=50,
|
|
490
|
+
* strict=15, else standard=30) so the step file never has to re-derive it.
|
|
491
|
+
*/
|
|
492
|
+
function detectFallowConfig(cwd) {
|
|
493
|
+
const enabled = readConfigJsonValue(cwd, ['code_quality', 'fallow', 'enabled']) === true;
|
|
494
|
+
const rawScope = readConfigJsonValue(cwd, ['code_quality', 'fallow', 'scope']);
|
|
495
|
+
const scope = typeof rawScope === 'string' && rawScope ? rawScope : 'phase';
|
|
496
|
+
const rawProfile = readConfigJsonValue(cwd, ['code_quality', 'fallow', 'profile']);
|
|
497
|
+
const profile = typeof rawProfile === 'string' && rawProfile ? rawProfile : 'standard';
|
|
498
|
+
const mcp = readConfigJsonValue(cwd, ['code_quality', 'fallow', 'mcp']) === true;
|
|
499
|
+
const maxCrap = profile === 'minimal' ? 50 : profile === 'strict' ? 15 : 30;
|
|
500
|
+
return { enabled, scope, profile, mcp, maxCrap };
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* `state:git-create-tag` ground truth (#2994): resolves `complete-milestone.md`'s
|
|
504
|
+
* `git_tag` step config gate — previously re-derived INSIDE a `<config-check>`
|
|
505
|
+
* sub-tag at the top of the step itself (`gsd-tools.cjs query config-get
|
|
506
|
+
* git.create_tag 2>/dev/null || echo "true"`), gating the step's OWN inclusion
|
|
507
|
+
* on a fact only that same step computed. Fail-OPEN default `true` (an unset
|
|
508
|
+
* or missing `git.create_tag` key means "create the tag"), matching the
|
|
509
|
+
* pre-hoist resolver's `|| echo "true"` fallback exactly — this is
|
|
510
|
+
* deliberately the inverse polarity of `detectFallowConfig`'s fail-closed
|
|
511
|
+
* default, mirroring the two source resolvers' own opposite defaults.
|
|
512
|
+
*/
|
|
513
|
+
function detectGitCreateTag(cwd) {
|
|
514
|
+
return readConfigJsonValue(cwd, ['git', 'create_tag']) !== false;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* `state:phase-mvp-mode` ground truth (design doc §Behavior table: ROADMAP.md
|
|
518
|
+
* `**Mode:** mvp` for the CURRENT phase). Bounded, non-throwing — an absent
|
|
519
|
+
* `phaseNumber`, an absent ROADMAP.md, an absent phase heading, or a phase
|
|
520
|
+
* section with no `**Mode:**` line (or a `**Mode:**` value other than the
|
|
521
|
+
* literal `mvp` token, case-insensitively) all degrade to `false` (D11; "a
|
|
522
|
+
* phase with no `**Mode:**` line and an absent ROADMAP are both false, but
|
|
523
|
+
* neither may throw"). Self-contained rather than reusing `phase.cts`'s
|
|
524
|
+
* private `getRoadmapModeForPhase` (unexported, and importing it here would
|
|
525
|
+
* be a cross-module surface change outside this task's scope) — but derived
|
|
526
|
+
* from the SAME extraction primitives (`extractCurrentMilestone`,
|
|
527
|
+
* `PHASE_NUMBER_TOKEN_SOURCE`-adjacent `escapeRegex`) already used by this
|
|
528
|
+
* file's own `cmdInitProgress` MVP-heading scan, so it is not a second
|
|
529
|
+
* ROADMAP-heading parser invented from scratch.
|
|
530
|
+
*/
|
|
531
|
+
function detectPhaseMvpMode(cwd, phaseNumber) {
|
|
532
|
+
if (!phaseNumber)
|
|
533
|
+
return false;
|
|
534
|
+
try {
|
|
535
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
536
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
537
|
+
return false;
|
|
538
|
+
const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
539
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
540
|
+
const escapedPhase = escapeRegex(phaseNumber);
|
|
541
|
+
const phaseHeader = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'i');
|
|
542
|
+
const headerMatch = content.match(phaseHeader);
|
|
543
|
+
if (!headerMatch || headerMatch.index === undefined)
|
|
544
|
+
return false;
|
|
545
|
+
const sectionStart = headerMatch.index;
|
|
546
|
+
const rest = content.slice(sectionStart + headerMatch[0].length);
|
|
547
|
+
const nextHeaderMatch = rest.match(/\n#{2,4}\s+Phase\s+\S/i);
|
|
548
|
+
const sectionEnd = nextHeaderMatch
|
|
549
|
+
? sectionStart + headerMatch[0].length + nextHeaderMatch.index
|
|
550
|
+
: content.length;
|
|
551
|
+
const section = content.slice(sectionStart, sectionEnd);
|
|
552
|
+
const modeMatch = section.match(/\*\*Mode:\*\*\s*([^\n]+)/i);
|
|
553
|
+
return modeMatch ? modeMatch[1].trim().toLowerCase() === 'mvp' : false;
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* `state:ui-phase-active` ground truth (#2994): whether the phase's active
|
|
561
|
+
* `plan:pre` loop hooks include the `ui-phase` step (`capabilities/ui/
|
|
562
|
+
* capability.json`'s `plan:pre` step, `ref.skill: "ui-phase"`, gated on
|
|
563
|
+
* config `workflow.ui_phase`), OR the phase directory already contains a
|
|
564
|
+
* `*-UI-SPEC.md` file. The disjunction is resolved to ONE boolean here —
|
|
565
|
+
* same discipline as `chunkedMode` above — so the `when=` grammar never
|
|
566
|
+
* sees an OR. Mirrors `cmdLoopRenderHooks`'s own registry/capability-state
|
|
567
|
+
* setup (`src/loop-resolver.cts`) rather than reinventing a second loop-hook
|
|
568
|
+
* resolution path. Bounded, non-throwing: any failure in loop-hook /
|
|
569
|
+
* registry / capability-state resolution degrades that half of the OR to
|
|
570
|
+
* `false`, never throws; the UI-SPEC file check is independently bounded.
|
|
571
|
+
*/
|
|
572
|
+
function detectUiPhaseActive(cwd, phaseInfo) {
|
|
573
|
+
let hasActiveUiStep = false;
|
|
574
|
+
try {
|
|
575
|
+
const config = loadConfig(cwd);
|
|
576
|
+
const state = resolveCapabilityRuntimeState(cwd, undefined, config);
|
|
577
|
+
const registry = loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] });
|
|
578
|
+
const capabilityStatesById = new Map();
|
|
579
|
+
for (const cap of state.capabilities || []) {
|
|
580
|
+
capabilityStatesById.set(cap.id, cap);
|
|
581
|
+
}
|
|
582
|
+
const resolved = resolveLoopHooks({ point: 'plan:pre', registry, config, cwd, capabilityStatesById });
|
|
583
|
+
hasActiveUiStep = resolved.activeHooks.some((h) => h.kind === 'step' && h.ref?.skill === 'ui-phase');
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
hasActiveUiStep = false;
|
|
587
|
+
}
|
|
588
|
+
let hasUiSpecFile = false;
|
|
589
|
+
const rawDir = phaseInfo?.['directory'];
|
|
590
|
+
if (typeof rawDir === 'string' && rawDir) {
|
|
591
|
+
try {
|
|
592
|
+
// Re-derive under planningDir(cwd)/phases/<basename> rather than trusting
|
|
593
|
+
// rawDir's own absolute/relative-ness (callers mix both — see the #2376
|
|
594
|
+
// comments elsewhere in this file), same technique as detectHasPriorPhases above.
|
|
595
|
+
const dirName = node_path_1.default.basename(rawDir);
|
|
596
|
+
const files = node_fs_1.default.readdirSync(node_path_1.default.join(planningDir(cwd), 'phases', dirName));
|
|
597
|
+
hasUiSpecFile = files.some((f) => f.endsWith('-UI-SPEC.md') || f === 'UI-SPEC.md');
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
hasUiSpecFile = false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return hasActiveUiStep || hasUiSpecFile;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Builds the `section_manifest` init-bundle field (#2932 Deliverable 2): resolves
|
|
607
|
+
* {@link sectionManifest.InvocationFacts} from this invocation, loads the generated
|
|
608
|
+
* manifest, and partitions it via the pure {@link sectionManifest.selectSections}
|
|
609
|
+
* evaluator. Returns `null` on any degraded condition (missing/malformed artifact,
|
|
610
|
+
* or an unexpected throw from the evaluator itself) — this field is additive and
|
|
611
|
+
* optional, never load-bearing for dispatch (Hyrum's Law: 22 direct init-bundle
|
|
612
|
+
* dependents must be unaffected by its absence).
|
|
613
|
+
*
|
|
614
|
+
* `flags` (D1-D5): built from `options`'s OWN keys, gated on VALUE TRUTHINESS
|
|
615
|
+
* — not merely `!== undefined`. `parseNamedArgs` (src/command-arg-projection.cts)
|
|
616
|
+
* never yields `undefined` for an absent flag of either kind: a value-flag's
|
|
617
|
+
* absence is `null`, a booleanFlag's absence is `false`. An `undefined`-only
|
|
618
|
+
* absence check therefore lets BOTH kinds of absent flag leak into `flags` as
|
|
619
|
+
* present. A present value-flag is always a non-empty string, and a present
|
|
620
|
+
* booleanFlag is always `true` — so skipping any falsy value (`undefined`,
|
|
621
|
+
* `null`, `false`, `''`, `0`) is a safe, single-rule absence test for both
|
|
622
|
+
* flag kinds; `--wave 0` still resolves to `true` via `booleanFlags`, so
|
|
623
|
+
* truthiness never misclassifies a real invocation as absent. `Object.keys`
|
|
624
|
+
* + a plain `new Set()` so a hostile option key (e.g. `constructor`) can
|
|
625
|
+
* never leak via the prototype chain.
|
|
626
|
+
*
|
|
627
|
+
* `needsCodebaseMap` is not computed in this shared facts-assembly scope —
|
|
628
|
+
* `isBrownfield && !hasCodebaseMap` is only meaningful for `new-project`
|
|
629
|
+
* (`cmdInitNewProject` already computes both operands for its own result
|
|
630
|
+
* object). Rather than recomputing it here (a second, divergence-prone
|
|
631
|
+
* codebase-map scan) or widening every call site's positional signature,
|
|
632
|
+
* callers that HAVE the fact pass it via the optional `overrides` param;
|
|
633
|
+
* every other caller passes nothing and gets `undefined` (falsy per
|
|
634
|
+
* `WHEN_PREDICATES`, never invented, never throws).
|
|
635
|
+
*/
|
|
636
|
+
function buildSectionManifestField(cwd, phaseInfo, options, workflow, overrides = {}) {
|
|
637
|
+
const sections = loadSectionManifestSections(workflow);
|
|
638
|
+
if (!sections)
|
|
639
|
+
return null;
|
|
640
|
+
const rawPhaseNumber = phaseInfo?.['phase_number'];
|
|
641
|
+
const phaseNumber = typeof rawPhaseNumber === 'string'
|
|
642
|
+
? rawPhaseNumber
|
|
643
|
+
: typeof rawPhaseNumber === 'number'
|
|
644
|
+
? String(rawPhaseNumber)
|
|
645
|
+
: null;
|
|
646
|
+
const flags = new Set();
|
|
647
|
+
for (const key of Object.keys(options)) {
|
|
648
|
+
if (!options[key])
|
|
649
|
+
continue;
|
|
650
|
+
flags.add(`--${key}`);
|
|
651
|
+
}
|
|
652
|
+
// `state:chunked-mode` (#2993) is a disjunction — `--chunked` flag OR
|
|
653
|
+
// `.planning/config.json` `workflow.plan_chunked` — resolved to ONE
|
|
654
|
+
// boolean HERE, in fact computation, never in the `when=` grammar itself
|
|
655
|
+
// (WHEN_PREDICATES['state:chunked-mode'] reads only `facts.chunkedMode`).
|
|
656
|
+
// That separation is what keeps ADR-1671:69's Greenspun guard intact: the
|
|
657
|
+
// grammar still sees exactly one atom with no operator.
|
|
658
|
+
const chunkedMode = flags.has('--chunked') || readConfigJsonBoolean(cwd, ['workflow', 'plan_chunked']);
|
|
659
|
+
const facts = {
|
|
660
|
+
flags,
|
|
661
|
+
phaseNumber,
|
|
662
|
+
hasPriorPhases: detectHasPriorPhases(cwd, phaseInfo),
|
|
663
|
+
worktreesEnabled: readConfigJsonBoolean(cwd, ['workflow', 'use_worktrees']),
|
|
664
|
+
phaseMvpMode: detectPhaseMvpMode(cwd, phaseNumber),
|
|
665
|
+
needsCodebaseMap: overrides.needsCodebaseMap,
|
|
666
|
+
chunkedMode,
|
|
667
|
+
uiPhaseActive: overrides.uiPhaseActive,
|
|
668
|
+
fallowEnabled: overrides.fallowEnabled,
|
|
669
|
+
gitCreateTag: overrides.gitCreateTag,
|
|
670
|
+
planStrategyConverge: overrides.planStrategyConverge,
|
|
671
|
+
reviewerInstancesConfigured: overrides.reviewerInstancesConfigured,
|
|
672
|
+
autoAdvanceActive: overrides.autoAdvanceActive,
|
|
673
|
+
isMonorepo: overrides.isMonorepo,
|
|
674
|
+
nextChannel: overrides.nextChannel,
|
|
675
|
+
workstreamActive: overrides.workstreamActive,
|
|
676
|
+
flatMode: overrides.flatMode,
|
|
677
|
+
};
|
|
678
|
+
try {
|
|
679
|
+
const selection = sectionManifest.selectSections(sections, facts);
|
|
680
|
+
const readById = new Map(sections.map((s) => [s.id, s.read]));
|
|
681
|
+
return {
|
|
682
|
+
workflow,
|
|
683
|
+
included: selection.included,
|
|
684
|
+
excluded: selection.excluded,
|
|
685
|
+
read: selection.included
|
|
686
|
+
.map((id) => readById.get(id))
|
|
687
|
+
.filter((p) => typeof p === 'string'),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
253
694
|
function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
254
695
|
if (!phase) {
|
|
255
696
|
error('phase required for init execute-phase');
|
|
@@ -258,28 +699,26 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
258
699
|
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
259
700
|
const milestone = getMilestoneInfo(cwd);
|
|
260
701
|
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (!phaseInfo && roadmapPhase?.['found']) {
|
|
265
|
-
const phaseName = roadmapPhase['phase_name'];
|
|
266
|
-
phaseInfo = {
|
|
702
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
703
|
+
const phaseName = rp['phase_name'];
|
|
704
|
+
return {
|
|
267
705
|
found: true,
|
|
268
706
|
directory: null,
|
|
269
|
-
phase_number:
|
|
707
|
+
phase_number: rp['phase_number'],
|
|
270
708
|
phase_name: phaseName,
|
|
271
|
-
phase_slug: phaseName
|
|
272
|
-
? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
273
|
-
: null,
|
|
709
|
+
phase_slug: slugifyPhaseName(phaseName),
|
|
274
710
|
plans: [],
|
|
275
711
|
summaries: [],
|
|
276
712
|
incomplete_plans: [],
|
|
713
|
+
halted_plans: [],
|
|
714
|
+
blocked_by: {},
|
|
715
|
+
runnable_plans: [],
|
|
277
716
|
has_research: false,
|
|
278
717
|
has_context: false,
|
|
279
718
|
has_verification: false,
|
|
280
719
|
has_reviews: false,
|
|
281
720
|
};
|
|
282
|
-
}
|
|
721
|
+
});
|
|
283
722
|
const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE);
|
|
284
723
|
const reqExtracted = reqMatch
|
|
285
724
|
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
|
@@ -313,6 +752,15 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
313
752
|
incomplete_plans: phaseInfo?.['incomplete_plans'] || [],
|
|
314
753
|
plan_count: phaseInfo?.['plans']?.length || 0,
|
|
315
754
|
incomplete_count: phaseInfo?.['incomplete_plans']?.length || 0,
|
|
755
|
+
// #2830: the halt-aware view, forwarded from the shared computation in
|
|
756
|
+
// phase-locator. Additive — `incomplete_plans`/`incomplete_count` above keep
|
|
757
|
+
// their exact name, type and semantics. Without this passthrough the shared
|
|
758
|
+
// truth is computed and then dropped at this consumer, which is the path the
|
|
759
|
+
// issue reports as regressed.
|
|
760
|
+
halted_plans: phaseInfo?.['halted_plans'] || [],
|
|
761
|
+
blocked_by: phaseInfo?.['blocked_by'] || {},
|
|
762
|
+
runnable_plans: phaseInfo?.['runnable_plans'] || [],
|
|
763
|
+
runnable_count: phaseInfo?.['runnable_plans']?.length || 0,
|
|
316
764
|
branch_name: config.branching_strategy === 'phase' && phaseInfo
|
|
317
765
|
? config.phase_branch_template
|
|
318
766
|
.replace('{project}', config.project_code || '')
|
|
@@ -359,6 +807,8 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
359
807
|
/* intentionally empty */
|
|
360
808
|
}
|
|
361
809
|
}
|
|
810
|
+
// #2932/#2992 (Phase 5/6.1): additive, optional field — degrades to null, never throws.
|
|
811
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, options, 'execute-phase');
|
|
362
812
|
output(withProjectRoot(cwd, result), raw);
|
|
363
813
|
}
|
|
364
814
|
function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
@@ -369,19 +819,14 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
369
819
|
// #2056/#2104: foreign-prefixed queries must not collapse to numeric phases.
|
|
370
820
|
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
371
821
|
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (!phaseInfo && roadmapPhase?.['found']) {
|
|
376
|
-
const phaseName = roadmapPhase['phase_name'];
|
|
377
|
-
phaseInfo = {
|
|
822
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
823
|
+
const phaseName = rp['phase_name'];
|
|
824
|
+
return {
|
|
378
825
|
found: true,
|
|
379
826
|
directory: null,
|
|
380
|
-
phase_number:
|
|
827
|
+
phase_number: rp['phase_number'],
|
|
381
828
|
phase_name: phaseName,
|
|
382
|
-
phase_slug: phaseName
|
|
383
|
-
? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
384
|
-
: null,
|
|
829
|
+
phase_slug: slugifyPhaseName(phaseName),
|
|
385
830
|
plans: [],
|
|
386
831
|
summaries: [],
|
|
387
832
|
incomplete_plans: [],
|
|
@@ -390,7 +835,7 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
390
835
|
has_verification: false,
|
|
391
836
|
has_reviews: false,
|
|
392
837
|
};
|
|
393
|
-
}
|
|
838
|
+
});
|
|
394
839
|
const reqMatch = roadmapPhase?.['section']?.match(REQUIREMENTS_HEADER_RE);
|
|
395
840
|
const reqExtracted = reqMatch
|
|
396
841
|
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
|
@@ -511,9 +956,11 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
511
956
|
/* intentionally empty */
|
|
512
957
|
}
|
|
513
958
|
}
|
|
959
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
960
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, options, 'plan-phase');
|
|
514
961
|
output(withProjectRoot(cwd, result), raw);
|
|
515
962
|
}
|
|
516
|
-
function cmdInitNewProject(cwd, raw) {
|
|
963
|
+
function cmdInitNewProject(cwd, raw, options = {}) {
|
|
517
964
|
const config = loadConfig(cwd);
|
|
518
965
|
const homedir = node_os_1.default.homedir();
|
|
519
966
|
const braveKeyFile = node_path_1.default.join(homedir, '.gsd', 'brave_api_key');
|
|
@@ -552,9 +999,16 @@ function cmdInitNewProject(cwd, raw) {
|
|
|
552
999
|
config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
553
1000
|
research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
|
|
554
1001
|
};
|
|
1002
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
1003
|
+
// needsCodebaseMap is threaded from this scope's own isBrownfield/hasCodebaseMap
|
|
1004
|
+
// computation (see `needs_codebase_map` above) so `state:needs-codebase-map` is
|
|
1005
|
+
// genuinely computed for this workflow, not left permanently false.
|
|
1006
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'new-project', {
|
|
1007
|
+
needsCodebaseMap: isBrownfield && !hasCodebaseMap,
|
|
1008
|
+
});
|
|
555
1009
|
output(withProjectRoot(cwd, result), raw);
|
|
556
1010
|
}
|
|
557
|
-
function cmdInitNewMilestone(cwd, raw) {
|
|
1011
|
+
function cmdInitNewMilestone(cwd, raw, options = {}) {
|
|
558
1012
|
const config = loadConfig(cwd);
|
|
559
1013
|
const milestone = getMilestoneInfo(cwd);
|
|
560
1014
|
const latestCompleted = getLatestCompletedMilestone(cwd);
|
|
@@ -601,9 +1055,32 @@ function cmdInitNewMilestone(cwd, raw) {
|
|
|
601
1055
|
research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
|
|
602
1056
|
milestones_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'MILESTONES.md')),
|
|
603
1057
|
};
|
|
1058
|
+
// `state:flat-mode` (#2994): whether NO workstream is active — the inverse
|
|
1059
|
+
// of `state:workstream-active` (introduced for `cmdInitTransition` below).
|
|
1060
|
+
// `new-milestone.md`'s Step 4 Part A (milestone-state write) runs ONLY in
|
|
1061
|
+
// flat mode; a workstream's own `.planning/workstreams/<name>/STATE.md`/
|
|
1062
|
+
// `ROADMAP.md`/`REQUIREMENTS.md` already carry the milestone state, so
|
|
1063
|
+
// writing the shared `## Current Milestone` heading here would clobber it
|
|
1064
|
+
// (#2308). The `when=` grammar has no negation operator (ADR-1671:69), so
|
|
1065
|
+
// Part A's condition — "skip when a workstream IS active" — cannot be
|
|
1066
|
+
// expressed by negating `state:workstream-active` in the marker; a
|
|
1067
|
+
// SEPARATE, positively-phrased atom whose fact is the inverse is the
|
|
1068
|
+
// sanctioned resolution (same discipline as `state:chunked-mode` folding
|
|
1069
|
+
// an OR — never an operator in the grammar itself). Same authoritative
|
|
1070
|
+
// source as `cmdInitTransition`: `GSD_WORKSTREAM` env, falling back to the
|
|
1071
|
+
// stored active-workstream pointer (mirrors `cmdInitProgress`'s own
|
|
1072
|
+
// resolution above).
|
|
1073
|
+
const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
|
|
1074
|
+
const workstreamActive = !!resolvedWorkstream;
|
|
1075
|
+
const flatMode = !workstreamActive;
|
|
1076
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
1077
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'new-milestone', {
|
|
1078
|
+
workstreamActive,
|
|
1079
|
+
flatMode,
|
|
1080
|
+
});
|
|
604
1081
|
output(withProjectRoot(cwd, result), raw);
|
|
605
1082
|
}
|
|
606
|
-
function cmdInitQuick(cwd, description, raw) {
|
|
1083
|
+
function cmdInitQuick(cwd, description, raw, options = {}) {
|
|
607
1084
|
const config = loadConfig(cwd);
|
|
608
1085
|
const now = new Date();
|
|
609
1086
|
const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
|
|
@@ -645,6 +1122,21 @@ function cmdInitQuick(cwd, description, raw) {
|
|
|
645
1122
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
646
1123
|
planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
|
|
647
1124
|
};
|
|
1125
|
+
// #2994: `--full` IMPLIES `--discuss`/`--research`/`--validate` — resolved to
|
|
1126
|
+
// ONE set of facts HERE, in fact computation, never in the `when=` grammar
|
|
1127
|
+
// itself (mirrors `state:chunked-mode`'s disjunction fold at
|
|
1128
|
+
// `buildSectionManifestField`'s `chunkedMode` computation above). The three
|
|
1129
|
+
// implied tokens are folded into the flags BEFORE `buildSectionManifestField`
|
|
1130
|
+
// builds its `InvocationFacts.flags` Set, so `discussion-phase`/`research-phase`/
|
|
1131
|
+
// `plan-checker-loop`/`quick-verification` (all gated on their own single
|
|
1132
|
+
// `flag:--discuss`/`flag:--research`/`flag:--validate` atom) include correctly
|
|
1133
|
+
// for a bare `/gsd:quick --full` invocation that never passed the individual
|
|
1134
|
+
// tokens — the grammar still sees exactly one atom per marker, no OR.
|
|
1135
|
+
const sectionManifestOptions = options['full']
|
|
1136
|
+
? { ...options, discuss: true, research: true, validate: true }
|
|
1137
|
+
: options;
|
|
1138
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
1139
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, sectionManifestOptions, 'quick');
|
|
648
1140
|
output(withProjectRoot(cwd, result), raw);
|
|
649
1141
|
}
|
|
650
1142
|
function cmdInitIngestDocs(cwd, raw) {
|
|
@@ -710,33 +1202,23 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
710
1202
|
const config = loadConfig(cwd);
|
|
711
1203
|
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
712
1204
|
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
: null,
|
|
731
|
-
plans: [],
|
|
732
|
-
summaries: [],
|
|
733
|
-
incomplete_plans: [],
|
|
734
|
-
has_research: false,
|
|
735
|
-
has_context: false,
|
|
736
|
-
has_verification: false,
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
}
|
|
1205
|
+
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1206
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1207
|
+
const phaseName = rp['phase_name'];
|
|
1208
|
+
return {
|
|
1209
|
+
found: true,
|
|
1210
|
+
directory: null,
|
|
1211
|
+
phase_number: rp['phase_number'],
|
|
1212
|
+
phase_name: phaseName,
|
|
1213
|
+
phase_slug: slugifyPhaseName(phaseName),
|
|
1214
|
+
plans: [],
|
|
1215
|
+
summaries: [],
|
|
1216
|
+
incomplete_plans: [],
|
|
1217
|
+
has_research: false,
|
|
1218
|
+
has_context: false,
|
|
1219
|
+
has_verification: false,
|
|
1220
|
+
};
|
|
1221
|
+
});
|
|
740
1222
|
const phaseDir = phaseInfo?.['directory'] || null;
|
|
741
1223
|
const planCount = phaseInfo?.['plans']?.length || 0;
|
|
742
1224
|
const summaryCount = phaseInfo?.['summaries']?.length || 0;
|
|
@@ -746,6 +1228,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
746
1228
|
policy: { requireVerification: true },
|
|
747
1229
|
})
|
|
748
1230
|
: null;
|
|
1231
|
+
const uiPhaseActive = detectUiPhaseActive(cwd, phaseInfo);
|
|
749
1232
|
const result = {
|
|
750
1233
|
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
|
|
751
1234
|
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
|
|
@@ -767,7 +1250,227 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
767
1250
|
uat_blockers: uatReport?.blockers ?? [],
|
|
768
1251
|
ready_to_transition: completion.phase_complete && (uatReport?.passed ?? false),
|
|
769
1252
|
},
|
|
1253
|
+
// #2994 (resolver-hoist-guard G5): hoisted `state:ui-phase-active` ground
|
|
1254
|
+
// truth (previously re-derived inline inside the automated_ui_verification
|
|
1255
|
+
// step body via its own `gsd_run loop render-hooks plan:pre --raw` call —
|
|
1256
|
+
// a circular, self-disabling resolver, since the section is only read
|
|
1257
|
+
// when this same fact is already true). Resolved once here, exposed so
|
|
1258
|
+
// the step body can consume it directly instead of recomputing it.
|
|
1259
|
+
ui_phase_active: uiPhaseActive,
|
|
1260
|
+
};
|
|
1261
|
+
// #2994 (Phase 6.3): additive, optional field — degrades to null, never throws.
|
|
1262
|
+
// phaseInfo is passed through directly (mirrors cmdInitExecutePhase / cmdInitPlanPhase)
|
|
1263
|
+
// so buildSectionManifestField's internal detectPhaseMvpMode call gets a real
|
|
1264
|
+
// phase_number/directory rather than permanently-false facts. uiPhaseActive is
|
|
1265
|
+
// computed once above (not re-derived here) and threaded through via overrides,
|
|
1266
|
+
// mirroring the fallow/git-create-tag hoist pattern.
|
|
1267
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, {}, 'verify-work', {
|
|
1268
|
+
uiPhaseActive,
|
|
1269
|
+
});
|
|
1270
|
+
output(withProjectRoot(cwd, result), raw);
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* `code-review.md`'s dedicated init entry point (#2994, epic #1671 Phase
|
|
1274
|
+
* 6.3). `code-review.md` previously routed through the shared, 20+-caller
|
|
1275
|
+
* `init.phase-op` (`cmdInitPhaseOp` below), reading only 6 of its fields
|
|
1276
|
+
* (`phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`,
|
|
1277
|
+
* `commit_docs` — verified against the workflow's own "Parse from init
|
|
1278
|
+
* JSON" line). `cmdInitPhaseOp` is CRITICAL blast radius (179 dependents
|
|
1279
|
+
* across 24 processes per the #2994 dispatch) and is never modified for
|
|
1280
|
+
* this — this function resolves phase info itself via the SAME shared
|
|
1281
|
+
* primitives `cmdInitPhaseOp` calls (`guardedFindPhase`/
|
|
1282
|
+
* `guardedGetRoadmapPhase`, plus the shared `applyRoadmapFallback` archived/
|
|
1283
|
+
* not-found fallback also used by execute-phase, plan-phase, verify-work
|
|
1284
|
+
* and review — see `applyRoadmapFallback`'s own doc comment; `cmdInitPhaseOp`
|
|
1285
|
+
* is deliberately excluded from that shared helper), producing the identical
|
|
1286
|
+
* 6-field shape rather than a second, hand-maintained copy of
|
|
1287
|
+
* `cmdInitPhaseOp`'s full ~60-field bundle. See
|
|
1288
|
+
* `tests/init-code-review-parity.test.cjs` for the DEFECT.GENERATIVE-FIX
|
|
1289
|
+
* parity guard between the two.
|
|
1290
|
+
*
|
|
1291
|
+
* Two further facts are resolved and exposed here that `init.phase-op`
|
|
1292
|
+
* never carried: the fallow structural-pre-pass config gate
|
|
1293
|
+
* (`detectFallowConfig`, `state:fallow-enabled`) and the `--fix` flag
|
|
1294
|
+
* (folded into `options` so `buildSectionManifestField` picks it up as
|
|
1295
|
+
* `flag:--fix`).
|
|
1296
|
+
*/
|
|
1297
|
+
function cmdInitCodeReview(cwd, phase, raw, options = {}) {
|
|
1298
|
+
const config = loadConfig(cwd);
|
|
1299
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1300
|
+
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1301
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1302
|
+
const rpName = rp['phase_name'];
|
|
1303
|
+
return {
|
|
1304
|
+
found: true,
|
|
1305
|
+
directory: null,
|
|
1306
|
+
phase_number: rp['phase_number'],
|
|
1307
|
+
phase_name: rpName,
|
|
1308
|
+
phase_slug: slugifyPhaseName(rpName),
|
|
1309
|
+
};
|
|
1310
|
+
});
|
|
1311
|
+
const phaseDir = phaseInfo?.['directory'] || null;
|
|
1312
|
+
const phaseNumber = phaseInfo?.['phase_number'] || null;
|
|
1313
|
+
const phaseName = phaseInfo?.['phase_name'] || null;
|
|
1314
|
+
const fallow = detectFallowConfig(cwd);
|
|
1315
|
+
const result = {
|
|
1316
|
+
commit_docs: config.commit_docs,
|
|
1317
|
+
phase_found: !!phaseInfo,
|
|
1318
|
+
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1319
|
+
phase_dir: phaseDir ? toPosixPath(node_path_1.default.join(cwd, phaseDir)) : null,
|
|
1320
|
+
phase_number: phaseNumber,
|
|
1321
|
+
phase_name: phaseName,
|
|
1322
|
+
padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
|
|
1323
|
+
// #2994: hoisted fallow config-gate resolution (previously re-derived
|
|
1324
|
+
// inline inside code-review.md's structural_pre_pass step body — a
|
|
1325
|
+
// circular self-disabling gate now resolved once here at init time).
|
|
1326
|
+
fallow_enabled: fallow.enabled,
|
|
1327
|
+
fallow_scope: fallow.scope,
|
|
1328
|
+
fallow_profile: fallow.profile,
|
|
1329
|
+
fallow_mcp: fallow.mcp,
|
|
1330
|
+
fallow_max_crap: fallow.maxCrap,
|
|
1331
|
+
};
|
|
1332
|
+
// #2994 (Phase 6.3): additive, optional field — degrades to null, never throws.
|
|
1333
|
+
const sectionManifestOptions = {
|
|
1334
|
+
...options,
|
|
1335
|
+
fix: options['fix'] || undefined,
|
|
1336
|
+
};
|
|
1337
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, sectionManifestOptions, 'code-review', {
|
|
1338
|
+
fallowEnabled: fallow.enabled,
|
|
1339
|
+
});
|
|
1340
|
+
output(withProjectRoot(cwd, result), raw);
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* `review.md`'s dedicated init entry point (#2994, epic #1671 Phase 6.3
|
|
1344
|
+
* amendment). `review.md` previously routed through the shared, 20+-caller
|
|
1345
|
+
* `init.phase-op` (`cmdInitPhaseOp` below), reading only 3 of its fields
|
|
1346
|
+
* (`phase_dir`, `phase_number`, `padded_phase` — verified against the
|
|
1347
|
+
* workflow's own "Read from init" line in `gather_context`). `cmdInitPhaseOp`
|
|
1348
|
+
* is CRITICAL blast radius (179 dependents across 24 processes) and is never
|
|
1349
|
+
* modified for this — this function resolves phase info itself via the SAME
|
|
1350
|
+
* shared primitives `cmdInitPhaseOp` calls (`guardedFindPhase`/
|
|
1351
|
+
* `guardedGetRoadmapPhase`), plus the shared `applyRoadmapFallback`
|
|
1352
|
+
* archived/not-found fallback (see its own doc comment), producing the
|
|
1353
|
+
* identical 3-field shape rather than a second, hand-maintained copy of
|
|
1354
|
+
* `cmdInitPhaseOp`'s full ~60-field bundle.
|
|
1355
|
+
*
|
|
1356
|
+
* One further fact is resolved and exposed here that `init.phase-op` never
|
|
1357
|
+
* carried: whether reviewer instances are configured
|
|
1358
|
+
* (`.planning/config.json`'s `review.reviewer_instances`, present AND
|
|
1359
|
+
* non-empty — `state:reviewer-instances-configured`), reusing
|
|
1360
|
+
* `readConfigJsonValue` (added for `detectFallowConfig`) rather than a
|
|
1361
|
+
* second, divergence-prone config reader (DEFECT.GENERATIVE-FIX).
|
|
1362
|
+
*/
|
|
1363
|
+
function cmdInitReview(cwd, phase, raw, options = {}) {
|
|
1364
|
+
const config = loadConfig(cwd);
|
|
1365
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1366
|
+
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1367
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => ({
|
|
1368
|
+
found: true,
|
|
1369
|
+
directory: null,
|
|
1370
|
+
phase_number: rp['phase_number'],
|
|
1371
|
+
phase_name: rp['phase_name'],
|
|
1372
|
+
}));
|
|
1373
|
+
const phaseDir = phaseInfo?.['directory'] || null;
|
|
1374
|
+
const phaseNumber = phaseInfo?.['phase_number'] || null;
|
|
1375
|
+
// #2994: `state:reviewer-instances-configured` ground truth — present AND
|
|
1376
|
+
// non-empty `review.reviewer_instances` object. A missing key, a non-object
|
|
1377
|
+
// value, or an empty object all resolve to `false` (fail-closed, matching
|
|
1378
|
+
// the workflow's own pre-hoist prose gate — "Unconfigured -> default path
|
|
1379
|
+
// unchanged").
|
|
1380
|
+
const rawReviewerInstances = readConfigJsonValue(cwd, ['review', 'reviewer_instances']);
|
|
1381
|
+
const reviewerInstancesConfigured = rawReviewerInstances !== null &&
|
|
1382
|
+
typeof rawReviewerInstances === 'object' &&
|
|
1383
|
+
!Array.isArray(rawReviewerInstances) &&
|
|
1384
|
+
Object.keys(rawReviewerInstances).length > 0;
|
|
1385
|
+
const result = {
|
|
1386
|
+
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1387
|
+
phase_dir: phaseDir ? toPosixPath(node_path_1.default.join(cwd, phaseDir)) : null,
|
|
1388
|
+
phase_number: phaseNumber,
|
|
1389
|
+
padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
|
|
1390
|
+
};
|
|
1391
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, options, 'review', {
|
|
1392
|
+
reviewerInstancesConfigured,
|
|
1393
|
+
});
|
|
1394
|
+
output(withProjectRoot(cwd, result), raw);
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* `discuss-phase-assumptions.md`'s dedicated init entry point (#2994, epic
|
|
1398
|
+
* #1671 Phase 6.3 amendment). Previously routed through the shared,
|
|
1399
|
+
* 20+-caller `init.phase-op` (`cmdInitPhaseOp` below), reading 14 of its
|
|
1400
|
+
* fields (`commit_docs`, `phase_found`, `phase_dir`, `phase_number`,
|
|
1401
|
+
* `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`,
|
|
1402
|
+
* `has_plans`, `has_verification`, `plan_count`, `roadmap_exists`,
|
|
1403
|
+
* `planning_exists` — verified against the workflow's own "Parse JSON for"
|
|
1404
|
+
* line). `cmdInitPhaseOp` is CRITICAL blast radius (179 dependents across 24
|
|
1405
|
+
* processes) and is never modified for this — this function resolves phase
|
|
1406
|
+
* info itself via the SAME shared primitives `cmdInitPhaseOp` calls
|
|
1407
|
+
* (`guardedFindPhase`/`guardedGetRoadmapPhase`), plus the shared
|
|
1408
|
+
* `applyRoadmapFallback` archived/not-found fallback (see its own doc
|
|
1409
|
+
* comment) producing the identical fallback shape (`plans: []`,
|
|
1410
|
+
* `has_research: false`, `has_context: false`, `has_verification: false`)
|
|
1411
|
+
* rather than a second, hand-maintained copy of `cmdInitPhaseOp`'s full
|
|
1412
|
+
* ~60-field bundle.
|
|
1413
|
+
*
|
|
1414
|
+
* One further fact is resolved and exposed here that `init.phase-op` never
|
|
1415
|
+
* carried: `state:auto-advance-active` — the workflow's own `auto_advance`
|
|
1416
|
+
* step resolves `--auto` OR a consolidated `check auto-mode --pick active`
|
|
1417
|
+
* fact (itself `workflow._auto_chain_active` OR `workflow.auto_advance`) via
|
|
1418
|
+
* a runtime `gsd_run` call; that identical disjunction is folded into ONE
|
|
1419
|
+
* boolean FACT here (same discipline as `state:chunked-mode` /
|
|
1420
|
+
* `state:plan-strategy-converge`), exposed as `auto_advance_active`.
|
|
1421
|
+
*/
|
|
1422
|
+
function cmdInitDiscussPhaseAssumptions(cwd, phase, raw, options = {}) {
|
|
1423
|
+
const config = loadConfig(cwd);
|
|
1424
|
+
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
|
|
1425
|
+
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
|
|
1426
|
+
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
|
|
1427
|
+
const rpName = rp['phase_name'];
|
|
1428
|
+
return {
|
|
1429
|
+
found: true,
|
|
1430
|
+
directory: null,
|
|
1431
|
+
phase_number: rp['phase_number'],
|
|
1432
|
+
phase_name: rpName,
|
|
1433
|
+
phase_slug: slugifyPhaseName(rpName),
|
|
1434
|
+
plans: [],
|
|
1435
|
+
has_research: false,
|
|
1436
|
+
has_context: false,
|
|
1437
|
+
has_verification: false,
|
|
1438
|
+
};
|
|
1439
|
+
});
|
|
1440
|
+
const phaseDir = phaseInfo?.['directory'] || null;
|
|
1441
|
+
const phaseNumber = phaseInfo?.['phase_number'] || null;
|
|
1442
|
+
const phaseName = phaseInfo?.['phase_name'] || null;
|
|
1443
|
+
// #2994: mirrors discuss-phase-assumptions.md's own auto_advance step
|
|
1444
|
+
// resolver — `--auto` flag OR the consolidated `check auto-mode --pick
|
|
1445
|
+
// active` fact (workflow._auto_chain_active OR workflow.auto_advance).
|
|
1446
|
+
const autoAdvanceActive = options['auto'] === true ||
|
|
1447
|
+
readConfigJsonBoolean(cwd, ['workflow', '_auto_chain_active']) ||
|
|
1448
|
+
readConfigJsonBoolean(cwd, ['workflow', 'auto_advance']);
|
|
1449
|
+
const result = {
|
|
1450
|
+
commit_docs: config.commit_docs,
|
|
1451
|
+
phase_found: !!phaseInfo,
|
|
1452
|
+
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
|
|
1453
|
+
phase_dir: phaseDir ? toPosixPath(node_path_1.default.join(cwd, phaseDir)) : null,
|
|
1454
|
+
phase_number: phaseNumber,
|
|
1455
|
+
phase_name: phaseName,
|
|
1456
|
+
phase_slug: phaseInfo?.['phase_slug'] || null,
|
|
1457
|
+
padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
|
|
1458
|
+
has_research: phaseInfo?.['has_research'] || false,
|
|
1459
|
+
has_context: phaseInfo?.['has_context'] || false,
|
|
1460
|
+
has_plans: (phaseInfo?.['plans']?.length || 0) > 0,
|
|
1461
|
+
has_verification: phaseInfo?.['has_verification'] || false,
|
|
1462
|
+
plan_count: phaseInfo?.['plans']?.length || 0,
|
|
1463
|
+
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1464
|
+
planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
|
|
1465
|
+
};
|
|
1466
|
+
// #2994 (Phase 6.3): additive, optional field — degrades to null, never throws.
|
|
1467
|
+
const sectionManifestOptions = {
|
|
1468
|
+
...options,
|
|
1469
|
+
auto: options['auto'] || undefined,
|
|
770
1470
|
};
|
|
1471
|
+
result['section_manifest'] = buildSectionManifestField(cwd, phaseInfo, sectionManifestOptions, 'discuss-phase-assumptions', {
|
|
1472
|
+
autoAdvanceActive,
|
|
1473
|
+
});
|
|
771
1474
|
output(withProjectRoot(cwd, result), raw);
|
|
772
1475
|
}
|
|
773
1476
|
function cmdInitPhaseOp(cwd, phase, raw) {
|
|
@@ -1205,7 +1908,15 @@ function cmdInitManager(cwd, raw) {
|
|
|
1205
1908
|
/* intentionally empty */
|
|
1206
1909
|
}
|
|
1207
1910
|
const roadmapComplete = _checkboxStates.get(phaseNum) || false;
|
|
1208
|
-
|
|
1911
|
+
// #3033: a zero-plan phase (split parent — intentionally plan-less, holds
|
|
1912
|
+
// shared context for sub-phases) whose roadmap checkbox is marked complete
|
|
1913
|
+
// must resolve as complete. The original gate required completion.phase_complete
|
|
1914
|
+
// (derived from plan/summary counts), which is always false for zero-plan
|
|
1915
|
+
// phases — so the checkbox override never fired and the parent was permanently
|
|
1916
|
+
// stuck as 'researched' (an in-progress state eligible for current-phase
|
|
1917
|
+
// selection). Now: when the roadmap marks it complete AND it has zero plans,
|
|
1918
|
+
// treat it as complete regardless of the plan-count derivation.
|
|
1919
|
+
if (roadmapComplete && (completion.phase_complete || planCount === 0) && diskStatus !== 'complete') {
|
|
1209
1920
|
diskStatus = 'complete';
|
|
1210
1921
|
}
|
|
1211
1922
|
phases.push({
|
|
@@ -1405,7 +2116,222 @@ function cmdInitManager(cwd, raw) {
|
|
|
1405
2116
|
};
|
|
1406
2117
|
output(withProjectRoot(cwd, result), raw);
|
|
1407
2118
|
}
|
|
1408
|
-
|
|
2119
|
+
/**
|
|
2120
|
+
* `complete-milestone.md`'s dedicated init entry point (#2994, epic #1671
|
|
2121
|
+
* Phase 6.3). Additive alongside the workflow's existing `init.manager`
|
|
2122
|
+
* (readiness/phase-projection, `cmdInitManager` above — CRITICAL blast
|
|
2123
|
+
* radius, never modified) and `init.execute-phase` (branching-strategy
|
|
2124
|
+
* fields) calls; `cmdInitCompleteMilestone` carries NO phase-listing logic
|
|
2125
|
+
* of its own to delegate — its only job is the `git.create_tag` config-gate
|
|
2126
|
+
* fact the `git_tag` step's `<config-check>` sub-tag used to re-derive
|
|
2127
|
+
* inline (gating the step's own inclusion on a fact only that step
|
|
2128
|
+
* computed), now hoisted here and exposed as `git_create_tag`, plus the
|
|
2129
|
+
* `section_manifest` field neither `init.manager` nor `init.execute-phase`
|
|
2130
|
+
* carries.
|
|
2131
|
+
*/
|
|
2132
|
+
function cmdInitCompleteMilestone(cwd, raw, options = {}) {
|
|
2133
|
+
const gitCreateTag = detectGitCreateTag(cwd);
|
|
2134
|
+
const result = {
|
|
2135
|
+
// #2994: hoisted from complete-milestone.md's git_tag step
|
|
2136
|
+
// <config-check> resolver (git.create_tag, fail-open default true).
|
|
2137
|
+
git_create_tag: gitCreateTag,
|
|
2138
|
+
};
|
|
2139
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'complete-milestone', {
|
|
2140
|
+
gitCreateTag,
|
|
2141
|
+
});
|
|
2142
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* `autonomous.md`'s dedicated init entry point (#2994, epic #1671 Phase
|
|
2146
|
+
* 6.3). Additive alongside the workflow's existing `init.milestone-op`
|
|
2147
|
+
* (`cmdInitMilestoneOp`), `init.manager` (`cmdInitManager`), and
|
|
2148
|
+
* `init.phase-op` (`cmdInitPhaseOp`) calls — all three are CRITICAL blast
|
|
2149
|
+
* radius (179 dependents across 24 processes) and are never modified for
|
|
2150
|
+
* this; `autonomous.md` keeps every one of those calls exactly as it had
|
|
2151
|
+
* them. `cmdInitAutonomous` carries NO phase-listing logic of its own to
|
|
2152
|
+
* delegate — its only job is the `PLAN_STRATEGY` disjunction the workflow's
|
|
2153
|
+
* own bash resolver (`PLAN_STRATEGY="converge"` on `--converge` OR
|
|
2154
|
+
* `--cross-ai`) already computes at the top of the `initialize` step, now
|
|
2155
|
+
* mirrored here as a single boolean FACT (same discipline as
|
|
2156
|
+
* `state:chunked-mode`/`state:ui-phase-active`: the disjunction is resolved
|
|
2157
|
+
* ONCE, in fact computation, never in the `when=` grammar), exposed as
|
|
2158
|
+
* `plan_strategy_converge`, plus the `section_manifest` field none of the
|
|
2159
|
+
* three existing calls carries.
|
|
2160
|
+
*/
|
|
2161
|
+
function cmdInitAutonomous(cwd, raw, options = {}) {
|
|
2162
|
+
const planStrategyConverge = options['converge'] === true || options['cross-ai'] === true;
|
|
2163
|
+
const result = {
|
|
2164
|
+
// #2994: mirrors autonomous.md's own PLAN_STRATEGY resolver
|
|
2165
|
+
// (--converge OR its documented alias --cross-ai).
|
|
2166
|
+
plan_strategy_converge: planStrategyConverge,
|
|
2167
|
+
};
|
|
2168
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'autonomous', {
|
|
2169
|
+
planStrategyConverge,
|
|
2170
|
+
});
|
|
2171
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* `docs-update.md`'s dedicated init entry point (#2994, epic #1671 Phase
|
|
2175
|
+
* 6.3 — final slice). `docs-update.md` previously carried NO `gsd_run query
|
|
2176
|
+
* init.*` call at all: its own `docs-init` command (`cmdDocsInit`,
|
|
2177
|
+
* src/docs.cts) is a SEPARATE, pre-existing entry point outside this
|
|
2178
|
+
* `init.*` family and is left untouched here. This function's only job is
|
|
2179
|
+
* the `section_manifest` field neither `docs-init` nor any other call
|
|
2180
|
+
* carries, gating `docs-update.md`'s `dispatch-monorepo-packages` section.
|
|
2181
|
+
*
|
|
2182
|
+
* `state:is-monorepo` ground truth: the project's monorepo workspaces list
|
|
2183
|
+
* is non-empty — reuses `detectMonorepoWorkspaces` (src/docs.cts, exported
|
|
2184
|
+
* for this purpose) rather than a second, divergence-prone workspace-glob
|
|
2185
|
+
* scan (DEFECT.GENERATIVE-FIX dual surface); this is the SAME detector that
|
|
2186
|
+
* already backs `docs-init`'s own `monorepo_workspaces` field.
|
|
2187
|
+
*/
|
|
2188
|
+
function cmdInitDocsUpdate(cwd, raw, options = {}) {
|
|
2189
|
+
const isMonorepo = detectMonorepoWorkspaces(cwd).length > 0;
|
|
2190
|
+
const result = {};
|
|
2191
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'docs-update', {
|
|
2192
|
+
isMonorepo,
|
|
2193
|
+
});
|
|
2194
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* `update.md`'s dedicated init entry point (#2994, epic #1671 Phase 6.3 —
|
|
2198
|
+
* final slice). `update.md` previously carried NO `gsd_run query init.*`
|
|
2199
|
+
* call at all: it resolves `gsd-tools.cjs` itself (its own bespoke
|
|
2200
|
+
* `PREFERRED_CONFIG_DIR`/`PREFERRED_RUNTIME`-aware `$GSD_TOOLS` cascade,
|
|
2201
|
+
* `update.md` ~lines 13-45) because the update workflow must run BEFORE any
|
|
2202
|
+
* install can be assumed resolvable — the canonical launcher preamble's
|
|
2203
|
+
* fixed candidate list is not a substitute for that cascade, and both
|
|
2204
|
+
* resolutions assign the identical `$GSD_TOOLS` shell variable, so copying
|
|
2205
|
+
* the canonical preamble in ADDITION to the existing cascade would silently
|
|
2206
|
+
* clobber the value `backup_custom_files`/`restore_custom_files` (later
|
|
2207
|
+
* steps) still depend on. This function is invoked via that ALREADY
|
|
2208
|
+
* resolved `$GSD_TOOLS`, not a redundant `gsd_run()` shell function.
|
|
2209
|
+
*
|
|
2210
|
+
* `state:next-channel` ground truth: `--next` OR its documented alias
|
|
2211
|
+
* `--rc` (same disjunction-to-one-boolean discipline as
|
|
2212
|
+
* `state:chunked-mode`/`state:plan-strategy-converge`). This DELIBERATELY
|
|
2213
|
+
* does not replace `update.md`'s own `parse_update_channel` case-statement
|
|
2214
|
+
* (`TAG="next"`/`TAG="latest"`) — issue #815's regression test
|
|
2215
|
+
* (`tests/issue-815-update-next-channel.test.cjs`) asserts that literal
|
|
2216
|
+
* case-statement text stays in `update.md` verbatim (the npm dist-tag
|
|
2217
|
+
* selection has to run in the workflow's own shell before any `gsd_run`
|
|
2218
|
+
* round-trip), so `next_channel` exists purely to gate the `channel-banner`
|
|
2219
|
+
* section's admission — a parallel, consistent-but-not-replacing
|
|
2220
|
+
* resolution of the same flags.
|
|
2221
|
+
*/
|
|
2222
|
+
function cmdInitUpdate(cwd, raw, options = {}) {
|
|
2223
|
+
const nextChannel = options['next'] === true || options['rc'] === true;
|
|
2224
|
+
const result = {
|
|
2225
|
+
next_channel: nextChannel,
|
|
2226
|
+
};
|
|
2227
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'update', {
|
|
2228
|
+
nextChannel,
|
|
2229
|
+
});
|
|
2230
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* `transition.md`'s dedicated init entry point (#2994, epic #1671 Phase
|
|
2234
|
+
* 6.3 — final slice). `transition.md` is an internal workflow (no
|
|
2235
|
+
* user-facing `/gsd-transition` command) that previously carried NO
|
|
2236
|
+
* `gsd_run query init.*` call at all; it already establishes `gsd_run()`
|
|
2237
|
+
* via the canonical launcher preamble in its `update_roadmap_and_state`
|
|
2238
|
+
* step (before this call's insertion point in `offer_next_phase`), so no
|
|
2239
|
+
* second preamble copy is needed in the host file.
|
|
2240
|
+
*
|
|
2241
|
+
* `state:workstream-active` ground truth: a workstream is active — resolved
|
|
2242
|
+
* via `GSD_WORKSTREAM` env, falling back to the stored active-workstream
|
|
2243
|
+
* pointer (mirrors `cmdInitProgress`'s own `_resolvedWorkstream` resolution
|
|
2244
|
+
* above, the established authoritative source for "is a workstream active"
|
|
2245
|
+
* in this file).
|
|
2246
|
+
*
|
|
2247
|
+
* `other_active_workstreams` hoists the resolver-in-body hazard out of
|
|
2248
|
+
* `transition.md`'s `workstream-collision-check` step: that step's body
|
|
2249
|
+
* previously re-derived this via an inline `gsd_run query workstream.list
|
|
2250
|
+
* --raw` call gated on the identical `if [ -n "$GSD_WORKSTREAM" ]`
|
|
2251
|
+
* condition that now backs this section's OWN admission — resolving it here
|
|
2252
|
+
* instead reuses `getOtherActiveWorkstreamInventories` (src/workstream-
|
|
2253
|
+
* inventory.cts), the SAME primitive `workstream.list` itself calls
|
|
2254
|
+
* (`cmdWorkstreamList`, src/workstream.cts), pre-filtered exactly as the
|
|
2255
|
+
* step's own prose described (excludes the current workstream and any
|
|
2256
|
+
* workstream whose status contains "milestone complete" or "archived",
|
|
2257
|
+
* case-insensitively — `isCompletedInventory`), so the step body becomes a
|
|
2258
|
+
* pure JSON consumer with no `gsd_run` call of its own.
|
|
2259
|
+
*/
|
|
2260
|
+
function cmdInitTransition(cwd, raw, options = {}) {
|
|
2261
|
+
const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
|
|
2262
|
+
const workstreamActive = !!resolvedWorkstream;
|
|
2263
|
+
const result = {
|
|
2264
|
+
other_active_workstreams: workstreamActive
|
|
2265
|
+
? getOtherActiveWorkstreamInventories(cwd, resolvedWorkstream).map((inv) => ({
|
|
2266
|
+
name: inv.name,
|
|
2267
|
+
status: inv.status,
|
|
2268
|
+
}))
|
|
2269
|
+
: [],
|
|
2270
|
+
};
|
|
2271
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'transition', {
|
|
2272
|
+
workstreamActive,
|
|
2273
|
+
});
|
|
2274
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* `debug.md`'s dedicated init entry point (#3149; prerequisite for #3128).
|
|
2278
|
+
* `debug.md` previously carried NO `gsd_run query init.*` call at all — it made
|
|
2279
|
+
* THREE separate round-trips instead: `state.load` (for `commit_docs`,
|
|
2280
|
+
* `config.response_language` and `debug_dir`), `resolve-model gsd-debugger
|
|
2281
|
+
* --pick model`, and `config-get workflow.tdd_mode --raw`. Because no
|
|
2282
|
+
* debug-scoped fact was computed at any entry point, a `when=` atom naming one
|
|
2283
|
+
* would have evaluated FALSE forever — ADR-1671's admission gate (2) and the
|
|
2284
|
+
* silent-exclusion bug it exists to prevent (`docs/adr/1671-…:122-131`).
|
|
2285
|
+
*
|
|
2286
|
+
* Every field is resolved through the SAME primitive the call it replaces used,
|
|
2287
|
+
* never a second hand-maintained copy (DEFECT.GENERATIVE-FIX):
|
|
2288
|
+
*
|
|
2289
|
+
* - `commit_docs` — `loadConfig`, the same loader `cmdStateLoad` calls.
|
|
2290
|
+
* - `response_language` — NOT read here: `withProjectRoot` already injects it
|
|
2291
|
+
* when configured (#2402), which is also the shape sibling init bundles use.
|
|
2292
|
+
* It is absent, not null, when unset.
|
|
2293
|
+
* - `debug_dir` — `planningPaths(cwd).debug`, the SAME expression `cmdStateLoad`
|
|
2294
|
+
* now uses; the `debug` field was added to `PlanningPaths` (#3149) so the
|
|
2295
|
+
* location has one source instead of two kept in sync by hand.
|
|
2296
|
+
* - `debugger_model` — `resolveModelInternal`, which IS what `query
|
|
2297
|
+
* resolve-model --pick model` returns (`cmdResolveModel`, src/commands.cts).
|
|
2298
|
+
* - `tdd_mode` — the `Boolean(wf['tdd_mode'])` idiom `cmdInitExecutePhase` and
|
|
2299
|
+
* `cmdInitPlanPhase` already use. `/gsd:debug` has no `--tdd` flag, so the
|
|
2300
|
+
* sibling handlers' `options['tdd'] ||` disjunct is deliberately omitted
|
|
2301
|
+
* rather than carried as a phantom.
|
|
2302
|
+
*
|
|
2303
|
+
* `state.load` is deliberately NOT narrowed — see the note beside its own
|
|
2304
|
+
* `debug_dir` field. This handler is purely additive alongside it.
|
|
2305
|
+
*
|
|
2306
|
+
* `diagnose` is the one flag `/gsd:debug` already documents. Exposing it as a
|
|
2307
|
+
* top-level fact follows `cmdInitUpdate`'s `next_channel` and
|
|
2308
|
+
* `cmdInitAutonomous`'s `plan_strategy_converge` precedent, and is what makes
|
|
2309
|
+
* the router's flag forwarding observable. No `when=` atom consumes it yet:
|
|
2310
|
+
* admission gate (1) — a consuming section of at least 400 bytes — is #3128's
|
|
2311
|
+
* to satisfy, and shipping the atom before its section is the same
|
|
2312
|
+
* silent-exclusion bug from the other direction.
|
|
2313
|
+
*/
|
|
2314
|
+
function cmdInitDebug(cwd, raw, options = {}) {
|
|
2315
|
+
const config = loadConfig(cwd);
|
|
2316
|
+
const wf = (config.workflow ?? {});
|
|
2317
|
+
const result = {
|
|
2318
|
+
commit_docs: config.commit_docs,
|
|
2319
|
+
// #2376: absolute — debug.md builds `debug_file_path` as
|
|
2320
|
+
// `{debug_dir}/{slug}.md` for its gsd-debug-session-manager spawns, whose
|
|
2321
|
+
// own cwd may differ from the orchestrator's.
|
|
2322
|
+
debug_dir: toPosixPath(planningPaths(cwd).debug),
|
|
2323
|
+
debugger_model: resolveModelInternal(cwd, 'gsd-debugger'),
|
|
2324
|
+
tdd_mode: Boolean(wf['tdd_mode']),
|
|
2325
|
+
diagnose: options['diagnose'] === true,
|
|
2326
|
+
};
|
|
2327
|
+
// Additive, optional field — degrades to null while `debug` has no key in
|
|
2328
|
+
// `gsd-core/workflows/section-manifest.json` (it has no `gsd:section` markers
|
|
2329
|
+
// until #3128). null means "read everything", which is NOT the same as a
|
|
2330
|
+
// computed empty selection.
|
|
2331
|
+
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'debug', {});
|
|
2332
|
+
output(withProjectRoot(cwd, result), raw);
|
|
2333
|
+
}
|
|
2334
|
+
function cmdInitProgress(cwd, raw, options = {}) {
|
|
1409
2335
|
try {
|
|
1410
2336
|
pruneOrphanedWorktrees(cwd);
|
|
1411
2337
|
}
|
|
@@ -1545,6 +2471,19 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1545
2471
|
if (pauseMatch)
|
|
1546
2472
|
pausedAt = pauseMatch[1].trim();
|
|
1547
2473
|
}
|
|
2474
|
+
// #2994: the CURRENT phase's number, used both to expose `phase_mvp_mode`
|
|
2475
|
+
// at the top level (so the `mvp-display` step body can consume an
|
|
2476
|
+
// already-resolved fact instead of re-invoking `gsd_run query
|
|
2477
|
+
// phase.mvp-mode` itself — that inline resolver would otherwise gate a
|
|
2478
|
+
// section on a fact the section's own body recomputes, which is circular
|
|
2479
|
+
// and self-disabling) and to thread a real `phase_number` into
|
|
2480
|
+
// `buildSectionManifestField` below so `state:phase-mvp-mode` is genuinely
|
|
2481
|
+
// computed for this workflow rather than permanently false (the previous
|
|
2482
|
+
// `buildSectionManifestField(cwd, null, ...)` call passed no phase info at
|
|
2483
|
+
// all, so `detectPhaseMvpMode` always short-circuited on the `!phaseNumber`
|
|
2484
|
+
// guard).
|
|
2485
|
+
const currentPhaseNumber = currentPhase?.['number'] ?? null;
|
|
2486
|
+
const phaseMvpMode = detectPhaseMvpMode(cwd, currentPhaseNumber);
|
|
1548
2487
|
const result = {
|
|
1549
2488
|
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
|
|
1550
2489
|
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
|
|
@@ -1559,6 +2498,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1559
2498
|
next_phase: nextPhase,
|
|
1560
2499
|
paused_at: pausedAt,
|
|
1561
2500
|
has_work_in_progress: !!currentPhase,
|
|
2501
|
+
phase_mvp_mode: phaseMvpMode,
|
|
1562
2502
|
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
|
1563
2503
|
roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
|
|
1564
2504
|
state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
|
|
@@ -1568,6 +2508,8 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1568
2508
|
project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
|
|
1569
2509
|
config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
|
|
1570
2510
|
};
|
|
2511
|
+
// #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
|
|
2512
|
+
result['section_manifest'] = buildSectionManifestField(cwd, currentPhaseNumber ? { phase_number: currentPhaseNumber } : null, options, 'progress');
|
|
1571
2513
|
output(withProjectRoot(cwd, result), raw);
|
|
1572
2514
|
}
|
|
1573
2515
|
function detectChildRepos(dir) {
|
|
@@ -1822,7 +2764,20 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
|
|
|
1822
2764
|
}
|
|
1823
2765
|
const skillMdPath = node_path_1.default.join(projectRoot, skillPath, 'SKILL.md');
|
|
1824
2766
|
if (!node_fs_1.default.existsSync(skillMdPath)) {
|
|
1825
|
-
|
|
2767
|
+
// #2941: if the bare name matches a global skill, hint at the global: prefix.
|
|
2768
|
+
// The bare name resolves as project-relative (which doesn't exist), but the
|
|
2769
|
+
// user likely meant to reference a global skill. getGlobalSkillDir is already
|
|
2770
|
+
// imported for the global: branch above; guard on globalSkillsBase being non-null
|
|
2771
|
+
// since runtimes without a skills directory don't support the prefix.
|
|
2772
|
+
let hint = '';
|
|
2773
|
+
if (globalSkillsBase !== null) {
|
|
2774
|
+
const baseName = node_path_1.default.basename(skillPath);
|
|
2775
|
+
const globalDir = (0, runtime_homes_cjs_1.getGlobalSkillDir)(runtime, baseName);
|
|
2776
|
+
if (globalDir && node_fs_1.default.existsSync(node_path_1.default.join(globalDir, 'SKILL.md'))) {
|
|
2777
|
+
hint = ` — a global skill named "${baseName}" exists; use "global:${baseName}" to reference it`;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
warn(`[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md"${hint} — skipping\n`);
|
|
1826
2781
|
continue;
|
|
1827
2782
|
}
|
|
1828
2783
|
validEntries.push({ kind: 'include', ref: `${skillPath}/SKILL.md`, display: skillPath });
|
|
@@ -1866,7 +2821,7 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
|
|
|
1866
2821
|
if (!block) {
|
|
1867
2822
|
const runtime = (config && config['runtime']) || process.env['GSD_RUNTIME'] || 'claude';
|
|
1868
2823
|
if (runtime !== 'claude') {
|
|
1869
|
-
const agentCheck = checkAgentsInstalled(runtime);
|
|
2824
|
+
const agentCheck = checkAgentsInstalled(runtime, projectRoot);
|
|
1870
2825
|
const agentsDir = agentCheck?.agents_dir;
|
|
1871
2826
|
if (typeof agentsDir === 'string' && agentsDir.length > 0) {
|
|
1872
2827
|
const agentFile = node_path_1.default.join(agentsDir, `${agentType}.md`);
|
|
@@ -2198,11 +3153,20 @@ module.exports = {
|
|
|
2198
3153
|
cmdInitResume,
|
|
2199
3154
|
cmdInitVerifyWork,
|
|
2200
3155
|
cmdInitPhaseOp,
|
|
3156
|
+
cmdInitCodeReview,
|
|
3157
|
+
cmdInitReview,
|
|
3158
|
+
cmdInitDiscussPhaseAssumptions,
|
|
2201
3159
|
cmdInitTodos,
|
|
2202
3160
|
cmdInitMilestoneOp,
|
|
2203
3161
|
cmdInitMapCodebase,
|
|
2204
3162
|
cmdInitProgress,
|
|
2205
3163
|
cmdInitManager,
|
|
3164
|
+
cmdInitCompleteMilestone,
|
|
3165
|
+
cmdInitAutonomous,
|
|
3166
|
+
cmdInitDocsUpdate,
|
|
3167
|
+
cmdInitUpdate,
|
|
3168
|
+
cmdInitTransition,
|
|
3169
|
+
cmdInitDebug,
|
|
2206
3170
|
cmdInitNewWorkspace,
|
|
2207
3171
|
cmdInitListWorkspaces,
|
|
2208
3172
|
cmdInitRemoveWorkspace,
|