@opengsd/gsd-core 1.7.0 → 1.8.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 +14 -0
- package/README.md +2 -0
- package/agents/gsd-debug-session-manager.md +42 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +29 -2
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1152 -80
- 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 +3 -2
- package/gsd-core/bin/gsd-tools.cjs +1878 -2507
- 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 +338 -45
- 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-registry.cjs +155 -86
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +128 -25
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
- package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +81 -4
- package/gsd-core/bin/lib/config-loader.cjs +14 -2
- package/gsd-core/bin/lib/config.cjs +69 -18
- 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/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/init.cjs +111 -47
- package/gsd-core/bin/lib/install-engine.cjs +298 -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 +246 -12
- package/gsd-core/bin/lib/model-catalog.cjs +19 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase.cjs +201 -12
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +7 -4
- package/gsd-core/bin/lib/roadmap.cjs +13 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -8
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +16 -0
- package/gsd-core/bin/lib/smart-entry.cjs +69 -4
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +22 -1
- package/gsd-core/bin/lib/state.cjs +65 -11
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat.cjs +420 -5
- package/gsd-core/bin/lib/validate.cjs +12 -8
- package/gsd-core/bin/lib/verification.cjs +112 -17
- package/gsd-core/bin/lib/verify.cjs +220 -22
- package/gsd-core/bin/shared/config-schema.manifest.json +3 -2
- 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/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/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/reviewer-instances.md +9 -7
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/templates/DEBUG.md +5 -3
- 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 +4 -2
- package/gsd-core/workflows/audit-fix.md +2 -2
- package/gsd-core/workflows/check-todos.md +3 -1
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review.md +17 -5
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +25 -5
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +1 -0
- package/gsd-core/workflows/eval-review.md +3 -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 +25 -34
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +6 -2
- package/gsd-core/workflows/import.md +8 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +15 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +69 -21
- package/gsd-core/workflows/new-project.md +17 -15
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +14 -5
- package/gsd-core/workflows/plan-review-convergence.md +48 -3
- 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 +19 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +89 -73
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -0
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +50 -3
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +3 -1
- package/gsd-core/workflows/ui-review.md +3 -0
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +2 -0
- package/gsd-core/workflows/validate-phase.md +3 -0
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +7 -3
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-statusline.js +88 -3
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +88 -3
- package/package.json +6 -4
- package/pi/gsd.cjs +8 -2
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-glossary-refs.cjs +220 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- 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 +3 -2
- package/vscode/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -645,7 +670,11 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
|
|
|
645
670
|
const planPath = node_path_1.default.join(phaseDir, file);
|
|
646
671
|
const content = readIfExists(planPath);
|
|
647
672
|
// Check frontmatter for type: tdd
|
|
648
|
-
|
|
673
|
+
// CRLF-tolerant: a PLAN.md written with Windows line endings (---\r\n...---)
|
|
674
|
+
// must still match. The same CRLF-tolerant form is already used at line 205
|
|
675
|
+
// (extractPlanDesignatedSections); this is the same canonical pattern, applied
|
|
676
|
+
// here for the tdd-classification path. Fixes #2449.
|
|
677
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
649
678
|
if (frontmatterMatch) {
|
|
650
679
|
const fm = frontmatterMatch[1];
|
|
651
680
|
if (/^type:\s*tdd\s*$/m.test(fm)) {
|
|
@@ -1010,6 +1039,38 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
|
|
|
1010
1039
|
}
|
|
1011
1040
|
const v = validateCoverageMatrix(matrixText);
|
|
1012
1041
|
if (v.valid) {
|
|
1042
|
+
if (v.none_declared) {
|
|
1043
|
+
// The declaration is the human override for the detector — it PASSES
|
|
1044
|
+
// even when detection fires (that is acceptance #5's point: the
|
|
1045
|
+
// detector is fallible and the declaration is the reasoned overrule).
|
|
1046
|
+
// But a contradiction must be VISIBLE, not silent: re-run detection
|
|
1047
|
+
// over the phase scope and surface any signals it still finds
|
|
1048
|
+
// (#2365 review S-1).
|
|
1049
|
+
const declScope = readPhaseScope(projectDir, resolvedDir, phaseNumber);
|
|
1050
|
+
const declDetection = detectApiIntegration(declScope.text);
|
|
1051
|
+
const declSignals = declDetection.signals.map((s) => ({ verb: s.verb, noun: s.noun }));
|
|
1052
|
+
// The declaration legitimately wins even over a read error (it is the
|
|
1053
|
+
// human overrule), but if scope was incomplete we say so — the contract
|
|
1054
|
+
// is that contradictions stay visible, not silent (#2365 review).
|
|
1055
|
+
const baseMsg = declDetection.detected
|
|
1056
|
+
? `api-coverage: COVERAGE.md declares no external API integration, overriding ${declSignals.length} detected signal(s) — confirm the declaration is accurate`
|
|
1057
|
+
: 'api-coverage: COVERAGE.md declares no external API integration — matrix not required';
|
|
1058
|
+
output({
|
|
1059
|
+
block: false,
|
|
1060
|
+
passed: true,
|
|
1061
|
+
coverage_present: true,
|
|
1062
|
+
matrix: coverageFile,
|
|
1063
|
+
counts: v.counts,
|
|
1064
|
+
none_declared: true,
|
|
1065
|
+
detected: declDetection.detected,
|
|
1066
|
+
...(declDetection.detected ? { signals: declSignals } : {}),
|
|
1067
|
+
...(declScope.readError ? { scope_read_error: declScope.readError } : {}),
|
|
1068
|
+
message: declScope.readError
|
|
1069
|
+
? `${baseMsg} (note: phase scope was incompletely read — ${declScope.readError})`
|
|
1070
|
+
: baseMsg,
|
|
1071
|
+
}, raw, undefined);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1013
1074
|
output({
|
|
1014
1075
|
block: false,
|
|
1015
1076
|
passed: true,
|
|
@@ -1044,8 +1105,22 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
|
|
|
1044
1105
|
return;
|
|
1045
1106
|
}
|
|
1046
1107
|
// (2) no matrix — detect whether this phase integrates an external API.
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1108
|
+
const scope = readPhaseScope(projectDir, resolvedDir, phaseNumber);
|
|
1109
|
+
if (scope.readError) {
|
|
1110
|
+
// Fail-closed: an unreadable plan could be the one describing the
|
|
1111
|
+
// integration, so we cannot certify "no integration" — block and surface it.
|
|
1112
|
+
output({
|
|
1113
|
+
block: true,
|
|
1114
|
+
passed: false,
|
|
1115
|
+
coverage_present: false,
|
|
1116
|
+
detected: false,
|
|
1117
|
+
message: `api-coverage: could not read the phase scope (${scope.readError}); ` +
|
|
1118
|
+
'refusing to certify no external-API integration from incomplete scope. ' +
|
|
1119
|
+
'Fix the unreadable plan file, or add a COVERAGE.md declaration.',
|
|
1120
|
+
}, raw, undefined);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const detection = detectApiIntegration(scope.text);
|
|
1049
1124
|
if (detection.detected) {
|
|
1050
1125
|
// Surface only verb/noun (typed, bounded) — NOT raw prose snippets — so the
|
|
1051
1126
|
// gate output cannot relay injected PLAN.md instructions to the orchestrator.
|
|
@@ -1070,15 +1145,16 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
|
|
|
1070
1145
|
message: 'api-coverage: no external-API integration detected; coverage matrix not required',
|
|
1071
1146
|
}, raw, undefined);
|
|
1072
1147
|
}
|
|
1073
|
-
/**
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1148
|
+
/** A filesystem error that is NOT "does not exist" — i.e. a real read failure
|
|
1149
|
+
* (EACCES/EIO/…) the gate must not swallow. `ENOENT` is a legitimate "not
|
|
1150
|
+
* there yet" and is treated as absence, not error. */
|
|
1151
|
+
function isRealReadFailure(err) {
|
|
1152
|
+
const code = err?.code;
|
|
1153
|
+
return err != null && code !== 'ENOENT';
|
|
1154
|
+
}
|
|
1080
1155
|
function readPhaseScope(projectDir, phaseDir, phaseNumber) {
|
|
1081
1156
|
const chunks = [];
|
|
1157
|
+
let readError = null;
|
|
1082
1158
|
try {
|
|
1083
1159
|
const entries = node_fs_1.default.readdirSync(phaseDir, { withFileTypes: true });
|
|
1084
1160
|
const plans = entries
|
|
@@ -1086,28 +1162,52 @@ function readPhaseScope(projectDir, phaseDir, phaseNumber) {
|
|
|
1086
1162
|
.map((e) => e.name)
|
|
1087
1163
|
.sort();
|
|
1088
1164
|
for (const p of plans) {
|
|
1089
|
-
|
|
1165
|
+
try {
|
|
1166
|
+
chunks.push(node_fs_1.default.readFileSync(node_path_1.default.join(phaseDir, p), 'utf8'));
|
|
1167
|
+
}
|
|
1168
|
+
catch (err) {
|
|
1169
|
+
// A plan file that exists but cannot be read — record it and keep
|
|
1170
|
+
// reading the rest so the message names the first failure.
|
|
1171
|
+
if (!readError) {
|
|
1172
|
+
readError = `could not read ${p}: ${err instanceof Error ? err.message : String(err)}`;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1090
1175
|
}
|
|
1091
1176
|
}
|
|
1092
|
-
catch {
|
|
1093
|
-
//
|
|
1177
|
+
catch (err) {
|
|
1178
|
+
// A MISSING phase directory is fine (no plans yet → fall through to the
|
|
1179
|
+
// roadmap). A directory that exists but cannot be enumerated (EACCES/EIO)
|
|
1180
|
+
// is a real read failure the gate must not silently pass (#2365 review).
|
|
1181
|
+
if (isRealReadFailure(err)) {
|
|
1182
|
+
return {
|
|
1183
|
+
text: '',
|
|
1184
|
+
readError: `could not read the phase directory: ${err instanceof Error ? err.message : String(err)}`,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1094
1187
|
}
|
|
1188
|
+
if (readError)
|
|
1189
|
+
return { text: chunks.join('\n\n'), readError };
|
|
1095
1190
|
if (chunks.join('').trim().length > 0)
|
|
1096
|
-
return chunks.join('\n\n');
|
|
1191
|
+
return { text: chunks.join('\n\n'), readError: null };
|
|
1097
1192
|
// Fallback: ONLY this phase's ROADMAP section (not the whole file, which
|
|
1098
|
-
// would pollute detection with sibling-phase prose).
|
|
1099
|
-
//
|
|
1193
|
+
// would pollute detection with sibling-phase prose). A MISSING roadmap/section
|
|
1194
|
+
// is non-fatal; a roadmap that exists but cannot be read is a real failure.
|
|
1100
1195
|
if (phaseNumber) {
|
|
1101
1196
|
try {
|
|
1102
1197
|
const section = getRoadmapPhaseWithFallback(projectDir, phaseNumber);
|
|
1103
1198
|
if (section)
|
|
1104
|
-
return section;
|
|
1199
|
+
return { text: section, readError: null };
|
|
1105
1200
|
}
|
|
1106
|
-
catch {
|
|
1107
|
-
|
|
1201
|
+
catch (err) {
|
|
1202
|
+
if (isRealReadFailure(err)) {
|
|
1203
|
+
return {
|
|
1204
|
+
text: '',
|
|
1205
|
+
readError: `could not read the roadmap fallback: ${err instanceof Error ? err.message : String(err)}`,
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1108
1208
|
}
|
|
1109
1209
|
}
|
|
1110
|
-
return '';
|
|
1210
|
+
return { text: '', readError: null };
|
|
1111
1211
|
}
|
|
1112
1212
|
function routeCheckCommand({ args, cwd, raw }) {
|
|
1113
1213
|
// Normalize dots to hyphens in the subcommand so both forms are accepted.
|
|
@@ -1197,4 +1297,7 @@ module.exports = {
|
|
|
1197
1297
|
cmdCheckPredicate,
|
|
1198
1298
|
buildPredicateDeps,
|
|
1199
1299
|
parsePredicateFlags,
|
|
1300
|
+
// Fail-closed phase-scope reader for the api-coverage gate — exported for
|
|
1301
|
+
// in-process failure-injection tests (#2365 review).
|
|
1302
|
+
readPhaseScope,
|
|
1200
1303
|
};
|
|
@@ -22,7 +22,20 @@
|
|
|
22
22
|
* emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]
|
|
23
23
|
* Reads a wave/plan manifest JSON file and emits the generated Workflow
|
|
24
24
|
* script + summary. The manifest shape matches emitWorkflowScript's input:
|
|
25
|
-
* { waves: [{ id, plans: [{ id, brief, files_modified: string[] }] }] }.
|
|
25
|
+
* { waves: [{ id, plans: [{ id, brief, files_modified: string[], use_worktree?: boolean }] }] }.
|
|
26
|
+
* `use_worktree` defaults to true; pass `false` for a plan the inline path
|
|
27
|
+
* (execute-phase.md step 2.5) would also keep out of worktree isolation
|
|
28
|
+
* (submodule-touching plans — #2772 / #2285 finding 1).
|
|
29
|
+
*
|
|
30
|
+
* resolve-wave-dispatch --waves <path> --run-id <id> [--runtime <id>]
|
|
31
|
+
* [--agent-sdk-version <ver>] [--no-nested-dispatch] [--phase-dir <dir>]
|
|
32
|
+
* [--budget <n>]
|
|
33
|
+
* #2285 — the single composed seam a PRE-wave dispatch-backend selector
|
|
34
|
+
* (`execute:wave:pre`) uses: resolves detect-backend + emit-workflow in
|
|
35
|
+
* ONE call. Emits { backend: 'inline'|'workflow', reason, script?, summary? }.
|
|
36
|
+
* Fail-closed identically to detect-backend/emit-workflow individually —
|
|
37
|
+
* any gate miss, or an emit failure on a malformed --waves manifest,
|
|
38
|
+
* resolves to 'inline' with no script.
|
|
26
39
|
*/
|
|
27
40
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
28
41
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -36,30 +49,26 @@ const core = require("./claude-orchestration.cjs");
|
|
|
36
49
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
50
|
const configLoader = require("./config-loader.cjs");
|
|
38
51
|
const { output } = io;
|
|
39
|
-
const { detectWorkflowBackend, emitWorkflowScript } = core;
|
|
52
|
+
const { detectWorkflowBackend, emitWorkflowScript, resolveWaveDispatch } = core;
|
|
40
53
|
const CAPABLE_HOST = { dispatch: { nested: true, background: true } };
|
|
41
54
|
function usage(error) {
|
|
42
|
-
error('Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow> [...]\n' +
|
|
55
|
+
error('Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow|resolve-wave-dispatch> [...]\n' +
|
|
43
56
|
' 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>]'
|
|
57
|
+
' emit-workflow --waves <path> --run-id <id> [--phase-dir <dir>] [--budget <n>]\n' +
|
|
58
|
+
' resolve-wave-dispatch --waves <path> --run-id <id> [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch] [--phase-dir <dir>] [--budget <n>]');
|
|
45
59
|
}
|
|
46
60
|
function argValue(args, flag) {
|
|
47
61
|
const i = args.indexOf(flag);
|
|
48
62
|
return i !== -1 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
49
63
|
}
|
|
50
64
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
65
|
+
* Resolve the `claude_orchestration.*` config slice from the project config
|
|
66
|
+
* (federated keys are merged by loadConfig as a nested object), flattened into
|
|
67
|
+
* the dotted-key shape `detectWorkflowBackend`/`resolveWaveDispatch` expect. A
|
|
68
|
+
* config read failure degrades to an empty slice — it must not break the core
|
|
69
|
+
* loop. Shared by `detect-backend` and `resolve-wave-dispatch`.
|
|
54
70
|
*/
|
|
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.
|
|
71
|
+
function resolveFlatClaudeOrchestrationConfig(cwd) {
|
|
63
72
|
let claudeSlice = {};
|
|
64
73
|
try {
|
|
65
74
|
const loaded = configLoader.loadConfig(cwd);
|
|
@@ -71,11 +80,56 @@ function cmdDetectBackend(args, cwd, raw) {
|
|
|
71
80
|
catch {
|
|
72
81
|
claudeSlice = {};
|
|
73
82
|
}
|
|
74
|
-
// Flatten the nested slice into the dotted-key shape detectWorkflowBackend expects.
|
|
75
83
|
const flatConfig = {};
|
|
76
84
|
for (const k of Object.keys(claudeSlice)) {
|
|
77
85
|
flatConfig['claude_orchestration.' + k] = claudeSlice[k];
|
|
78
86
|
}
|
|
87
|
+
return flatConfig;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve `--runtime`/`--agent-sdk-version`/`--no-nested-dispatch` into the
|
|
91
|
+
* `{ runtimeId, hostIntegration, agentSdkVersion }` triple both `detect-backend`
|
|
92
|
+
* and `resolve-wave-dispatch` pass to the pure detection seam.
|
|
93
|
+
*/
|
|
94
|
+
function resolveDetectionArgs(args) {
|
|
95
|
+
const runtimeId = argValue(args, '--runtime') || process.env['GSD_RUNTIME'] || 'unknown';
|
|
96
|
+
const agentSdkVersion = argValue(args, '--agent-sdk-version');
|
|
97
|
+
const noNested = args.includes('--no-nested-dispatch');
|
|
98
|
+
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
|
|
99
|
+
return { runtimeId, hostIntegration, agentSdkVersion };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Read and parse a `--waves <path>` manifest file.
|
|
103
|
+
*
|
|
104
|
+
* #2285 finding 2: a real read/parse failure (`ok:false`) is DISTINCT from a
|
|
105
|
+
* manifest that parsed fine but has no top-level `waves` key (`ok:true, waves:
|
|
106
|
+
* undefined`) — collapsing both into the same sentinel made the missing-key
|
|
107
|
+
* case exit 0 with ZERO output (fail-silent), breaking the "exit 0 => parseable
|
|
108
|
+
* JSON verdict" contract callers rely on. Only the `ok:false` (read/parse threw)
|
|
109
|
+
* case calls `error(...)` and should short-circuit the caller; `ok:true` with a
|
|
110
|
+
* missing/malformed `waves` value must flow through to `emitWorkflowScript`'s
|
|
111
|
+
* own validation (matching how `{"waves": null}` already behaves) so the caller
|
|
112
|
+
* emits an explicit, non-empty verdict instead of silently doing nothing.
|
|
113
|
+
*/
|
|
114
|
+
function readWavesManifest(wavesPath, error) {
|
|
115
|
+
try {
|
|
116
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.resolve(wavesPath), 'utf8');
|
|
117
|
+
const parsed = JSON.parse(content);
|
|
118
|
+
return { ok: true, waves: parsed['waves'] };
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
error('could not read/parse --waves file "' + wavesPath + '": ' + (e instanceof Error ? e.message : String(e)));
|
|
122
|
+
return { ok: false };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Detect whether the Workflow backend should activate for the current/given
|
|
127
|
+
* runtime. Reads `claude_orchestration.*` from the project config; runtime and
|
|
128
|
+
* SDK version come from flags (the orchestrator already knows these) or env.
|
|
129
|
+
*/
|
|
130
|
+
function cmdDetectBackend(args, cwd, raw) {
|
|
131
|
+
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args);
|
|
132
|
+
const flatConfig = resolveFlatClaudeOrchestrationConfig(cwd);
|
|
79
133
|
const result = detectWorkflowBackend({ runtimeId, hostIntegration, config: flatConfig, agentSdkVersion });
|
|
80
134
|
output(result, raw);
|
|
81
135
|
}
|
|
@@ -95,22 +149,15 @@ function cmdEmitWorkflow(args, _cwd, raw, error) {
|
|
|
95
149
|
error('emit-workflow requires --run-id <id>');
|
|
96
150
|
return;
|
|
97
151
|
}
|
|
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
|
-
}
|
|
152
|
+
const read = readWavesManifest(wavesPath, (msg) => error('emit-workflow: ' + msg));
|
|
153
|
+
if (!read.ok)
|
|
154
|
+
return; // read/parse failure — error() already surfaced it loudly above
|
|
108
155
|
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
|
109
156
|
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
|
110
157
|
const result = emitWorkflowScript({
|
|
111
158
|
phaseDir,
|
|
112
159
|
runId,
|
|
113
|
-
waves: waves,
|
|
160
|
+
waves: read.waves,
|
|
114
161
|
budgetTokens: budget,
|
|
115
162
|
});
|
|
116
163
|
if (!result.ok) {
|
|
@@ -119,6 +166,44 @@ function cmdEmitWorkflow(args, _cwd, raw, error) {
|
|
|
119
166
|
}
|
|
120
167
|
output({ script: result.script, summary: result.summary }, raw);
|
|
121
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* #2285 — the single composed seam a PRE-wave dispatch-backend selector
|
|
171
|
+
* (`execute:wave:pre`) uses: resolves `detect-backend` + `emit-workflow` in
|
|
172
|
+
* ONE call via `resolveWaveDispatch`. Emits
|
|
173
|
+
* `{ backend: 'inline'|'workflow', reason, script?, summary? }`.
|
|
174
|
+
*/
|
|
175
|
+
function cmdResolveWaveDispatch(args, cwd, raw, error) {
|
|
176
|
+
const wavesPath = argValue(args, '--waves');
|
|
177
|
+
const runId = argValue(args, '--run-id');
|
|
178
|
+
const phaseDir = argValue(args, '--phase-dir') || '.planning/phases/current';
|
|
179
|
+
const budgetRaw = argValue(args, '--budget');
|
|
180
|
+
if (!wavesPath) {
|
|
181
|
+
error('resolve-wave-dispatch requires --waves <path>');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!runId) {
|
|
185
|
+
error('resolve-wave-dispatch requires --run-id <id>');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const read = readWavesManifest(wavesPath, (msg) => error('resolve-wave-dispatch: ' + msg));
|
|
189
|
+
if (!read.ok)
|
|
190
|
+
return; // read/parse failure — error() already surfaced it loudly above
|
|
191
|
+
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args);
|
|
192
|
+
const flatConfig = resolveFlatClaudeOrchestrationConfig(cwd);
|
|
193
|
+
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
|
194
|
+
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
|
195
|
+
const result = resolveWaveDispatch({
|
|
196
|
+
runtimeId,
|
|
197
|
+
hostIntegration,
|
|
198
|
+
config: flatConfig,
|
|
199
|
+
agentSdkVersion,
|
|
200
|
+
phaseDir,
|
|
201
|
+
runId,
|
|
202
|
+
waves: read.waves,
|
|
203
|
+
budgetTokens: budget,
|
|
204
|
+
});
|
|
205
|
+
output(result, raw);
|
|
206
|
+
}
|
|
122
207
|
function routeClaudeOrchestrationCommand(opts) {
|
|
123
208
|
const { args, cwd, raw, error } = opts;
|
|
124
209
|
// args[0] is the family ('claude-orchestration'); the subcommand is args[1].
|
|
@@ -129,6 +214,9 @@ function routeClaudeOrchestrationCommand(opts) {
|
|
|
129
214
|
else if (subcommand === 'emit-workflow') {
|
|
130
215
|
cmdEmitWorkflow(args, cwd, raw, error);
|
|
131
216
|
}
|
|
217
|
+
else if (subcommand === 'resolve-wave-dispatch') {
|
|
218
|
+
cmdResolveWaveDispatch(args, cwd, raw, error);
|
|
219
|
+
}
|
|
132
220
|
else {
|
|
133
221
|
usage(error);
|
|
134
222
|
}
|
|
@@ -16,15 +16,20 @@
|
|
|
16
16
|
* → { ok:true, script, summary } | { ok:false, reason }
|
|
17
17
|
* Maps GSD's wave/plan model 1:1 onto Workflow primitives:
|
|
18
18
|
* wave → sequential `parallel()` stage barriers,
|
|
19
|
-
* plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })
|
|
19
|
+
* plan → `agent(brief, { agentType:'gsd-executor', isolation:'worktree' })`
|
|
20
|
+
* — UNLESS the plan's `use_worktree` is explicitly `false`, in which case
|
|
21
|
+
* `isolation` is omitted entirely for that plan (#2772 / #2285 finding 1:
|
|
22
|
+
* a submodule-touching plan must never be forced into worktree isolation
|
|
23
|
+
* the inline path (execute-phase.md step 2.5) would keep it out of),
|
|
20
24
|
* files_modified overlap → forces plans into separate sequential stages
|
|
21
25
|
* (the same overlap rule execute-phase already applies inline),
|
|
22
26
|
* resumeFromRunId → wired to the phase run id,
|
|
23
27
|
* budgetTokens → a shared token pool.
|
|
24
|
-
* The emitted script composes the SAME gsd-executor agent
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
+
* The emitted script composes the SAME gsd-executor agent the inline path
|
|
29
|
+
* uses, with per-plan worktree isolation mirroring the inline path's own
|
|
30
|
+
* per-plan decision, so it produces the same artifacts/commits (criterion 2).
|
|
31
|
+
* It is a generated string consumed by the orchestrator; this module never
|
|
32
|
+
* invokes the Workflow tool itself.
|
|
28
33
|
*
|
|
29
34
|
* Design laws:
|
|
30
35
|
* - Gall's Law: ship a small working slice that composes existing primitives
|
|
@@ -259,6 +264,17 @@ function partitionStages(plans) {
|
|
|
259
264
|
function quoteString(s) {
|
|
260
265
|
return JSON.stringify(s);
|
|
261
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Render the `agent()` options object for a single plan — `isolation: "worktree"`
|
|
269
|
+
* ONLY when the plan's `use_worktree` is not explicitly `false` (#2772 / #2285
|
|
270
|
+
* finding 1). This is the single place that decides worktree isolation for the
|
|
271
|
+
* Workflow backend; it must never diverge from the inline path's per-plan gate.
|
|
272
|
+
*/
|
|
273
|
+
function agentOptions(p) {
|
|
274
|
+
return p.use_worktree === false
|
|
275
|
+
? '{ agentType: "gsd-executor" }'
|
|
276
|
+
: '{ agentType: "gsd-executor", isolation: "worktree" }';
|
|
277
|
+
}
|
|
262
278
|
/**
|
|
263
279
|
* True if `s` is a safe identifier/path token to interpolate into the generated
|
|
264
280
|
* script WITHOUT requiring a string-literal context — i.e. it contains no
|
|
@@ -318,6 +334,9 @@ function emitWorkflowScript(input) {
|
|
|
318
334
|
if (!isScriptableIdentifier(p.id)) {
|
|
319
335
|
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].id must not contain newlines/quotes/backslash/control chars' };
|
|
320
336
|
}
|
|
337
|
+
if (p.use_worktree !== undefined && typeof p.use_worktree !== 'boolean') {
|
|
338
|
+
return { ok: false, reason: 'waves[' + i + '].plans[' + j + '].use_worktree must be a boolean if present' };
|
|
339
|
+
}
|
|
321
340
|
if (seenIds.has(p.id)) {
|
|
322
341
|
return { ok: false, reason: 'waves[' + i + '] has duplicate plan id "' + p.id + '"' };
|
|
323
342
|
}
|
|
@@ -336,8 +355,9 @@ function emitWorkflowScript(input) {
|
|
|
336
355
|
lines.push('// GSD Workflow script — generated by the claude-orchestration capability (#1143)');
|
|
337
356
|
lines.push('// phase: ' + phaseDir);
|
|
338
357
|
lines.push('// BETA: preview-grade; on any failure the orchestrator falls back to inline dispatch.');
|
|
339
|
-
lines.push('// Composes the SAME gsd-executor agent
|
|
340
|
-
lines.push('//
|
|
358
|
+
lines.push('// Composes the SAME gsd-executor agent as the inline path, so artifacts (SUMMARY.md)');
|
|
359
|
+
lines.push('// and commits are produced identically. Worktree isolation is per-plan (use_worktree)');
|
|
360
|
+
lines.push('// and mirrors execute-phase.md step 2.5\'s submodule gate exactly (#2772 / #2285).');
|
|
341
361
|
lines.push('resumeFromRunId(' + quoteString(runId) + ')');
|
|
342
362
|
if (budgetTokens !== null) {
|
|
343
363
|
lines.push('budget(' + budgetTokens + ')');
|
|
@@ -361,13 +381,13 @@ function emitWorkflowScript(input) {
|
|
|
361
381
|
if (stagePlans.length === 1) {
|
|
362
382
|
const p = stagePlans[0];
|
|
363
383
|
lines.push('parallel(');
|
|
364
|
-
lines.push(' agent(' + quoteString(p.brief) + ',
|
|
384
|
+
lines.push(' agent(' + quoteString(p.brief) + ', ' + agentOptions(p) + ')');
|
|
365
385
|
lines.push(')');
|
|
366
386
|
}
|
|
367
387
|
else {
|
|
368
388
|
lines.push('parallel(');
|
|
369
389
|
for (const p of stagePlans) {
|
|
370
|
-
lines.push(' agent(' + quoteString(p.brief) + ',
|
|
390
|
+
lines.push(' agent(' + quoteString(p.brief) + ', ' + agentOptions(p) + '),');
|
|
371
391
|
}
|
|
372
392
|
// Replace trailing comma on the last agent line with nothing.
|
|
373
393
|
const lastIdx = lines.length - 1;
|
|
@@ -393,9 +413,64 @@ function emitWorkflowScript(input) {
|
|
|
393
413
|
},
|
|
394
414
|
};
|
|
395
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* #2285 — single composed decision seam for a PRE-wave dispatch-backend selector
|
|
418
|
+
* (e.g. the `execute:wave:pre` claude-orchestration contribution). Composes
|
|
419
|
+
* `detectWorkflowBackend` (gate ladder) with `emitWorkflowScript` (wave→plan
|
|
420
|
+
* mapping) into ONE call so the orchestrator (and its CLI wrapper,
|
|
421
|
+
* `claude-orchestration resolve-wave-dispatch`) never has to re-implement the
|
|
422
|
+
* two-step "detect, then maybe emit" sequencing.
|
|
423
|
+
*
|
|
424
|
+
* Fail-closed at every layer, matching the two composed functions:
|
|
425
|
+
* - `detectWorkflowBackend` resolving anything other than `'workflow'` →
|
|
426
|
+
* `inline` immediately; `emitWorkflowScript` is never invoked (no wasted
|
|
427
|
+
* work, no risk of a bad emit masking a correct inline fallback).
|
|
428
|
+
* - `detectWorkflowBackend` resolves `'workflow'` but `emitWorkflowScript`
|
|
429
|
+
* fails (`ok:false` — e.g. a malformed wave manifest) → `inline`, carrying
|
|
430
|
+
* the emit failure reason so the caller can surface it. Never a partial or
|
|
431
|
+
* broken script.
|
|
432
|
+
*
|
|
433
|
+
* This is the designated non-CLI-router, non-test caller of
|
|
434
|
+
* `detectWorkflowBackend` and `emitWorkflowScript` — the standalone CLI
|
|
435
|
+
* subcommands (`detect-backend`, `emit-workflow`) remain for inspection/
|
|
436
|
+
* debugging, but the orchestrator's real per-wave dispatch decision goes
|
|
437
|
+
* through this seam.
|
|
438
|
+
*
|
|
439
|
+
* Never throws on bad input.
|
|
440
|
+
*/
|
|
441
|
+
function resolveWaveDispatch(input) {
|
|
442
|
+
if (input === null || input === undefined || typeof input !== 'object') {
|
|
443
|
+
return { backend: 'inline', reason: 'invalid_input' };
|
|
444
|
+
}
|
|
445
|
+
const detected = detectWorkflowBackend({
|
|
446
|
+
runtimeId: input.runtimeId,
|
|
447
|
+
hostIntegration: input.hostIntegration,
|
|
448
|
+
config: input.config,
|
|
449
|
+
agentSdkVersion: input.agentSdkVersion,
|
|
450
|
+
});
|
|
451
|
+
if (detected.backend !== 'workflow') {
|
|
452
|
+
return { backend: 'inline', reason: detected.reason };
|
|
453
|
+
}
|
|
454
|
+
const emitted = emitWorkflowScript({
|
|
455
|
+
phaseDir: input.phaseDir,
|
|
456
|
+
waves: input.waves,
|
|
457
|
+
runId: input.runId,
|
|
458
|
+
budgetTokens: input.budgetTokens,
|
|
459
|
+
});
|
|
460
|
+
if (!emitted.ok) {
|
|
461
|
+
return { backend: 'inline', reason: 'emit_failed: ' + emitted.reason };
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
backend: 'workflow',
|
|
465
|
+
reason: detected.reason,
|
|
466
|
+
script: emitted.script,
|
|
467
|
+
summary: emitted.summary,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
396
470
|
module.exports = {
|
|
397
471
|
detectWorkflowBackend,
|
|
398
472
|
emitWorkflowScript,
|
|
473
|
+
resolveWaveDispatch,
|
|
399
474
|
compareSemver,
|
|
400
475
|
isValidSemver,
|
|
401
476
|
WORKFLOW_TOOL_FLOOR_VERSION,
|
|
@@ -728,6 +728,20 @@ exports.NON_FAMILY_COMMAND_ALIASES = [
|
|
|
728
728
|
],
|
|
729
729
|
"mutation": true
|
|
730
730
|
},
|
|
731
|
+
{
|
|
732
|
+
"canonical": "requirements.ready-ids",
|
|
733
|
+
"aliases": [
|
|
734
|
+
"requirements ready-ids"
|
|
735
|
+
],
|
|
736
|
+
"mutation": false
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
"canonical": "requirements.revert-phase",
|
|
740
|
+
"aliases": [
|
|
741
|
+
"requirements revert-phase"
|
|
742
|
+
],
|
|
743
|
+
"mutation": true
|
|
744
|
+
},
|
|
731
745
|
{
|
|
732
746
|
"canonical": "stats.json",
|
|
733
747
|
"aliases": [
|