@ikon85/agent-workflow-kit 0.32.0 → 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.
Files changed (33) hide show
  1. package/.agents/skills/census-update/SKILL.md +34 -3
  2. package/.agents/skills/kit-update/SKILL.md +18 -4
  3. package/.agents/skills/orchestrate-wave/SKILL.md +54 -54
  4. package/.agents/skills/orchestrate-wave/references/builder-contract.md +16 -3
  5. package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +79 -0
  6. package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +58 -0
  7. package/.agents/skills/orchestrate-wave/references/report-contracts.md +42 -0
  8. package/.agents/skills/setup-workflow/census.md +14 -7
  9. package/.claude/skills/census-update/SKILL.md +34 -3
  10. package/.claude/skills/kit-update/SKILL.md +18 -4
  11. package/.claude/skills/orchestrate-wave/SKILL.md +54 -54
  12. package/.claude/skills/orchestrate-wave/references/builder-contract.md +16 -3
  13. package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +79 -0
  14. package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +58 -0
  15. package/.claude/skills/orchestrate-wave/references/report-contracts.md +42 -0
  16. package/.claude/skills/setup-workflow/census.md +14 -7
  17. package/README.md +61 -1
  18. package/agent-workflow-kit.package.json +105 -13
  19. package/docs/adr/0002-capability-gated-orchestration.md +70 -0
  20. package/package.json +1 -1
  21. package/scripts/readiness.mjs +48 -14
  22. package/scripts/test_census_update_contract.test.mjs +14 -0
  23. package/scripts/test_orchestrate_wave_contract.py +136 -0
  24. package/scripts/test_worktree_setup_base_guard.py +140 -0
  25. package/scripts/worktree-lifecycle/setup.py +40 -0
  26. package/src/cli.mjs +2 -2
  27. package/src/commands/update.mjs +21 -3
  28. package/src/lib/bundle.mjs +10 -0
  29. package/src/lib/capabilityMatrix.mjs +94 -0
  30. package/src/lib/reconcileReconReports.mjs +111 -0
  31. package/src/lib/reportValidator.mjs +186 -0
  32. package/src/lib/updateCandidate.mjs +78 -4
  33. package/src/lib/waveClaim.mjs +134 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.32.0",
2
+ "kitVersion": "0.33.0",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -47,7 +47,7 @@
47
47
  "ownerSkill": "census-update",
48
48
  "surface": "codex",
49
49
  "installRole": "consumer",
50
- "sha256": "3a04a2667005cae51cfad7952ff85fe43ba1ae390458b4ce746e23f6e872b3fb",
50
+ "sha256": "b122fe2eb46c070696e38660b021411458b455b7a4e977174f943f2ce4c00819",
51
51
  "mode": 420,
52
52
  "origin": "kit"
53
53
  },
@@ -377,7 +377,7 @@
377
377
  "ownerSkill": "kit-update",
378
378
  "surface": "codex",
379
379
  "installRole": "consumer",
380
- "sha256": "880766b7aef88760f6d79dd31c3333356eef060623fc053fc61f5d5611765bb8",
380
+ "sha256": "249a45761bcb95148b1567d1bc4d1b622b9b8c41519de4e67b1ef4e33c9ee9fd",
381
381
  "mode": 420,
382
382
  "origin": "kit"
383
383
  },
@@ -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
  },
@@ -567,7 +597,7 @@
567
597
  "ownerSkill": "setup-workflow",
568
598
  "surface": "codex",
569
599
  "installRole": "consumer",
570
- "sha256": "ec80f4ad36073e0c7c903434fa098d24f31b9d9a46ce55623782173ddba2e5f9",
600
+ "sha256": "d495758ae52bebd11c81bbe28fc5a1f9b614f972afdba55d1b728630d12cfee8",
571
601
  "mode": 420,
572
602
  "origin": "kit"
573
603
  },
@@ -1209,7 +1239,7 @@
1209
1239
  "ownerSkill": "census-update",
1210
1240
  "surface": "claude",
1211
1241
  "installRole": "consumer",
1212
- "sha256": "3a04a2667005cae51cfad7952ff85fe43ba1ae390458b4ce746e23f6e872b3fb",
1242
+ "sha256": "b122fe2eb46c070696e38660b021411458b455b7a4e977174f943f2ce4c00819",
1213
1243
  "mode": 420,
1214
1244
  "origin": "kit"
1215
1245
  },
@@ -1659,7 +1689,7 @@
1659
1689
  "ownerSkill": "kit-update",
1660
1690
  "surface": "claude",
1661
1691
  "installRole": "consumer",
1662
- "sha256": "880766b7aef88760f6d79dd31c3333356eef060623fc053fc61f5d5611765bb8",
1692
+ "sha256": "249a45761bcb95148b1567d1bc4d1b622b9b8c41519de4e67b1ef4e33c9ee9fd",
1663
1693
  "mode": 420,
1664
1694
  "origin": "kit"
1665
1695
  },
@@ -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
  },
@@ -1869,7 +1929,7 @@
1869
1929
  "ownerSkill": "setup-workflow",
1870
1930
  "surface": "claude",
1871
1931
  "installRole": "consumer",
1872
- "sha256": "ec80f4ad36073e0c7c903434fa098d24f31b9d9a46ce55623782173ddba2e5f9",
1932
+ "sha256": "d495758ae52bebd11c81bbe28fc5a1f9b614f972afdba55d1b728630d12cfee8",
1873
1933
  "mode": 420,
1874
1934
  "origin": "kit"
1875
1935
  },
@@ -2587,7 +2647,7 @@
2587
2647
  "path": "scripts/readiness.mjs",
2588
2648
  "kind": "script",
2589
2649
  "installRole": "consumer",
2590
- "sha256": "f5e8bc2e724f72d1aa9f94c4f8d87b8f4d3f9843797cd6ae59f48e563dc7c7fb",
2650
+ "sha256": "9e7a9c6882e372f38383fe92d98ff2f818bae0be61a62301b7e9a6a2842dd21b",
2591
2651
  "mode": 493,
2592
2652
  "origin": "kit"
2593
2653
  },
@@ -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.0",
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": {
@@ -99,22 +99,53 @@ function section(text, heading) {
99
99
  return body.join('\n').trim();
100
100
  }
101
101
 
102
- async function prodVerdict(root, paths) {
103
- const bodies = [];
102
+ export async function inspectProdSections(root, paths) {
103
+ const sections = [];
104
104
  for (const path of paths) {
105
105
  const text = await readText(root, path);
106
- if (text === null) continue;
107
106
  const body = section(text, '## Prod');
108
- bodies.push(body);
107
+ const problem = text === null ? 'missing-file'
108
+ : (body === null ? 'missing-section' : (body ? null : 'empty-section'));
109
+ sections.push({
110
+ path,
111
+ state: body === null ? 'missing' : (body ? 'valid' : 'invalid'),
112
+ body,
113
+ problem,
114
+ });
115
+ }
116
+ return sections;
117
+ }
118
+
119
+ async function prodEvidence(root, paths) {
120
+ const sections = await inspectProdSections(root, paths);
121
+ const present = sections.filter(({ problem }) => problem !== 'missing-file');
122
+ const valid = present.filter(({ state }) => state === 'valid');
123
+ const empty = present.filter(({ problem }) => problem === 'empty-section');
124
+ if (!valid.length && !empty.length) {
125
+ return {
126
+ verdict: 'absent',
127
+ diagnostics: sections.map(({ path, problem }) => ({ path, problem })),
128
+ };
129
+ }
130
+ const malformed = present.filter(({ state }) => state !== 'valid');
131
+ if (malformed.length) {
132
+ return {
133
+ verdict: 'invalid',
134
+ diagnostics: malformed.map(({ path, problem }) => ({ path, problem })),
135
+ };
136
+ }
137
+ const bodies = [...new Set(present.map(({ body }) => body))];
138
+ if (bodies.length > 1) {
139
+ return {
140
+ verdict: 'invalid',
141
+ diagnostics: present.map(({ path }) => ({ path, problem: 'divergent-section' })),
142
+ };
109
143
  }
110
- if (!bodies.length) return 'absent';
111
- if (bodies.every((body) => body === null)) return 'absent';
112
- if (bodies.some((body) => !body)) return 'invalid';
113
- return bodies.every((body) => body === bodies[0]) ? 'valid' : 'invalid';
144
+ return { verdict: 'valid', diagnostics: [] };
114
145
  }
115
146
 
116
147
  async function evidenceVerdict(root, evidence) {
117
- if (evidence.type === 'prod-section') return prodVerdict(root, evidence.paths);
148
+ if (evidence.type === 'prod-section') return (await prodEvidence(root, evidence.paths)).verdict;
118
149
  if (evidence.type === 'runbook-reference') return runbookVerdict(root, evidence);
119
150
  const verdicts = await Promise.all((evidence.paths ?? []).map(async (path) => {
120
151
  const text = await readText(root, path);
@@ -129,14 +160,17 @@ async function evidenceVerdict(root, evidence) {
129
160
  }
130
161
 
131
162
  export async function evaluateCapability({ root, capability, decision }) {
132
- const evidence = await evidenceVerdict(root, capability.evidence);
133
- if (evidence === 'invalid') return { state: 'invalid', clearDecision: false };
163
+ const prod = capability.evidence.type === 'prod-section'
164
+ ? await prodEvidence(root, capability.evidence.paths) : null;
165
+ const evidence = prod?.verdict ?? await evidenceVerdict(root, capability.evidence);
166
+ const diagnostics = prod?.diagnostics?.length ? { diagnostics: prod.diagnostics } : {};
167
+ if (evidence === 'invalid') return { state: 'invalid', clearDecision: false, ...diagnostics };
134
168
  if (evidence === 'valid') return { state: 'ready', clearDecision: Boolean(decision) };
135
- if (decision === 'pending') return { state: 'pending', clearDecision: false };
169
+ if (decision === 'pending') return { state: 'pending', clearDecision: false, ...diagnostics };
136
170
  if (decision === 'not-applicable' && capability.allowNotApplicable) {
137
- return { state: 'not-applicable', clearDecision: false };
171
+ return { state: 'not-applicable', clearDecision: false, ...diagnostics };
138
172
  }
139
- return { state: 'missing', clearDecision: false };
173
+ return { state: 'missing', clearDecision: false, ...diagnostics };
140
174
  }
141
175
 
142
176
  async function loadManifest(root) {
@@ -169,6 +169,20 @@ test('census-update coordinates the stable foundation API without a second engin
169
169
  assert.match(skill, /\.census\/profile\.json/);
170
170
  assert.match(skill, /\.census\/active\.json/);
171
171
  assert.match(skill, /schemaVersion/);
172
+ assert.match(skill, /every `localScanners\[\]\.test`/);
173
+ assert.match(skill, /shared project-local census check entry point/i);
174
+ assert.match(
175
+ skill,
176
+ /It must execute every declared focused test,\s+and both local CI and pre-push must transitively reach that same entry point\./i,
177
+ );
178
+ assert.match(skill, /executable wiring test/i);
179
+ assert.match(skill, /missing or partial[\s\S]*failed/i);
180
+ assert.match(skill, /previous active\s+census bytes are unchanged/i);
181
+ assert.match(skill, /idempotent/i);
182
+
183
+ const setupCensus = await text('.claude/skills/setup-workflow/census.md');
184
+ assert.match(setupCensus, /activation[\s\S]*durable[\s\S]*local CI[\s\S]*pre-push/i);
185
+ assert.match(setupCensus, /remove only[\s\S]*kit-owned census wiring/i);
172
186
  });
173
187
 
174
188
  test('brownfield bootstrap activates real surface coverage with a separate behavior overview', async () => {
@@ -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),