@opengsd/gsd-core 1.8.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 +31 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- 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 +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +186 -55
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +849 -2
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- 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 +1353 -132
- 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/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +5 -5
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- 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 +278 -5
- package/gsd-core/bin/lib/phase.cjs +57 -5
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- 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 +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- 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 +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +128 -20
- 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 +1 -13
- 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/context-budget.md +40 -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/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- 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/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +76 -19
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -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-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -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.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- 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 +9 -6
- 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-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 +9 -6
- 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 +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- 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/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-resolution-provenance.cjs +9 -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/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -260,12 +260,22 @@ function loadDecisionExtraction(contextPath) {
|
|
|
260
260
|
}
|
|
261
261
|
function cmdDecisionCoveragePlan(projectDir, args, raw) {
|
|
262
262
|
const phaseDir = args[2] ? resolvePath(args[2], projectDir) : '';
|
|
263
|
-
const
|
|
263
|
+
const contextArg = args[3];
|
|
264
|
+
const contextPath = contextArg ? resolvePath(contextArg, projectDir) : '';
|
|
264
265
|
if (!gateEnabled(projectDir)) {
|
|
265
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);
|
|
266
267
|
return;
|
|
267
268
|
}
|
|
268
|
-
|
|
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)) {
|
|
269
279
|
output({ passed: true, skipped: true, reason: 'CONTEXT.md missing', total: 0, covered: 0, uncovered: [], message: 'No CONTEXT.md - nothing to check.' }, raw, undefined);
|
|
270
280
|
return;
|
|
271
281
|
}
|
|
@@ -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,12 +14,15 @@
|
|
|
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
28
|
* { waves: [{ id, plans: [{ id, brief, files_modified: string[], use_worktree?: boolean }] }] }.
|
|
@@ -29,7 +32,7 @@
|
|
|
29
32
|
*
|
|
30
33
|
* resolve-wave-dispatch --waves <path> --run-id <id> [--runtime <id>]
|
|
31
34
|
* [--agent-sdk-version <ver>] [--no-nested-dispatch] [--phase-dir <dir>]
|
|
32
|
-
* [--budget <n>]
|
|
35
|
+
* [--budget <n>] [--executor-model <id>]
|
|
33
36
|
* #2285 — the single composed seam a PRE-wave dispatch-backend selector
|
|
34
37
|
* (`execute:wave:pre`) uses: resolves detect-backend + emit-workflow in
|
|
35
38
|
* ONE call. Emits { backend: 'inline'|'workflow', reason, script?, summary? }.
|
|
@@ -48,14 +51,18 @@ const io = require("./io.cjs");
|
|
|
48
51
|
const core = require("./claude-orchestration.cjs");
|
|
49
52
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
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");
|
|
51
58
|
const { output } = io;
|
|
52
59
|
const { detectWorkflowBackend, emitWorkflowScript, resolveWaveDispatch } = core;
|
|
53
60
|
const CAPABLE_HOST = { dispatch: { nested: true, background: true } };
|
|
54
61
|
function usage(error) {
|
|
55
62
|
error('Usage: gsd-tools claude-orchestration <detect-backend|emit-workflow|resolve-wave-dispatch> [...]\n' +
|
|
56
63
|
' detect-backend [--runtime <id>] [--agent-sdk-version <ver>] [--no-nested-dispatch]\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>]');
|
|
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>]');
|
|
59
66
|
}
|
|
60
67
|
function argValue(args, flag) {
|
|
61
68
|
const i = args.indexOf(flag);
|
|
@@ -86,18 +93,99 @@ function resolveFlatClaudeOrchestrationConfig(cwd) {
|
|
|
86
93
|
}
|
|
87
94
|
return flatConfig;
|
|
88
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
|
+
}
|
|
89
142
|
/**
|
|
90
143
|
* Resolve `--runtime`/`--agent-sdk-version`/`--no-nested-dispatch` into the
|
|
91
144
|
* `{ runtimeId, hostIntegration, agentSdkVersion }` triple both `detect-backend`
|
|
92
145
|
* and `resolve-wave-dispatch` pass to the pure detection seam.
|
|
93
146
|
*/
|
|
94
|
-
function resolveDetectionArgs(args) {
|
|
95
|
-
|
|
96
|
-
|
|
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());
|
|
97
159
|
const noNested = args.includes('--no-nested-dispatch');
|
|
98
160
|
const hostIntegration = noNested ? { dispatch: { nested: false, background: true } } : CAPABLE_HOST;
|
|
99
161
|
return { runtimeId, hostIntegration, agentSdkVersion };
|
|
100
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
|
+
}
|
|
101
189
|
/**
|
|
102
190
|
* Read and parse a `--waves <path>` manifest file.
|
|
103
191
|
*
|
|
@@ -128,7 +216,7 @@ function readWavesManifest(wavesPath, error) {
|
|
|
128
216
|
* SDK version come from flags (the orchestrator already knows these) or env.
|
|
129
217
|
*/
|
|
130
218
|
function cmdDetectBackend(args, cwd, raw) {
|
|
131
|
-
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args);
|
|
219
|
+
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args, cwd);
|
|
132
220
|
const flatConfig = resolveFlatClaudeOrchestrationConfig(cwd);
|
|
133
221
|
const result = detectWorkflowBackend({ runtimeId, hostIntegration, config: flatConfig, agentSdkVersion });
|
|
134
222
|
output(result, raw);
|
|
@@ -136,7 +224,7 @@ function cmdDetectBackend(args, cwd, raw) {
|
|
|
136
224
|
/**
|
|
137
225
|
* Emit a Workflow script from a wave/plan manifest file.
|
|
138
226
|
*/
|
|
139
|
-
function cmdEmitWorkflow(args,
|
|
227
|
+
function cmdEmitWorkflow(args, cwd, raw, error) {
|
|
140
228
|
const wavesPath = argValue(args, '--waves');
|
|
141
229
|
const runId = argValue(args, '--run-id');
|
|
142
230
|
const phaseDir = argValue(args, '--phase-dir') || '.planning/phases/current';
|
|
@@ -159,6 +247,7 @@ function cmdEmitWorkflow(args, _cwd, raw, error) {
|
|
|
159
247
|
runId,
|
|
160
248
|
waves: read.waves,
|
|
161
249
|
budgetTokens: budget,
|
|
250
|
+
executorModel: resolveExecutorModel(args, cwd),
|
|
162
251
|
});
|
|
163
252
|
if (!result.ok) {
|
|
164
253
|
error('emit-workflow: ' + result.reason);
|
|
@@ -188,7 +277,7 @@ function cmdResolveWaveDispatch(args, cwd, raw, error) {
|
|
|
188
277
|
const read = readWavesManifest(wavesPath, (msg) => error('resolve-wave-dispatch: ' + msg));
|
|
189
278
|
if (!read.ok)
|
|
190
279
|
return; // read/parse failure — error() already surfaced it loudly above
|
|
191
|
-
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args);
|
|
280
|
+
const { runtimeId, hostIntegration, agentSdkVersion } = resolveDetectionArgs(args, cwd);
|
|
192
281
|
const flatConfig = resolveFlatClaudeOrchestrationConfig(cwd);
|
|
193
282
|
const budgetTokens = budgetRaw !== undefined ? parseInt(budgetRaw, 10) : undefined;
|
|
194
283
|
const budget = (typeof budgetTokens === 'number' && !Number.isNaN(budgetTokens)) ? budgetTokens : undefined;
|
|
@@ -201,6 +290,7 @@ function cmdResolveWaveDispatch(args, cwd, raw, error) {
|
|
|
201
290
|
runId,
|
|
202
291
|
waves: read.waves,
|
|
203
292
|
budgetTokens: budget,
|
|
293
|
+
executorModel: resolveExecutorModel(args, cwd),
|
|
204
294
|
});
|
|
205
295
|
output(result, raw);
|
|
206
296
|
}
|
|
@@ -264,16 +264,52 @@ function partitionStages(plans) {
|
|
|
264
264
|
function quoteString(s) {
|
|
265
265
|
return JSON.stringify(s);
|
|
266
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* #2686 — the single decision of whether a resolved executor model is emittable,
|
|
269
|
+
* and what to emit. Shared by `agentOptions` (the emission) and the provenance
|
|
270
|
+
* comment (the claim about it) so the two can never disagree — a generated
|
|
271
|
+
* comment asserting something the generator does not actually do is the exact
|
|
272
|
+
* failure #2686 was filed for.
|
|
273
|
+
*
|
|
274
|
+
* Returns the model to emit, or `undefined` for "emit nothing":
|
|
275
|
+
* - non-string → malformed config; omit rather than throw, matching the
|
|
276
|
+
* defensive typeof guard `mapClaudeOverrideForRuntime`
|
|
277
|
+
* already carries in model-resolver for the same reason.
|
|
278
|
+
* - empty/whitespace → #2517: emitting `model: ""` 404s on runtimes without
|
|
279
|
+
* native tier aliases. Trimmed, so `" "` is also "none".
|
|
280
|
+
* - "inherit" → same rule; matched case-insensitively after trimming,
|
|
281
|
+
* since config is user-authored free text.
|
|
282
|
+
*
|
|
283
|
+
* NOTE it does NOT reject unscriptable characters — that is a hard input error,
|
|
284
|
+
* not a silent omission, and is rejected up front by `emitWorkflowScript` so the
|
|
285
|
+
* caller sees a reason instead of quietly losing their model routing.
|
|
286
|
+
*/
|
|
287
|
+
function emittableModel(executorModel) {
|
|
288
|
+
if (typeof executorModel !== 'string')
|
|
289
|
+
return undefined;
|
|
290
|
+
const trimmed = executorModel.trim();
|
|
291
|
+
if (trimmed.length === 0)
|
|
292
|
+
return undefined;
|
|
293
|
+
if (trimmed.toLowerCase() === 'inherit')
|
|
294
|
+
return undefined;
|
|
295
|
+
return trimmed;
|
|
296
|
+
}
|
|
267
297
|
/**
|
|
268
298
|
* Render the `agent()` options object for a single plan — `isolation: "worktree"`
|
|
269
299
|
* ONLY when the plan's `use_worktree` is not explicitly `false` (#2772 / #2285
|
|
270
300
|
* finding 1). This is the single place that decides worktree isolation for the
|
|
271
301
|
* Workflow backend; it must never diverge from the inline path's per-plan gate.
|
|
272
302
|
*/
|
|
273
|
-
function agentOptions(p) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
303
|
+
function agentOptions(p, executorModel) {
|
|
304
|
+
const parts = ['agentType: "gsd-executor"'];
|
|
305
|
+
if (p.use_worktree !== false)
|
|
306
|
+
parts.push('isolation: "worktree"');
|
|
307
|
+
// #2686: carry the resolved executor model so this backend honors
|
|
308
|
+
// model_overrides / model_policy / model_profile exactly as the inline path.
|
|
309
|
+
const model = emittableModel(executorModel);
|
|
310
|
+
if (model !== undefined)
|
|
311
|
+
parts.push('model: ' + quoteString(model));
|
|
312
|
+
return '{ ' + parts.join(', ') + ' }';
|
|
277
313
|
}
|
|
278
314
|
/**
|
|
279
315
|
* True if `s` is a safe identifier/path token to interpolate into the generated
|
|
@@ -301,7 +337,7 @@ function emitWorkflowScript(input) {
|
|
|
301
337
|
if (input === null || input === undefined || typeof input !== 'object') {
|
|
302
338
|
return { ok: false, reason: 'invalid_input' };
|
|
303
339
|
}
|
|
304
|
-
const { phaseDir, waves, runId } = input;
|
|
340
|
+
const { phaseDir, waves, runId, executorModel } = input;
|
|
305
341
|
// Identifiers/paths interpolated into the generated script must be free of any
|
|
306
342
|
// character that could terminate a comment, break out of a string literal, or
|
|
307
343
|
// smuggle control bytes — reject up front (security: #1143 review Finding 1).
|
|
@@ -311,9 +347,28 @@ function emitWorkflowScript(input) {
|
|
|
311
347
|
if (!isScriptableIdentifier(runId)) {
|
|
312
348
|
return { ok: false, reason: 'runId must be a non-empty string without newlines/quotes/backslash/control chars' };
|
|
313
349
|
}
|
|
350
|
+
// #2686 security: the resolved model is interpolated into BOTH an object
|
|
351
|
+
// literal (safe under quoteString) and a `//` provenance comment (NOT safe
|
|
352
|
+
// under quoteString — U+2028/U+2029 are LineTerminators that end a single-line
|
|
353
|
+
// comment in every engine, so a hostile model id would make the rest of the
|
|
354
|
+
// line live code). Reject the whole emission rather than silently dropping the
|
|
355
|
+
// model: an unscriptable id is malformed input, and `resolveWaveDispatch` maps
|
|
356
|
+
// an emit failure to the inline backend WITH a reason, so the user sees it.
|
|
357
|
+
// Only a STRING carrying such a character is rejected. A non-string is a
|
|
358
|
+
// malformed config rather than an injection attempt, and stays on the existing
|
|
359
|
+
// defensive path: `emittableModel` omits it and emission proceeds.
|
|
360
|
+
if (typeof executorModel === 'string' && UNSCRIPTABLE_CHAR_RE.test(executorModel)) {
|
|
361
|
+
return { ok: false, reason: 'executorModel must not contain newlines/quotes/backslash/control/line-separator chars' };
|
|
362
|
+
}
|
|
314
363
|
if (!Array.isArray(waves) || waves.length === 0) {
|
|
315
364
|
return { ok: false, reason: 'waves must be a non-empty array' };
|
|
316
365
|
}
|
|
366
|
+
// Wave ids must be unique ACROSS waves, not just plan ids within one (#2590).
|
|
367
|
+
// Each wave emits a `phase("Wave <id>")` call plus a matching meta.phases
|
|
368
|
+
// entry, and the Workflow tool matches phase titles by exact string — two
|
|
369
|
+
// waves sharing an id would collapse into one progress group and misattribute
|
|
370
|
+
// every agent in the second wave to the first.
|
|
371
|
+
const seenWaveIds = new Set();
|
|
317
372
|
for (let i = 0; i < waves.length; i++) {
|
|
318
373
|
const w = waves[i];
|
|
319
374
|
if (w === null || typeof w !== 'object' || typeof w.id !== 'string') {
|
|
@@ -322,6 +377,10 @@ function emitWorkflowScript(input) {
|
|
|
322
377
|
if (!isScriptableIdentifier(w.id)) {
|
|
323
378
|
return { ok: false, reason: 'waves[' + i + '].id must not contain newlines/quotes/backslash/control chars' };
|
|
324
379
|
}
|
|
380
|
+
if (seenWaveIds.has(w.id)) {
|
|
381
|
+
return { ok: false, reason: 'duplicate wave id "' + w.id + '" — wave ids must be unique (phase titles must map 1:1)' };
|
|
382
|
+
}
|
|
383
|
+
seenWaveIds.add(w.id);
|
|
325
384
|
if (!Array.isArray(w.plans) || w.plans.length === 0) {
|
|
326
385
|
return { ok: false, reason: 'waves[' + i + '] must have a non-empty plans array' };
|
|
327
386
|
}
|
|
@@ -352,15 +411,63 @@ function emitWorkflowScript(input) {
|
|
|
352
411
|
? Math.floor(input.budgetTokens)
|
|
353
412
|
: null;
|
|
354
413
|
const lines = [];
|
|
414
|
+
// `export const meta = {…}` MUST be the first statement in the script — the
|
|
415
|
+
// Workflow tool rejects the whole script otherwise (#2590). Leading comments
|
|
416
|
+
// are not statements, but the meta block is emitted first regardless so the
|
|
417
|
+
// contract holds under the strictest reading of "first statement".
|
|
418
|
+
//
|
|
419
|
+
// meta.phases must be a PURE LITERAL (no variables, calls, spreads, or
|
|
420
|
+
// template interpolation), and its titles are matched EXACTLY against the
|
|
421
|
+
// phase() calls emitted below.
|
|
422
|
+
lines.push('export const meta = {');
|
|
423
|
+
lines.push(' name: ' + quoteString('gsd-execute-' + runId) + ',');
|
|
424
|
+
lines.push(' description: ' + quoteString('GSD wave dispatch for ' + phaseDir) + ',');
|
|
425
|
+
lines.push(' phases: [');
|
|
426
|
+
for (const w of waves) {
|
|
427
|
+
lines.push(' { title: ' + quoteString('Wave ' + w.id) + ', detail: '
|
|
428
|
+
+ quoteString(w.plans.length + ' plan(s)') + ' },');
|
|
429
|
+
}
|
|
430
|
+
lines.push(' ],');
|
|
431
|
+
lines.push('}');
|
|
432
|
+
lines.push('');
|
|
355
433
|
lines.push('// GSD Workflow script — generated by the claude-orchestration capability (#1143)');
|
|
356
434
|
lines.push('// phase: ' + phaseDir);
|
|
357
435
|
lines.push('// BETA: preview-grade; on any failure the orchestrator falls back to inline dispatch.');
|
|
358
436
|
lines.push('// Composes the SAME gsd-executor agent as the inline path, so artifacts (SUMMARY.md)');
|
|
359
437
|
lines.push('// and commits are produced identically. Worktree isolation is per-plan (use_worktree)');
|
|
360
438
|
lines.push('// and mirrors execute-phase.md step 2.5\'s submodule gate exactly (#2772 / #2285).');
|
|
361
|
-
|
|
439
|
+
// #2686 / ADR-1411: state which model was applied — or that none resolved —
|
|
440
|
+
// so an opted-in user can SEE the routing decision instead of having to read
|
|
441
|
+
// the emitted options. A fallback must be a visible value, never silent.
|
|
442
|
+
//
|
|
443
|
+
// SECURITY: this is a `//` comment, and U+2028/U+2029 are ECMAScript
|
|
444
|
+
// LineTerminators that END a single-line comment in every engine — the ES2019
|
|
445
|
+
// change legalized them inside string LITERALS only, so `quoteString` alone is
|
|
446
|
+
// NOT sufficient here even though it is sufficient in the object literal
|
|
447
|
+
// above. An unscriptable model id would otherwise close the comment and make
|
|
448
|
+
// the remainder live top-level code. `emitWorkflowScript` rejects such ids
|
|
449
|
+
// before reaching this point (see the validation above), which is what makes
|
|
450
|
+
// interpolating here safe.
|
|
451
|
+
const provenanceModel = emittableModel(executorModel);
|
|
452
|
+
if (provenanceModel !== undefined) {
|
|
453
|
+
lines.push('// model: ' + quoteString(provenanceModel) + ' (resolved for gsd-executor, same source as the inline path)');
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
lines.push('// model: none applied — resolved to "inherit"/empty, so each agent inherits the');
|
|
457
|
+
lines.push('// orchestrator model (#2517: emitting an empty model 404s on some runtimes).');
|
|
458
|
+
}
|
|
459
|
+
lines.push('//');
|
|
460
|
+
// resumeFromRunId is a Workflow TOOL INPUT parameter, not a script function —
|
|
461
|
+
// calling it threw "resumeFromRunId is not defined" (#2590). The run id is
|
|
462
|
+
// carried in summary.resumeRunId for the caller to pass as that input.
|
|
463
|
+
lines.push('// resume: pass ' + quoteString(runId) + ' as the Workflow tool\'s resumeFromRunId input');
|
|
464
|
+
lines.push('// (it is a tool parameter, NOT a script function).');
|
|
362
465
|
if (budgetTokens !== null) {
|
|
363
|
-
|
|
466
|
+
// `budget` is a read-only object ({ total, spent(), remaining() }) supplied
|
|
467
|
+
// by the caller's token directive — a script cannot SET it, and `budget(n)`
|
|
468
|
+
// threw "budget is not a function" (#2590). Recorded as intent only.
|
|
469
|
+
lines.push('// budget: ' + budgetTokens + ' output tokens intended for this run; `budget` is');
|
|
470
|
+
lines.push('// read-only in a Workflow script — set it via the caller\'s token directive.');
|
|
364
471
|
}
|
|
365
472
|
lines.push('');
|
|
366
473
|
const stagesByWave = [];
|
|
@@ -371,6 +478,8 @@ function emitWorkflowScript(input) {
|
|
|
371
478
|
stagesByWave.push(stages);
|
|
372
479
|
totalPlans += wave.plans.length;
|
|
373
480
|
lines.push('// Wave ' + wave.id);
|
|
481
|
+
// Title must match this wave's meta.phases entry EXACTLY.
|
|
482
|
+
lines.push('phase(' + quoteString('Wave ' + wave.id) + ')');
|
|
374
483
|
for (let si = 0; si < stages.length; si++) {
|
|
375
484
|
const stagePlanIds = stages[si];
|
|
376
485
|
// Resolve back to plan objects for briefs (ids are unique within a wave — validated above).
|
|
@@ -378,22 +487,15 @@ function emitWorkflowScript(input) {
|
|
|
378
487
|
if (stages.length > 1) {
|
|
379
488
|
lines.push('// Stage ' + si + (si > 0 ? ' (sequential — files_modified overlap)' : ''));
|
|
380
489
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
lines.push('parallel(');
|
|
389
|
-
for (const p of stagePlans) {
|
|
390
|
-
lines.push(' agent(' + quoteString(p.brief) + ', ' + agentOptions(p) + '),');
|
|
391
|
-
}
|
|
392
|
-
// Replace trailing comma on the last agent line with nothing.
|
|
393
|
-
const lastIdx = lines.length - 1;
|
|
394
|
-
lines[lastIdx] = lines[lastIdx].replace(/,$/, '');
|
|
395
|
-
lines.push(')');
|
|
490
|
+
// parallel() takes an ARRAY OF THUNKS — `parallel(agent(…), agent(…))`
|
|
491
|
+
// threw "parallel() expects an array of functions" (#2590). Passing
|
|
492
|
+
// agent() results directly would also start every agent eagerly, before
|
|
493
|
+
// parallel() could bound concurrency.
|
|
494
|
+
lines.push('await parallel([');
|
|
495
|
+
for (const p of stagePlans) {
|
|
496
|
+
lines.push(' () => agent(' + quoteString(p.brief) + ', ' + agentOptions(p, executorModel) + '),');
|
|
396
497
|
}
|
|
498
|
+
lines.push('])');
|
|
397
499
|
}
|
|
398
500
|
if (wi < waves.length - 1)
|
|
399
501
|
lines.push('');
|
|
@@ -456,6 +558,7 @@ function resolveWaveDispatch(input) {
|
|
|
456
558
|
waves: input.waves,
|
|
457
559
|
runId: input.runId,
|
|
458
560
|
budgetTokens: input.budgetTokens,
|
|
561
|
+
executorModel: input.executorModel,
|
|
459
562
|
});
|
|
460
563
|
if (!emitted.ok) {
|
|
461
564
|
return { backend: 'inline', reason: 'emit_failed: ' + emitted.reason };
|