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');