@opengsd/gsd-core 1.7.0 → 1.9.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/.opencode/plugins/gsd-core.js +45 -1
- package/README.md +2 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +78 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +49 -9
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +44 -59
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1338 -135
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +7 -2
- package/gsd-core/bin/gsd-tools.cjs +2690 -2472
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +360 -53
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +140 -27
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
- package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +326 -21
- package/gsd-core/bin/lib/config-loader.cjs +214 -30
- package/gsd-core/bin/lib/config.cjs +158 -22
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +155 -66
- package/gsd-core/bin/lib/install-engine.cjs +299 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +248 -14
- package/gsd-core/bin/lib/model-catalog.cjs +69 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +304 -9
- package/gsd-core/bin/lib/phase.cjs +258 -17
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
- package/gsd-core/bin/lib/roadmap.cjs +23 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +70 -5
- package/gsd-core/bin/lib/state-document.cjs +171 -24
- package/gsd-core/bin/lib/state-transition.cjs +50 -11
- package/gsd-core/bin/lib/state.cjs +206 -32
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +428 -11
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +44 -8
- package/gsd-core/bin/lib/verification.cjs +163 -31
- package/gsd-core/bin/lib/verify.cjs +348 -42
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -19
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +8 -6
- package/gsd-core/workflows/audit-fix.md +6 -2
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -24
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +35 -7
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +9 -0
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +110 -149
- package/gsd-core/workflows/execute-plan.md +20 -8
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +9 -5
- package/gsd-core/workflows/import.md +11 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +19 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +17 -10
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +79 -23
- package/gsd-core/workflows/new-project.md +28 -19
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +56 -51
- package/gsd-core/workflows/plan-review-convergence.md +61 -12
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +33 -10
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +172 -585
- package/gsd-core/workflows/scan.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -2
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +88 -11
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +4 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +11 -2
- package/gsd-core/workflows/ui-review.md +11 -1
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +106 -5
- package/gsd-core/workflows/validate-phase.md +13 -2
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +15 -4
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +97 -9
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +97 -9
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +10 -8
- package/pi/gsd.cjs +34 -3
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +244 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +528 -0
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -255,8 +255,13 @@ function setCapabilityState(cwd, runtimeConfigDir, desired, opts) {
|
|
|
255
255
|
try {
|
|
256
256
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
257
257
|
const runtimeArtifactLayout = require('./runtime-artifact-layout.cjs');
|
|
258
|
+
// #2322: thread the SAME composed registry (loaded above, includeInstalled:true)
|
|
259
|
+
// into layout resolution so the skills kind's stage() closure can bind a
|
|
260
|
+
// third-party capability skill to its declaring capId at staging time —
|
|
261
|
+
// required for BOTH the '*' (full-profile) fill-in and the ownership binding
|
|
262
|
+
// (see resolveRuntimeArtifactLayout's #2322 doc comment).
|
|
258
263
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
259
|
-
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, resolvedConfigDir, scope);
|
|
264
|
+
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, resolvedConfigDir, scope, registry);
|
|
260
265
|
const commandsGsdDir = _resolveCommandsGsdDir();
|
|
261
266
|
const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir);
|
|
262
267
|
// #1575: applySurface now accepts opts.resolveAttribution so surface-path
|
|
@@ -134,7 +134,26 @@ function loadPlanContents(phaseDir) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
|
|
137
|
-
|
|
137
|
+
// #2372: scanned-tag set must match the planner-canonical surfaces where a D-NN citation
|
|
138
|
+
// is meaningful. `<objective>`/`<tasks>`/`<task>`/`<action>` are the historical core. The
|
|
139
|
+
// planner is also explicitly told (plan-phase.md) to cite decisions in `<read_first>`,
|
|
140
|
+
// `<behavior>`, `<verify>`, `<acceptance_criteria>`, and `<done>` — those are now scanned too,
|
|
141
|
+
// so the gate no longer reports a false coverage gap when a decision is cited in any of them.
|
|
142
|
+
//
|
|
143
|
+
// Implementation: per-tag matching, NOT a single wide alternation. A single alternation
|
|
144
|
+
// like `<(?:a|b|c)>...<\/(?:a|b|c)>` halts the outer tag's body capture at any inner tag
|
|
145
|
+
// in the set, dropping any citation in the outer tag's prefix prose — e.g.
|
|
146
|
+
// `<action>per D-05 <verify>npm test</verify></action>` would lose D-05 because `<verify>`
|
|
147
|
+
// halts the `<action>` body before the citation. Per-tag matching avoids this: each tag's
|
|
148
|
+
// body terminates only at its OWN closing tag, so `<verify>` inside `<action>` is absorbed
|
|
149
|
+
// into `<action>`'s body (D-05 caught) AND `<verify>` is matched separately on its own pass.
|
|
150
|
+
// Each per-tag regex keeps the ReDoS-safe negative-lookahead tempering (#2128).
|
|
151
|
+
const XML_DECISION_TAG_NAMES = ['objective', 'tasks', 'task', 'action', 'read_first', 'behavior', 'verify', 'acceptance_criteria', 'done'];
|
|
152
|
+
function buildXmlDecisionTagRegex(tagName) {
|
|
153
|
+
// Per-tag: body tempering stops only at the SAME tag's reopening or closing — other
|
|
154
|
+
// scanned tags pass through as text into this body. Non-greedy `*?` to first close.
|
|
155
|
+
return new RegExp(`<${tagName}(?:\\s[^>]{0,1000})?>((?:(?!<${tagName}[\\s>])[\\s\\S])*?)<\\/${tagName}>`, 'gi');
|
|
156
|
+
}
|
|
138
157
|
function stripCommentsAndFences(text) {
|
|
139
158
|
// HTML-comment stripping stays caller-side (the seam does not strip HTML comments).
|
|
140
159
|
// Stop-at-next-open body (ReDoS-safe, #2128); an UNCLOSED `<!--` does not match,
|
|
@@ -162,9 +181,12 @@ function extractYamlBlock(frontmatter, key) {
|
|
|
162
181
|
}
|
|
163
182
|
function extractXmlTagBodies(text) {
|
|
164
183
|
const parts = [];
|
|
165
|
-
for (const
|
|
166
|
-
|
|
167
|
-
|
|
184
|
+
for (const tagName of XML_DECISION_TAG_NAMES) {
|
|
185
|
+
const re = buildXmlDecisionTagRegex(tagName);
|
|
186
|
+
for (const match of text.matchAll(re)) {
|
|
187
|
+
if (match[1])
|
|
188
|
+
parts.push(match[1]);
|
|
189
|
+
}
|
|
168
190
|
}
|
|
169
191
|
return parts.join('\n');
|
|
170
192
|
}
|
|
@@ -209,7 +231,10 @@ function buildPlanMessage(uncovered) {
|
|
|
209
231
|
'',
|
|
210
232
|
...uncovered.map((item) => `- **${item.id}** (${item.category || 'uncategorized'}): ${item.text}`),
|
|
211
233
|
'',
|
|
212
|
-
'Resolve by citing `D-NN:` in
|
|
234
|
+
'Resolve by citing `D-NN:` in any of the scanned plan surfaces: front-matter',
|
|
235
|
+
'`must_haves`/`truths`/`objective`, a `## must_haves`/`truths`/`tasks`/`objective`',
|
|
236
|
+
'heading, or an `<objective>`/`<tasks>`/`<task>`/`<action>`/`<read_first>`/`<behavior>`/`<verify>`/`<acceptance_criteria>`/`<done>`',
|
|
237
|
+
'tag body. Other locations (prose outside those headings, comments, other XML tags) are not scanned.',
|
|
213
238
|
'OR move the decision to `### Claude\'s Discretion` / tag it `[informational]` if it should not be tracked.',
|
|
214
239
|
].join('\n');
|
|
215
240
|
}
|
|
@@ -235,12 +260,22 @@ function loadDecisionExtraction(contextPath) {
|
|
|
235
260
|
}
|
|
236
261
|
function cmdDecisionCoveragePlan(projectDir, args, raw) {
|
|
237
262
|
const phaseDir = args[2] ? resolvePath(args[2], projectDir) : '';
|
|
238
|
-
const
|
|
263
|
+
const contextArg = args[3];
|
|
264
|
+
const contextPath = contextArg ? resolvePath(contextArg, projectDir) : '';
|
|
239
265
|
if (!gateEnabled(projectDir)) {
|
|
240
266
|
output({ passed: true, skipped: true, reason: 'workflow.context_coverage_gate is false', total: 0, covered: 0, uncovered: [], message: 'Decision coverage gate disabled by config.' }, raw, undefined);
|
|
241
267
|
return;
|
|
242
268
|
}
|
|
243
|
-
|
|
269
|
+
// #2770: an EMPTY/MISSING contextPath argument is a CALLER ERROR (the workflow
|
|
270
|
+
// forgot to pass the path — e.g. a shell variable lost between Bash blocks), not
|
|
271
|
+
// evidence the phase has no CONTEXT.md. Fail closed (mirrors #1365 fail-loud) so a
|
|
272
|
+
// blocking gate cannot silently certify success on a caller mistake.
|
|
273
|
+
if (!contextArg || contextArg === '') {
|
|
274
|
+
output({ passed: false, skipped: false, reason: 'missing context path argument', total: 0, covered: 0, uncovered: [], message: 'Decision coverage gate called without a context path argument — the caller (e.g. the plan-phase workflow) must pass the CONTEXT.md path. An empty argument is a caller error, not evidence there is nothing to check (#2770).' }, raw, undefined);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// A REAL path whose file genuinely does not exist is the LEGITIMATE green skip.
|
|
278
|
+
if (!node_fs_1.default.existsSync(contextPath)) {
|
|
244
279
|
output({ passed: true, skipped: true, reason: 'CONTEXT.md missing', total: 0, covered: 0, uncovered: [], message: 'No CONTEXT.md - nothing to check.' }, raw, undefined);
|
|
245
280
|
return;
|
|
246
281
|
}
|
|
@@ -645,7 +680,11 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
|
645
680
|
const planPath = node_path_1.default.join(phaseDir, file);
|
|
646
681
|
const content = readIfExists(planPath);
|
|
647
682
|
// Check frontmatter for type: tdd
|
|
648
|
-
|
|
683
|
+
// CRLF-tolerant: a PLAN.md written with Windows line endings (---\r\n...---)
|
|
684
|
+
// must still match. The same CRLF-tolerant form is already used at line 205
|
|
685
|
+
// (extractPlanDesignatedSections); this is the same canonical pattern, applied
|
|
686
|
+
// here for the tdd-classification path. Fixes #2449.
|
|
687
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
649
688
|
if (frontmatterMatch) {
|
|
650
689
|
const fm = frontmatterMatch[1];
|
|
651
690
|
if (/^type:\s*tdd\s*$/m.test(fm)) {
|
|
@@ -1010,6 +1049,38 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
|
|
|
1010
1049
|
}
|
|
1011
1050
|
const v = validateCoverageMatrix(matrixText);
|
|
1012
1051
|
if (v.valid) {
|
|
1052
|
+
if (v.none_declared) {
|
|
1053
|
+
// The declaration is the human override for the detector — it PASSES
|
|
1054
|
+
// even when detection fires (that is acceptance #5's point: the
|
|
1055
|
+
// detector is fallible and the declaration is the reasoned overrule).
|
|
1056
|
+
// But a contradiction must be VISIBLE, not silent: re-run detection
|
|
1057
|
+
// over the phase scope and surface any signals it still finds
|
|
1058
|
+
// (#2365 review S-1).
|
|
1059
|
+
const declScope = readPhaseScope(projectDir, resolvedDir, phaseNumber);
|
|
1060
|
+
const declDetection = detectApiIntegration(declScope.text);
|
|
1061
|
+
const declSignals = declDetection.signals.map((s) => ({ verb: s.verb, noun: s.noun }));
|
|
1062
|
+
// The declaration legitimately wins even over a read error (it is the
|
|
1063
|
+
// human overrule), but if scope was incomplete we say so — the contract
|
|
1064
|
+
// is that contradictions stay visible, not silent (#2365 review).
|
|
1065
|
+
const baseMsg = declDetection.detected
|
|
1066
|
+
? `api-coverage: COVERAGE.md declares no external API integration, overriding ${declSignals.length} detected signal(s) — confirm the declaration is accurate`
|
|
1067
|
+
: 'api-coverage: COVERAGE.md declares no external API integration — matrix not required';
|
|
1068
|
+
output({
|
|
1069
|
+
block: false,
|
|
1070
|
+
passed: true,
|
|
1071
|
+
coverage_present: true,
|
|
1072
|
+
matrix: coverageFile,
|
|
1073
|
+
counts: v.counts,
|
|
1074
|
+
none_declared: true,
|
|
1075
|
+
detected: declDetection.detected,
|
|
1076
|
+
...(declDetection.detected ? { signals: declSignals } : {}),
|
|
1077
|
+
...(declScope.readError ? { scope_read_error: declScope.readError } : {}),
|
|
1078
|
+
message: declScope.readError
|
|
1079
|
+
? `${baseMsg} (note: phase scope was incompletely read — ${declScope.readError})`
|
|
1080
|
+
: baseMsg,
|
|
1081
|
+
}, raw, undefined);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1013
1084
|
output({
|
|
1014
1085
|
block: false,
|
|
1015
1086
|
passed: true,
|
|
@@ -1044,8 +1115,22 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
|
|
|
1044
1115
|
return;
|
|
1045
1116
|
}
|
|
1046
1117
|
// (2) no matrix — detect whether this phase integrates an external API.
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1118
|
+
const scope = readPhaseScope(projectDir, resolvedDir, phaseNumber);
|
|
1119
|
+
if (scope.readError) {
|
|
1120
|
+
// Fail-closed: an unreadable plan could be the one describing the
|
|
1121
|
+
// integration, so we cannot certify "no integration" — block and surface it.
|
|
1122
|
+
output({
|
|
1123
|
+
block: true,
|
|
1124
|
+
passed: false,
|
|
1125
|
+
coverage_present: false,
|
|
1126
|
+
detected: false,
|
|
1127
|
+
message: `api-coverage: could not read the phase scope (${scope.readError}); ` +
|
|
1128
|
+
'refusing to certify no external-API integration from incomplete scope. ' +
|
|
1129
|
+
'Fix the unreadable plan file, or add a COVERAGE.md declaration.',
|
|
1130
|
+
}, raw, undefined);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const detection = detectApiIntegration(scope.text);
|
|
1049
1134
|
if (detection.detected) {
|
|
1050
1135
|
// Surface only verb/noun (typed, bounded) — NOT raw prose snippets — so the
|
|
1051
1136
|
// gate output cannot relay injected PLAN.md instructions to the orchestrator.
|
|
@@ -1070,15 +1155,16 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
|
|
|
1070
1155
|
message: 'api-coverage: no external-API integration detected; coverage matrix not required',
|
|
1071
1156
|
}, raw, undefined);
|
|
1072
1157
|
}
|
|
1073
|
-
/**
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1158
|
+
/** A filesystem error that is NOT "does not exist" — i.e. a real read failure
|
|
1159
|
+
* (EACCES/EIO/…) the gate must not swallow. `ENOENT` is a legitimate "not
|
|
1160
|
+
* there yet" and is treated as absence, not error. */
|
|
1161
|
+
function isRealReadFailure(err) {
|
|
1162
|
+
const code = err?.code;
|
|
1163
|
+
return err != null && code !== 'ENOENT';
|
|
1164
|
+
}
|
|
1080
1165
|
function readPhaseScope(projectDir, phaseDir, phaseNumber) {
|
|
1081
1166
|
const chunks = [];
|
|
1167
|
+
let readError = null;
|
|
1082
1168
|
try {
|
|
1083
1169
|
const entries = node_fs_1.default.readdirSync(phaseDir, { withFileTypes: true });
|
|
1084
1170
|
const plans = entries
|
|
@@ -1086,28 +1172,52 @@ function readPhaseScope(projectDir, phaseDir, phaseNumber) {
|
|
|
1086
1172
|
.map((e) => e.name)
|
|
1087
1173
|
.sort();
|
|
1088
1174
|
for (const p of plans) {
|
|
1089
|
-
|
|
1175
|
+
try {
|
|
1176
|
+
chunks.push(node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, p), 'utf8'));
|
|
1177
|
+
}
|
|
1178
|
+
catch (err) {
|
|
1179
|
+
// A plan file that exists but cannot be read — record it and keep
|
|
1180
|
+
// reading the rest so the message names the first failure.
|
|
1181
|
+
if (!readError) {
|
|
1182
|
+
readError = `could not read ${p}: ${err instanceof Error ? err.message : String(err)}`;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1090
1185
|
}
|
|
1091
1186
|
}
|
|
1092
|
-
catch {
|
|
1093
|
-
//
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
// A MISSING phase directory is fine (no plans yet → fall through to the
|
|
1189
|
+
// roadmap). A directory that exists but cannot be enumerated (EACCES/EIO)
|
|
1190
|
+
// is a real read failure the gate must not silently pass (#2365 review).
|
|
1191
|
+
if (isRealReadFailure(err)) {
|
|
1192
|
+
return {
|
|
1193
|
+
text: '',
|
|
1194
|
+
readError: `could not read the phase directory: ${err instanceof Error ? err.message : String(err)}`,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1094
1197
|
}
|
|
1198
|
+
if (readError)
|
|
1199
|
+
return { text: chunks.join('\n\n'), readError };
|
|
1095
1200
|
if (chunks.join('').trim().length > 0)
|
|
1096
|
-
return chunks.join('\n\n');
|
|
1201
|
+
return { text: chunks.join('\n\n'), readError: null };
|
|
1097
1202
|
// Fallback: ONLY this phase's ROADMAP section (not the whole file, which
|
|
1098
|
-
// would pollute detection with sibling-phase prose).
|
|
1099
|
-
//
|
|
1203
|
+
// would pollute detection with sibling-phase prose). A MISSING roadmap/section
|
|
1204
|
+
// is non-fatal; a roadmap that exists but cannot be read is a real failure.
|
|
1100
1205
|
if (phaseNumber) {
|
|
1101
1206
|
try {
|
|
1102
1207
|
const section = getRoadmapPhaseWithFallback(projectDir, phaseNumber);
|
|
1103
1208
|
if (section)
|
|
1104
|
-
return section;
|
|
1209
|
+
return { text: section, readError: null };
|
|
1105
1210
|
}
|
|
1106
|
-
catch {
|
|
1107
|
-
|
|
1211
|
+
catch (err) {
|
|
1212
|
+
if (isRealReadFailure(err)) {
|
|
1213
|
+
return {
|
|
1214
|
+
text: '',
|
|
1215
|
+
readError: `could not read the roadmap fallback: ${err instanceof Error ? err.message : String(err)}`,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1108
1218
|
}
|
|
1109
1219
|
}
|
|
1110
|
-
return '';
|
|
1220
|
+
return { text: '', readError: null };
|
|
1111
1221
|
}
|
|
1112
1222
|
function routeCheckCommand({ args, cwd, raw }) {
|
|
1113
1223
|
// Normalize dots to hyphens in the subcommand so both forms are accepted.
|
|
@@ -1197,4 +1307,7 @@ module.exports = {
|
|
|
1197
1307
|
cmdCheckPredicate,
|
|
1198
1308
|
buildPredicateDeps,
|
|
1199
1309
|
parsePredicateFlags,
|
|
1310
|
+
// Fail-closed phase-scope reader for the api-coverage gate — exported for
|
|
1311
|
+
// in-process failure-injection tests (#2365 review).
|
|
1312
|
+
readPhaseScope,
|
|
1200
1313
|
};
|
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
14
|
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
15
15
|
const { createHub, ERROR_KINDS } = commandRoutingHub;
|
|
16
|
+
// #2620 (ADR-0174 §6): the Hub defaults to a no-op logger and the live CLI
|
|
17
|
+
// dispatch path never injected the reference DispatchLogger, so GSD_AUDIT and
|
|
18
|
+
// config.audit.enabled were inert. Inject the reference logger ONLY when
|
|
19
|
+
// observability is opt-in enabled; when off, inject nothing so the Hub stays
|
|
20
|
+
// byte-for-byte silent (preserving the default dispatch output contract, incl.
|
|
21
|
+
// --json-errors). Enabling stderr-on-error unconditionally by default is a
|
|
22
|
+
// separate, blast-radius-bearing change (it adds a second stderr line to the
|
|
23
|
+
// --json-errors envelope) — deferred as its own follow-up.
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
|
+
const observabilityLogger = require("./observability/logger.cjs");
|
|
26
|
+
const { createDefaultLogger, isAuditEnabled } = observabilityLogger;
|
|
16
27
|
// Phase 2 (#1646): import ERROR_REASON so the UnknownCommand translation can
|
|
17
28
|
// pass `sdk_unknown_command` as the second arg to error(), preserving the
|
|
18
29
|
// JSON-error envelope contract that capability routers' tests assert on.
|
|
@@ -73,6 +84,10 @@ function routeHubCommandFamily({ family, args, subcommands, handlers, defaultSub
|
|
|
73
84
|
const hub = createHub({
|
|
74
85
|
cjsRegistry: { [family]: registryHandlers },
|
|
75
86
|
manifest: { [family]: available },
|
|
87
|
+
// #2620: wire the reference logger onto the live dispatch path (ADR-0174 §6),
|
|
88
|
+
// but only when observability is opt-in enabled — otherwise leave it unset
|
|
89
|
+
// so the Hub falls back to the no-op logger and stays byte-for-byte silent.
|
|
90
|
+
logger: isAuditEnabled() ? createDefaultLogger({ cwd }) : undefined,
|
|
76
91
|
});
|
|
77
92
|
const result = hub.dispatch({
|
|
78
93
|
family,
|
|
@@ -14,15 +14,31 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Subcommands:
|
|
16
16
|
* detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]
|
|
17
|
-
* Resolves whether the Workflow backend should activate.
|
|
18
|
-
*
|
|
17
|
+
* Resolves whether the Workflow backend should activate. Both flags are
|
|
18
|
+
* OPTIONAL (#2590): `--runtime` falls back to the canonical
|
|
19
|
+
* `GSD_RUNTIME > config.runtime > 'claude'` chain, and
|
|
20
|
+
* `--agent-sdk-version` to `GSD_AGENT_SDK_VERSION` then the installed
|
|
21
|
+
* @anthropic-ai/claude-agent-sdk version. Reads the
|
|
19
22
|
* `claude_orchestration.*` keys from .planning/config.json. Emits
|
|
20
23
|
* { available, backend, reason }.
|
|
21
24
|
*
|
|
22
|
-
* emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]
|
|
25
|
+
* emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>] [--executor-model <id>]
|
|
23
26
|
* Reads a wave/plan manifest JSON file and emits the generated Workflow
|
|
24
27
|
* script + summary. The manifest shape matches emitWorkflowScript's input:
|
|
25
|
-
* { waves: [{ id, plans: [{ id, brief, files_modified: string[] }] }] }.
|
|
28
|
+
* { waves: [{ id, plans: [{ id, brief, files_modified: string[], use_worktree?: boolean }] }] }.
|
|
29
|
+
* `use_worktree` defaults to true; pass `false` for a plan the inline path
|
|
30
|
+
* (execute-phase.md step 2.5) would also keep out of worktree isolation
|
|
31
|
+
* (submodule-touching plans — #2772 / #2285 finding 1).
|
|
32
|
+
*
|
|
33
|
+
* resolve-wave-dispatch --waves <path> --run-id <id> [--runtime <id>]
|
|
34
|
+
* [--agent-sdk-version <ver>] [--no-nested-dispatch] [--phase-dir <dir>]
|
|
35
|
+
* [--budget <n>] [--executor-model <id>]
|
|
36
|
+
* #2285 — the single composed seam a PRE-wave dispatch-backend selector
|
|
37
|
+
* (`execute:wave:pre`) uses: resolves detect-backend + emit-workflow in
|
|
38
|
+
* ONE call. Emits { backend: 'inline'|'workflow', reason, script?, summary? }.
|
|
39
|
+
* Fail-closed identically to detect-backend/emit-workflow individually —
|
|
40
|
+
* any gate miss, or an emit failure on a malformed --waves manifest,
|
|
41
|
+
* resolves to 'inline' with no script.
|
|
26
42
|
*/
|
|
27
43
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
28
44
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -35,31 +51,31 @@ const io = require("./io.cjs");
|
|
|
35
51
|
const core = require("./claude-orchestration.cjs");
|
|
36
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
53
|
const configLoader = require("./config-loader.cjs");
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
55
|
+
const runtimeSlash = require("./runtime-slash.cjs");
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
|
|
57
|
+
const modelResolver = require("./model-resolver.cjs");
|
|
38
58
|
const { output } = io;
|
|
39
|
-
const { detectWorkflowBackend, emitWorkflowScript } = core;
|
|
59
|
+
const { detectWorkflowBackend, emitWorkflowScript, resolveWaveDispatch } = core;
|
|
40
60
|
const CAPABLE_HOST = { dispatch: { nested: true, background: true } };
|
|
41
61
|
function usage(error) {
|
|
42
|
-
error('Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow> [...]\n' +
|
|
62
|
+
error('Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow|resolve-wave-dispatch> [...]\n' +
|
|
43
63
|
' detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]\n' +
|
|
44
|
-
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]'
|
|
64
|
+
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>] [--executor-model <id>]\n' +
|
|
65
|
+
' resolve-wave-dispatch --waves <path> --run-id <id> [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch] [--phase-dir <dir>] [--budget <n>] [--executor-model <id>]');
|
|
45
66
|
}
|
|
46
67
|
function argValue(args, flag) {
|
|
47
68
|
const i = args.indexOf(flag);
|
|
48
69
|
return i !== -1 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
49
70
|
}
|
|
50
71
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
72
|
+
* Resolve the `claude_orchestration.*` config slice from the project config
|
|
73
|
+
* (federated keys are merged by loadConfig as a nested object), flattened into
|
|
74
|
+
* the dotted-key shape `detectWorkflowBackend`/`resolveWaveDispatch` expect. A
|
|
75
|
+
* config read failure degrades to an empty slice — it must not break the core
|
|
76
|
+
* loop. Shared by `detect-backend` and `resolve-wave-dispatch`.
|
|
54
77
|
*/
|
|
55
|
-
function
|
|
56
|
-
const runtimeId = argValue(args, '--runtime') || process.env['GSD_RUNTIME'] || 'unknown';
|
|
57
|
-
const agentSdkVersion = argValue(args, '--agent-sdk-version');
|
|
58
|
-
const noNested = args.includes('--no-nested-dispatch');
|
|
59
|
-
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
|
|
60
|
-
// Resolve the claude_orchestration.* slice from the project config (federated
|
|
61
|
-
// keys are merged by loadConfig as a nested object). A config read failure
|
|
62
|
-
// degrades to inline — it must not break the core loop.
|
|
78
|
+
function resolveFlatClaudeOrchestrationConfig(cwd) {
|
|
63
79
|
let claudeSlice = {};
|
|
64
80
|
try {
|
|
65
81
|
const loaded = configLoader.loadConfig(cwd);
|
|
@@ -71,18 +87,144 @@ function cmdDetectBackend(args, cwd, raw) {
|
|
|
71
87
|
catch {
|
|
72
88
|
claudeSlice = {};
|
|
73
89
|
}
|
|
74
|
-
// Flatten the nested slice into the dotted-key shape detectWorkflowBackend expects.
|
|
75
90
|
const flatConfig = {};
|
|
76
91
|
for (const k of Object.keys(claudeSlice)) {
|
|
77
92
|
flatConfig['claude_orchestration.' + k] = claudeSlice[k];
|
|
78
93
|
}
|
|
94
|
+
return flatConfig;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the installed Agent SDK version (#2590).
|
|
98
|
+
*
|
|
99
|
+
* The `execute:wave:pre` fragment claimed the orchestrator "has no scriptable
|
|
100
|
+
* way to introspect the live Agent SDK version" and told callers to omit the
|
|
101
|
+
* flag — so gate 5 returned `agent_sdk_version_unknown` on every automated run
|
|
102
|
+
* and the Workflow backend never activated, while `capability state` still
|
|
103
|
+
* reported it `active: true`. That claim is true for BASH, but this router runs
|
|
104
|
+
* in Node: the installed package's own package.json is authoritative and
|
|
105
|
+
* requires no flag at all.
|
|
106
|
+
*
|
|
107
|
+
* Resolution is side-effect-free and fails closed to undefined (gate 5 then
|
|
108
|
+
* declines, exactly as before) rather than guessing a version.
|
|
109
|
+
*/
|
|
110
|
+
const AGENT_SDK_PKG = node_path_1.default.join('@anthropic-ai', 'claude-agent-sdk', 'package.json');
|
|
111
|
+
function resolveInstalledAgentSdkVersion(cwd) {
|
|
112
|
+
// Walk node_modules up the tree by hand rather than require.resolve: the SDK's
|
|
113
|
+
// `exports` map does not expose './package.json', so require.resolve throws
|
|
114
|
+
// ERR_PACKAGE_PATH_NOT_EXPORTED. Reading the file directly is exports-map
|
|
115
|
+
// independent and cannot execute package code.
|
|
116
|
+
for (const start of [cwd, __dirname]) {
|
|
117
|
+
let dir;
|
|
118
|
+
try {
|
|
119
|
+
dir = node_path_1.default.resolve(start);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (;;) {
|
|
125
|
+
try {
|
|
126
|
+
const pkgPath = node_path_1.default.join(dir, 'node_modules', AGENT_SDK_PKG);
|
|
127
|
+
if (node_fs_1.default.existsSync(pkgPath)) {
|
|
128
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf8'));
|
|
129
|
+
if (typeof parsed.version === 'string' && parsed.version.length > 0)
|
|
130
|
+
return parsed.version;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch { /* unreadable/malformed — keep walking */ }
|
|
134
|
+
const parent = node_path_1.default.dirname(dir);
|
|
135
|
+
if (parent === dir)
|
|
136
|
+
break;
|
|
137
|
+
dir = parent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolve `--runtime`/`--agent-sdk-version`/`--no-nested-dispatch` into the
|
|
144
|
+
* `{ runtimeId, hostIntegration, agentSdkVersion }` triple both `detect-backend`
|
|
145
|
+
* and `resolve-wave-dispatch` pass to the pure detection seam.
|
|
146
|
+
*/
|
|
147
|
+
function resolveDetectionArgs(args, cwd) {
|
|
148
|
+
// #2590: the old fallback chain was `--runtime > GSD_RUNTIME > 'unknown'`,
|
|
149
|
+
// diverging from the canonical `GSD_RUNTIME > config.runtime > 'claude'` used
|
|
150
|
+
// by runtime-slash.resolveRuntime — so ANY manual invocation without
|
|
151
|
+
// --runtime reported `runtime_not_claude` on a perfectly ordinary Claude
|
|
152
|
+
// project. Delegate to the canonical resolver instead of re-deriving it.
|
|
153
|
+
const runtimeId = argValue(args, '--runtime') || runtimeSlash.resolveRuntime(cwd || null);
|
|
154
|
+
// Explicit flag wins (lets a caller pin a version); then the environment;
|
|
155
|
+
// then the actually-installed SDK.
|
|
156
|
+
const agentSdkVersion = argValue(args, '--agent-sdk-version')
|
|
157
|
+
|| process.env['GSD_AGENT_SDK_VERSION']
|
|
158
|
+
|| resolveInstalledAgentSdkVersion(cwd || process.cwd());
|
|
159
|
+
const noNested = args.includes('--no-nested-dispatch');
|
|
160
|
+
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
|
|
161
|
+
return { runtimeId, hostIntegration, agentSdkVersion };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* #2686 — resolve the `gsd-executor` model this dispatch should carry.
|
|
165
|
+
*
|
|
166
|
+
* Defaults from the project config rather than requiring a flag. The Workflow
|
|
167
|
+
* backend previously emitted no model at all, so `model_overrides` /
|
|
168
|
+
* `model_policy` / `model_profile` were silently inert on that path while the
|
|
169
|
+
* inline path honored them. Reading the same source the inline path reads is
|
|
170
|
+
* what makes the two backends agree by construction: an orchestrator that never
|
|
171
|
+
* learns about a new flag would otherwise silently keep the old bug.
|
|
172
|
+
*
|
|
173
|
+
* `--executor-model` exists only to pin/override. Resolution is side-effect-free
|
|
174
|
+
* and fails closed to `undefined` (emission then omits the key, i.e. exactly the
|
|
175
|
+
* pre-#2686 output) rather than guessing a model.
|
|
176
|
+
*/
|
|
177
|
+
function resolveExecutorModel(args, cwd) {
|
|
178
|
+
const pinned = argValue(args, '--executor-model');
|
|
179
|
+
if (pinned !== undefined)
|
|
180
|
+
return pinned;
|
|
181
|
+
try {
|
|
182
|
+
const resolved = modelResolver.resolveModelInternal(cwd, 'gsd-executor');
|
|
183
|
+
return typeof resolved === 'string' ? resolved : undefined;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Read and parse a `--waves <path>` manifest file.
|
|
191
|
+
*
|
|
192
|
+
* #2285 finding 2: a real read/parse failure (`ok:false`) is DISTINCT from a
|
|
193
|
+
* manifest that parsed fine but has no top-level `waves` key (`ok:true, waves:
|
|
194
|
+
* undefined`) — collapsing both into the same sentinel made the missing-key
|
|
195
|
+
* case exit 0 with ZERO output (fail-silent), breaking the "exit 0 => parseable
|
|
196
|
+
* JSON verdict" contract callers rely on. Only the `ok:false` (read/parse threw)
|
|
197
|
+
* case calls `error(...)` and should short-circuit the caller; `ok:true` with a
|
|
198
|
+
* missing/malformed `waves` value must flow through to `emitWorkflowScript`'s
|
|
199
|
+
* own validation (matching how `{"waves": null}` already behaves) so the caller
|
|
200
|
+
* emits an explicit, non-empty verdict instead of silently doing nothing.
|
|
201
|
+
*/
|
|
202
|
+
function readWavesManifest(wavesPath, error) {
|
|
203
|
+
try {
|
|
204
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.resolve(wavesPath), 'utf8');
|
|
205
|
+
const parsed = JSON.parse(content);
|
|
206
|
+
return { ok: true, waves: parsed['waves'] };
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
error('could not read/parse --waves file "' + wavesPath + '": ' + (e instanceof Error ? e.message : String(e)));
|
|
210
|
+
return { ok: false };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Detect whether the Workflow backend should activate for the current/given
|
|
215
|
+
* runtime. Reads `claude_orchestration.*` from the project config; runtime and
|
|
216
|
+
* SDK version come from flags (the orchestrator already knows these) or env.
|
|
217
|
+
*/
|
|
218
|
+
function cmdDetectBackend(args, cwd, raw) {
|
|
219
|
+
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args, cwd);
|
|
220
|
+
const flatConfig = resolveFlatClaudeOrchestrationConfig(cwd);
|
|
79
221
|
const result = detectWorkflowBackend({ runtimeId, hostIntegration, config: flatConfig, agentSdkVersion });
|
|
80
222
|
output(result, raw);
|
|
81
223
|
}
|
|
82
224
|
/**
|
|
83
225
|
* Emit a Workflow script from a wave/plan manifest file.
|
|
84
226
|
*/
|
|
85
|
-
function cmdEmitWorkflow(args,
|
|
227
|
+
function cmdEmitWorkflow(args, cwd, raw, error) {
|
|
86
228
|
const wavesPath = argValue(args, '--waves');
|
|
87
229
|
const runId = argValue(args, '--run-id');
|
|
88
230
|
const phaseDir = argValue(args, '--phase-dir') || '.planning/phases/current';
|
|
@@ -95,23 +237,17 @@ function cmdEmitWorkflow(args, _cwd, raw, error) {
|
|
|
95
237
|
error('emit-workflow requires --run-id <id>');
|
|
96
238
|
return;
|
|
97
239
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const parsed = JSON.parse(content);
|
|
102
|
-
waves = parsed['waves'];
|
|
103
|
-
}
|
|
104
|
-
catch (e) {
|
|
105
|
-
error('emit-workflow: could not read/parse --waves file "' + wavesPath + '": ' + (e instanceof Error ? e.message : String(e)));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
240
|
+
const read = readWavesManifest(wavesPath, (msg) => error('emit-workflow: ' + msg));
|
|
241
|
+
if (!read.ok)
|
|
242
|
+
return; // read/parse failure — error() already surfaced it loudly above
|
|
108
243
|
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
|
109
244
|
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
|
110
245
|
const result = emitWorkflowScript({
|
|
111
246
|
phaseDir,
|
|
112
247
|
runId,
|
|
113
|
-
waves: waves,
|
|
248
|
+
waves: read.waves,
|
|
114
249
|
budgetTokens: budget,
|
|
250
|
+
executorModel: resolveExecutorModel(args, cwd),
|
|
115
251
|
});
|
|
116
252
|
if (!result.ok) {
|
|
117
253
|
error('emit-workflow: ' + result.reason);
|
|
@@ -119,6 +255,45 @@ function cmdEmitWorkflow(args, _cwd, raw, error) {
|
|
|
119
255
|
}
|
|
120
256
|
output({ script: result.script, summary: result.summary }, raw);
|
|
121
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* #2285 — the single composed seam a PRE-wave dispatch-backend selector
|
|
260
|
+
* (`execute:wave:pre`) uses: resolves `detect-backend` + `emit-workflow` in
|
|
261
|
+
* ONE call via `resolveWaveDispatch`. Emits
|
|
262
|
+
* `{ backend: 'inline'|'workflow', reason, script?, summary? }`.
|
|
263
|
+
*/
|
|
264
|
+
function cmdResolveWaveDispatch(args, cwd, raw, error) {
|
|
265
|
+
const wavesPath = argValue(args, '--waves');
|
|
266
|
+
const runId = argValue(args, '--run-id');
|
|
267
|
+
const phaseDir = argValue(args, '--phase-dir') || '.planning/phases/current';
|
|
268
|
+
const budgetRaw = argValue(args, '--budget');
|
|
269
|
+
if (!wavesPath) {
|
|
270
|
+
error('resolve-wave-dispatch requires --waves <path>');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (!runId) {
|
|
274
|
+
error('resolve-wave-dispatch requires --run-id <id>');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const read = readWavesManifest(wavesPath, (msg) => error('resolve-wave-dispatch: ' + msg));
|
|
278
|
+
if (!read.ok)
|
|
279
|
+
return; // read/parse failure — error() already surfaced it loudly above
|
|
280
|
+
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args, cwd);
|
|
281
|
+
const flatConfig = resolveFlatClaudeOrchestrationConfig(cwd);
|
|
282
|
+
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
|
283
|
+
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
|
284
|
+
const result = resolveWaveDispatch({
|
|
285
|
+
runtimeId,
|
|
286
|
+
hostIntegration,
|
|
287
|
+
config: flatConfig,
|
|
288
|
+
agentSdkVersion,
|
|
289
|
+
phaseDir,
|
|
290
|
+
runId,
|
|
291
|
+
waves: read.waves,
|
|
292
|
+
budgetTokens: budget,
|
|
293
|
+
executorModel: resolveExecutorModel(args, cwd),
|
|
294
|
+
});
|
|
295
|
+
output(result, raw);
|
|
296
|
+
}
|
|
122
297
|
function routeClaudeOrchestrationCommand(opts) {
|
|
123
298
|
const { args, cwd, raw, error } = opts;
|
|
124
299
|
// args[0] is the family ('claude-orchestration'); the subcommand is args[1].
|
|
@@ -129,6 +304,9 @@ function routeClaudeOrchestrationCommand(opts) {
|
|
|
129
304
|
else if (subcommand === 'emit-workflow') {
|
|
130
305
|
cmdEmitWorkflow(args, cwd, raw, error);
|
|
131
306
|
}
|
|
307
|
+
else if (subcommand === 'resolve-wave-dispatch') {
|
|
308
|
+
cmdResolveWaveDispatch(args, cwd, raw, error);
|
|
309
|
+
}
|
|
132
310
|
else {
|
|
133
311
|
usage(error);
|
|
134
312
|
}
|