@ikon85/agent-workflow-kit 0.32.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ # Path B — Native subagent dispatch
2
+
3
+ Load this recipe only when the capability selector returns Path B. That result
4
+ requires proven native spawn, wait, and aggregate primitives plus effective
5
+ concurrency and thread capacity ≥2. Route from proven capabilities only, never
6
+ from a surface name, and do not emulate a missing primitive.
7
+
8
+ This recipe is **dormant on the current Codex host.** A verify spike against
9
+ codex-cli 0.144.6 (2026-07-21) proved native start plus bounded wait, and an
10
+ effective concurrency of 4, but the host exposes tool entries carrying only a
11
+ name and a description: no tool schema, no callable or permitted flags, no
12
+ thread capacity. The adapter therefore emits `unknown` for those fields and the
13
+ selector fails closed to Path C. Only a future host that supplies the complete
14
+ normalized inventory selects this path.
15
+
16
+ ## Main-thread preparation
17
+
18
+ 1. Read the canonical `RECON_REPORT_SCHEMA` and `BUILDER_REPORT_SCHEMA` values
19
+ from `src/lib/reportValidator.mjs`. They are the same contract every
20
+ orchestration path reports against.
21
+ 2. Embed the relevant schema verbatim in each subagent prompt, together with the
22
+ instruction to return **exactly ONE JSON object** and nothing else — no
23
+ prose, no fences, no second object. The host does not enforce output shape,
24
+ so the contract lives in the prompt and the check lives in the main thread.
25
+ 3. Acquire the wave claim and create every worktree from the integrated base
26
+ through `claimWave` in `src/lib/waveClaim.mjs` before any spawn. Claim and
27
+ worktree creation stay serial in the main thread; only the agents fan out.
28
+
29
+ ## Round 1 — Recon
30
+
31
+ Spawn **one read-only explorer per slice**, all in one batch so they run
32
+ concurrently, then join them with an **explicit wait** on the spawned set —
33
+ never a sleep, a poll loop, or an implicit continue. Aggregate only after the
34
+ wait returns.
35
+
36
+ Each explorer returns one recon report. Validate every returned payload against
37
+ the recon schema in the main thread: a payload that does not parse, carries
38
+ extra top-level keys, or fails the schema **is not a PASS** — it is a missing
39
+ report. Re-spawn that single explorer with the failure quoted back to it; never
40
+ accept a prose summary in place of the object.
41
+
42
+ ## Main-thread reconcile boundary
43
+
44
+ Pass the complete, validated recon set to `reconcileReconReports`. A
45
+ reconciliation failure stops the wave. A successful result is the only boundary
46
+ that permits builders to start; use its overlap graph and dependency edges to
47
+ cut safe dispatch batches. Multiple editors are safe only when dependency
48
+ reachability totally orders them; shared-mutable files still require exactly one
49
+ edit owner.
50
+
51
+ Every orchestration path crosses this same main-thread reconciliation boundary.
52
+ Reconciliation is never delegated to a subagent.
53
+
54
+ ## Round 2 — Build
55
+
56
+ For the current reconciled batch, spawn **one builder per slice**, again as one
57
+ concurrent batch joined by an explicit wait. Give each builder the verbatim
58
+ builder contract, its reconciled allowlist, and its required commands. The host
59
+ exposes no per-agent role, model, or reasoning selector, so routing falls back
60
+ to the parent session configuration — record the tier you intended in the
61
+ prompt itself rather than assuming the host honoured it.
62
+
63
+ Each builder returns exactly one builder report. Validate it against the builder
64
+ schema and then run `semanticVerify` on it in the main thread. A subagent's own
65
+ claim of success is a hypothesis, never the gate.
66
+
67
+ ## B1 fan-out is out of scope
68
+
69
+ The **B1** shape — a single fan-out over a work list (`spawn_agents_on_csv`)
70
+ plus a host-enforced per-agent `output_schema` — is explicitly excluded. The
71
+ same spike found the callable spawn schema exposes only `task_name`, `message`,
72
+ and `fork_turns`: no work-list fan-out and no output-schema parameter. This
73
+ recipe therefore documents the **B2** shape only: explicit per-slice spawns
74
+ joined by an explicit wait, with validation owned by the main thread.
75
+
76
+ Re-opening B1 requires its own version-pinned verify spike — tracked as an open
77
+ spike issue on the kit's own tracker — before any recipe or adapter change.
78
+ Never infer the capability from a release note; prove it against the pinned
79
+ runtime first.
@@ -0,0 +1,58 @@
1
+ # Path A — Scripted Workflow dispatch
2
+
3
+ Load this recipe only when the capability selector returns Path A. That result
4
+ requires the literal `Workflow` tool and every required primitive. Route from
5
+ proven capabilities only, never from a surface name, and do not emulate
6
+ Workflow. The current Codex host lacks complete capability evidence and selects
7
+ Path C; a future host selects whichever path its evidence proves.
8
+
9
+ ## Main-thread preparation
10
+
11
+ 1. Read the canonical `RECON_REPORT_SCHEMA` and `BUILDER_REPORT_SCHEMA` values
12
+ from `src/lib/reportValidator.mjs`.
13
+ 2. Serialize each value as an inline schema literal in its generated Workflow
14
+ script. Workflow scripts cannot import modules, so never put an `import()`,
15
+ `require`, `process`, or `fetch` workaround in them.
16
+ 3. Build deterministic run arguments. Pass run identity, slice IDs, prompts,
17
+ models, effort levels, and timestamps through `args`; never call
18
+ `Date.now()` or `Math.random()` in a script.
19
+
20
+ ## Run 1 — Recon
21
+
22
+ Declare the named recon phase in `meta.phases`. Within it, make one `agent()`
23
+ call per slice, with explicit `model`, `effort`, and `phase` options. Give every
24
+ call the inline recon schema so the Workflow runtime rejects prose or malformed
25
+ output before aggregation.
26
+
27
+ Use the run identity supplied in `args` and retain the Workflow-managed
28
+ `journal.jsonl`. The main thread waits for the Recon run to finish and collects
29
+ its schema-valid reports. It must not dispatch a builder yet.
30
+
31
+ ## Main-thread reconcile boundary
32
+
33
+ Pass the complete Recon-run result to `reconcileReconReports`. A reconciliation
34
+ failure stops the wave. A successful result is the only boundary that permits
35
+ the Build run to start; use its overlap graph and dependency edges to cut safe
36
+ dispatch batches. Multiple editors are safe only when dependency reachability
37
+ totally orders them; shared-mutable files still require exactly one edit owner.
38
+
39
+ Every orchestration path, including Paths B and C, must cross this same
40
+ main-thread reconciliation boundary. Reconciliation is not Workflow-script
41
+ logic.
42
+
43
+ ## Run 2 — Build
44
+
45
+ Generate a separate Build run with its own named entry in `meta.phases`. For the
46
+ current reconciled batch, make one `agent()` call per slice with explicit
47
+ `model`, `effort`, and `phase` options and the inline builder schema. Preserve
48
+ the reconciled allowlist and required commands in each verbatim builder prompt;
49
+ the main thread still performs `semanticVerify` on every returned report.
50
+
51
+ ## Resume exactly once
52
+
53
+ Resume an interrupted Recon or Build run with its recorded `resumeFromRunId`
54
+ and the same deterministic `args`. Workflow returns cached results for completed
55
+ agents: accept those results and dispatch only unfinished calls. Never create a
56
+ replacement run for completed work, and never replay a completed builder. This
57
+ makes each builder phase execute exactly once while the retained
58
+ `journal.jsonl` remains the recovery and progress record.
@@ -0,0 +1,42 @@
1
+ # Wave Report Contracts
2
+
3
+ `src/lib/reportValidator.mjs` is the source of truth. The JSON blocks below are
4
+ machine-checked mirrors for hosts that must inline a schema and cannot import the
5
+ module. Do not edit a block without changing the exported constant first.
6
+
7
+ ## Recon report
8
+
9
+ Required fields: <!-- fields:recon -->sliceId, plannedFiles, dependencyEdges
10
+
11
+ `plannedFiles[].role` is `edit`, `consume`, or `sharedMutable`.
12
+ `dependencyEdges` records directed slice dependencies as `from`/`to` IDs.
13
+
14
+ <!-- schema:recon:start -->
15
+ ```json
16
+ {"type":"object","required":["sliceId","plannedFiles","dependencyEdges"],"additionalProperties":false,"properties":{"sliceId":{"type":"string","pattern":"^[0-9]+$"},"plannedFiles":{"type":"array","items":{"type":"object","required":["path","role"],"additionalProperties":false,"properties":{"path":{"type":"string"},"role":{"type":"string","enum":["edit","consume","sharedMutable"]}}}},"dependencyEdges":{"type":"array","items":{"type":"object","required":["from","to"],"additionalProperties":false,"properties":{"from":{"type":"string","pattern":"^[0-9]+$"},"to":{"type":"string","pattern":"^[0-9]+$"}}}}}}
17
+ ```
18
+ <!-- schema:recon:end -->
19
+
20
+ ## Builder report
21
+
22
+ Required fields: <!-- fields:builder -->status, filesTouched, testDecisions, commands, commitSha, stopItems, visualVerify
23
+
24
+ `status` discriminates the `oneOf`: PASS requires a full Git object ID and an
25
+ empty `stopItems`; STOP requires at least one stop item and permits a null SHA.
26
+ Schema validation intentionally accepts nonzero command exits. The orchestrator
27
+ must additionally call `semanticVerify` before accepting PASS.
28
+
29
+ <!-- schema:builder:start -->
30
+ ```json
31
+ {"type":"object","required":["status","filesTouched","testDecisions","commands","commitSha","stopItems","visualVerify"],"additionalProperties":false,"properties":{"status":{"type":"string","enum":["pass","stop"]},"filesTouched":{"type":"array","items":{"type":"string"}},"testDecisions":{"type":"array","items":{"type":"string"}},"commands":{"type":"array","items":{"type":"object","required":["command","exitCode","summary"],"additionalProperties":false,"properties":{"command":{"type":"string"},"exitCode":{"type":"integer"},"summary":{"type":"string"}}}},"commitSha":{"anyOf":[{"type":"string","pattern":"^[0-9a-f]{40}$|^[0-9a-f]{64}$"},{"type":"null"}]},"stopItems":{"type":"array","items":{"type":"string"}},"visualVerify":{"type":"string"}},"oneOf":[{"required":["status","commitSha","stopItems"],"properties":{"status":{"const":"pass"},"commitSha":{"type":"string","pattern":"^[0-9a-f]{40}$|^[0-9a-f]{64}$"},"stopItems":{"type":"array","maxItems":0}}},{"required":["status","commitSha","stopItems"],"properties":{"status":{"const":"stop"},"commitSha":{"anyOf":[{"type":"string","pattern":"^[0-9a-f]{40}$|^[0-9a-f]{64}$"},{"type":"null"}]},"stopItems":{"type":"array","minItems":1}}}]}
32
+ ```
33
+ <!-- schema:builder:end -->
34
+
35
+ ## Semantic facts
36
+
37
+ Call `semanticVerify(report, { gitFacts, allowlist, requiredCommands })` with
38
+ independently collected Git facts: `objectFormat` (`sha1` or `sha256`), the
39
+ resolved `commitSha`, `baseIsAncestorOfCommit`, and `changedFiles`. The ancestry
40
+ fact means the integration base is an ancestor of the builder commit. The helper
41
+ verifies the full SHA against the repository object format and resolved commit, diff subset,
42
+ required command presence, command exits for PASS, and PASS/STOP exclusivity.
package/README.md CHANGED
@@ -349,8 +349,55 @@ concurrency-safe. Do not run manifest-mutating commands concurrently. Flags:
349
349
  `--force` (overwrite pre-existing untracked files on `init`), `--yes` / `-y`
350
350
  (accept non-interactive update decisions).
351
351
 
352
+ ## Upgrade notes
353
+
354
+ ### 0.33.0 — capability-gated orchestration
355
+
356
+ `kit-update` reconciles this release for you. These notes matter only if you
357
+ maintain a **local fork of `orchestrate-wave`**: the blocks below changed
358
+ behaviour, so a forked copy must re-apply them by hand or it keeps orchestrating
359
+ the old way. Decision record:
360
+ [`docs/adr/0002-capability-gated-orchestration.md`](docs/adr/0002-capability-gated-orchestration.md).
361
+
362
+ - **Capability matrix, fail-closed.** The dispatch mechanic is selected from
363
+ host-supplied capability evidence, degrading A → B → C. Missing or `unknown`
364
+ evidence never proves a capability, and Path C (direct, serial) is always
365
+ available.
366
+ - **No emulation of a missing primitive.** A host without a scripted workflow
367
+ runtime gets the simpler recipe, never a hand-rolled imitation of one.
368
+ - **Codex Path B is native subagents.** Explicit per-slice spawns joined by an
369
+ explicit wait — dormant until a host supplies the complete normalized
370
+ inventory.
371
+ - **JSON-schema reports plus independent verification.** Every path returns
372
+ schema-valid recon and builder reports and crosses the same main-thread
373
+ boundary; a builder's own PASS is a hypothesis until `semanticVerify` confirms
374
+ it against independently collected Git facts.
375
+ - **Per-batch, post-hub worktree provisioning.** Worktrees are created serially
376
+ in the main thread from the integrated base after the hub lands, under an
377
+ atomic compare-and-set wave claim; reuse on a stale base now STOPs.
378
+ - **Heartbeat during long gates.** Long-running gates report status rather than
379
+ waiting silently.
380
+
352
381
  ## Release notes
353
382
 
383
+ ### 0.33.0
384
+
385
+ - added: `.agents/skills/orchestrate-wave/references/dispatch-subagents.md`
386
+ - added: `.agents/skills/orchestrate-wave/references/dispatch-workflow.md`
387
+ - added: `.agents/skills/orchestrate-wave/references/report-contracts.md`
388
+ - added: `.claude/skills/orchestrate-wave/references/dispatch-subagents.md`
389
+ - added: `.claude/skills/orchestrate-wave/references/dispatch-workflow.md`
390
+ - added: `.claude/skills/orchestrate-wave/references/report-contracts.md`
391
+ - added: `src/lib/capabilityMatrix.mjs`
392
+ - added: `src/lib/reconcileReconReports.mjs`
393
+ - added: `src/lib/reportValidator.mjs`
394
+ - added: `src/lib/waveClaim.mjs`
395
+ - changed: `.agents/skills/orchestrate-wave/SKILL.md`
396
+ - changed: `.agents/skills/orchestrate-wave/references/builder-contract.md`
397
+ - changed: `.claude/skills/orchestrate-wave/SKILL.md`
398
+ - changed: `.claude/skills/orchestrate-wave/references/builder-contract.md`
399
+ - changed: `scripts/worktree-lifecycle/setup.py`
400
+
354
401
  ### 0.32.1
355
402
 
356
403
  - changed: `.agents/skills/census-update/SKILL.md`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.32.1",
2
+ "kitVersion": "0.33.0",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -407,7 +407,37 @@
407
407
  "ownerSkill": "orchestrate-wave",
408
408
  "surface": "codex",
409
409
  "installRole": "consumer",
410
- "sha256": "a3c3dc04411372f509b07e67f82217eb9656933f7ce5101beb5c123c2272d0bb",
410
+ "sha256": "9f8012a21441a0faff31845f38338078134c11ac60417f0db50b2ee65f0cc6af",
411
+ "mode": 420,
412
+ "origin": "kit"
413
+ },
414
+ {
415
+ "path": ".agents/skills/orchestrate-wave/references/dispatch-subagents.md",
416
+ "kind": "skill",
417
+ "ownerSkill": "orchestrate-wave",
418
+ "surface": "codex",
419
+ "installRole": "consumer",
420
+ "sha256": "38247cadf15179e6f916674118fed6f0a19eb1097f123183a91bb18802113943",
421
+ "mode": 420,
422
+ "origin": "kit"
423
+ },
424
+ {
425
+ "path": ".agents/skills/orchestrate-wave/references/dispatch-workflow.md",
426
+ "kind": "skill",
427
+ "ownerSkill": "orchestrate-wave",
428
+ "surface": "codex",
429
+ "installRole": "consumer",
430
+ "sha256": "2e2be2fb931147cf232b33c9c23379bcce47558a25756d54f89c6ef2e8a365c3",
431
+ "mode": 420,
432
+ "origin": "kit"
433
+ },
434
+ {
435
+ "path": ".agents/skills/orchestrate-wave/references/report-contracts.md",
436
+ "kind": "skill",
437
+ "ownerSkill": "orchestrate-wave",
438
+ "surface": "codex",
439
+ "installRole": "consumer",
440
+ "sha256": "4ecd45229e6558d7b47918616ed199682767f2fd7ff209f6ae97258cbf8aec24",
411
441
  "mode": 420,
412
442
  "origin": "kit"
413
443
  },
@@ -417,7 +447,7 @@
417
447
  "ownerSkill": "orchestrate-wave",
418
448
  "surface": "codex",
419
449
  "installRole": "consumer",
420
- "sha256": "7a94293b7db49f42497d34e6ae0fe9992ab1c770ae0766557618818d6d671c50",
450
+ "sha256": "bfb547225c6950d4566f87a0da42e85c4c5803236d5adc0787aba265d5c91a53",
421
451
  "mode": 420,
422
452
  "origin": "kit"
423
453
  },
@@ -1689,7 +1719,37 @@
1689
1719
  "ownerSkill": "orchestrate-wave",
1690
1720
  "surface": "claude",
1691
1721
  "installRole": "consumer",
1692
- "sha256": "a3c3dc04411372f509b07e67f82217eb9656933f7ce5101beb5c123c2272d0bb",
1722
+ "sha256": "9f8012a21441a0faff31845f38338078134c11ac60417f0db50b2ee65f0cc6af",
1723
+ "mode": 420,
1724
+ "origin": "kit"
1725
+ },
1726
+ {
1727
+ "path": ".claude/skills/orchestrate-wave/references/dispatch-subagents.md",
1728
+ "kind": "skill",
1729
+ "ownerSkill": "orchestrate-wave",
1730
+ "surface": "claude",
1731
+ "installRole": "consumer",
1732
+ "sha256": "38247cadf15179e6f916674118fed6f0a19eb1097f123183a91bb18802113943",
1733
+ "mode": 420,
1734
+ "origin": "kit"
1735
+ },
1736
+ {
1737
+ "path": ".claude/skills/orchestrate-wave/references/dispatch-workflow.md",
1738
+ "kind": "skill",
1739
+ "ownerSkill": "orchestrate-wave",
1740
+ "surface": "claude",
1741
+ "installRole": "consumer",
1742
+ "sha256": "2e2be2fb931147cf232b33c9c23379bcce47558a25756d54f89c6ef2e8a365c3",
1743
+ "mode": 420,
1744
+ "origin": "kit"
1745
+ },
1746
+ {
1747
+ "path": ".claude/skills/orchestrate-wave/references/report-contracts.md",
1748
+ "kind": "skill",
1749
+ "ownerSkill": "orchestrate-wave",
1750
+ "surface": "claude",
1751
+ "installRole": "consumer",
1752
+ "sha256": "4ecd45229e6558d7b47918616ed199682767f2fd7ff209f6ae97258cbf8aec24",
1693
1753
  "mode": 420,
1694
1754
  "origin": "kit"
1695
1755
  },
@@ -1699,7 +1759,7 @@
1699
1759
  "ownerSkill": "orchestrate-wave",
1700
1760
  "surface": "claude",
1701
1761
  "installRole": "consumer",
1702
- "sha256": "9690ec772a99131c225037dd4bca884e899c133a8e460c36b26a25d6c6c797e1",
1762
+ "sha256": "c9b4308875d59415b074de8b98130d0c2378fda6a4d1e6ac9c332c74b9a9ea97",
1703
1763
  "mode": 420,
1704
1764
  "origin": "kit"
1705
1765
  },
@@ -2731,7 +2791,7 @@
2731
2791
  "path": "scripts/worktree-lifecycle/setup.py",
2732
2792
  "kind": "script",
2733
2793
  "installRole": "consumer",
2734
- "sha256": "c720cbff9123f136a53658671d880b9a73f9da4faa0611e37a2a981fa89cda92",
2794
+ "sha256": "0021c50809373335e9dafe97196f42ad039152418baa49e2004b25ba33d137e2",
2735
2795
  "mode": 493,
2736
2796
  "origin": "kit"
2737
2797
  },
@@ -2751,6 +2811,14 @@
2751
2811
  "mode": 420,
2752
2812
  "origin": "kit"
2753
2813
  },
2814
+ {
2815
+ "path": "src/lib/capabilityMatrix.mjs",
2816
+ "kind": "script",
2817
+ "installRole": "consumer",
2818
+ "sha256": "9ea051edad14972281e8d19664eeec7dbd91d57b21e5d97e4eeea7323c5241e1",
2819
+ "mode": 420,
2820
+ "origin": "kit"
2821
+ },
2754
2822
  {
2755
2823
  "path": "src/lib/manifest.mjs",
2756
2824
  "kind": "script",
@@ -2759,6 +2827,14 @@
2759
2827
  "mode": 420,
2760
2828
  "origin": "kit"
2761
2829
  },
2830
+ {
2831
+ "path": "src/lib/reconcileReconReports.mjs",
2832
+ "kind": "script",
2833
+ "installRole": "consumer",
2834
+ "sha256": "d433504b66c62f02e3c909fc8b664a1a2c0373a04138a0da919d3ce7d45ba56a",
2835
+ "mode": 420,
2836
+ "origin": "kit"
2837
+ },
2762
2838
  {
2763
2839
  "path": "src/lib/release-apply.mjs",
2764
2840
  "kind": "script",
@@ -2775,6 +2851,14 @@
2775
2851
  "mode": 420,
2776
2852
  "origin": "kit"
2777
2853
  },
2854
+ {
2855
+ "path": "src/lib/reportValidator.mjs",
2856
+ "kind": "script",
2857
+ "installRole": "consumer",
2858
+ "sha256": "f5b06eecd244ee177f94c9afc9f0f04107493ed1e0d661cadc85cbe251591052",
2859
+ "mode": 420,
2860
+ "origin": "kit"
2861
+ },
2778
2862
  {
2779
2863
  "path": "src/lib/semver.mjs",
2780
2864
  "kind": "script",
@@ -2790,6 +2874,14 @@
2790
2874
  "sha256": "f99d28d2053d55222650d2ca426509d3f56eec5ef70709bbf8c4a278bf80024e",
2791
2875
  "mode": 420,
2792
2876
  "origin": "kit"
2877
+ },
2878
+ {
2879
+ "path": "src/lib/waveClaim.mjs",
2880
+ "kind": "script",
2881
+ "installRole": "consumer",
2882
+ "sha256": "dba4a321283e4f3ac3ab87eb726ce193c7e874c4b5ebd5cd5bbea7d28bb67c97",
2883
+ "mode": 420,
2884
+ "origin": "kit"
2793
2885
  }
2794
2886
  ]
2795
2887
  }
@@ -0,0 +1,70 @@
1
+ # Capability-gated orchestration: scripted-workflow / native-subagents / direct, fail-closed, no emulation
2
+
3
+ Status: accepted (2026-07-22, issue #167)
4
+
5
+ `orchestrate-wave` fans out recon and building across agents. The mechanic that
6
+ performs that fan-out is not the same on every host: one host exposes a scripted
7
+ workflow runtime, another exposes native subagent spawn/wait primitives, a third
8
+ exposes neither. The skill ships from one body for both surfaces, so the mechanic
9
+ cannot be baked into the prose — and a wrong guess is expensive, because a wave
10
+ that half-dispatches leaves worktrees and branches behind.
11
+
12
+ We decided to select the mechanic from **proven, host-supplied capability
13
+ evidence**, never from a surface name and never from the model's own belief
14
+ about what it can call.
15
+
16
+ 1. **A capability matrix over normalized tool entries.** The host supplies an
17
+ inventory; `capabilityAdapter.claude` / `.codex` only normalize what was
18
+ handed to them and perform no ambient discovery. Each entry carries
19
+ `callable`, `permitted`, a schema, and `capabilities: string[] | "unknown"`.
20
+ `classifyCapabilities` returns exactly one of three paths, and
21
+ `selectOrchestrationReference` returns one discriminated target: a reference
22
+ path for A and B, the inline marker for C.
23
+ 2. **Fail closed, A → B → C.** Missing or `unknown` evidence never proves a
24
+ capability. Path A additionally requires the *literal* tool name plus every
25
+ named primitive individually proven; Path B requires proven spawn, wait, and
26
+ aggregate plus effective concurrency and thread capacity ≥2. Anything short
27
+ of that degrades to Path C — direct, serial, in the main thread — which is
28
+ always available.
29
+ 3. **Never emulate a missing primitive.** A host without a scripted workflow
30
+ runtime does not get a hand-rolled imitation of one. The degraded path is a
31
+ different, simpler recipe, not the same recipe with the primitive faked.
32
+ 4. **The report contract is path-independent.** Every path produces the same
33
+ schema-valid recon and builder reports (`src/lib/reportValidator.mjs`, mirrored
34
+ in `references/report-contracts.md`), and every path crosses the same
35
+ main-thread boundary: schema validation, then `reconcileReconReports`, then
36
+ `semanticVerify` on builder reports. A subagent's own PASS is a hypothesis.
37
+ 5. **Progressive disclosure.** `SKILL.md` carries only the selector, the
38
+ invariants, Path C, and pointers; each path's recipe lives in `references/`
39
+ and is loaded only when selected.
40
+
41
+ ## Considered options
42
+
43
+ - **Route by surface name** (Claude → workflow, Codex → subagents): rejected —
44
+ it is a claim, not evidence. A verify spike against codex-cli 0.144.6 found
45
+ native spawn and wait callable and concurrency ≥2, yet the host exposed tool
46
+ entries with only a name and a description: no schema, no callable/permitted
47
+ flags, no thread capacity. Name-based routing would have selected Path B on a
48
+ host that cannot prove it.
49
+ - **Emulate the missing primitive** (hand-rolled fan-out where no workflow
50
+ runtime exists): rejected — it reproduces the failure modes the runtime exists
51
+ to prevent (run identity, resume-exactly-once, runtime output validation)
52
+ without any of its guarantees, and it fails at the worst moment, mid-wave.
53
+ - **Fail open on unknown evidence** (assume a capability when the host is
54
+ silent): rejected — the cost asymmetry is severe. A false Path C is slower; a
55
+ false Path A or B strands a partially dispatched wave.
56
+ - **One monolithic SKILL.md carrying all three recipes**: rejected — every
57
+ session would pay for two recipes it will never run.
58
+
59
+ ## Consequences
60
+
61
+ - The current Codex host selects Path C. `references/dispatch-subagents.md` is
62
+ written and tested but **dormant** until a host supplies the complete
63
+ normalized inventory; that dormancy is pinned from both sides by tests.
64
+ - Adding a path means adding evidence requirements and a reference, not
65
+ branching the skill body.
66
+ - Hosts that improve their inventory reporting gain a faster path with no skill
67
+ change — the selector picks it up as soon as the evidence arrives.
68
+ - The B1 fan-out shape (`spawn_agents_on_csv` plus a host-enforced per-agent
69
+ `output_schema`) stays out of scope until its own version-pinned verify spike
70
+ returns a positive verdict.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.32.1",
3
+ "version": "0.33.0",
4
4
  "description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,19 @@ CLAUDE_BUILDER = (
21
21
  CODEX_BUILDER = (
22
22
  REPO / ".agents/skills/orchestrate-wave/references/builder-contract.md"
23
23
  )
24
+ CLAUDE_WORKFLOW = (
25
+ REPO / ".claude/skills/orchestrate-wave/references/dispatch-workflow.md"
26
+ )
27
+ CODEX_WORKFLOW = (
28
+ REPO / ".agents/skills/orchestrate-wave/references/dispatch-workflow.md"
29
+ )
30
+ CLAUDE_SUBAGENTS = (
31
+ REPO / ".claude/skills/orchestrate-wave/references/dispatch-subagents.md"
32
+ )
33
+ CODEX_SUBAGENTS = (
34
+ REPO / ".agents/skills/orchestrate-wave/references/dispatch-subagents.md"
35
+ )
36
+ CODEX_SURFACE = REPO / ".agents/skills/orchestrate-wave"
24
37
 
25
38
 
26
39
  # Outcome -> fragments whose conjunction proves that portable behavior.
@@ -32,10 +45,17 @@ BEHAVIORAL_PARITY = {
32
45
  "LOCAL annotated tag",
33
46
  "never push",
34
47
  ),
48
+ "compare-and-set claim protocol": (
49
+ "src/lib/waveClaim.mjs",
50
+ "claimWave",
51
+ "compare-and-set",
52
+ "acquired",
53
+ ),
35
54
  "owner-safe abort cleanup": (
36
55
  "this run planted",
37
56
  "On ANY wave STOP/abort",
38
57
  "Never delete a claim marker observed during a preflight collision",
58
+ "releaseWaveClaim",
39
59
  ),
40
60
  "dependency-aware retirement": (
41
61
  "topological",
@@ -91,6 +111,18 @@ class OrchestrateWaveContract(unittest.TestCase):
91
111
  ):
92
112
  self.assertIn(" ".join(fragment.split()), prose)
93
113
 
114
+ def test_builder_report_contract_is_identical_on_every_path(self):
115
+ """One report contract, whichever mechanic dispatched the builder."""
116
+ prose = " ".join(self.builder.split())
117
+ for fragment in (
118
+ "identical on every orchestration path",
119
+ "references/report-contracts.md",
120
+ "exactly ONE JSON object",
121
+ "src/lib/reportValidator.mjs",
122
+ "semanticVerify",
123
+ ):
124
+ self.assertIn(" ".join(fragment.split()), prose)
125
+
94
126
  def test_current_portable_contracts_survive_the_port(self):
95
127
  for fragment in (
96
128
  "project layer",
@@ -101,6 +133,110 @@ class OrchestrateWaveContract(unittest.TestCase):
101
133
  ):
102
134
  self.assertIn(fragment, self.skill)
103
135
 
136
+ def test_capability_selector_routes_exactly_one_orchestration_mechanic(self):
137
+ for fragment in (
138
+ "## Orchestration mechanics",
139
+ "literal `Workflow`",
140
+ "do not emulate",
141
+ "returns exactly one",
142
+ "references/dispatch-workflow.md",
143
+ "references/dispatch-subagents.md",
144
+ "Path C",
145
+ "Phase 1 uses the selected orchestration mechanics",
146
+ "Phase 2 uses the selected orchestration mechanics",
147
+ ):
148
+ self.assertIn(fragment, self.skill)
149
+
150
+ self.assertLessEqual(len(self.skill.splitlines()), 345)
151
+
152
+ def test_registry_ownership_distinguishes_safe_from_eager_registries(self):
153
+ prose = " ".join(self.skill.split())
154
+ for fragment in (
155
+ "declaration-only registries",
156
+ "may be predeclared by one hub",
157
+ "eager/validated registries",
158
+ "each appending only its own existing artifact after creation",
159
+ "dependency edges",
160
+ ):
161
+ self.assertIn(fragment, prose)
162
+
163
+ phase_one = prose.split("## Phase 1", 1)[1].split("## Phase 2", 1)[0]
164
+ done = phase_one.split("**Done when:**", 1)[1]
165
+ for fragment in (
166
+ "either one declaration-only owner",
167
+ "verbatim consume-only dependents",
168
+ "or an explicit serialized owner sequence",
169
+ "each owner appends only its own existing artifact",
170
+ ):
171
+ self.assertIn(fragment, done)
172
+
173
+ def test_phase_two_keeps_per_slice_routing_decisions(self):
174
+ prose = " ".join(self.skill.split())
175
+ self.assertIn("(a) inline vs delegate", prose)
176
+ self.assertIn("(b) tier + effort", prose)
177
+ self.assertIn("Standing rules", prose)
178
+
179
+ def test_path_a_reference_locks_the_two_run_dispatch_contract(self):
180
+ workflow = CLAUDE_WORKFLOW.read_text(encoding="utf-8")
181
+ for fragment in (
182
+ "meta.phases",
183
+ "one `agent()` call per slice",
184
+ "model`, `effort`, and `phase`",
185
+ "inline schema literal",
186
+ "Recon run",
187
+ "reconcileReconReports",
188
+ "Build run",
189
+ "resumeFromRunId",
190
+ "exactly once",
191
+ "journal.jsonl",
192
+ "timestamps through `args`",
193
+ "Date.now()",
194
+ "Math.random()",
195
+ "Every orchestration path",
196
+ ):
197
+ self.assertIn(fragment, workflow)
198
+
199
+ self.assertEqual(workflow, CODEX_WORKFLOW.read_text(encoding="utf-8"))
200
+
201
+ def test_path_b_reference_locks_the_native_subagent_contract(self):
202
+ subagents = CLAUDE_SUBAGENTS.read_text(encoding="utf-8")
203
+ prose = " ".join(subagents.split())
204
+ for fragment in (
205
+ "one read-only explorer per slice",
206
+ "one builder per slice",
207
+ "explicit wait",
208
+ "exactly ONE JSON object",
209
+ "reportValidator.mjs",
210
+ "reconcileReconReports",
211
+ "main thread",
212
+ "is not a PASS",
213
+ "waveClaim",
214
+ "spawn_agents_on_csv",
215
+ "output_schema",
216
+ "dormant",
217
+ ):
218
+ self.assertIn(" ".join(fragment.split()), prose)
219
+
220
+ self.assertEqual(subagents, CODEX_SUBAGENTS.read_text(encoding="utf-8"))
221
+
222
+ def test_codex_surface_carries_no_path_a_primitive_outside_its_pointer_target(self):
223
+ """B-surface prose must not require Workflow-only primitives."""
224
+ offenders = {}
225
+ for path in sorted(CODEX_SURFACE.rglob("*.md")):
226
+ if path.name == "dispatch-workflow.md":
227
+ continue
228
+ body = path.read_text(encoding="utf-8")
229
+ hits = [term for term in ("journal.jsonl", "resumeFromRunId") if term in body]
230
+ # The literal tool name is admissible ONLY inside the capability gate.
231
+ if "`Workflow`" in body:
232
+ gate = body.split("## Orchestration mechanics", 1)
233
+ remainder = gate[0] + (gate[1].split("\n## ", 1)[1] if len(gate) > 1 and "\n## " in gate[1] else "")
234
+ if "`Workflow`" in remainder:
235
+ hits.append("`Workflow`")
236
+ if hits:
237
+ offenders[str(path.relative_to(REPO))] = hits
238
+ self.assertEqual(offenders, {})
239
+
104
240
  def test_claude_and_codex_surfaces_match(self):
105
241
  self.assertEqual(
106
242
  markdown_body(self.skill),