@ikon85/agent-workflow-kit 0.33.0 → 0.34.1
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-release/SKILL.md +29 -4
- 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-release/SKILL.md +29 -4
- 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 +89 -13
- package/agent-workflow-kit.package.json +204 -44
- package/docs/adr/0004-release-intent-is-a-version-tag.md +42 -0
- 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/kit-release.test.mjs +178 -0
- package/scripts/release-state.mjs +35 -5
- package/scripts/release-state.test.mjs +122 -5
- 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,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> ·
|
|
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.1",
|
|
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;
|
|
@@ -1,12 +1,170 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
3
4
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { tmpdir } from 'node:os';
|
|
5
6
|
import { dirname, join } from 'node:path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
6
8
|
import { fileURLToPath } from 'node:url';
|
|
7
9
|
import { nextVersion, prepareRelease } from './kit-release.mjs';
|
|
8
10
|
|
|
9
11
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const exec = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
async function releaseWorkflow() {
|
|
15
|
+
return readFile(join(REPO, '.github/workflows/release.yml'), 'utf8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function workflowRunScript(workflow, stepName) {
|
|
19
|
+
const lines = workflow.split('\n');
|
|
20
|
+
const nameIndex = lines.findIndex((line) => line.trim() === `- name: ${stepName}`);
|
|
21
|
+
assert.notEqual(nameIndex, -1, `workflow step not found: ${stepName}`);
|
|
22
|
+
const runIndex = lines.findIndex(
|
|
23
|
+
(line, index) => index > nameIndex && line.trim() === 'run: |',
|
|
24
|
+
);
|
|
25
|
+
assert.notEqual(runIndex, -1, `run block not found for workflow step: ${stepName}`);
|
|
26
|
+
const indent = lines[runIndex].match(/^\s*/)[0].length + 2;
|
|
27
|
+
const body = [];
|
|
28
|
+
for (const line of lines.slice(runIndex + 1)) {
|
|
29
|
+
if (line.trim() && line.match(/^\s*/)[0].length < indent) break;
|
|
30
|
+
body.push(line.slice(indent));
|
|
31
|
+
}
|
|
32
|
+
return body.join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function git(runCwd, args, options = {}) {
|
|
36
|
+
return exec('git', args, { cwd: runCwd, ...options });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function releaseIntentFixture({
|
|
40
|
+
packageVersion = '1.2.3',
|
|
41
|
+
annotated = true,
|
|
42
|
+
onMain = true,
|
|
43
|
+
mainAhead = false,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const root = await mkdtemp(join(tmpdir(), 'kit-release-intent-'));
|
|
46
|
+
const origin = join(root, 'origin.git');
|
|
47
|
+
const repo = join(root, 'repo');
|
|
48
|
+
await git(root, ['init', '--bare', origin]);
|
|
49
|
+
await git(root, ['init', '--initial-branch=main', repo]);
|
|
50
|
+
await git(repo, ['config', 'user.name', 'Release Test']);
|
|
51
|
+
await git(repo, ['config', 'user.email', 'release-test@example.invalid']);
|
|
52
|
+
await writeFile(join(repo, 'package.json'), `${JSON.stringify({
|
|
53
|
+
name: '@ikon85/agent-workflow-kit',
|
|
54
|
+
version: packageVersion,
|
|
55
|
+
}, null, 2)}\n`);
|
|
56
|
+
await git(repo, ['add', 'package.json']);
|
|
57
|
+
await git(repo, ['commit', '-m', 'release fixture']);
|
|
58
|
+
await git(repo, ['remote', 'add', 'origin', origin]);
|
|
59
|
+
await git(repo, ['push', '-u', 'origin', 'main']);
|
|
60
|
+
if (!onMain) {
|
|
61
|
+
await git(repo, ['checkout', '-b', 'not-main']);
|
|
62
|
+
await writeFile(join(repo, 'outside-main.txt'), 'not canonical\n');
|
|
63
|
+
await git(repo, ['add', 'outside-main.txt']);
|
|
64
|
+
await git(repo, ['commit', '-m', 'outside main']);
|
|
65
|
+
}
|
|
66
|
+
const tag = 'v1.2.3';
|
|
67
|
+
await git(repo, annotated
|
|
68
|
+
? ['tag', '-a', tag, '-m', `Release ${tag}`]
|
|
69
|
+
: ['tag', tag]);
|
|
70
|
+
if (mainAhead) {
|
|
71
|
+
await writeFile(join(repo, 'after-release.txt'), 'main moved\n');
|
|
72
|
+
await git(repo, ['add', 'after-release.txt']);
|
|
73
|
+
await git(repo, ['commit', '-m', 'move canonical main']);
|
|
74
|
+
await git(repo, ['push', 'origin', 'main']);
|
|
75
|
+
}
|
|
76
|
+
return { root, repo, tag };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function validateReleaseIntent(fixture, releaseTag = fixture.tag) {
|
|
80
|
+
const workflow = await releaseWorkflow();
|
|
81
|
+
const script = workflowRunScript(workflow, 'Validate release intent');
|
|
82
|
+
const output = join(fixture.root, 'github-output');
|
|
83
|
+
try {
|
|
84
|
+
const result = await exec('bash', ['-euo', 'pipefail', '-c', script], {
|
|
85
|
+
cwd: fixture.repo,
|
|
86
|
+
env: {
|
|
87
|
+
...process.env,
|
|
88
|
+
GITHUB_OUTPUT: output,
|
|
89
|
+
RELEASE_TAG: releaseTag,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
return { ...result, output: await readFile(output, 'utf8') };
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return { error, stderr: error.stderr ?? '', stdout: error.stdout ?? '' };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
test('release workflow treats an annotated version tag as normal intent and merge as integration only', async () => {
|
|
99
|
+
const workflow = await releaseWorkflow();
|
|
100
|
+
assert.match(workflow, /push:\s*\n\s+tags:\s*\n\s+- ['"]v\*\.\*\.\*['"]/);
|
|
101
|
+
assert.doesNotMatch(workflow, /push:\s*\n\s+branches:\s*\[main\]/);
|
|
102
|
+
assert.match(workflow, /workflow_dispatch:\s*\n\s+inputs:\s*\n\s+tag:/);
|
|
103
|
+
assert.match(workflow, /tag:[\s\S]*?required:\s*true/);
|
|
104
|
+
assert.match(
|
|
105
|
+
workflow,
|
|
106
|
+
/group:\s*release-\$\{\{ github\.event_name == 'workflow_dispatch' && inputs\.tag \|\| github\.ref_name \}\}/,
|
|
107
|
+
);
|
|
108
|
+
assert.doesNotMatch(workflow, /git diff --name-only[\s\S]*package\.json/);
|
|
109
|
+
assert.ok(
|
|
110
|
+
workflow.indexOf('- name: Validate release intent')
|
|
111
|
+
< workflow.indexOf('node scripts/release-state.mjs'),
|
|
112
|
+
'release intent must be validated before the reconciler can publish',
|
|
113
|
+
);
|
|
114
|
+
assert.equal(workflow.match(/node scripts\/release-state\.mjs/g)?.length, 1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('the same pre-publish validator accepts a matching annotated tag', async () => {
|
|
118
|
+
const fixture = await releaseIntentFixture();
|
|
119
|
+
try {
|
|
120
|
+
const result = await validateReleaseIntent(fixture);
|
|
121
|
+
assert.equal(result.error, undefined, result.stderr);
|
|
122
|
+
assert.match(result.output, /^tag=v1\.2\.3$/m);
|
|
123
|
+
assert.match(result.output, /^commit=[0-9a-f]{40}$/m);
|
|
124
|
+
} finally {
|
|
125
|
+
await rm(fixture.root, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
for (const scenario of [
|
|
130
|
+
{
|
|
131
|
+
name: 'missing manual recovery tag',
|
|
132
|
+
options: {},
|
|
133
|
+
tag: 'v9.9.9',
|
|
134
|
+
error: /does not exist/,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'tag/package version mismatch',
|
|
138
|
+
options: { packageVersion: '1.2.4' },
|
|
139
|
+
error: /does not match package version/,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'lightweight tag',
|
|
143
|
+
options: { annotated: false },
|
|
144
|
+
error: /must be annotated/,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'tag outside canonical main ancestry',
|
|
148
|
+
options: { onMain: false },
|
|
149
|
+
error: /not an ancestor of origin\/main/,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'stale canonical main commit with the same package version',
|
|
153
|
+
options: { mainAhead: true },
|
|
154
|
+
error: /does not identify the current origin\/main commit/,
|
|
155
|
+
},
|
|
156
|
+
]) {
|
|
157
|
+
test(`the pre-publish validator rejects a ${scenario.name}`, async () => {
|
|
158
|
+
const fixture = await releaseIntentFixture(scenario.options);
|
|
159
|
+
try {
|
|
160
|
+
const result = await validateReleaseIntent(fixture, scenario.tag);
|
|
161
|
+
assert.ok(result.error, 'validation unexpectedly succeeded');
|
|
162
|
+
assert.match(`${result.stderr}\n${result.stdout}`, scenario.error);
|
|
163
|
+
} finally {
|
|
164
|
+
await rm(fixture.root, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
10
168
|
|
|
11
169
|
test('the install manifest ships both release primitives named by the skill', async () => {
|
|
12
170
|
const manifest = JSON.parse(await readFile(join(REPO, 'agent-workflow-kit.package.json')));
|
|
@@ -20,11 +178,31 @@ test('the install manifest ships both release primitives named by the skill', as
|
|
|
20
178
|
test('both release skill surfaces name only the owned scoped npm package', async () => {
|
|
21
179
|
const claude = await readFile(join(REPO, '.claude/skills/kit-release/SKILL.md'), 'utf8');
|
|
22
180
|
const codex = await readFile(join(REPO, '.agents/skills/kit-release/SKILL.md'), 'utf8');
|
|
181
|
+
assert.equal(codex, claude);
|
|
23
182
|
for (const body of [claude, codex]) {
|
|
24
183
|
assert.match(body, /`@ikon85\/agent-workflow-kit`/);
|
|
25
184
|
}
|
|
26
185
|
});
|
|
27
186
|
|
|
187
|
+
test('maintainer docs and the accepted ADR agree that merge integrates and an annotated tag publishes', async () => {
|
|
188
|
+
const paths = [
|
|
189
|
+
'CLAUDE.md',
|
|
190
|
+
'AGENTS.md',
|
|
191
|
+
'README.md',
|
|
192
|
+
'.claude/skills/kit-release/SKILL.md',
|
|
193
|
+
'.agents/skills/kit-release/SKILL.md',
|
|
194
|
+
'docs/adr/0004-release-intent-is-a-version-tag.md',
|
|
195
|
+
];
|
|
196
|
+
const bodies = await Promise.all(
|
|
197
|
+
paths.map((path) => readFile(join(REPO, path), 'utf8')),
|
|
198
|
+
);
|
|
199
|
+
for (const [index, body] of bodies.entries()) {
|
|
200
|
+
assert.match(body, /annotated\s+[`*]?v<version>[`*]?\s+tag/i, paths[index]);
|
|
201
|
+
assert.match(body, /integrat/i, paths[index]);
|
|
202
|
+
}
|
|
203
|
+
assert.match(bodies.at(-1), /Status: accepted \(2026-07-22, issue #204\)/);
|
|
204
|
+
});
|
|
205
|
+
|
|
28
206
|
test('patch, minor, and major confirmations select exactly one target version', () => {
|
|
29
207
|
assert.equal(nextVersion('1.2.3', 'patch'), '1.2.4');
|
|
30
208
|
assert.equal(nextVersion('1.2.3', 'minor'), '1.3.0');
|