@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.
- package/.agents/skills/census-update/SKILL.md +34 -3
- package/.agents/skills/kit-update/SKILL.md +18 -4
- package/.agents/skills/orchestrate-wave/SKILL.md +54 -54
- package/.agents/skills/orchestrate-wave/references/builder-contract.md +16 -3
- package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +79 -0
- package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +58 -0
- package/.agents/skills/orchestrate-wave/references/report-contracts.md +42 -0
- package/.agents/skills/setup-workflow/census.md +14 -7
- package/.claude/skills/census-update/SKILL.md +34 -3
- package/.claude/skills/kit-update/SKILL.md +18 -4
- package/.claude/skills/orchestrate-wave/SKILL.md +54 -54
- package/.claude/skills/orchestrate-wave/references/builder-contract.md +16 -3
- package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +79 -0
- package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +58 -0
- package/.claude/skills/orchestrate-wave/references/report-contracts.md +42 -0
- package/.claude/skills/setup-workflow/census.md +14 -7
- package/README.md +61 -1
- package/agent-workflow-kit.package.json +105 -13
- package/docs/adr/0002-capability-gated-orchestration.md +70 -0
- package/package.json +1 -1
- package/scripts/readiness.mjs +48 -14
- package/scripts/test_census_update_contract.test.mjs +14 -0
- package/scripts/test_orchestrate_wave_contract.py +136 -0
- package/scripts/test_worktree_setup_base_guard.py +140 -0
- package/scripts/worktree-lifecycle/setup.py +40 -0
- package/src/cli.mjs +2 -2
- package/src/commands/update.mjs +21 -3
- package/src/lib/bundle.mjs +10 -0
- package/src/lib/capabilityMatrix.mjs +94 -0
- package/src/lib/reconcileReconReports.mjs +111 -0
- package/src/lib/reportValidator.mjs +186 -0
- package/src/lib/updateCandidate.mjs +78 -4
- package/src/lib/waveClaim.mjs +134 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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": {
|
package/scripts/readiness.mjs
CHANGED
|
@@ -99,22 +99,53 @@ function section(text, heading) {
|
|
|
99
99
|
return body.join('\n').trim();
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
async function
|
|
103
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
133
|
-
|
|
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),
|