diff --git a/capabilities/claude-orchestration/capability.json b/capabilities/claude-orchestration/capability.json
index 921704c67..95474d492 100644
--- a/capabilities/claude-orchestration/capability.json
+++ b/capabilities/claude-orchestration/capability.json
@@ -3,7 +3,7 @@
"role": "feature",
"version": "1.7.0-rc.3",
"title": "Claude orchestration (Workflow backend)",
- "description": "Default-off, BETA, claude-only capability that adopts Claude Code's Workflow tool (the engine behind /effort ultracode) as an optional parallel-execution backend for the GSD loop. When the runtime exposes the Workflow tool and claude_orchestration.execution_backend resolves to 'workflow', execute-phase emits a generated Workflow script (waves -> parallel() barriers, plans -> agent({ agentType: 'gsd-executor', isolation: 'worktree' }), files_modified overlap -> separate sequential stages, resumeFromRunId wired to the phase run id, shared token budget) that composes the SAME gsd-executor agent and worktree isolation the inline path uses, restoring the parallelism + plan-checker + verifier that the #853 backgrounded-agent nesting limitation forces inline on Claude Code. Also folds the ultraplan plan-offload under one runtime gate (plan:* surface). On any runtime lacking the Workflow tool, or when the capability is disabled, behaviour is byte-identical to today (inline/manual dispatch). Detection + emission live in gsd-core/bin/lib/claude-orchestration.cjs (pure, fail-closed). Mirrors the existing gsd-ultraplan-phase BETA-isolation posture.",
+ "description": "Default-off, BETA, claude-only capability that adopts Claude Code's Workflow tool (the engine behind /effort ultracode) as an optional parallel-execution backend for the GSD loop. When the runtime exposes the Workflow tool and claude_orchestration.execution_backend resolves to 'workflow', execute-phase emits a generated Workflow script (waves -> parallel() barriers, plans -> agent({ agentType: 'gsd-executor', isolation: 'worktree' }), files_modified overlap -> separate sequential stages, resumeFromRunId wired to the phase run id, shared token budget) that composes the SAME gsd-executor agent and worktree isolation the inline path uses, restoring the wave parallelism the #853 backgrounded-agent nesting limitation forces inline on Claude Code. (The plan-checker and verifier remain inline until separately wired — this capability delivers the parallel-execution backend, not those gates.) Also folds the ultraplan plan-offload under one runtime gate (plan:* surface). On any runtime lacking the Workflow tool, or when the capability is disabled, behaviour is byte-identical to today (inline/manual dispatch). Detection + emission live in gsd-core/bin/lib/claude-orchestration.cjs (pure, fail-closed). Mirrors the existing gsd-ultraplan-phase BETA-isolation posture.",
"tier": "full",
"requires": [],
"engines": {
diff --git a/capabilities/claude-orchestration/fragments/execute-wave-post.md b/capabilities/claude-orchestration/fragments/execute-wave-post.md
index 8e9bdb8a7..db0e76d5a 100644
--- a/capabilities/claude-orchestration/fragments/execute-wave-post.md
+++ b/capabilities/claude-orchestration/fragments/execute-wave-post.md
@@ -29,8 +29,10 @@ cannot nest further subagents — #853 — and so degrades to sequential inline
execution), execute-phase **emits a generated Workflow script** and lets the main
loop orchestrate it:
-- **waves → `parallel()` barriers** — each wave is one barrier; the next wave
- waits for the previous to complete.
+- **waves → one or more sequential `parallel()` barriers** — each wave is a
+ barrier group; when plans within a wave share `files_modified`, they are split
+ into separate sequential stages within that wave's barrier (the next wave
+ still waits for the previous wave to complete).
- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**
— the SAME executor agent and worktree isolation the inline path uses, so the
produced `SUMMARY.md` and commits are identical.
@@ -48,8 +50,11 @@ The emitter is a pure function exposed through the capability command surface:
[--phase-dir
] [--budget ]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`
directly). It maps the phase's wave/plan manifest to the Workflow script string
and never invokes the Workflow tool itself; the orchestrator runs the emitted
-script. Use `gsd-tools claude-orchestration detect-backend` to resolve whether
-the Workflow backend should activate for the current runtime.
+script. Detection is resolved by the orchestrator calling the pure
+`detectWorkflowBackend` with the LIVE host descriptor (the CLI
+`gsd-tools claude-orchestration detect-backend` is a simulation harness that
+assumes a capable host unless `--no-nested-dispatch` is passed — it does not probe
+the real runtime; the orchestrator supplies the real descriptor).
## Fallback contract
diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs
index 796ac88da..33e4b1ce1 100644
--- a/gsd-core/bin/lib/capability-registry.cjs
+++ b/gsd-core/bin/lib/capability-registry.cjs
@@ -429,7 +429,7 @@ const capabilities = {
"role": "feature",
"version": "1.7.0-rc.3",
"title": "Claude orchestration (Workflow backend)",
- "description": "Default-off, BETA, claude-only capability that adopts Claude Code's Workflow tool (the engine behind /effort ultracode) as an optional parallel-execution backend for the GSD loop. When the runtime exposes the Workflow tool and claude_orchestration.execution_backend resolves to 'workflow', execute-phase emits a generated Workflow script (waves -> parallel() barriers, plans -> agent({ agentType: 'gsd-executor', isolation: 'worktree' }), files_modified overlap -> separate sequential stages, resumeFromRunId wired to the phase run id, shared token budget) that composes the SAME gsd-executor agent and worktree isolation the inline path uses, restoring the parallelism + plan-checker + verifier that the #853 backgrounded-agent nesting limitation forces inline on Claude Code. Also folds the ultraplan plan-offload under one runtime gate (plan:* surface). On any runtime lacking the Workflow tool, or when the capability is disabled, behaviour is byte-identical to today (inline/manual dispatch). Detection + emission live in gsd-core/bin/lib/claude-orchestration.cjs (pure, fail-closed). Mirrors the existing gsd-ultraplan-phase BETA-isolation posture.",
+ "description": "Default-off, BETA, claude-only capability that adopts Claude Code's Workflow tool (the engine behind /effort ultracode) as an optional parallel-execution backend for the GSD loop. When the runtime exposes the Workflow tool and claude_orchestration.execution_backend resolves to 'workflow', execute-phase emits a generated Workflow script (waves -> parallel() barriers, plans -> agent({ agentType: 'gsd-executor', isolation: 'worktree' }), files_modified overlap -> separate sequential stages, resumeFromRunId wired to the phase run id, shared token budget) that composes the SAME gsd-executor agent and worktree isolation the inline path uses, restoring the wave parallelism the #853 backgrounded-agent nesting limitation forces inline on Claude Code. (The plan-checker and verifier remain inline until separately wired — this capability delivers the parallel-execution backend, not those gates.) Also folds the ultraplan plan-offload under one runtime gate (plan:* surface). On any runtime lacking the Workflow tool, or when the capability is disabled, behaviour is byte-identical to today (inline/manual dispatch). Detection + emission live in gsd-core/bin/lib/claude-orchestration.cjs (pure, fail-closed). Mirrors the existing gsd-ultraplan-phase BETA-isolation posture.",
"tier": "full",
"requires": [],
"engines": {
@@ -485,7 +485,7 @@ const capabilities = {
"into": "executor",
"fragment": {
"path": "fragments/execute-wave-post.md",
- "inline": "# Claude orchestration — Workflow execution backend (BETA)\n\n> Injected at `execute:wave:post` `into: executor` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## When this contribution is active\n\nThe Claude orchestration capability is **default-off and BETA**. It activates only\nwhen ALL of the following hold:\n\n1. `claude_orchestration.enabled` is `true` in `.planning/config.json`, AND\n2. the active runtime is **Claude Code** (the Workflow tool is Claude / Agent\n SDK-specific), AND\n3. `claude_orchestration.execution_backend` resolves to `workflow` — either\n explicitly, or via `auto` — **and** the Agent SDK version is\n `>= claude_orchestration.min_agent_sdk_version` (default `0.3.149`). The SDK\n floor applies in both `auto` and `workflow` modes (fail-closed: a pre-release\n or older SDK never activates the preview backend).\n\nDetection is fail-closed: any miss degrades to **inline, manual, one-agent-per-\nmessage dispatch** — exactly today's behaviour. On a non-Claude runtime this\ncontribution is a no-op.\n\n## What the executor does when the Workflow backend is active\n\nInstead of the orchestrator fanning out one `Agent(subagent_type=gsd-executor,\nisolation=worktree, run_in_background=true)` per message (which on Claude Code\ncannot nest further subagents — #853 — and so degrades to sequential inline\nexecution), execute-phase **emits a generated Workflow script** and lets the main\nloop orchestrate it:\n\n- **waves → `parallel()` barriers** — each wave is one barrier; the next wave\n waits for the previous to complete.\n- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**\n — the SAME executor agent and worktree isolation the inline path uses, so the\n produced `SUMMARY.md` and commits are identical.\n- **`files_modified` overlap → separate sequential stages** — two plans that\n touch the same file are placed in different stages within the wave (the same\n overlap rule execute-phase already applies inline).\n- **`resumeFromRunId`** — wired to the phase run id, so an interrupted phase\n resumes without re-running completed plans.\n- **`budget(tokens)`** — a shared token pool across the whole phase when the\n orchestrator passes a `budgetTokens` value to `emitWorkflowScript` (it is a\n function parameter, not a config key; the orchestrator decides the budget).\n\nThe emitter is a pure function exposed through the capability command surface:\n`gsd-tools claude-orchestration emit-workflow --waves --run-id \n[--phase-dir ] [--budget ]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`\ndirectly). It maps the phase's wave/plan manifest to the Workflow script string\nand never invokes the Workflow tool itself; the orchestrator runs the emitted\nscript. Use `gsd-tools claude-orchestration detect-backend` to resolve whether\nthe Workflow backend should activate for the current runtime.\n\n## Fallback contract\n\nIf detection resolves to `inline` (tool absent, SDK too old, runtime not Claude,\nor the capability disabled), execute-phase MUST proceed with the standard inline\nwave dispatch. The executor MUST NOT assume parallelism, a shared budget, or\nresume-from-run-id semantics in that mode.\n"
+ "inline": "# Claude orchestration — Workflow execution backend (BETA)\n\n> Injected at `execute:wave:post` `into: executor` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## When this contribution is active\n\nThe Claude orchestration capability is **default-off and BETA**. It activates only\nwhen ALL of the following hold:\n\n1. `claude_orchestration.enabled` is `true` in `.planning/config.json`, AND\n2. the active runtime is **Claude Code** (the Workflow tool is Claude / Agent\n SDK-specific), AND\n3. `claude_orchestration.execution_backend` resolves to `workflow` — either\n explicitly, or via `auto` — **and** the Agent SDK version is\n `>= claude_orchestration.min_agent_sdk_version` (default `0.3.149`). The SDK\n floor applies in both `auto` and `workflow` modes (fail-closed: a pre-release\n or older SDK never activates the preview backend).\n\nDetection is fail-closed: any miss degrades to **inline, manual, one-agent-per-\nmessage dispatch** — exactly today's behaviour. On a non-Claude runtime this\ncontribution is a no-op.\n\n## What the executor does when the Workflow backend is active\n\nInstead of the orchestrator fanning out one `Agent(subagent_type=gsd-executor,\nisolation=worktree, run_in_background=true)` per message (which on Claude Code\ncannot nest further subagents — #853 — and so degrades to sequential inline\nexecution), execute-phase **emits a generated Workflow script** and lets the main\nloop orchestrate it:\n\n- **waves → one or more sequential `parallel()` barriers** — each wave is a\n barrier group; when plans within a wave share `files_modified`, they are split\n into separate sequential stages within that wave's barrier (the next wave\n still waits for the previous wave to complete).\n- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**\n — the SAME executor agent and worktree isolation the inline path uses, so the\n produced `SUMMARY.md` and commits are identical.\n- **`files_modified` overlap → separate sequential stages** — two plans that\n touch the same file are placed in different stages within the wave (the same\n overlap rule execute-phase already applies inline).\n- **`resumeFromRunId`** — wired to the phase run id, so an interrupted phase\n resumes without re-running completed plans.\n- **`budget(tokens)`** — a shared token pool across the whole phase when the\n orchestrator passes a `budgetTokens` value to `emitWorkflowScript` (it is a\n function parameter, not a config key; the orchestrator decides the budget).\n\nThe emitter is a pure function exposed through the capability command surface:\n`gsd-tools claude-orchestration emit-workflow --waves --run-id \n[--phase-dir ] [--budget ]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`\ndirectly). It maps the phase's wave/plan manifest to the Workflow script string\nand never invokes the Workflow tool itself; the orchestrator runs the emitted\nscript. Detection is resolved by the orchestrator calling the pure\n`detectWorkflowBackend` with the LIVE host descriptor (the CLI\n`gsd-tools claude-orchestration detect-backend` is a simulation harness that\nassumes a capable host unless `--no-nested-dispatch` is passed — it does not probe\nthe real runtime; the orchestrator supplies the real descriptor).\n\n## Fallback contract\n\nIf detection resolves to `inline` (tool absent, SDK too old, runtime not Claude,\nor the capability disabled), execute-phase MUST proceed with the standard inline\nwave dispatch. The executor MUST NOT assume parallelism, a shared budget, or\nresume-from-run-id semantics in that mode.\n"
},
"produces": [],
"consumes": [
@@ -2993,7 +2993,7 @@ const byLoopPoint = {
"into": "executor",
"fragment": {
"path": "fragments/execute-wave-post.md",
- "inline": "# Claude orchestration — Workflow execution backend (BETA)\n\n> Injected at `execute:wave:post` `into: executor` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## When this contribution is active\n\nThe Claude orchestration capability is **default-off and BETA**. It activates only\nwhen ALL of the following hold:\n\n1. `claude_orchestration.enabled` is `true` in `.planning/config.json`, AND\n2. the active runtime is **Claude Code** (the Workflow tool is Claude / Agent\n SDK-specific), AND\n3. `claude_orchestration.execution_backend` resolves to `workflow` — either\n explicitly, or via `auto` — **and** the Agent SDK version is\n `>= claude_orchestration.min_agent_sdk_version` (default `0.3.149`). The SDK\n floor applies in both `auto` and `workflow` modes (fail-closed: a pre-release\n or older SDK never activates the preview backend).\n\nDetection is fail-closed: any miss degrades to **inline, manual, one-agent-per-\nmessage dispatch** — exactly today's behaviour. On a non-Claude runtime this\ncontribution is a no-op.\n\n## What the executor does when the Workflow backend is active\n\nInstead of the orchestrator fanning out one `Agent(subagent_type=gsd-executor,\nisolation=worktree, run_in_background=true)` per message (which on Claude Code\ncannot nest further subagents — #853 — and so degrades to sequential inline\nexecution), execute-phase **emits a generated Workflow script** and lets the main\nloop orchestrate it:\n\n- **waves → `parallel()` barriers** — each wave is one barrier; the next wave\n waits for the previous to complete.\n- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**\n — the SAME executor agent and worktree isolation the inline path uses, so the\n produced `SUMMARY.md` and commits are identical.\n- **`files_modified` overlap → separate sequential stages** — two plans that\n touch the same file are placed in different stages within the wave (the same\n overlap rule execute-phase already applies inline).\n- **`resumeFromRunId`** — wired to the phase run id, so an interrupted phase\n resumes without re-running completed plans.\n- **`budget(tokens)`** — a shared token pool across the whole phase when the\n orchestrator passes a `budgetTokens` value to `emitWorkflowScript` (it is a\n function parameter, not a config key; the orchestrator decides the budget).\n\nThe emitter is a pure function exposed through the capability command surface:\n`gsd-tools claude-orchestration emit-workflow --waves --run-id \n[--phase-dir ] [--budget ]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`\ndirectly). It maps the phase's wave/plan manifest to the Workflow script string\nand never invokes the Workflow tool itself; the orchestrator runs the emitted\nscript. Use `gsd-tools claude-orchestration detect-backend` to resolve whether\nthe Workflow backend should activate for the current runtime.\n\n## Fallback contract\n\nIf detection resolves to `inline` (tool absent, SDK too old, runtime not Claude,\nor the capability disabled), execute-phase MUST proceed with the standard inline\nwave dispatch. The executor MUST NOT assume parallelism, a shared budget, or\nresume-from-run-id semantics in that mode.\n"
+ "inline": "# Claude orchestration — Workflow execution backend (BETA)\n\n> Injected at `execute:wave:post` `into: executor` only when\n> `claude_orchestration.enabled` is true. Default-off; `onError: skip`.\n\n## When this contribution is active\n\nThe Claude orchestration capability is **default-off and BETA**. It activates only\nwhen ALL of the following hold:\n\n1. `claude_orchestration.enabled` is `true` in `.planning/config.json`, AND\n2. the active runtime is **Claude Code** (the Workflow tool is Claude / Agent\n SDK-specific), AND\n3. `claude_orchestration.execution_backend` resolves to `workflow` — either\n explicitly, or via `auto` — **and** the Agent SDK version is\n `>= claude_orchestration.min_agent_sdk_version` (default `0.3.149`). The SDK\n floor applies in both `auto` and `workflow` modes (fail-closed: a pre-release\n or older SDK never activates the preview backend).\n\nDetection is fail-closed: any miss degrades to **inline, manual, one-agent-per-\nmessage dispatch** — exactly today's behaviour. On a non-Claude runtime this\ncontribution is a no-op.\n\n## What the executor does when the Workflow backend is active\n\nInstead of the orchestrator fanning out one `Agent(subagent_type=gsd-executor,\nisolation=worktree, run_in_background=true)` per message (which on Claude Code\ncannot nest further subagents — #853 — and so degrades to sequential inline\nexecution), execute-phase **emits a generated Workflow script** and lets the main\nloop orchestrate it:\n\n- **waves → one or more sequential `parallel()` barriers** — each wave is a\n barrier group; when plans within a wave share `files_modified`, they are split\n into separate sequential stages within that wave's barrier (the next wave\n still waits for the previous wave to complete).\n- **plans → `agent(brief, { agentType: 'gsd-executor', isolation: 'worktree' })`**\n — the SAME executor agent and worktree isolation the inline path uses, so the\n produced `SUMMARY.md` and commits are identical.\n- **`files_modified` overlap → separate sequential stages** — two plans that\n touch the same file are placed in different stages within the wave (the same\n overlap rule execute-phase already applies inline).\n- **`resumeFromRunId`** — wired to the phase run id, so an interrupted phase\n resumes without re-running completed plans.\n- **`budget(tokens)`** — a shared token pool across the whole phase when the\n orchestrator passes a `budgetTokens` value to `emitWorkflowScript` (it is a\n function parameter, not a config key; the orchestrator decides the budget).\n\nThe emitter is a pure function exposed through the capability command surface:\n`gsd-tools claude-orchestration emit-workflow --waves --run-id \n[--phase-dir ] [--budget ]` (or `require('gsd-core/bin/lib/claude-orchestration.cjs').emitWorkflowScript`\ndirectly). It maps the phase's wave/plan manifest to the Workflow script string\nand never invokes the Workflow tool itself; the orchestrator runs the emitted\nscript. Detection is resolved by the orchestrator calling the pure\n`detectWorkflowBackend` with the LIVE host descriptor (the CLI\n`gsd-tools claude-orchestration detect-backend` is a simulation harness that\nassumes a capable host unless `--no-nested-dispatch` is passed — it does not probe\nthe real runtime; the orchestrator supplies the real descriptor).\n\n## Fallback contract\n\nIf detection resolves to `inline` (tool absent, SDK too old, runtime not Claude,\nor the capability disabled), execute-phase MUST proceed with the standard inline\nwave dispatch. The executor MUST NOT assume parallelism, a shared budget, or\nresume-from-run-id semantics in that mode.\n"
},
"produces": [],
"consumes": [
diff --git a/gsd-core/bin/lib/claude-orchestration.cjs b/gsd-core/bin/lib/claude-orchestration.cjs
index 802329a29..956fdc2ed 100644
--- a/gsd-core/bin/lib/claude-orchestration.cjs
+++ b/gsd-core/bin/lib/claude-orchestration.cjs
@@ -75,6 +75,7 @@ function compareSemver(a, b) {
return [parseInt(core[0], 10), parseInt(core[1], 10), parseInt(core[2], 10)];
};
const hasPre = (s) => s.indexOf('-') !== -1;
+ const preIdentifiers = (s) => (s.split('-')[1] || '').split('+')[0].split('.').filter((x) => x.length > 0);
const am = parseTriple(a);
const bm = parseTriple(b);
for (let i = 0; i < 3; i++) {
@@ -83,15 +84,53 @@ function compareSemver(a, b) {
if (am[i] > bm[i])
return 1;
}
- // Numeric triple is equal. SemVer 2.0.0 precedence: a version WITH a pre-release
- // tag is LOWER than the same triple WITHOUT one. This keeps the floor fail-closed
- // for pre-release builds of the floor (e.g. 0.3.149-rc.1 < 0.3.149 → inline).
+ // Numeric triple is equal. SemVer 2.0.0 §11 precedence:
+ // - a version WITH a pre-release tag is LOWER than the same triple WITHOUT one
+ // (keeps the floor fail-closed for pre-release builds of the GA floor);
+ // - two pre-releases of the same triple are ordered by their dot-separated
+ // identifiers (numeric < alphanumeric; numeric compared numerically,
+ // alphanumeric lexically; fewer identifiers < more).
const aPre = hasPre(a);
const bPre = hasPre(b);
if (aPre && !bPre)
return -1;
if (!aPre && bPre)
return 1;
+ if (aPre && bPre) {
+ const ai = preIdentifiers(a);
+ const bi = preIdentifiers(b);
+ const len = Math.min(ai.length, bi.length);
+ for (let i = 0; i < len; i++) {
+ const ax = ai[i];
+ const bx = bi[i];
+ const aNum = /^\d+$/.test(ax);
+ const bNum = /^\d+$/.test(bx);
+ if (aNum && bNum) {
+ const an = parseInt(ax, 10);
+ const bn = parseInt(bx, 10);
+ if (an < bn)
+ return -1;
+ if (an > bn)
+ return 1;
+ }
+ else if (aNum && !bNum) {
+ return -1; // numeric identifiers always lower than alphanumeric
+ }
+ else if (!aNum && bNum) {
+ return 1;
+ }
+ else {
+ if (ax < bx)
+ return -1;
+ if (ax > bx)
+ return 1;
+ }
+ }
+ if (ai.length < bi.length)
+ return -1;
+ if (ai.length > bi.length)
+ return 1;
+ }
return 0;
}
/** Inline result shorthand. */
@@ -170,9 +209,15 @@ function detectWorkflowBackend(input) {
return { available: true, backend: 'workflow', reason: 'workflow_backend_active' };
}
/**
- * Partition a wave's plans into the fewest sequential stages such that no two
- * plans in the same stage share a modified file. Greedy first-fit: each plan
- * goes into the earliest stage where it does not overlap any plan already there.
+ * Partition a wave's plans into a near-minimal number of sequential stages (via
+ * greedy first-fit — not guaranteed optimal for arbitrary overlap graphs, but
+ * correct: no two plans sharing a file ever cohabit a stage) such that no two
+ * plans in the same stage share a modified file. Each plan goes into the earliest
+ * stage where it does not overlap any plan already there.
+ *
+ * A plan with an EMPTY files_modified set declares no files; it overlaps nothing
+ * and coalesces into stage 0 (same behavior as the inline path, which also cannot
+ * guard against undeclared concurrent writes — declare filesModified accurately).
*
* This is the same overlap rule execute-phase applies inline — the only difference
* is the execution vehicle (Workflow `parallel()` vs one-agent-per-message).
diff --git a/src/claude-orchestration.cts b/src/claude-orchestration.cts
index 897636a8d..8e877edbf 100644
--- a/src/claude-orchestration.cts
+++ b/src/claude-orchestration.cts
@@ -81,19 +81,49 @@ function compareSemver(a: string, b: string): number {
return [parseInt(core[0], 10), parseInt(core[1], 10), parseInt(core[2], 10)];
};
const hasPre = (s: string): boolean => s.indexOf('-') !== -1;
+ const preIdentifiers = (s: string): string[] => (s.split('-')[1] || '').split('+')[0].split('.').filter((x) => x.length > 0);
const am = parseTriple(a);
const bm = parseTriple(b);
for (let i = 0; i < 3; i++) {
if (am[i] < bm[i]) return -1;
if (am[i] > bm[i]) return 1;
}
- // Numeric triple is equal. SemVer 2.0.0 precedence: a version WITH a pre-release
- // tag is LOWER than the same triple WITHOUT one. This keeps the floor fail-closed
- // for pre-release builds of the floor (e.g. 0.3.149-rc.1 < 0.3.149 → inline).
+ // Numeric triple is equal. SemVer 2.0.0 §11 precedence:
+ // - a version WITH a pre-release tag is LOWER than the same triple WITHOUT one
+ // (keeps the floor fail-closed for pre-release builds of the GA floor);
+ // - two pre-releases of the same triple are ordered by their dot-separated
+ // identifiers (numeric < alphanumeric; numeric compared numerically,
+ // alphanumeric lexically; fewer identifiers < more).
const aPre = hasPre(a);
const bPre = hasPre(b);
if (aPre && !bPre) return -1;
if (!aPre && bPre) return 1;
+ if (aPre && bPre) {
+ const ai = preIdentifiers(a);
+ const bi = preIdentifiers(b);
+ const len = Math.min(ai.length, bi.length);
+ for (let i = 0; i < len; i++) {
+ const ax = ai[i];
+ const bx = bi[i];
+ const aNum = /^\d+$/.test(ax);
+ const bNum = /^\d+$/.test(bx);
+ if (aNum && bNum) {
+ const an = parseInt(ax, 10);
+ const bn = parseInt(bx, 10);
+ if (an < bn) return -1;
+ if (an > bn) return 1;
+ } else if (aNum && !bNum) {
+ return -1; // numeric identifiers always lower than alphanumeric
+ } else if (!aNum && bNum) {
+ return 1;
+ } else {
+ if (ax < bx) return -1;
+ if (ax > bx) return 1;
+ }
+ }
+ if (ai.length < bi.length) return -1;
+ if (ai.length > bi.length) return 1;
+ }
return 0;
}
@@ -253,9 +283,15 @@ interface EmitErr {
}
/**
- * Partition a wave's plans into the fewest sequential stages such that no two
- * plans in the same stage share a modified file. Greedy first-fit: each plan
- * goes into the earliest stage where it does not overlap any plan already there.
+ * Partition a wave's plans into a near-minimal number of sequential stages (via
+ * greedy first-fit — not guaranteed optimal for arbitrary overlap graphs, but
+ * correct: no two plans sharing a file ever cohabit a stage) such that no two
+ * plans in the same stage share a modified file. Each plan goes into the earliest
+ * stage where it does not overlap any plan already there.
+ *
+ * A plan with an EMPTY files_modified set declares no files; it overlaps nothing
+ * and coalesces into stage 0 (same behavior as the inline path, which also cannot
+ * guard against undeclared concurrent writes — declare filesModified accurately).
*
* This is the same overlap rule execute-phase applies inline — the only difference
* is the execution vehicle (Workflow `parallel()` vs one-agent-per-message).
diff --git a/tests/claude-orchestration.test.cjs b/tests/claude-orchestration.test.cjs
index 03a3e6c66..808501e1e 100644
--- a/tests/claude-orchestration.test.cjs
+++ b/tests/claude-orchestration.test.cjs
@@ -243,6 +243,14 @@ describe('detectWorkflowBackend', () => {
assert.strictEqual(r.available, false);
});
+ test('two pre-releases of the same triple order by their identifiers (SemVer §11)', () => {
+ assert.ok(compareSemver('0.3.149-rc.0', '0.3.149-rc.1') < 0, 'rc.0 < rc.1');
+ assert.ok(compareSemver('1.0.0-alpha.1', '1.0.0-alpha.2') < 0, 'alpha.1 < alpha.2');
+ assert.ok(compareSemver('1.0.0-rc.1', '1.0.0-rc.2') < 0, 'rc.1 < rc.2');
+ // numeric < alphanumeric at the same position
+ assert.ok(compareSemver('1.0.0-1', '1.0.0-alpha') < 0, 'numeric identifier < alphanumeric');
+ });
+
test('missing/empty input -> inline, never throws (Postel: liberal-in-input)', () => {
assert.strictEqual(detectWorkflowBackend({}).backend, 'inline');
assert.strictEqual(detectWorkflowBackend(null).backend, 'inline');