@ikon85/agent-workflow-kit 0.33.0 → 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.
- package/.agents/skills/ask-matt/SKILL.md +4 -3
- package/.agents/skills/audit-skills/SKILL.md +6 -0
- package/.agents/skills/board-to-waves/SKILL.md +6 -2
- package/.agents/skills/code-review/SKILL.md +6 -0
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +11 -1
- package/.agents/skills/codex-adapter-sync/SKILL.md +23 -19
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +10 -1
- package/.agents/skills/kit-update/SKILL.md +13 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +1 -1
- package/.agents/skills/orchestrate-wave/references/dispatch-subagents.md +23 -11
- package/.agents/skills/orchestrate-wave/references/dispatch-workflow.md +8 -0
- package/.agents/skills/research/SKILL.md +6 -0
- package/.agents/skills/scale-check/SKILL.md +9 -7
- package/.agents/skills/setup-workflow/SKILL.md +47 -1
- package/.agents/skills/setup-workflow/board-sync.md +7 -2
- package/.agents/skills/setup-workflow/workflow-overview.md +1 -2
- package/.agents/skills/to-issues/SKILL.md +66 -8
- package/.agents/skills/to-waves/SKILL.md +24 -12
- package/.claude/skills/ask-matt/SKILL.md +4 -3
- package/.claude/skills/audit-skills/SKILL.md +6 -0
- package/.claude/skills/board-to-waves/SKILL.md +6 -2
- package/.claude/skills/code-review/SKILL.md +6 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +8 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +6 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +6 -0
- package/.claude/skills/kit-update/SKILL.md +13 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +1 -1
- package/.claude/skills/orchestrate-wave/references/dispatch-subagents.md +23 -11
- package/.claude/skills/orchestrate-wave/references/dispatch-workflow.md +8 -0
- package/.claude/skills/research/SKILL.md +6 -0
- package/.claude/skills/scale-check/SKILL.md +9 -7
- package/.claude/skills/setup-workflow/SKILL.md +47 -1
- package/.claude/skills/setup-workflow/board-sync.md +7 -2
- package/.claude/skills/setup-workflow/workflow-overview.md +1 -2
- package/.claude/skills/to-issues/SKILL.md +66 -8
- package/.claude/skills/to-waves/SKILL.md +24 -12
- package/README.md +79 -11
- package/agent-workflow-kit.package.json +201 -41
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +42 -0
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +91 -0
- package/docs/agents/skills/local-ci.md +89 -0
- package/docs/agents/wave-anchor-template.md +2 -2
- package/docs/methodology.html +1 -1
- package/docs/methodology.svg +1 -1
- package/docs/workflow.html +2 -2
- package/docs/workflow.png +0 -0
- package/package.json +1 -1
- package/scripts/codex-exec.sh +47 -8
- package/scripts/codex-exec.test.mjs +56 -0
- package/scripts/test_codex_adapter_sync_contract.py +30 -15
- package/scripts/test_orchestrate_wave_contract.py +73 -0
- package/scripts/test_program_planning_contract.py +70 -0
- package/scripts/test_skill_portability_lint.py +54 -0
- package/src/cli.mjs +109 -2
- package/src/commands/init.mjs +17 -1
- package/src/commands/routing-policy-update.mjs +204 -0
- package/src/commands/update.mjs +22 -0
- package/src/lib/agentSurfaceRegistry.mjs +60 -0
- package/src/lib/bundle.mjs +24 -0
- package/src/lib/capabilityMatrix.mjs +85 -0
- package/src/lib/dispatchReceipt.mjs +162 -0
- package/src/lib/frontendWorkloads.mjs +170 -0
- package/src/lib/routeDispatcher.mjs +158 -0
- package/src/lib/routingAccessGraph.mjs +105 -0
- package/src/lib/routingAdapters/claude.mjs +62 -0
- package/src/lib/routingAdapters/codex.mjs +136 -0
- package/src/lib/routingCatalog.mjs +123 -0
- package/src/lib/routingEvidenceCache.mjs +222 -0
- package/src/lib/routingIntent.mjs +93 -0
- package/src/lib/routingPolicy.mjs +67 -0
- package/src/lib/routingProfile.mjs +334 -0
- package/src/lib/routingResolver.mjs +176 -0
- package/src/lib/routingSources/artificialAnalysis.mjs +129 -0
- package/src/lib/routingSources/benchlm.mjs +151 -0
- package/src/lib/routingSources/codeArena.mjs +162 -0
- package/src/lib/routingSources/deepswe.mjs +106 -0
- package/src/lib/routingSources/openhands.mjs +102 -0
- package/src/lib/routingSources/openhandsFrontend.mjs +135 -0
|
@@ -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> ·
|
|
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,
|
|
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
|
|
package/docs/methodology.html
CHANGED
|
@@ -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-
|
|
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>
|
package/docs/methodology.svg
CHANGED
|
@@ -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-
|
|
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 -->
|
package/docs/workflow.html
CHANGED
|
@@ -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-
|
|
185
|
-
<span class="decision">preview gate · zero
|
|
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.
|
|
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": {
|
package/scripts/codex-exec.sh
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
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
|
|
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
|
|
48
|
-
routing = " ".join(section(self.body, "
|
|
49
|
-
|
|
50
|
-
self.assertIn("
|
|
51
|
-
self.assertIn("`
|
|
52
|
-
self.assertIn("
|
|
53
|
-
self.assertIn("
|
|
54
|
-
self.assertIn("
|
|
55
|
-
self.assertIn("
|
|
56
|
-
self.assertIn("
|
|
57
|
-
self.assertIn("
|
|
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
|
|
@@ -34,6 +34,25 @@ CODEX_SUBAGENTS = (
|
|
|
34
34
|
REPO / ".agents/skills/orchestrate-wave/references/dispatch-subagents.md"
|
|
35
35
|
)
|
|
36
36
|
CODEX_SURFACE = REPO / ".agents/skills/orchestrate-wave"
|
|
37
|
+
EXPECTED_DIRECT_SPAWN_CONTRACTS = (
|
|
38
|
+
"audit-skills/SKILL.md",
|
|
39
|
+
"code-review/SKILL.md",
|
|
40
|
+
"codebase-design/DESIGN-IT-TWICE.md",
|
|
41
|
+
"improve-codebase-architecture/INTERFACE-DESIGN.md",
|
|
42
|
+
"improve-codebase-architecture/SKILL.md",
|
|
43
|
+
"research/SKILL.md",
|
|
44
|
+
)
|
|
45
|
+
DIRECT_SPAWN_PATTERN = re.compile(
|
|
46
|
+
r"Run \*\*one read-only research subagent|"
|
|
47
|
+
r"Run both axes as \*\*parallel sub-agents|"
|
|
48
|
+
r"Spawn 3\+ sub-agents|"
|
|
49
|
+
r"spawn 3\+ sub-agents|"
|
|
50
|
+
r"make 3\+ parallel\s+`spawn_agent` calls|"
|
|
51
|
+
r"Then use the Agent tool|"
|
|
52
|
+
r"use `spawn_agent` with `agent_type: explorer`|"
|
|
53
|
+
r"use `spawn_agent` with only|"
|
|
54
|
+
r"Spin up a \*\*background agent",
|
|
55
|
+
)
|
|
37
56
|
|
|
38
57
|
|
|
39
58
|
# Outcome -> fragments whose conjunction proves that portable behavior.
|
|
@@ -86,6 +105,15 @@ def markdown_body(text: str) -> str:
|
|
|
86
105
|
return text.split("\n---\n", 1)[1]
|
|
87
106
|
|
|
88
107
|
|
|
108
|
+
def direct_spawn_contracts(surface: str) -> tuple[str, ...]:
|
|
109
|
+
root = REPO / surface
|
|
110
|
+
return tuple(sorted(
|
|
111
|
+
str(path.relative_to(root))
|
|
112
|
+
for path in root.rglob("*.md")
|
|
113
|
+
if DIRECT_SPAWN_PATTERN.search(path.read_text(encoding="utf-8"))
|
|
114
|
+
))
|
|
115
|
+
|
|
116
|
+
|
|
89
117
|
class OrchestrateWaveContract(unittest.TestCase):
|
|
90
118
|
@classmethod
|
|
91
119
|
def setUpClass(cls):
|
|
@@ -176,6 +204,47 @@ class OrchestrateWaveContract(unittest.TestCase):
|
|
|
176
204
|
self.assertIn("(b) tier + effort", prose)
|
|
177
205
|
self.assertIn("Standing rules", prose)
|
|
178
206
|
|
|
207
|
+
def test_every_surface_dispatch_crosses_resolver_and_spawn_guard(self):
|
|
208
|
+
prose = " ".join(self.skill.split())
|
|
209
|
+
for fragment in (
|
|
210
|
+
"provider-neutral Routing intent",
|
|
211
|
+
"shared resolver",
|
|
212
|
+
"spawn guard",
|
|
213
|
+
"Claude or Codex adapter",
|
|
214
|
+
"routingAdapters/codex.mjs",
|
|
215
|
+
"Dispatch receipt",
|
|
216
|
+
"requested and applied route",
|
|
217
|
+
"environment precedence",
|
|
218
|
+
"detected transport is not authorization",
|
|
219
|
+
"AFK",
|
|
220
|
+
):
|
|
221
|
+
self.assertIn(fragment, prose)
|
|
222
|
+
|
|
223
|
+
def test_every_direct_spawn_contract_routes_through_the_shared_guard(self):
|
|
224
|
+
required = (
|
|
225
|
+
"provider-neutral Routing intent",
|
|
226
|
+
"routeDispatcher.mjs",
|
|
227
|
+
"shared spawn guard",
|
|
228
|
+
"Dispatch receipt",
|
|
229
|
+
)
|
|
230
|
+
claude_actual = direct_spawn_contracts(".claude/skills")
|
|
231
|
+
codex_actual = direct_spawn_contracts(".agents/skills")
|
|
232
|
+
self.assertEqual(claude_actual, EXPECTED_DIRECT_SPAWN_CONTRACTS)
|
|
233
|
+
self.assertEqual(codex_actual, EXPECTED_DIRECT_SPAWN_CONTRACTS)
|
|
234
|
+
for relative in claude_actual:
|
|
235
|
+
claude = " ".join(
|
|
236
|
+
(REPO / ".claude/skills" / relative).read_text(encoding="utf-8").split()
|
|
237
|
+
)
|
|
238
|
+
codex = " ".join(
|
|
239
|
+
(REPO / ".agents/skills" / relative).read_text(encoding="utf-8").split()
|
|
240
|
+
)
|
|
241
|
+
with self.subTest(contract=relative):
|
|
242
|
+
for fragment in required:
|
|
243
|
+
self.assertIn(fragment, claude)
|
|
244
|
+
self.assertIn(fragment, codex)
|
|
245
|
+
for unsupported in ("Agent tool", "subagent_type", "agent_type"):
|
|
246
|
+
self.assertNotIn(unsupported, codex)
|
|
247
|
+
|
|
179
248
|
def test_path_a_reference_locks_the_two_run_dispatch_contract(self):
|
|
180
249
|
workflow = CLAUDE_WORKFLOW.read_text(encoding="utf-8")
|
|
181
250
|
for fragment in (
|
|
@@ -214,6 +283,10 @@ class OrchestrateWaveContract(unittest.TestCase):
|
|
|
214
283
|
"spawn_agents_on_csv",
|
|
215
284
|
"output_schema",
|
|
216
285
|
"dormant",
|
|
286
|
+
"task_name`, `message`, and `fork_turns",
|
|
287
|
+
"no model or effort selector",
|
|
288
|
+
"blocks differentiated AFK",
|
|
289
|
+
"routingAdapters/codex.mjs",
|
|
217
290
|
):
|
|
218
291
|
self.assertIn(" ".join(fragment.split()), prose)
|
|
219
292
|
|
|
@@ -11,6 +11,76 @@ class ProgramPlanningContractTest(unittest.TestCase):
|
|
|
11
11
|
def skill(self, name: str) -> str:
|
|
12
12
|
return (ROOT / ".claude" / "skills" / name / "SKILL.md").read_text()
|
|
13
13
|
|
|
14
|
+
def file(self, path: str) -> str:
|
|
15
|
+
return (ROOT / path).read_text()
|
|
16
|
+
|
|
17
|
+
def test_to_issues_is_the_public_planning_facade(self):
|
|
18
|
+
text = self.skill("to-issues")
|
|
19
|
+
self.assertIn("single public Planning facade", text)
|
|
20
|
+
self.assertIn("planning-mode=feature", text)
|
|
21
|
+
self.assertIn("planning-mode=program", text)
|
|
22
|
+
self.assertIn("Missing or contradictory identity", text)
|
|
23
|
+
self.assertIn("Size, prose, and model judgment never select the mode", text)
|
|
24
|
+
|
|
25
|
+
def test_program_identity_delegates_to_the_internal_graph_engine(self):
|
|
26
|
+
issues = self.skill("to-issues")
|
|
27
|
+
waves = self.skill("to-waves")
|
|
28
|
+
self.assertIn("delegate to the internal", issues)
|
|
29
|
+
self.assertIn("`to-waves` graph engine", issues)
|
|
30
|
+
self.assertIn("complete Program preview", issues)
|
|
31
|
+
self.assertIn("Internal Program graph engine", waves)
|
|
32
|
+
self.assertIn("compatibility entrypoint", waves)
|
|
33
|
+
self.assertIn("carries both", waves)
|
|
34
|
+
self.assertIn("never infer Program mode", waves)
|
|
35
|
+
|
|
36
|
+
def test_feature_identity_keeps_the_existing_decomposition_path(self):
|
|
37
|
+
text = self.skill("to-issues")
|
|
38
|
+
self.assertIn("Feature identity", text)
|
|
39
|
+
self.assertIn("PROMOTE", text)
|
|
40
|
+
self.assertIn("ATOMIC", text)
|
|
41
|
+
|
|
42
|
+
def test_normal_routes_name_only_the_planning_facade(self):
|
|
43
|
+
route_files = (
|
|
44
|
+
".claude/skills/ask-matt/SKILL.md",
|
|
45
|
+
".claude/skills/scale-check/SKILL.md",
|
|
46
|
+
".claude/skills/setup-workflow/workflow-overview.md",
|
|
47
|
+
".claude/skills/board-to-waves/SKILL.md",
|
|
48
|
+
"CLAUDE.md",
|
|
49
|
+
"README.md",
|
|
50
|
+
"docs/workflow.html",
|
|
51
|
+
"docs/methodology.html",
|
|
52
|
+
"docs/methodology.svg",
|
|
53
|
+
)
|
|
54
|
+
for path in route_files:
|
|
55
|
+
with self.subTest(path=path):
|
|
56
|
+
text = self.file(path)
|
|
57
|
+
self.assertIn("to-issues", text)
|
|
58
|
+
self.assertNotIn("→ `to-waves`", text)
|
|
59
|
+
self.assertNotIn("→ to-waves", text)
|
|
60
|
+
|
|
61
|
+
def test_dual_surface_planning_skills_are_coherent(self):
|
|
62
|
+
skill_snippets = (
|
|
63
|
+
("to-issues", "single public Planning facade"),
|
|
64
|
+
("to-waves", "Internal Program graph engine"),
|
|
65
|
+
("ask-matt", "the user never chooses that engine"),
|
|
66
|
+
("scale-check", "explicit Program identity selects the internal graph path"),
|
|
67
|
+
("setup-workflow", "explicit Program identity"),
|
|
68
|
+
("board-to-waves", "not directly to a hidden engine"),
|
|
69
|
+
)
|
|
70
|
+
for skill, snippet in skill_snippets:
|
|
71
|
+
with self.subTest(skill=skill):
|
|
72
|
+
claude = self.file(f".claude/skills/{skill}/SKILL.md")
|
|
73
|
+
codex = self.file(f".agents/skills/{skill}/SKILL.md")
|
|
74
|
+
self.assertIn(snippet, claude)
|
|
75
|
+
self.assertIn(snippet, codex)
|
|
76
|
+
|
|
77
|
+
support_files = ("workflow-overview.md", "board-sync.md")
|
|
78
|
+
for name in support_files:
|
|
79
|
+
with self.subTest(support=name):
|
|
80
|
+
claude = self.file(f".claude/skills/setup-workflow/{name}")
|
|
81
|
+
codex = self.file(f".agents/skills/setup-workflow/{name}")
|
|
82
|
+
self.assertEqual(claude, codex)
|
|
83
|
+
|
|
14
84
|
def test_to_waves_finishes_with_every_wave_execute_ready_by_default(self):
|
|
15
85
|
text = self.skill("to-waves")
|
|
16
86
|
self.assertIn("## 7. Program completion contract", text)
|