@ikon85/agent-workflow-kit 0.32.1 → 0.34.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 (89) hide show
  1. package/.agents/skills/ask-matt/SKILL.md +4 -3
  2. package/.agents/skills/audit-skills/SKILL.md +6 -0
  3. package/.agents/skills/board-to-waves/SKILL.md +6 -2
  4. package/.agents/skills/code-review/SKILL.md +6 -0
  5. package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +11 -1
  6. package/.agents/skills/codex-adapter-sync/SKILL.md +23 -19
  7. package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
  8. package/.agents/skills/improve-codebase-architecture/SKILL.md +10 -1
  9. package/.agents/skills/kit-update/SKILL.md +13 -0
  10. package/.agents/skills/orchestrate-wave/SKILL.md +54 -54
  11. package/.agents/skills/orchestrate-wave/references/builder-contract.md +16 -3
  12. package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +91 -0
  13. package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +66 -0
  14. package/.agents/skills/orchestrate-wave/references/report-contracts.md +42 -0
  15. package/.agents/skills/research/SKILL.md +6 -0
  16. package/.agents/skills/scale-check/SKILL.md +9 -7
  17. package/.agents/skills/setup-workflow/SKILL.md +47 -1
  18. package/.agents/skills/setup-workflow/board-sync.md +7 -2
  19. package/.agents/skills/setup-workflow/workflow-overview.md +1 -2
  20. package/.agents/skills/to-issues/SKILL.md +66 -8
  21. package/.agents/skills/to-waves/SKILL.md +24 -12
  22. package/.claude/skills/ask-matt/SKILL.md +4 -3
  23. package/.claude/skills/audit-skills/SKILL.md +6 -0
  24. package/.claude/skills/board-to-waves/SKILL.md +6 -2
  25. package/.claude/skills/code-review/SKILL.md +6 -0
  26. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +8 -0
  27. package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
  28. package/.claude/skills/improve-codebase-architecture/SKILL.md +6 -0
  29. package/.claude/skills/kit-update/SKILL.md +13 -0
  30. package/.claude/skills/orchestrate-wave/SKILL.md +54 -54
  31. package/.claude/skills/orchestrate-wave/references/builder-contract.md +16 -3
  32. package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +91 -0
  33. package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +66 -0
  34. package/.claude/skills/orchestrate-wave/references/report-contracts.md +42 -0
  35. package/.claude/skills/research/SKILL.md +6 -0
  36. package/.claude/skills/scale-check/SKILL.md +9 -7
  37. package/.claude/skills/setup-workflow/SKILL.md +47 -1
  38. package/.claude/skills/setup-workflow/board-sync.md +7 -2
  39. package/.claude/skills/setup-workflow/workflow-overview.md +1 -2
  40. package/.claude/skills/to-issues/SKILL.md +66 -8
  41. package/.claude/skills/to-waves/SKILL.md +24 -12
  42. package/README.md +126 -11
  43. package/agent-workflow-kit.package.json +291 -39
  44. package/docs/adr/0002-capability-gated-orchestration.md +70 -0
  45. package/docs/adr/0005-to-issues-is-the-planning-facade.md +42 -0
  46. package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +91 -0
  47. package/docs/agents/skills/local-ci.md +89 -0
  48. package/docs/agents/wave-anchor-template.md +2 -2
  49. package/docs/methodology.html +1 -1
  50. package/docs/methodology.svg +1 -1
  51. package/docs/workflow.html +2 -2
  52. package/docs/workflow.png +0 -0
  53. package/package.json +1 -1
  54. package/scripts/codex-exec.sh +47 -8
  55. package/scripts/codex-exec.test.mjs +56 -0
  56. package/scripts/test_codex_adapter_sync_contract.py +30 -15
  57. package/scripts/test_orchestrate_wave_contract.py +209 -0
  58. package/scripts/test_program_planning_contract.py +70 -0
  59. package/scripts/test_skill_portability_lint.py +54 -0
  60. package/scripts/test_worktree_setup_base_guard.py +140 -0
  61. package/scripts/worktree-lifecycle/setup.py +40 -0
  62. package/src/cli.mjs +109 -2
  63. package/src/commands/init.mjs +17 -1
  64. package/src/commands/routing-policy-update.mjs +204 -0
  65. package/src/commands/update.mjs +22 -0
  66. package/src/lib/agentSurfaceRegistry.mjs +60 -0
  67. package/src/lib/bundle.mjs +34 -0
  68. package/src/lib/capabilityMatrix.mjs +179 -0
  69. package/src/lib/dispatchReceipt.mjs +162 -0
  70. package/src/lib/frontendWorkloads.mjs +170 -0
  71. package/src/lib/reconcileReconReports.mjs +111 -0
  72. package/src/lib/reportValidator.mjs +186 -0
  73. package/src/lib/routeDispatcher.mjs +158 -0
  74. package/src/lib/routingAccessGraph.mjs +105 -0
  75. package/src/lib/routingAdapters/claude.mjs +62 -0
  76. package/src/lib/routingAdapters/codex.mjs +136 -0
  77. package/src/lib/routingCatalog.mjs +123 -0
  78. package/src/lib/routingEvidenceCache.mjs +222 -0
  79. package/src/lib/routingIntent.mjs +93 -0
  80. package/src/lib/routingPolicy.mjs +67 -0
  81. package/src/lib/routingProfile.mjs +334 -0
  82. package/src/lib/routingResolver.mjs +176 -0
  83. package/src/lib/routingSources/artificialAnalysis.mjs +129 -0
  84. package/src/lib/routingSources/benchlm.mjs +151 -0
  85. package/src/lib/routingSources/codeArena.mjs +162 -0
  86. package/src/lib/routingSources/deepswe.mjs +106 -0
  87. package/src/lib/routingSources/openhands.mjs +102 -0
  88. package/src/lib/routingSources/openhandsFrontend.mjs +135 -0
  89. package/src/lib/waveClaim.mjs +134 -0
@@ -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.
@@ -0,0 +1,42 @@
1
+ # to-issues is the single planning facade
2
+
3
+ Status: accepted (2026-07-22, issue #197)
4
+
5
+ The planning workflow exposes two mechanisms after `to-prd`: Feature PRDs are
6
+ decomposed by `to-issues`, while Program PRDs are unfolded by a separately
7
+ invoked `to-waves` skill which later calls `to-issues` for each wave. This makes
8
+ the user identify an internal planning altitude even though the PRD already
9
+ carries an explicit, durable identity that can select the correct mechanism.
10
+
11
+ We decided that `to-issues` is the single user-facing Planning facade:
12
+
13
+ 1. A Feature PRD selects the existing atomic/promote mechanism.
14
+ 2. A Program PRD selects the existing program-graph engine currently owned by
15
+ `to-waves`, including its complete preview, validation, publication,
16
+ per-wave maturation, and execute-ready audits.
17
+ 3. Dispatch uses only explicit PRD identity (`prd: program` or its canonical
18
+ program type), never size heuristics or model judgment.
19
+ 4. The selected mode is reported visibly before mutation. Program mode retains
20
+ its full preview approval gate.
21
+ 5. `to-waves` remains an independently testable internal mechanism and may keep
22
+ a compatibility entrypoint, but routers and normal documentation always send
23
+ the user from `to-prd` to `to-issues`.
24
+
25
+ ## Considered options
26
+
27
+ - **Keep two public next commands:** rejected because it leaks internal
28
+ decomposition mechanics and makes users repeat an altitude decision already
29
+ encoded in the PRD.
30
+ - **Infer the mode from plan size or prose:** rejected because hidden heuristics
31
+ make the same command unpredictable and can publish the wrong board shape.
32
+ - **Merge all program code physically into `to-issues`:** rejected because the
33
+ program graph is a coherent deep module with valuable separate tests; only
34
+ its public routing belongs behind the facade.
35
+
36
+ ## Consequences
37
+
38
+ - The normal user path is always `grill → to-prd → to-issues`.
39
+ - Feature and Program behavior remain distinguishable in audit output without
40
+ requiring the user to select their implementation skill.
41
+ - Router, overview, skill prose, mirrors, and contract tests must agree on the
42
+ one-entry contract.
@@ -0,0 +1,91 @@
1
+ # Routing knowledge, access, and policy are separate
2
+
3
+ Status: accepted (2026-07-22, Program #197)
4
+
5
+ Planning cannot safely persist a concrete recommended model. Provider catalogs
6
+ change, model names age, effort behavior differs by model, agent surfaces expose
7
+ different controls, and a surface may reach another provider through a plugin
8
+ or CLI transport. A recommendation that is correct in one session can therefore
9
+ be nonexistent, unreachable, or unenforceable in the session that implements
10
+ the work.
11
+
12
+ We decided to keep three identities separate:
13
+
14
+ 1. The Kit-maintained **Evidence catalog** contains all known provider models
15
+ and observations, whether or not the active surface can use them. Each
16
+ decision-grade observation retains the complete configuration identity:
17
+ model, effort, harness, workload, source, benchmark version, uncertainty,
18
+ freshness, and cost.
19
+ 2. The user-local **Access graph** records the native and cross-provider paths
20
+ available from each agent surface. Claude Code and Codex attest only their
21
+ own runtime capabilities. A detected transport is not automatically approved,
22
+ and neither surface claims another surface's unverified capabilities.
23
+ 3. The user-local **Routing policy** records allowed surfaces and transports,
24
+ switching autonomy, optimization goals, and optional advanced overrides.
25
+ Personal choices do not alter catalog facts.
26
+
27
+ Durable plans and issues contain only a provider-neutral **Routing intent**.
28
+ At execution time a resolver compares all evidence-backed candidates, filters
29
+ them through the Access graph and Routing policy, and produces a one-execution
30
+ **Route decision**. It reports the best overall route separately from the best
31
+ currently executable route. An unreachable preference follows the user's
32
+ explicit handoff, fallback, or block policy.
33
+
34
+ Every surface adapter declares how independently it can control model and
35
+ effort: per spawn, through a named agent definition, through a session default,
36
+ or not at all. Environment precedence is part of the capability proof. An AFK
37
+ subagent requires enforced model and effort selection and emits a **Dispatch
38
+ receipt** containing the requested and applied route, enforcement method, and
39
+ policy/evidence revisions. Silent inheritance and unverified degradation are
40
+ not valid AFK routes.
41
+
42
+ Setup keeps the technical model behind a simple user decision. It presents a
43
+ registry-driven list of familiar agent surfaces, preselects detected entries,
44
+ and asks whether the Kit may switch automatically, ask before switching, or use
45
+ only the current surface. Adapters establish providers, transports, model
46
+ selectors, and effort controls. Model preferences and optimization overrides
47
+ remain optional advanced settings.
48
+
49
+ An existing installation without a routing profile receives this choice once
50
+ through setup or the first compatible Kit update. Later updates perform a
51
+ read-only preflight and ask again only when the profile is missing, invalid,
52
+ materially stale, references a removed route, or a newly detected surface makes
53
+ a meaningful choice available. Installing Kit mechanics and changing personal
54
+ policy are separate transactions; unattended update never invents a decision.
55
+
56
+ BenchLM may populate discovery, catalog, pricing, freshness, coverage, and
57
+ corroboration signals. Its aggregate scores are not decision-grade routing
58
+ observations because effort and harness identity may be collapsed and missing
59
+ evidence may be estimated. DeepSWE, Artificial Analysis, OpenHands, Arena, and
60
+ other benchmark-owner artifacts remain authoritative for the claims they
61
+ actually measure. Local experience may calibrate policy but does not rewrite
62
+ public evidence.
63
+
64
+ ## Considered options
65
+
66
+ - **Persist a concrete model and effort in every issue:** rejected because the
67
+ recommendation ages and may not be executable on the implementing surface.
68
+ - **Maintain separate Claude and Codex routing tables:** rejected because it
69
+ duplicates evidence, hides cross-provider routes, and lets the two surfaces
70
+ drift.
71
+ - **Filter the shared table during setup:** rejected because current access is
72
+ a user/runtime fact, not a limit on routing knowledge.
73
+ - **Ask users to describe transports and enforcement capabilities:** rejected
74
+ because these are adapter facts most users cannot answer reliably.
75
+ - **Route directly from a composite leaderboard:** rejected because benchmark,
76
+ harness, effort, uncertainty, and workload boundaries would be lost.
77
+ - **Re-run the routing interview on every Kit update:** rejected because a valid
78
+ user-owned profile should remain stable until a material choice changes.
79
+
80
+ ## Consequences
81
+
82
+ - Model/provider churn updates the catalog and resolver without rewriting
83
+ durable issues.
84
+ - Both Claude Code and Codex read one local policy while proving their own
85
+ execution capabilities independently.
86
+ - Cross-provider dispatch is expressible without pretending that every surface
87
+ can reach every model.
88
+ - The Kit must maintain source adapters, schema migration, capability probes,
89
+ last-known-good evidence, and dispatch receipts.
90
+ - Different machines do not share personal routing state unless the user later
91
+ chooses an explicit export or private synchronization mechanism.
@@ -0,0 +1,89 @@
1
+ <!-- setup-workflow: state=filled -->
2
+ # Project layer — local-ci
3
+
4
+ This repo is a Node + stdlib-Python repo with no database, no dev server and no
5
+ typecheck step, so the two generic profiles map onto a short command set. Run the
6
+ two commands below; do not infer others.
7
+
8
+ Prerequisite once per clone (worktrees inherit it):
9
+
10
+ ```sh
11
+ git config core.hooksPath .githooks
12
+ ```
13
+
14
+ ## Fast static guards
15
+
16
+ The skill/manifest lints, ~3s, no network, no DB. This is exactly what
17
+ `.githooks/pre-commit` runs, so a normal commit already covers it:
18
+
19
+ ```sh
20
+ python3 -m unittest discover -s scripts -p 'test_skill_*.py' -q
21
+ ```
22
+
23
+ ## Full gate
24
+
25
+ Mirrors the `test` job in `.github/workflows/ci.yml` step for step. Run it from
26
+ the branch's worktree root before opening a PR:
27
+
28
+ ```sh
29
+ npm test
30
+ npm run kit:staleness
31
+ npm run release:guard -- --base "$(git merge-base origin/main HEAD)"
32
+ npm pack --dry-run
33
+ ```
34
+
35
+ Notes on the individual steps:
36
+
37
+ - `npm test` is `test:node` (`node --test`) plus `test:python`
38
+ (`python3 -m unittest discover -s scripts -p 'test_*.py'`). Several negative-path
39
+ tests print `[FAIL] …` lines by design; only the runner's own summary and exit
40
+ code decide red or green.
41
+ - CI runs `npm install --ignore-scripts` first. Locally that is only needed after
42
+ a dependency change.
43
+ - `release:guard` runs in CI on `pull_request` only, against
44
+ `github.event.pull_request.base.sha`. `git merge-base origin/main HEAD` is the
45
+ local equivalent; `git fetch origin main` first if `origin/main` is stale.
46
+ - `npm pack --dry-run` catches packaging drift (a file added outside the
47
+ published set) without publishing anything.
48
+
49
+ ## Enforcement
50
+
51
+ Since #220 the host **does** enforce: the `main protection` ruleset makes the CI
52
+ job `test` a required status check on `main`, with no bypass actor. So the
53
+ generic skill's "When the host CAN enforce" branch applies to this repo — the
54
+ local gate is a pre-flight that shortens the feedback loop, not the only thing
55
+ standing between a red branch and `main`.
56
+
57
+ Layered, from cheapest to strongest:
58
+
59
+ | Layer | Runs | Scope |
60
+ | --- | --- | --- |
61
+ | `pre-commit` | automatic, every commit | fast skill/manifest lints |
62
+ | `pre-push` | automatic, every push | full `npm test` |
63
+ | this full gate | explicit, before a PR | `npm test` + staleness + release guard + pack |
64
+ | CI `test` | automatic, on the PR | the same set, machine-enforced at merge |
65
+
66
+ Never bypass a hook with `--no-verify`.
67
+
68
+ **Drift rule.** The full gate above and `.github/workflows/ci.yml` are two copies
69
+ of one list. Change one, change the other in the same PR — otherwise the local
70
+ gate goes green on a set the required check does not accept.
71
+
72
+ ## Contention
73
+
74
+ Not applicable here. This repo has no dev server and the suites are process-level
75
+ (`node --test`, `unittest`), so the generic boot-contention false-red guidance has
76
+ no target. A red is a real red until an isolated re-run of that one test proves
77
+ otherwise.
78
+
79
+ ## On a red
80
+
81
+ 1. Read the failing assertion — the guards print the exact drift (`file:line`,
82
+ the offending token, the missing manifest key).
83
+ 2. Fix the source. A guard that legitimately cannot be satisfied yet gets a
84
+ documented allowlist entry with a reason; never widen a guard silently.
85
+ 3. Re-run the single failing suite, then the full gate for sign-off.
86
+
87
+ `kit:staleness` red almost always means the generated kit artefacts lag the
88
+ skill sources — run `npm run kit:build` and commit the result rather than
89
+ editing the generated output by hand.
@@ -25,7 +25,7 @@ The **candidate stub** (Stage 1) has no cluster/Wave. On **`to-issues` promotion
25
25
  - **Source:** <board-to-waves | external-prd | raw-issue | plan | grill> *(provenance-neutral — the shape is the same regardless of origin; fill the following lines where they apply, delete where they don't)*
26
26
  - **Member issues:** #<a> #<b> #<c> … *(listed; linked via `to-issues` promotion, To-Do below)*
27
27
  - **Why together (firing criteria):** Gate=<Outcome> · <B1 code proximity / B2 type homogeneity / B3 dependency / B4 verify surface, where applicable>
28
- - **Size + risk:** ~<N> slices · Backend: <yes/no> · Model mix: <Model [Effort], e.g. Sonnet [medium] / Opus [high] / gpt-5.5 [medium]> · Risk: <low/medium/high — reason, e.g. race/cache/forecast/migration>
28
+ - **Size + risk:** ~<N> slices · Backend: <yes/no> · Routing-intent mix: <judgment/development/mechanical + deep/balanced/light> · Risk: <low/medium/high — reason, e.g. race/cache/forecast/migration>
29
29
  - **`grill-needed`:** <no> | <yes — this session> | <yes — own session (too big/fuzzy)>
30
30
 
31
31
  ### To-Do (maturation: grill → to-prd → to-issues) — *(Stage 1/1p only; at promotion this whole checklist collapses to ONE line: `Maturation: grill <✓/–> · to-prd ✓ · to-issues ✓ (<Date>) — Wave gate + tracking open`)*
@@ -53,7 +53,7 @@ Order (WSJF-lite): visible + low-risk first → logic/backend → cleanup. Depen
53
53
  | 1 | ⬜ | <Slice title> | #<sub> | <—/🧭/🔬/📐/📝> | <closes #x / refs #y> |
54
54
  <!-- slice-table:end -->
55
55
 
56
- Status legend: ⬜ open · 🔄 in progress · ✅ merged #<PR>. **Every slice = one sub-issue** (`#<sub>`). **The volatile Status column is generated by `board-sync.py anchor-sync <anchor#>` from the board** (between the `<!-- slice-table:start/end -->` markers; `wrapup` Step 5e.1 calls it on merge) — monotone (never flips a `✅`/`🔄` back), drift-free idempotent; **stable plan columns (Slice/Gate/refs) stay hand-maintained** and survive verbatim. It appends missing sub-issue rows (gen-b split). **Don't delete the markers** — without them `anchor-sync` can't locate the table (the first run locates it via the `Status`+`Sub-Issue` header row and sets the markers itself). The native "Sub-issues progress" rollup is the secondary %-view. *(Slimmed in: Branch is derivable from the `feat/<#>-<slug>` convention, the model recommendation lives in the leaf's handoff, Backend? carried no navigation value — legacy anchors that still have those columns keep working, `anchor-sync` matches columns by header name and refreshes Branch/Blocked-by only where the column exists.)*
56
+ Status legend: ⬜ open · 🔄 in progress · ✅ merged #<PR>. **Every slice = one sub-issue** (`#<sub>`). **The volatile Status column is generated by `board-sync.py anchor-sync <anchor#>` from the board** (between the `<!-- slice-table:start/end -->` markers; `wrapup` Step 5e.1 calls it on merge) — monotone (never flips a `✅`/`🔄` back), drift-free idempotent; **stable plan columns (Slice/Gate/refs) stay hand-maintained** and survive verbatim. It appends missing sub-issue rows (gen-b split). **Don't delete the markers** — without them `anchor-sync` can't locate the table (the first run locates it via the `Status`+`Sub-Issue` header row and sets the markers itself). The native "Sub-issues progress" rollup is the secondary %-view. *(Slimmed in: Branch is derivable from the `feat/<#>-<slug>` convention, provider-neutral routing intent lives in the leaf's handoff, Backend? carried no navigation value — legacy anchors that still have those columns keep working, `anchor-sync` matches columns by header name and refreshes Branch/Blocked-by only where the column exists.)*
57
57
 
58
58
  **Gate legend (retro):** `—` AFK build (`/implement`) · 🧭 design grill (`grill-with-docs-codex`, ADR) · 🔬 verify spike (read-only fact question) · 📐 trade-off/research (read-only, below grill threshold) · 📝 review note (not a build slice). A gate slice (🧭/🔬/📐) sits **before** its dependent build slice (gate-before-build) and blocks it.
59
59
 
@@ -324,7 +324,7 @@
324
324
  <span class="station st-program">grill</span><span class="arrow">→</span>
325
325
  <span class="station st-program">Program document</span><span class="arrow">→</span>
326
326
  <span class="station st-gate">preview gate</span><span class="arrow">→</span>
327
- <span class="station st-wave">to-waves</span><span class="arrow">→</span>
327
+ <span class="station st-wave">to-issues · Program mode</span><span class="arrow">→</span>
328
328
  <span class="station st-phase">phases stamped</span><span class="arrow">+</span>
329
329
  <span class="station st-wave">Wave 1…n + slices</span>
330
330
  </div>
@@ -49,7 +49,7 @@
49
49
 
50
50
  <!-- top branch: planned top-down -->
51
51
  <rect x="350" y="350" width="260" height="40" rx="7" fill="#2E3850" stroke="B2" stroke-width="1.2"/>
52
- <text x="480" y="375" font-size="12.5" fill="#8FA9C4" text-anchor="middle">scale-check → grill → to-waves</text>
52
+ <text x="480" y="375" font-size="12.5" fill="#8FA9C4" text-anchor="middle">scale-check → grill → to-issues</text>
53
53
  <line x1="480" y1="390" x2="553" y2="430" stroke="#8A95A6" stroke-width="1.4" marker-end="url(#arrow)"/>
54
54
 
55
55
  <!-- main spine -->
@@ -181,8 +181,8 @@
181
181
  <div class="sw-track" style="--lbg: var(--plan-bg);">
182
182
  <span class="station st-plan">scale-check</span><span class="arrow">→</span>
183
183
  <span class="station st-gate">grill</span><span class="arrow">→</span>
184
- <span class="station st-plan">to-waves</span>
185
- <span class="decision">preview gate · zero board writes until yes</span><span class="arrow">→</span>
184
+ <span class="station st-plan">to-issues</span>
185
+ <span class="decision">Program identity · preview gate · zero writes until yes</span><span class="arrow">→</span>
186
186
  <span class="station st-exec">orchestrate-wave</span>
187
187
  <span class="decision">per wave, often AFK</span><span class="arrow">→</span>
188
188
  <span class="station st-land">wrapup</span><span class="arrow">→</span>
package/docs/workflow.png CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.32.1",
3
+ "version": "0.34.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": {
@@ -4,6 +4,7 @@ set -u
4
4
  SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
5
  PROC_HELPER="$SCRIPT_DIR/codex_proc.py"
6
6
  TESTED_VERSIONS=("0.137.0" "0.144.6")
7
+ SUPPORTED_EFFORTS=("low" "medium" "high" "xhigh" "max" "ultra")
7
8
  STATE_ROOT=${CODEX_EXEC_STATE_ROOT:-${TMPDIR:-/tmp}/codex-exec-state}
8
9
 
9
10
  emit_json() {
@@ -118,10 +119,10 @@ preflight() {
118
119
  }
119
120
 
120
121
  parse_options() {
121
- CODEX_BIN=codex PROFILE= MODE= SANDBOX= PROMPT= PROMPT_FILE= RUN_ID= TIMEOUT= PROBE_TIMEOUT= DEBUG_RETAIN=false
122
+ CODEX_BIN=codex PROFILE= MODE= SANDBOX= PROMPT= PROMPT_FILE= RUN_ID= TIMEOUT= PROBE_TIMEOUT= MODEL= EFFORT= DEBUG_RETAIN=false
122
123
  while (($#)); do
123
124
  case $1 in
124
- --codex-bin|--profile|--mode|--prompt|--prompt-file|--run-id|--timeout|--probe-timeout)
125
+ --codex-bin|--profile|--mode|--prompt|--prompt-file|--run-id|--timeout|--probe-timeout|--model|--effort)
125
126
  (($# >= 2)) || { fail INVALID_ARGUMENT "Missing value for $1"; return 1; }
126
127
  case $1 in
127
128
  --codex-bin) CODEX_BIN=$2 ;;
@@ -132,6 +133,8 @@ parse_options() {
132
133
  --run-id) RUN_ID=$2 ;;
133
134
  --timeout) TIMEOUT=$2 ;;
134
135
  --probe-timeout) PROBE_TIMEOUT=$2 ;;
136
+ --model) MODEL=$2 ;;
137
+ --effort) EFFORT=$2 ;;
135
138
  esac
136
139
  shift 2 ;;
137
140
  --debug-retain) DEBUG_RETAIN=true; shift ;;
@@ -140,6 +143,22 @@ parse_options() {
140
143
  done
141
144
  }
142
145
 
146
+ validate_route_controls() {
147
+ if [[ -z $MODEL && -z $EFFORT ]]; then
148
+ return 0
149
+ fi
150
+ local effort_supported=false supported
151
+ for supported in "${SUPPORTED_EFFORTS[@]}"; do
152
+ [[ $EFFORT == "$supported" ]] && effort_supported=true
153
+ done
154
+ if [[ -z $MODEL || -z $EFFORT \
155
+ || ! $MODEL =~ ^[A-Za-z0-9._:-]+$ \
156
+ || $effort_supported != true ]]; then
157
+ fail INVALID_ROUTE_CONTROL "Model and effort must be supplied together from the exact safe vocabulary"
158
+ return 1
159
+ fi
160
+ }
161
+
143
162
  prepare_prompt() {
144
163
  local state_dir=$1
145
164
  TEMP_PROMPT="$state_dir/.prompt-input"
@@ -160,10 +179,14 @@ launch_round() {
160
179
  prepare_prompt "$state_dir" || return 1
161
180
  local command
162
181
  if [[ -z $thread_id ]]; then
163
- command=("$CODEX_BIN" exec --json --sandbox "$SANDBOX" -)
182
+ command=("$CODEX_BIN" exec --json --sandbox "$SANDBOX")
164
183
  else
165
- command=("$CODEX_BIN" exec resume "$thread_id" -c "sandbox_mode=$SANDBOX" --json -)
184
+ command=("$CODEX_BIN" exec resume "$thread_id" -c "sandbox_mode=$SANDBOX")
166
185
  fi
186
+ if [[ -n $MODEL ]]; then
187
+ command+=(-c "model=$MODEL" -c "model_reasoning_effort=$EFFORT")
188
+ fi
189
+ command+=(--json -)
167
190
  python3 "$PROC_HELPER" run --state-dir "$state_dir" --round "$round" \
168
191
  --profile "$PROFILE" --sandbox "$SANDBOX" --timeout "$TIMEOUT" \
169
192
  --probe-timeout "$PROBE_TIMEOUT" --prompt-file "$TEMP_PROMPT" \
@@ -252,6 +275,7 @@ PY
252
275
 
253
276
  new_run() {
254
277
  parse_options "$@" || return 1
278
+ validate_route_controls || return 1
255
279
  PROFILE=${PROFILE:-review}
256
280
  MODE=${MODE:-read-only}
257
281
  [[ $PROFILE == review || $PROFILE == build ]] || { fail INVALID_PROFILE "Profile must be review or build"; return 1; }
@@ -285,15 +309,18 @@ new_run() {
285
309
  state_dir=$(mktemp -d "$STATE_ROOT/codex-exec.XXXXXXXX") || return 1
286
310
  chmod 700 "$state_dir"
287
311
  run_id=${state_dir##*.}
288
- printf '%s\n' "$run_id" >"$state_dir/run-id"
289
312
  printf '%s\n' "$PROFILE" >"$state_dir/profile"
290
313
  printf '%s\n' "$SANDBOX" >"$state_dir/sandbox"
314
+ printf '%s\n' "$MODEL" >"$state_dir/model"
315
+ printf '%s\n' "$EFFORT" >"$state_dir/effort"
291
316
  printf '1\n' >"$state_dir/next-round"
292
317
  chmod 600 "$state_dir"/*
293
318
  lease_token=$(acquire_lease "$state_dir") || {
294
319
  fail ACTIVE_RUN "Run state is already owned by another lifecycle action"
295
320
  return 1
296
321
  }
322
+ printf '%s\n' "$run_id" >"$state_dir/run-id"
323
+ chmod 600 "$state_dir/run-id"
297
324
  output=$(launch_round "$state_dir" 1 "$lease_token")
298
325
  rc=$?
299
326
  release_or_fail "$state_dir" "$lease_token" "$run_id" "$output" || return 1
@@ -307,16 +334,20 @@ new_run() {
307
334
  }
308
335
 
309
336
  resume_run_locked() {
310
- local state_dir=$1 lease_token=$2 stored_profile stored_sandbox thread_id round
337
+ local state_dir=$1 lease_token=$2 stored_profile stored_sandbox stored_model stored_effort thread_id round
311
338
  [[ ! -f $state_dir/debug-retain ]] || {
312
339
  fail RUN_FINALIZED "Run was finalized with debug retention"; return 1;
313
340
  }
314
341
  [[ -f $state_dir/profile && ! -L $state_dir/profile \
315
- && -f $state_dir/sandbox && ! -L $state_dir/sandbox ]] || {
316
- fail INVALID_STATE "Persisted profile or sandbox state is missing or unsafe"; return 1;
342
+ && -f $state_dir/sandbox && ! -L $state_dir/sandbox \
343
+ && -f $state_dir/model && ! -L $state_dir/model \
344
+ && -f $state_dir/effort && ! -L $state_dir/effort ]] || {
345
+ fail INVALID_STATE "Persisted profile, sandbox, or route controls are missing or unsafe"; return 1;
317
346
  }
318
347
  stored_profile=$(<"$state_dir/profile")
319
348
  stored_sandbox=$(<"$state_dir/sandbox")
349
+ stored_model=$(<"$state_dir/model")
350
+ stored_effort=$(<"$state_dir/effort")
320
351
  [[ $stored_profile == review || $stored_profile == build ]] || {
321
352
  fail INVALID_STATE "Persisted profile is not in the exact allowlist"; return 1;
322
353
  }
@@ -328,6 +359,13 @@ resume_run_locked() {
328
359
  fail MODE_MISMATCH "Resume mode differs from persisted mode"
329
360
  return 1
330
361
  fi
362
+ if [[ -n $MODEL && $MODEL != "$stored_model" ]] || [[ -n $EFFORT && $EFFORT != "$stored_effort" ]]; then
363
+ fail ROUTE_CONTROL_MISMATCH "Resume model or effort differs from the persisted applied route"
364
+ return 1
365
+ fi
366
+ MODEL=$stored_model
367
+ EFFORT=$stored_effort
368
+ validate_route_controls || return 1
331
369
  SANDBOX=$stored_sandbox
332
370
  [[ -f $state_dir/thread-id ]] || { fail NO_THREAD "Run has no resumable thread" NO-THREAD; return 1; }
333
371
  thread_id=$(<"$state_dir/thread-id")
@@ -346,6 +384,7 @@ resume_run_locked() {
346
384
 
347
385
  resume_run() {
348
386
  parse_options "$@" || return 1
387
+ validate_route_controls || return 1
349
388
  [[ -n $RUN_ID ]] || { fail RUN_ID_REQUIRED "Resume requires --run-id"; return 1; }
350
389
  local state_dir lease_token rc output
351
390
  state_dir=$(find_state "$RUN_ID") || { fail RUN_NOT_FOUND "Run state does not exist"; return 1; }
@@ -143,6 +143,62 @@ test('invalid timeout and mode inputs fail before launch', () => {
143
143
  assert.equal(exists(fx.launchLog), false);
144
144
  });
145
145
 
146
+ test('approved route passes explicit model and effort controls to Codex', () => {
147
+ const fx = fixture();
148
+ const result = invoke(fx, [
149
+ ...launchArgs(),
150
+ '--model', 'coding-model',
151
+ '--effort', 'high',
152
+ ]);
153
+ assert.equal(result.output.status, 'OK');
154
+ const command = JSON.parse(readFileSync(fx.launchLog, 'utf8').trim());
155
+ assert.ok(command.includes('model=coding-model'));
156
+ assert.ok(command.includes('model_reasoning_effort=high'));
157
+
158
+ const resumed = invoke(fx, [
159
+ 'resume', result.output.runId, '--codex-bin', fake,
160
+ '--prompt', 'Again', '--timeout', '2',
161
+ ]);
162
+ assert.equal(resumed.output.status, 'OK');
163
+ const resumeCommand = JSON.parse(readFileSync(fx.launchLog, 'utf8').trim().split('\n').at(-1));
164
+ assert.ok(resumeCommand.includes('model=coding-model'));
165
+ assert.ok(resumeCommand.includes('model_reasoning_effort=high'));
166
+
167
+ const mismatch = invoke(fx, [
168
+ 'resume', result.output.runId, '--codex-bin', fake,
169
+ '--model', 'other-model', '--effort', 'high', '--prompt', 'No',
170
+ ]);
171
+ assert.equal(mismatch.output.error, 'ROUTE_CONTROL_MISMATCH');
172
+ });
173
+
174
+ test('route controls fail closed when incomplete or unsafe', () => {
175
+ for (const args of [
176
+ [...launchArgs(), '--model', 'coding-model'],
177
+ [...launchArgs(), '--effort', 'high'],
178
+ [...launchArgs(), '--model', 'unsafe value', '--effort', 'high'],
179
+ [...launchArgs(), '--model', 'coding-model', '--effort', 'unknown'],
180
+ ]) {
181
+ const fx = fixture();
182
+ const result = invoke(fx, args);
183
+ assert.equal(result.output.error, 'INVALID_ROUTE_CONTROL');
184
+ assert.equal(exists(fx.launchLog), false);
185
+ }
186
+ });
187
+
188
+ test('current extended reasoning efforts are passed through explicitly', () => {
189
+ for (const effort of ['max', 'ultra']) {
190
+ const fx = fixture();
191
+ const result = invoke(fx, [
192
+ ...launchArgs(),
193
+ '--model', 'coding-model',
194
+ '--effort', effort,
195
+ ]);
196
+ assert.equal(result.output.status, 'OK', effort);
197
+ const command = JSON.parse(readFileSync(fx.launchLog, 'utf8').trim());
198
+ assert.ok(command.includes(`model_reasoning_effort=${effort}`));
199
+ }
200
+ });
201
+
146
202
  test('finalize deletes state, debug-retain preserves diagnostics, and finalized resume fails', () => {
147
203
  const fx = fixture();
148
204
  const run = invoke(fx, launchArgs()).output;
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
 
9
9
  REPO_ROOT = Path(__file__).resolve().parent.parent
10
10
  ADAPTER = REPO_ROOT / ".agents/skills/codex-adapter-sync/SKILL.md"
11
+ AGENTS_README = REPO_ROOT / ".codex/agents/README.md"
11
12
  _FRONTMATTER_SPEC = importlib.util.spec_from_file_location(
12
13
  "skill_frontmatter_lint", REPO_ROOT / "scripts/test_skill_frontmatter_lint.py")
13
14
  frontmatter = importlib.util.module_from_spec(_FRONTMATTER_SPEC)
@@ -39,27 +40,24 @@ class AdapterModesContract(unittest.TestCase):
39
40
  self.assertNotIn("before inventory or edits", self.body)
40
41
 
41
42
 
42
- class AdapterModelContract(unittest.TestCase):
43
+ class AdapterRoutingContract(unittest.TestCase):
43
44
  @classmethod
44
45
  def setUpClass(cls):
45
46
  cls.body = ADAPTER.read_text(encoding="utf-8")
46
47
 
47
- def test_current_models_inherit_by_default_and_route_by_task_shape(self):
48
- routing = " ".join(section(self.body, "Model routing").split())
49
-
50
- self.assertIn("inherited parent model configuration", routing)
51
- self.assertIn("`gpt-5.6-sol`", routing)
52
- self.assertIn("complex, open-ended judgment work", routing)
53
- self.assertIn("`gpt-5.6-terra`", routing)
54
- self.assertIn("everyday tool-using development", routing)
55
- self.assertIn("`gpt-5.6-luna`", routing)
56
- self.assertIn("clear, repeatable, high-volume work", routing)
57
- self.assertIn("`model_reasoning_effort`", routing)
48
+ def test_durable_routing_is_provider_neutral_and_resolved_at_dispatch(self):
49
+ routing = " ".join(section(self.body, "Routing intent").split())
50
+
51
+ self.assertIn("`routing-intent`", routing)
52
+ self.assertIn("`reasoning-intent`", routing)
53
+ self.assertIn("Evidence catalog", routing)
54
+ self.assertIn("Access graph", routing)
55
+ self.assertIn("Routing policy", routing)
56
+ self.assertIn("dispatch time", routing)
57
+ self.assertIn("explicit `inherit`", routing)
58
+ self.assertIn("Dispatch receipt", routing)
58
59
  self.assertIn("`default`, `worker`, and `explorer`", routing)
59
- self.assertIn("not a fourth concrete variant", routing)
60
60
 
61
- for stale in ("gpt-5.4-mini", "gpt-5.5"):
62
- self.assertNotIn(stale, self.body)
63
61
  self.assertNotRegex(self.body, r"(?<!model_)\breasoning_effort\b")
64
62
  self.assertNotRegex(self.body, r"\bagent_type\b")
65
63
 
@@ -104,6 +102,23 @@ class AdapterAgentContract(unittest.TestCase):
104
102
  self.assertIn("Parse every `.codex/agents/*.toml`", agents)
105
103
  self.assertIn("Reject a file", agents)
106
104
 
105
+ def test_codex_host_inventory_is_dated_and_does_not_invent_spawn_selectors(self):
106
+ inventory = " ".join(
107
+ AGENTS_README.read_text(encoding="utf-8").split()
108
+ )
109
+ for fragment in (
110
+ "2026-07-23",
111
+ "`task_name`",
112
+ "`message`",
113
+ "`fork_turns`",
114
+ "no per-spawn `model`",
115
+ "no per-spawn effort selector",
116
+ "`src/lib/routingAdapters/codex.mjs`",
117
+ "blocks differentiated AFK",
118
+ "Dispatch receipt v2",
119
+ ):
120
+ self.assertIn(fragment, inventory)
121
+
107
122
 
108
123
  class AdapterValidationContract(unittest.TestCase):
109
124
  @classmethod