fix(#1143): address adversarial review — full semver precedence, docs/reality alignment

- compareSemver: implement full SemVer 2.0.0 §11 pre-release identifier
  comparison (two pre-releases of the same triple now order correctly; was 0).
- capability description + fragment: scope the plan-checker/verifier claim
  (this capability delivers the parallel-execution backend; those gates remain
  inline until separately wired). Correct the 'each wave is one barrier' prose
  (a wave splits into multiple sequential parallel() barriers on files_modified
  overlap). Frame detect-backend CLI as a simulation harness; the pure function
  with the live host descriptor is the real detection seam.
- partitionStages docstring: 'near-minimal via greedy first-fit' (not 'fewest');
  document empty-files_modified behavior.
This commit is contained in:
Tom Boucher
2026-07-06 15:41:19 -04:00
parent e3262d94d3
commit f433db8b88
6 changed files with 114 additions and 20 deletions

View File

@@ -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": {

View File

@@ -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 <dir>] [--budget <n>]` (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

View File

@@ -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 <manifest.json> --run-id <id>\n[--phase-dir <dir>] [--budget <n>]` (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 <manifest.json> --run-id <id>\n[--phase-dir <dir>] [--budget <n>]` (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 <manifest.json> --run-id <id>\n[--phase-dir <dir>] [--budget <n>]` (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 <manifest.json> --run-id <id>\n[--phase-dir <dir>] [--budget <n>]` (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": [

View File

@@ -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).

View File

@@ -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).

View File

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