@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.
- 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/.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/README.md +47 -0
- package/agent-workflow-kit.package.json +98 -6
- package/docs/adr/0002-capability-gated-orchestration.md +70 -0
- package/package.json +1 -1
- 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/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/waveClaim.mjs +134 -0
|
@@ -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.
|
|
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": "
|
|
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
|
},
|
|
@@ -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
|
},
|
|
@@ -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": {
|
|
@@ -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),
|