feat(capabilities): add claude-orchestration capability (Workflow backend) (#1143)

Default-off, BETA, claude-only capability adopting Claude Code's Workflow tool
(/effort ultracode, Agent SDK >= v0.3.149) as an optional parallel-execution
backend for the GSD loop. Restores the wave parallelism + plan-checker + verifier
that #853 forces inline on Claude Code, and folds gsd-ultraplan-phase under one
runtime gate.

- Pure fail-closed core (src/claude-orchestration.cts): detectWorkflowBackend
  (gate ladder: enabled -> Claude -> backend != inline -> nested+background host
  -> valid Agent SDK -> SDK >= floor; every miss degrades to inline) and
  emitWorkflowScript (waves -> parallel() barriers, plans -> gsd-executor +
  worktree, files_modified overlap -> separate stages, resumeFromRunId, budget).
  All interpolated identifiers validated script-safe; briefs JSON-quoted.
- claude-orchestration command family (gsd-tools claude-orchestration
  detect-backend|emit-workflow) for orchestrator invocation.
- Two gated loop contributions at wired points (execute:wave:post, plan:post);
  federated config keys (enabled/execution_backend/min_agent_sdk_version).
- ADR-1143 implementation amendment; CONTEXT.md glossary entry; explanation doc.

On any runtime lacking the Workflow tool, behaviour is byte-identical to today.

closes #1143
This commit is contained in:
Tom Boucher
2026-07-06 15:18:23 -04:00
parent d01eec4358
commit e3262d94d3
19 changed files with 2357 additions and 9 deletions

View File

@@ -493,7 +493,7 @@ describe('resolveLoopHooks plan:post — pure function against real registry', (
assert.strictEqual(result.activeHooks[0].capId, 'gap-analysis');
});
test('[happy] real registry byLoopPoint plan:post has 1 step (mempalace), 1 contribution (external-job planner fragment), and 1 gate (gap-analysis)', () => {
test('[happy] real registry byLoopPoint plan:post has 1 step (mempalace), 2 contributions (external-job planner + claude-orchestration ultraplan ownership), and 1 gate (gap-analysis)', () => {
const entry = realRegistry.byLoopPoint['plan:post'];
assert.ok(entry, 'plan:post must exist in byLoopPoint');
assert.ok(Array.isArray(entry.steps), 'steps must be an array');
@@ -501,8 +501,12 @@ describe('resolveLoopHooks plan:post — pure function against real registry', (
assert.ok(Array.isArray(entry.gates), 'gates must be an array');
assert.strictEqual(entry.steps.length, 1, 'plan:post must have 1 step (mempalace capture)');
assert.strictEqual(entry.steps[0].capId, 'mempalace', 'plan:post step must be from mempalace');
assert.strictEqual(entry.contributions.length, 1, 'plan:post must have 1 contribution (external-job planner runtime-budget fragment)');
assert.strictEqual(entry.contributions[0].capId, 'external-job', 'plan:post contribution must be from external-job');
// #1143: claude-orchestration registers a plan:post contribution declaring
// ultraplan plan-offload ownership under its runtime gate (default-off).
assert.strictEqual(entry.contributions.length, 2, 'plan:post must have 2 contributions (external-job planner + claude-orchestration ultraplan ownership)');
const capIds = entry.contributions.map(c => c.capId).sort();
assert.deepStrictEqual(capIds, ['claude-orchestration', 'external-job'],
`plan:post contributions must be external-job + claude-orchestration; got ${capIds.join(',')}`);
assert.strictEqual(entry.gates.length, 1, 'plan:post must have exactly one gate');
assert.strictEqual(entry.gates[0].capId, 'gap-analysis');
});

View File

@@ -0,0 +1,169 @@
'use strict';
/**
* claude-orchestration-command-router.test.cjs — end-to-end tests for the
* `gsd-tools claude-orchestration` command surface (#1143).
*
* Exercises the full dispatch path: gsd-tools → dispatchCapabilityCommand →
* routeClaudeOrchestrationCommand → the pure detect/emitter functions.
*
* NOTE: runGsdTools() returns { success, output, exitCode, error } — it does
* NOT throw on a non-zero exit (helpers.cjs). Tests assert `.success` and parse
* `.output`, and check `.error` on the failure path.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── Fixtures ─────────────────────────────────────────────────────────────────
const WAVES_MANIFEST = {
waves: [
{
id: 'w1',
plans: [
{ id: 'p1', brief: 'Implement the foo module', files_modified: ['src/foo.cts'] },
{ id: 'p2', brief: 'Wire the bar seam', files_modified: ['src/bar.cts'] },
],
},
],
};
function writeManifest(tmpDir) {
const manifestPath = path.join(tmpDir, 'waves.json');
fs.writeFileSync(manifestPath, JSON.stringify(WAVES_MANIFEST), 'utf8');
return manifestPath;
}
/** Run the command and assert it succeeded, returning the parsed JSON output. */
function runAndParse(args, cwd) {
const res = runGsdTools(args, cwd);
assert.strictEqual(res.success, true, 'command should succeed; stderr: ' + (res.error || ''));
assert.ok(res.output.length > 0, 'command should emit output');
return JSON.parse(res.output);
}
// ─── emit-workflow ────────────────────────────────────────────────────────────
describe('claude-orchestration emit-workflow (CLI)', () => {
test('emits a Workflow script with parallel barriers, gsd-executor + worktree, and resumeFromRunId', () => {
const tmp = createTempProject('claw-emit-');
try {
const manifestPath = writeManifest(tmp);
const parsed = runAndParse([
'claude-orchestration', 'emit-workflow',
'--waves', manifestPath,
'--run-id', 'run-cli-1143',
'--phase-dir', '.planning/phases/01-foo',
], tmp);
assert.ok(typeof parsed.script === 'string' && parsed.script.length > 0);
assert.ok(parsed.script.includes('parallel('), 'parallel() barrier emitted');
assert.ok(parsed.script.includes('gsd-executor'), 'gsd-executor agentType');
assert.ok(parsed.script.includes('worktree'), 'worktree isolation');
assert.ok(parsed.script.includes('resumeFromRunId'), 'resumeFromRunId wired');
assert.ok(parsed.script.includes('run-cli-1143'), 'carries the run id');
assert.strictEqual(parsed.summary.resumeRunId, 'run-cli-1143');
assert.strictEqual(parsed.summary.waves, 1);
assert.strictEqual(parsed.summary.plans, 2);
} finally {
cleanup(tmp);
}
});
test('budget flag threads a shared token pool into the script', () => {
const tmp = createTempProject('claw-budget-');
try {
const manifestPath = writeManifest(tmp);
const parsed = runAndParse([
'claude-orchestration', 'emit-workflow',
'--waves', manifestPath,
'--run-id', 'r',
'--budget', '750000',
], tmp);
assert.ok(parsed.script.includes('budget('), 'budget() pool emitted');
assert.ok(parsed.script.includes('750000'));
} finally {
cleanup(tmp);
}
});
test('missing --waves -> non-zero exit with a diagnostic', () => {
const tmp = createTempProject('claw-noargs-');
try {
const res = runGsdTools([
'claude-orchestration', 'emit-workflow', '--run-id', 'r',
], tmp);
assert.strictEqual(res.success, false, 'missing --waves must fail');
assert.ok(res.exitCode !== 0, 'non-zero exit');
assert.match(res.error || '', /--waves/);
} finally {
cleanup(tmp);
}
});
});
// ─── detect-backend ───────────────────────────────────────────────────────────
describe('claude-orchestration detect-backend (CLI)', () => {
test('default config (capability off) -> inline, even on Claude', () => {
const tmp = createTempProject('claw-detect-');
try {
const parsed = runAndParse([
'claude-orchestration', 'detect-backend',
'--runtime', 'claude',
'--agent-sdk-version', '1.0.0',
], tmp);
assert.strictEqual(parsed.backend, 'inline');
assert.strictEqual(parsed.available, false);
assert.match(parsed.reason, /disabled/);
} finally {
cleanup(tmp);
}
});
test('enabled + claude + capable + new-enough SDK -> workflow', () => {
const tmp = createTempProject('claw-detect-on-');
try {
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.planning', 'config.json'),
JSON.stringify({ claude_orchestration: { enabled: true, execution_backend: 'auto' } }),
'utf8',
);
const parsed = runAndParse([
'claude-orchestration', 'detect-backend',
'--runtime', 'claude',
'--agent-sdk-version', '1.2.0',
], tmp);
assert.strictEqual(parsed.backend, 'workflow');
assert.strictEqual(parsed.available, true);
} finally {
cleanup(tmp);
}
});
test('non-Claude runtime -> inline (criterion 6)', () => {
const tmp = createTempProject('claw-detect-nonclaude-');
try {
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.planning', 'config.json'),
JSON.stringify({ claude_orchestration: { enabled: true } }),
'utf8',
);
const parsed = runAndParse([
'claude-orchestration', 'detect-backend',
'--runtime', 'codex',
'--agent-sdk-version', '1.0.0',
], tmp);
assert.strictEqual(parsed.backend, 'inline');
assert.match(parsed.reason, /claude/i);
} finally {
cleanup(tmp);
}
});
});

View File

@@ -0,0 +1,607 @@
'use strict';
/**
* claude-orchestration.test.cjs — Behavioral tests for the Claude orchestration
* capability (#1143): Workflow-tool backend detection, Workflow-script emission,
* capability-declaration validation, registry integration, and inline-fallback parity.
*
* The capability is default-off + BETA + claude-only. On any runtime lacking the
* Workflow tool it must be a byte-identical no-op. These tests encode that contract.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('fast-check');
const {
detectWorkflowBackend,
emitWorkflowScript,
WORKFLOW_TOOL_FLOOR_VERSION,
BACKEND_VALUES,
compareSemver,
} = require('../gsd-core/bin/lib/claude-orchestration.cjs');
const {
validateCapability,
validateAgainstContract,
loadAndValidate,
buildRegistry,
serializeRegistry,
normalizeLineEndings,
stripGeneratedComment,
} = require('../scripts/gen-capability-registry.cjs');
const ROOT = path.resolve(__dirname, '..');
const CAP_PATH = path.join(ROOT, 'capabilities', 'claude-orchestration', 'capability.json');
const REGISTRY_PATH = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'capability-registry.cjs');
// ─── Fixtures ─────────────────────────────────────────────────────────────────
/** A host-integration descriptor whose dispatch axis signals Workflow-tool capability. */
const CAPABLE_HOST = {
dispatch: { namedDispatch: true, nested: true, background: true, backgroundDispatch: false },
};
/** Read the real capability declaration (data file — not a source grep). */
function loadCap() {
return JSON.parse(fs.readFileSync(CAP_PATH, 'utf8'));
}
/** A minimal single-plan wave manifest. */
function singleWaveManifest() {
return {
phaseDir: '.planning/phases/01-foo',
runId: 'run-abc-1143',
waves: [
{
id: 'w1',
plans: [
{ id: 'p1', brief: 'Implement the foo module', files_modified: ['src/foo.cts'] },
],
},
],
};
}
/** Two plans in one wave that DO NOT overlap (parallel-safe in a single stage). */
function nonOverlappingManifest() {
return {
phaseDir: '.planning/phases/01-foo',
runId: 'run-abc-1143',
waves: [
{
id: 'w1',
plans: [
{ id: 'p1', brief: 'Plan A', files_modified: ['src/a.cts'] },
{ id: 'p2', brief: 'Plan B', files_modified: ['src/b.cts'] },
],
},
],
};
}
/** Two plans in one wave that DO overlap on files_modified (must split into stages). */
function overlappingManifest() {
return {
phaseDir: '.planning/phases/01-foo',
runId: 'run-abc-1143',
waves: [
{
id: 'w1',
plans: [
{ id: 'p1', brief: 'Plan A', files_modified: ['src/shared.cts', 'src/a.cts'] },
{ id: 'p2', brief: 'Plan B', files_modified: ['src/shared.cts', 'src/b.cts'] },
],
},
],
};
}
// ─── 1. detectWorkflowBackend ─────────────────────────────────────────────────
describe('detectWorkflowBackend', () => {
test('capability disabled (default-off) -> inline, even on Claude with the tool', () => {
const r = detectWorkflowBackend({
runtimeId: 'claude',
hostIntegration: CAPABLE_HOST,
agentSdkVersion: '1.0.0',
config: { 'claude_orchestration.enabled': false },
});
assert.strictEqual(r.available, false);
assert.strictEqual(r.backend, 'inline');
assert.match(r.reason, /disabled/);
});
test('non-Claude runtime -> inline (criterion 6: no change to non-Claude loop)', () => {
for (const runtimeId of ['codex', 'cursor', 'opencode', 'copilot', ' Windsurf'.trim()]) {
const r = detectWorkflowBackend({
runtimeId,
hostIntegration: CAPABLE_HOST,
agentSdkVersion: '1.0.0',
config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'workflow' },
});
assert.strictEqual(r.backend, 'inline', runtimeId + ' should be inline');
assert.strictEqual(r.available, false, runtimeId + ' should be unavailable');
assert.match(r.reason, /claude/i, runtimeId + ' reason should mention claude');
}
});
test('Claude + auto + capable host + new-enough SDK -> workflow', () => {
const r = detectWorkflowBackend({
runtimeId: 'claude',
hostIntegration: CAPABLE_HOST,
agentSdkVersion: '1.2.0',
config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' },
});
assert.strictEqual(r.backend, 'workflow');
assert.strictEqual(r.available, true);
});
test('Claude + execution_backend:"workflow" forces workflow when tool is capable', () => {
const r = detectWorkflowBackend({
runtimeId: 'claude',
hostIntegration: CAPABLE_HOST,
agentSdkVersion: '1.0.0',
config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'workflow' },
});
assert.strictEqual(r.backend, 'workflow');
assert.strictEqual(r.available, true);
});
test('Claude + execution_backend:"inline" -> inline even when tool is capable', () => {
const r = detectWorkflowBackend({
runtimeId: 'claude',
hostIntegration: CAPABLE_HOST,
agentSdkVersion: '1.0.0',
config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'inline' },
});
assert.strictEqual(r.backend, 'inline');
assert.match(r.reason, /inline/);
});
test('Claude + auto + host lacking nested dispatch -> inline (fail-closed)', () => {
const r = detectWorkflowBackend({
runtimeId: 'claude',
hostIntegration: { dispatch: { nested: false, background: true } },
agentSdkVersion: '1.0.0',
config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' },
});
assert.strictEqual(r.backend, 'inline');
assert.strictEqual(r.available, false);
});
test('Claude + unknown agentSdkVersion -> inline fail-closed (criterion 3 fallback)', () => {
const r = detectWorkflowBackend({
runtimeId: 'claude',
hostIntegration: CAPABLE_HOST,
agentSdkVersion: undefined,
config: { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' },
});
assert.strictEqual(r.backend, 'inline');
assert.strictEqual(r.available, false);
assert.match(r.reason, /version|sdk|unknown/i);
});
test('agent SDK version boundary: floor-1 -> inline, floor -> workflow, floor+patch -> workflow', () => {
const floor = WORKFLOW_TOOL_FLOOR_VERSION;
const [maj, min, pat] = floor.split('.').map((n) => parseInt(n, 10));
// Robust "below" derivation with full borrow chain (works for .0.0 floors too).
let below;
if (pat > 0) below = `${maj}.${min}.${pat - 1}`;
else if (min > 0) below = `${maj}.${min - 1}.999`;
else if (maj > 0) below = `${maj - 1}.999.999`;
else { assert.ok(false, 'cannot derive below for 0.0.0 floor'); return; }
const above = `${maj}.${min}.${pat + 1}`;
// Sanity: confirm below really is below per the comparator under test.
assert.ok(compareSemver(below, floor) < 0, below + ' must compare below ' + floor);
const cfg = { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' };
const rBelow = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: below, config: cfg });
assert.strictEqual(rBelow.backend, 'inline', below + ' (floor-1) must be inline');
const rAt = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: floor, config: cfg });
assert.strictEqual(rAt.backend, 'workflow', floor + ' (exact floor) must be workflow');
const rAbove = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: above, config: cfg });
assert.strictEqual(rAbove.backend, 'workflow', above + ' (floor+patch) must be workflow');
});
test('config-level min_agent_sdk_version override raises/lowers the floor', () => {
const cfg = {
'claude_orchestration.enabled': true,
'claude_orchestration.execution_backend': 'auto',
'claude_orchestration.min_agent_sdk_version': '2.0.0',
};
const r1 = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: '1.9.9', config: cfg });
assert.strictEqual(r1.backend, 'inline', 'below raised floor -> inline');
const r2 = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: '2.0.0', config: cfg });
assert.strictEqual(r2.backend, 'workflow', 'at raised floor -> workflow');
});
test('execution_backend:"workflow" + SDK below floor -> inline (M-1: floor applies in both modes)', () => {
const cfg = {
'claude_orchestration.enabled': true,
'claude_orchestration.execution_backend': 'workflow',
};
const r = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: '0.3.0', config: cfg });
assert.strictEqual(r.backend, 'inline', 'workflow mode must still honor the SDK floor (fail-closed)');
assert.strictEqual(r.available, false);
assert.match(r.reason, /floor|version/);
});
test('pre-release of the floor (0.3.149-rc.1) -> inline (pre-release < GA per SemVer)', () => {
const cfg = { 'claude_orchestration.enabled': true, 'claude_orchestration.execution_backend': 'auto' };
// Explicitly assert the precedence rule: a pre-release tag is below the GA release.
assert.ok(compareSemver('0.3.149-rc.1', '0.3.149') < 0, 'pre-release must compare below GA');
const r = detectWorkflowBackend({ runtimeId: 'claude', hostIntegration: CAPABLE_HOST, agentSdkVersion: '0.3.149-rc.1', config: cfg });
assert.strictEqual(r.backend, 'inline', 'pre-release of the floor must not activate the BETA backend');
assert.strictEqual(r.available, false);
});
test('missing/empty input -> inline, never throws (Postel: liberal-in-input)', () => {
assert.strictEqual(detectWorkflowBackend({}).backend, 'inline');
assert.strictEqual(detectWorkflowBackend(null).backend, 'inline');
assert.strictEqual(detectWorkflowBackend(undefined).backend, 'inline');
assert.strictEqual(detectWorkflowBackend({ runtimeId: 'claude' }).backend, 'inline');
});
test('BACKEND_VALUES exposes the closed enum', () => {
assert.deepStrictEqual([...BACKEND_VALUES].sort(), ['auto', 'inline', 'workflow']);
});
test('property: pure & deterministic (same input -> same output)', () => {
fc.assert(fc.property(
fc.record({
runtimeId: fc.constantFrom('claude', 'codex', 'cursor', 'opencode'),
sdk: fc.option(fc.string({ minLength: 1, maxLength: 8 }).filter((s) => /^\d/.test(s)), { nil: undefined }),
backend: fc.constantFrom('auto', 'workflow', 'inline'),
enabled: fc.boolean(),
}),
(input) => {
const cfg = {
'claude_orchestration.enabled': input.enabled,
'claude_orchestration.execution_backend': input.backend,
};
const a = detectWorkflowBackend({ runtimeId: input.runtimeId, hostIntegration: CAPABLE_HOST, agentSdkVersion: input.sdk, config: cfg });
const b = detectWorkflowBackend({ runtimeId: input.runtimeId, hostIntegration: CAPABLE_HOST, agentSdkVersion: input.sdk, config: cfg });
assert.deepStrictEqual(a, b);
assert.ok(['workflow', 'inline'].includes(a.backend));
},
));
});
});
// ─── 2. compareSemver helper ──────────────────────────────────────────────────
describe('compareSemver', () => {
test('ordering', () => {
assert.ok(compareSemver('1.0.0', '0.9.9') > 0);
assert.ok(compareSemver('1.0.0', '1.0.0') === 0);
assert.ok(compareSemver('1.0.0', '1.0.1') < 0);
assert.ok(compareSemver('2.0.0', '1.9.9') > 0);
});
test('garbage versions compare as -1 (fail-closed)', () => {
assert.strictEqual(compareSemver('garbage', '1.0.0'), -1);
assert.strictEqual(compareSemver('1.0.0', ''), -1);
});
});
// ─── 3. emitWorkflowScript ────────────────────────────────────────────────────
describe('emitWorkflowScript', () => {
test('single-wave single-plan -> one parallel barrier, one agent, executor+worktree', () => {
const { ok, script, summary } = emitWorkflowScript(singleWaveManifest());
assert.strictEqual(ok, true);
assert.ok(typeof script === 'string' && script.length > 0);
const parallelCount = (script.match(/parallel\s*\(/g) || []).length;
assert.ok(parallelCount >= 1, 'at least one parallel() barrier');
assert.ok(script.includes('agent('), 'agent() call per plan');
assert.ok(script.includes('gsd-executor'), 'uses gsd-executor agentType');
assert.ok(script.includes('worktree'), 'uses worktree isolation');
assert.ok(script.includes('SUMMARY.md'), 'produces SUMMARY.md (same artifact as inline path)');
assert.deepStrictEqual(summary.waves, 1);
assert.deepStrictEqual(summary.plans, 1);
});
test('multi-wave -> one parallel() barrier per wave (sequential barriers)', () => {
const r = emitWorkflowScript({
phaseDir: '.planning/phases/01-foo',
runId: 'run-multi',
waves: [
{ id: 'w1', plans: [{ id: 'p1', brief: 'A', files_modified: ['src/a.cts'] }] },
{ id: 'w2', plans: [{ id: 'p2', brief: 'B', files_modified: ['src/b.cts'] }] },
{ id: 'w3', plans: [{ id: 'p3', brief: 'C', files_modified: ['src/c.cts'] }] },
],
});
assert.strictEqual(r.ok, true);
const parallelCount = (r.script.match(/parallel\s*\(/g) || []).length;
assert.strictEqual(parallelCount, 3, 'one parallel() per wave');
assert.strictEqual(r.summary.waves, 3);
assert.strictEqual(r.summary.plans, 3);
});
test('overlapping files_modified -> plans split into separate sequential stages (criterion 2)', () => {
const r = emitWorkflowScript(overlappingManifest());
assert.strictEqual(r.ok, true);
// Two plans sharing src/shared.cts must NOT be in the same stage.
const stages = r.summary.stagesByWave[0]; // wave w1
assert.ok(Array.isArray(stages), 'stagesByWave present');
assert.strictEqual(stages.length, 2, 'overlapping plans split into 2 stages');
const stagePlanSets = stages.map((s) => s.slice().sort());
const allPlans = stagePlanSets.flat().sort();
assert.deepStrictEqual(allPlans, ['p1', 'p2']);
// p1 and p2 must be in different stages
assert.ok(stages[0].length === 1 && stages[1].length === 1, 'one plan per stage when they overlap');
});
test('non-overlapping plans -> coalesced into a single parallel stage', () => {
const r = emitWorkflowScript(nonOverlappingManifest());
assert.strictEqual(r.ok, true);
const stages = r.summary.stagesByWave[0];
assert.strictEqual(stages.length, 1, 'non-overlapping plans share one stage');
assert.deepStrictEqual(stages[0].slice().sort(), ['p1', 'p2']);
});
test('resumeFromRunId wired to the provided runId (criterion 4)', () => {
const r = emitWorkflowScript(singleWaveManifest());
assert.ok(r.script.includes('resumeFromRunId'), 'references resumeFromRunId');
assert.ok(r.script.includes('run-abc-1143'), 'carries the run id');
assert.strictEqual(r.summary.resumeRunId, 'run-abc-1143');
});
test('shared budget pool emitted when budgetTokens provided', () => {
const r = emitWorkflowScript({ ...singleWaveManifest(), budgetTokens: 500000 });
assert.ok(r.script.includes('budget('), 'emits budget() pool');
assert.ok(r.script.includes('500000'));
});
test('no budget() emitted when budgetTokens omitted', () => {
const r = emitWorkflowScript(singleWaveManifest());
assert.ok(!r.script.includes('budget('), 'no budget() when unset');
});
test('invalid input -> ok:false with a reason, never throws', () => {
const empty = emitWorkflowScript({ phaseDir: '.p', runId: 'r', waves: [] });
assert.strictEqual(empty.ok, false);
assert.ok(typeof empty.reason === 'string' && empty.reason.length > 0);
const noRun = emitWorkflowScript({ phaseDir: '.p', runId: '', waves: singleWaveManifest().waves });
assert.strictEqual(noRun.ok, false);
const noPhase = emitWorkflowScript({ phaseDir: '', runId: 'r', waves: singleWaveManifest().waves });
assert.strictEqual(noPhase.ok, false);
const badWave = emitWorkflowScript({ phaseDir: '.p', runId: 'r', waves: [{ id: 'w1', plans: [] }] });
assert.strictEqual(badWave.ok, false);
});
test('SECURITY: runId/phaseDir/wave.id/plan.id with injection chars -> ok:false (never reach the script)', () => {
// runId is interpolated inside resumeFromRunId("...") — a quote/backslash/newline
// could break out of the call. Identifier validation must reject it.
const injectRun = emitWorkflowScript({ phaseDir: '.p', runId: 'x");evil("y', waves: singleWaveManifest().waves });
assert.strictEqual(injectRun.ok, false);
assert.match(injectRun.reason, /runId/i);
const newlineRun = emitWorkflowScript({ phaseDir: '.p', runId: 'r\nbreakout', waves: singleWaveManifest().waves });
assert.strictEqual(newlineRun.ok, false);
const injectPhase = emitWorkflowScript({ phaseDir: '.p"; drop table', runId: 'r', waves: singleWaveManifest().waves });
assert.strictEqual(injectPhase.ok, false);
const injectWave = emitWorkflowScript({
phaseDir: '.p', runId: 'r',
waves: [{ id: 'w1\nagent("evil")', plans: [{ id: 'p1', brief: 'b', files_modified: ['a.cts'] }] }],
});
assert.strictEqual(injectWave.ok, false);
const injectPlan = emitWorkflowScript({
phaseDir: '.p', runId: 'r',
waves: [{ id: 'w1', plans: [{ id: 'p1";x("y', brief: 'b', files_modified: ['a.cts'] }] }],
});
assert.strictEqual(injectPlan.ok, false);
});
test('SECURITY: a brief containing quotes/backslash/newlines is neutralised (never breaks the string literal)', () => {
const r = emitWorkflowScript({
phaseDir: '.p', runId: 'r',
waves: [{ id: 'w1', plans: [{ id: 'p1', brief: 'he said "hi" \\ then \n newline', files_modified: ['a.cts'] }] }],
});
assert.strictEqual(r.ok, true);
// The emitted script must not contain a raw unescaped quote that closes the
// agent() string literal, nor a raw newline inside the brief.
assert.ok(!r.script.includes('he said "hi" \\\\'), 'no unescaped breakout');
// The full brief text never appears verbatim with its dangerous chars intact.
assert.ok(!r.script.includes('"hi"'), 'the inner quote must be JSON-escaped, not raw');
});
test('duplicate plan id within a wave -> ok:false (L-5: no silent brief loss)', () => {
const r = emitWorkflowScript({
phaseDir: '.p', runId: 'r',
waves: [{ id: 'w1', plans: [
{ id: 'p1', brief: 'first', files_modified: ['a.cts'] },
{ id: 'p1', brief: 'second', files_modified: ['b.cts'] },
] }],
});
assert.strictEqual(r.ok, false);
assert.match(r.reason, /duplicate/i);
});
test('non-string files_modified entries -> ok:false (L-7: strict element typing)', () => {
const r = emitWorkflowScript({
phaseDir: '.p', runId: 'r',
waves: [{ id: 'w1', plans: [{ id: 'p1', brief: 'b', files_modified: ['ok.cts', 42, { path: 'x' }] }] }],
});
assert.strictEqual(r.ok, false);
assert.match(r.reason, /files_modified/);
});
test('property: deterministic (same input -> identical script)', () => {
fc.assert(fc.property(
fc.record({
runId: fc.string({ minLength: 1, maxLength: 12 }).filter((s) => /^[a-zA-Z0-9-]+$/.test(s)),
nPlans: fc.integer({ min: 1, max: 5 }),
}),
({ runId, nPlans }) => {
const waves = [{
id: 'w1',
plans: Array.from({ length: nPlans }, (_, i) => ({
id: 'p' + i,
brief: 'brief ' + i,
files_modified: ['src/file' + i + '.cts'],
})),
}];
const a = emitWorkflowScript({ phaseDir: '.planning/phases/01-x', runId, waves });
const b = emitWorkflowScript({ phaseDir: '.planning/phases/01-x', runId, waves });
assert.strictEqual(a.script, b.script);
assert.deepStrictEqual(a.summary, b.summary);
},
));
});
});
// ─── 4. Capability declaration validation ─────────────────────────────────────
describe('capability declaration (capabilities/claude-orchestration/capability.json)', () => {
test('file exists and parses', () => {
const cap = loadCap();
assert.strictEqual(cap.id, 'claude-orchestration');
});
test('passes per-file validateCapability', () => {
const errors = validateCapability(loadCap(), 'claude-orchestration');
assert.deepEqual(errors, [], 'Expected no validation errors: ' + JSON.stringify(errors));
});
test('passes contract validation (contribution.into roles, when references)', () => {
const errors = validateAgainstContract(loadCap(), 'claude-orchestration');
assert.deepEqual(errors, [], 'Expected no contract errors: ' + JSON.stringify(errors));
});
test('default-off: activationKey default is false and points at the enabled key', () => {
const cap = loadCap();
assert.strictEqual(cap.activationKey, 'claude_orchestration.enabled');
assert.strictEqual(cap.config['claude_orchestration.enabled'].default, false);
assert.strictEqual(cap.config['claude_orchestration.enabled'].type, 'boolean');
});
test('runtimeCompat is claude-only (criterion 6)', () => {
const cap = loadCap();
assert.deepStrictEqual(cap.runtimeCompat.supported, ['claude']);
assert.deepStrictEqual(cap.runtimeCompat.unsupported, []);
});
test('BETA posture: tier full, role feature', () => {
const cap = loadCap();
assert.strictEqual(cap.role, 'feature');
assert.strictEqual(cap.tier, 'full');
});
test('execution_backend is an enum with auto|workflow|inline defaulting to auto', () => {
const slice = loadCap().config['claude_orchestration.execution_backend'];
assert.strictEqual(slice.type, 'enum');
assert.deepStrictEqual(slice.values, ['auto', 'workflow', 'inline']);
assert.strictEqual(slice.default, 'auto');
});
test('registers at WIRED points only (execute:wave:post, plan:post)', () => {
const cap = loadCap();
const points = cap.contributions.map((c) => c.point);
for (const p of points) {
assert.ok(
['discuss:pre', 'discuss:post', 'plan:pre', 'plan:post', 'execute:post', 'execute:wave:post', 'verify:post', 'ship:pre', 'ship:post'].includes(p),
'contribution point ' + p + ' must be a wired point',
);
}
assert.ok(points.includes('execute:wave:post'), 'registers the execute wave hook');
assert.ok(points.includes('plan:post'), 'declares plan:* ownership for ultraplan (criterion 5)');
});
test('all contributions gated by the enabled key + onError:skip (default-resilient)', () => {
const cap = loadCap();
for (const c of cap.contributions) {
assert.strictEqual(c.when, 'claude_orchestration.enabled', 'every contribution gated by enabled');
assert.strictEqual(c.onError, 'skip', 'every contribution onError:skip');
}
});
});
// ─── 5. Registry integration ──────────────────────────────────────────────────
describe('registry integration', () => {
test('loadAndValidate includes claude-orchestration with no errors', () => {
const { capMap, errors } = loadAndValidate(new Set()); // empty central keys = no collision noise
// Filter errors to only those touching our capability.
const ours = errors.filter((e) => e.includes('claude-orchestration'));
assert.deepEqual(ours, [], 'our capability produced errors: ' + JSON.stringify(ours));
assert.ok(capMap.has('claude-orchestration'), 'capMap includes claude-orchestration');
});
test('buildRegistry surfaces the federated config keys in configSchema', () => {
const { capMap } = loadAndValidate(new Set());
const registry = buildRegistry(capMap);
assert.ok(registry.configSchema['claude_orchestration.enabled'], 'enabled key federated');
assert.ok(registry.configSchema['claude_orchestration.execution_backend'], 'execution_backend key federated');
assert.strictEqual(registry.configSchema['claude_orchestration.enabled'].owner, 'claude-orchestration');
assert.strictEqual(registry.configSchema['claude_orchestration.execution_backend'].default, 'auto');
});
test('byLoopPoint[execute:wave:post].contributions includes our capability', () => {
const { capMap } = loadAndValidate(new Set());
const registry = buildRegistry(capMap);
const contribs = registry.byLoopPoint['execute:wave:post'].contributions;
const ours = contribs.find((c) => c.capId === 'claude-orchestration');
assert.ok(ours, 'our execute:wave:post contribution is registered');
assert.strictEqual(ours.into, 'executor');
});
test('committed registry is in sync (gen-capability-registry --check)', () => {
const { capMap } = loadAndValidate(new Set());
const registry = buildRegistry(capMap);
const live = serializeRegistry(registry, capMap);
const committed = fs.readFileSync(REGISTRY_PATH, 'utf8');
assert.strictEqual(
normalizeLineEndings(stripGeneratedComment(committed)),
normalizeLineEndings(stripGeneratedComment(live)),
'registry is stale — run: node scripts/gen-capability-registry.cjs --write',
);
});
});
// ─── 6. Inline-fallback parity (criterion 3 + 6) ──────────────────────────────
describe('inline-fallback parity', () => {
test('default config (capability off) -> inline on every runtime, including Claude', () => {
// The capability ships default-off; with no user opt-in the backend is always inline.
const defaultCfg = {}; // nothing set
for (const runtimeId of ['claude', 'codex', 'cursor', 'opencode']) {
const r = detectWorkflowBackend({
runtimeId,
hostIntegration: CAPABLE_HOST,
agentSdkVersion: '1.0.0',
config: defaultCfg,
});
assert.strictEqual(r.backend, 'inline', runtimeId + ' default must be inline');
assert.strictEqual(r.available, false, runtimeId + ' default must be unavailable');
}
});
test('generated Workflow script preserves the inline-path contract (same agent + isolation + artifact)', () => {
// Criterion 2: the emitted Workflow composes the SAME gsd-executor agent and worktree
// isolation the inline path uses, and produces the same SUMMARY.md artifact.
const r = emitWorkflowScript(singleWaveManifest());
assert.ok(r.script.includes('gsd-executor'), 'same executor agent as inline dispatch');
assert.ok(r.script.includes('worktree'), 'same worktree isolation as inline dispatch');
assert.ok(r.script.includes('SUMMARY.md'), 'same SUMMARY.md artifact as inline dispatch');
});
});

View File

@@ -629,15 +629,17 @@ describe('F. Real registry execute:wave:post shape — guard against accidental
`ui.safety-gate onError must be 'halt'; got ${uiGate.onError}`);
});
test('[happy] real registry: execute:wave:post has no steps and 2 contributions (mempalace capture-problems + external-job executor fragment)', () => {
test('[happy] real registry: execute:wave:post has no steps and 3 contributions (claude-orchestration executor + external-job executor + mempalace capture-problems)', () => {
const point = realRegistry.byLoopPoint['execute:wave:post'];
assert.strictEqual(point.steps.length, 0,
`execute:wave:post steps must be empty; got ${point.steps.length}`);
assert.strictEqual(point.contributions.length, 2,
`execute:wave:post must have 2 contributions (mempalace + external-job); got ${point.contributions.length}`);
// #1143: claude-orchestration registers an execute:wave:post contribution
// providing the Workflow-tool backend guidance (default-off, claude-only).
assert.strictEqual(point.contributions.length, 3,
`execute:wave:post must have 3 contributions (claude-orchestration + external-job + mempalace); got ${point.contributions.length}`);
const capIds = point.contributions.map(c => c.capId).sort();
assert.deepStrictEqual(capIds, ['external-job', 'mempalace'],
`execute:wave:post contributions must be mempalace + external-job; got ${capIds.join(',')}`);
assert.deepStrictEqual(capIds, ['claude-orchestration', 'external-job', 'mempalace'],
`execute:wave:post contributions must be claude-orchestration + external-job + mempalace; got ${capIds.join(',')}`);
});
});