@sabaiway/agent-workflow-kit 14.35.0 → 14.36.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/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/review-lens.md +1 -1
- package/references/modes/agents.md +4 -4
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/recommendations.md +1 -1
- package/references/templates/vehicles.json +5 -1
- package/tools/cheap-agents-read.mjs +25 -5
- package/tools/cheap-agents.mjs +28 -17
- package/tools/recommendations.mjs +16 -10
- package/tools/reference-profile.mjs +2 -2
- package/tools/vehicle-settings.mjs +41 -19
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
|
|
|
4
4
|
is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
|
|
5
5
|
every `migrations/<version>-<slug>.md` newer than it, in semver order.
|
|
6
6
|
|
|
7
|
+
## 14.36.0 — the review lens runs on opus/high by default and its model is a setting; the task tier offer proposes the executor, not codex (AD-188)
|
|
8
|
+
|
|
9
|
+
**The bundled review lens now takes its model and effort from `docs/ai/vehicles.json`, opus and high unless you set
|
|
10
|
+
otherwise, and the agents writer keeps the placed `review-lens.md` in step with that setting.** Before, the lens was
|
|
11
|
+
pinned to sonnet in its bundled file, and a project that wanted another model edited the placed copy by hand.
|
|
12
|
+
|
|
13
|
+
- **One setting.** `docs/ai/vehicles.json` takes an optional `review-lens` section of exactly `model` and `effort`
|
|
14
|
+
(bare tokens, checked like the executor's). Without the section the lens runs opus/high; a malformed section makes
|
|
15
|
+
the file unreadable, and the agents writer stops with `CHEAP_AGENTS_VEHICLES` and places nothing. New projects get
|
|
16
|
+
the section in the seed; an existing file is never rewritten and needs no section.
|
|
17
|
+
- **The placed lens is derived.** `/agent-workflow-kit agents` builds `review-lens.md` from the bundled body with
|
|
18
|
+
exactly its `model:` and `effort:` lines set from the posture, and re-derives a placed copy that differs on
|
|
19
|
+
`--apply` (`would re-derive` in the preview). The report prints
|
|
20
|
+
`review-lens posture: model=… effort=… (source: file|default) — set in docs/ai/vehicles.json`. The cheap-lane
|
|
21
|
+
vehicles and the lenses a review roster derives stay preserved when you customize them.
|
|
22
|
+
- **The upgrade reaches it through the `agents` item.** When the placed lens differs from its derived body, the
|
|
23
|
+
advisor's `agents` item says so beside any vehicles still to place; the agent applies it on your yes after its
|
|
24
|
+
check.
|
|
25
|
+
- **The tier offer proposes `subagent`, then `solo`, for `task.author` and `task.execute`.** `delegated` is no
|
|
26
|
+
longer offered, so a ready codex changes no offered task value. A recorded decline of the offer stays current.
|
|
27
|
+
|
|
28
|
+
**Upgrade note.** Run `/agent-workflow-kit upgrade` and answer yes to the `agents` item: the placed review lens moves
|
|
29
|
+
to opus/high. To keep another posture, set it in the `review-lens` section of `docs/ai/vehicles.json` first, for
|
|
30
|
+
example `"review-lens": { "model": "sonnet", "effort": "high" }`. A task slot an earlier tier offer set to
|
|
31
|
+
`delegated` stays as it is; `/agent-workflow-kit set-recipe --set task.execute=subagent` (and `task.author`) moves it.
|
|
32
|
+
|
|
7
33
|
## 14.35.0 — every advisor item that only said "do it by hand" now names the kit tool the agent runs on your yes, or plainly says it is your own step; a shipped table lists each item's writer (AD-187)
|
|
8
34
|
|
|
9
35
|
**The `HAND-APPLY` marker is gone from the advisor, its mode docs and the README: each item it marked either has a
|
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
262
262
|
| `/agent-workflow-kit worktrees` | any time | **parallel feature worktrees** — run several features in DIFFERENT agent sessions on one repo, zero interference on working-tree files (the ONE exception is the dependency cache, below): `provision <slug> --plan <file>` creates a sibling git worktree on branch `aw/<slug>` and populates it (registry-derived footprint copy-if-missing — a tracked file is never overwritten; EXACTLY ONE seeded feature plan; the `handoff-<slug>.md` record from minute zero; `node_modules` symlinked where the link stays ignored — a shared MUTABLE dependency cache: writes through it hit MAIN's node_modules; for isolation run the printed isolated-install command (`--install` only PRINTS it; on `--resume` run the printed unlink-first recovery first — printed only for a link whose RAW TARGET is MAIN's `node_modules`, since a foreign or unreadable link is reported as such with no removal advised); absolute root-pinned gate commands rebased on untracked copies only, and only while their bytes equal the MAIN source or its rebased form — user-modified copies stay untouched); `list` is read-only (slug, branch, base, dirty, handoff); `prompt <slug>` is read-only too and re-prints the satellite's cold-start prompt — the same text `provision` ends its report with: where the worktree is, its ONE seeded plan, the handoff as the one channel back, MAIN's orientation (shared series index, landing) and, under its own heading because it is probed on the satellite, that checkout's install posture — every value derived LIVE rather than replayed from the frozen provision record, a recorded value that no longer matches NAMED as a divergence (with the cause its source makes likely) instead of printed as the runnable one, every offered command marked with WHO runs it (`MAIN $ …` for the landing, `HERE $ …` for this checkout's own install, a posture with nothing to run staying prose), and a control character in any rendered value a typed STOP rather than a forged prompt line; `land <slug> --prepare` locks the common git dir, fail-closes on divergence or incomplete satellite state, transfers the complete accepted satellite diff onto a CLEAN main, runs sync plus the declared gates, and reports HEAD/TRANSFER/PREPARED OIDs — the commit ALWAYS stays a dialogue ask; `cleanup <slug>` takes the same lock and removes a LANDED worktree only after live landed-verification against main HEAD, while `--abandon` is the ONE destructive arm (destroys unlanded work; **no preview step** on any writer). The parent dir is the `docs/ai/worktrees.json` `parentDir` setting (default: the repo's sibling parent); an unwritable parent prints the `worktrees-dir` check line: the agent applies the grant on your chat yes after the check, one entry in this copy's `.claude/settings.local.json`, never the committed project settings (`init` is the route you may take instead, and the ack is the fallback for a host that ignores the key), else the printed terminal command runs it. Never commits, never pushes, never runs a subscription CLI. |
|
|
263
263
|
| `/agent-workflow-kit uninstall` | opt-in, any time | **guarded teardown** — the inverse of `init` / `setup`. Removes only what's **provably ours** (managed skill dirs + bridge wrappers; in a project, the hidden-mode git-ignore block it added + the pre-commit hook it installed); **never deletes** your `docs/ai` / `AGENTS.md` (prints the exact `rm` to run by hand) or your `.claude/settings.json` (prints an **edit** — remove the attribution key, review any velocity `permissions.*` — never an `rm`). Always `--dry-run` first; preflight-then-mutate; never commits. |
|
|
264
264
|
| `/agent-workflow-kit velocity` | Claude Code · opt-in | **onboarding velocity profile** — seeds a fixed, audited **read-only** allowlist into `.claude/settings.json` so routine read-only commands stop idling on approval prompts while you're away; every allowlist apply (flagless, `--kit-tools`, `--bridge-tier`) also seeds one `permissions.ask` rule, so all work can stay in auto mode: without it auto mode refuses a harness apply (the `apply:` line of each harness item the upgrade advisor renders) and you must leave auto mode for it; with it the harness shows its usual dialog — the rule allows nothing; opt-in `acceptEdits`; plus a **read-only advisory** of likely project gate commands to add by hand. Writes `.claude/settings.json`; a fourth opt-in, `--worktrees`, writes the worktrees grant into the local settings, `.claude/settings.local.json`, alone — **never** allowlists commit/push/publish, never commits. A seeded entry is a **trust posture, not a sandbox** (a runtime residual remains at the settings level — its guard ships as the opt-in `hook` command); a direct commit/push/publish still asks. `--dry-run` first. |
|
|
265
|
-
| `/agent-workflow-kit agents` | Claude Code · opt-in | **subagent vehicles** — places the five bundled subagent definitions into `.claude/agents/`: **four read-only vehicles + one full-tool `executor`**. **No read-only vehicle gets `Bash`**, and that is the load-bearing property: a read-only fan-out on a full-tool subagent shells out for facts it could have read, and every shelled command is an approval prompt you never needed to see. Three of the four ride a **cheap model** (haiku, low effort) for mechanical work — extraction sweeps, changelog fact-skeletons, gate-failure triage; the fourth, **`review-lens`**, is an ADDITIONAL independent read-only review opinion on code your configured backends have already seen (never a replacement for your plan or diff review recipe, advisory like every review; an epic is reviewed by the lens alone, whatever `epic.review` resolves to). The fifth, **`executor`** (all tools; model and effort from `docs/ai/vehicles.json`), is the ONE vehicle with a shell and the instrument behind the **Subagent** recipe: dispatched only for a bounded, file-disjoint execution / authoring / routine slice you verify afterwards — never for read-only work, never as a review backend, and it never commits. Its state (`placed` / `customized` / `unusable` / `missing`) is what a `subagent` slot's readiness resolves from. Writing code and running gates stay off the four read-only vehicles; the executor carries a bounded slice you re-verify on your main lane, and every vehicle's output is verified. Preview by default (`--apply` writes); an existing customized read-only vehicle or lens is **preserved, never overwritten**; the executor's body
|
|
265
|
+
| `/agent-workflow-kit agents` | Claude Code · opt-in | **subagent vehicles** — places the five bundled subagent definitions into `.claude/agents/`: **four read-only vehicles + one full-tool `executor`**. **No read-only vehicle gets `Bash`**, and that is the load-bearing property: a read-only fan-out on a full-tool subagent shells out for facts it could have read, and every shelled command is an approval prompt you never needed to see. Three of the four ride a **cheap model** (haiku, low effort) for mechanical work — extraction sweeps, changelog fact-skeletons, gate-failure triage; the fourth, **`review-lens`**, is an ADDITIONAL independent read-only review opinion on code your configured backends have already seen (never a replacement for your plan or diff review recipe, advisory like every review; an epic is reviewed by the lens alone, whatever `epic.review` resolves to). The fifth, **`executor`** (all tools; model and effort from `docs/ai/vehicles.json`), is the ONE vehicle with a shell and the instrument behind the **Subagent** recipe: dispatched only for a bounded, file-disjoint execution / authoring / routine slice you verify afterwards — never for read-only work, never as a review backend, and it never commits. Its state (`placed` / `customized` / `unusable` / `missing`) is what a `subagent` slot's readiness resolves from. Writing code and running gates stay off the four read-only vehicles; the executor carries a bounded slice you re-verify on your main lane, and every vehicle's output is verified. Preview by default (`--apply` writes); an existing customized read-only vehicle or roster-derived lens is **preserved, never overwritten**; the executor's body and the bundled `review-lens`'s (opus / high by default, set in the file's optional `review-lens` section) are re-derived from `docs/ai/vehicles.json` (the earlier body is replaced); never touches `settings*.json`, never commits. |
|
|
266
266
|
| `/agent-workflow-kit hook` | Claude Code · opt-in | **gate-approval hook** — places a self-contained PreToolUse hook (`.claude/hooks/`) and wires it into `.claude/settings.json`: a Bash command **byte-identical** to a gate you declared in `docs/ai/gates.json` (run from the project root) is auto-approved — no prompt, no idle; a seeded read-only command carrying a runtime residual (output redirection, command substitution, `--output` writes) now **asks** even where an allow rule would have silently passed it (proven live — hook `ask` overrides an allow rule). Detection is a conservative string scan, so it over-asks on a byte that is ordinary text — searching for `=>`, or a read wearing `2>/dev/null`, prompts. That is a documented open limit: 4.1.0 built three mechanisms to narrow it and removed all three in review, each counterexample kept as a test (see Mode: hook). Exact matches only, never patterns; never `deny`; a broken `gates.json` only disables auto-approval, never the guard. An opt-in **read-lane** (`--read-lane` → `docs/ai/lanes.json`, a **separate** file from `gates.json`) additionally auto-approves *compounds* of seeded read-only commands carrying no shell metaprogramming; `--apply --read-lane` verifies the placed hook is current first; a stale or diverged placed hook is replaced by the reseed route (`--apply --reseed`, the bundled bytes in place). Preview by default; never writes `settings.local.json`; never commits. |
|
|
267
267
|
| `/agent-workflow-kit state-block-guard` | Claude Code · opt-in | **closing-block detector** — a `Stop` hook that reads the turn's final assistant message and warns when the closing state block is defective: the «what I need from you» slot answering *nothing* (a turn that ENDS always needs a resume, so the answer is false, not merely unhelpful), or a first-person promise of imminent work in a turn that is over. Judged on the slot's first clause, so a real ask followed by a clause break and "nothing else" passes, and a promise gated on something named passes; matching is word-bounded and Unicode-aware, quoted and fenced examples are stripped, and the last STARTED block decides — an incomplete tail counts as no block rather than falling back to an earlier one. A message carrying no block at all is silent unless you pass `--require-block` — this kit does not mandate the block, and a hook that runs every turn must not warn every turn. The judgement is lexical and the mode doc names every residual it leaves. **Detection, never prevention** — a `Stop` hook cannot un-send the message it judges; what it buys is that a silent recurrence becomes a visible one. Warns via `systemMessage` and **exits 0 on every path**: never `deny`, never blocks the stop, never re-enters the model. Reads nothing but the message, makes no network call, approves nothing. Its writer is `tools/state-block-hook.mjs`: it places the self-contained hook and merges its `Stop` entry into `.claude/settings.json` by the three merge cases, keeping every foreign hook — the agent applies it on the chat yes after the danger check. |
|
|
268
268
|
| `/agent-workflow-kit bridge-settings` | opt-in, any time | **host-level bridge settings** — read or change the bridges' `KEY=VALUE` config file (`${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf`), the **one place a knob survives kit upgrades** (it lives outside every kit tree; a refresh never touches it, and now states loudly if it ever overwrote a local edit). The **model each bridge runs**: `CODEX_MODEL` + `CODEX_EFFORT` (default `gpt-6.1-sol` at `high`) and `AGY_MODEL` (default `Gemini 3.8 Flash (High)`) — the read-out shows the models each installed CLI offers, and a set one is checked against that list before anything is written. Other knobs: the **codex Fast tier** (`CODEX_SERVICE_TIER=priority` — "2x speed, **increased usage**" in the catalog's own words, quality-neutral, default off — a consented spend), the codex/agy hard timeouts, the codex review byte cap, and `AGY_REVIEW_MAX_TOTAL_BYTES` (the ceiling on what an oversized agy code review may feed before it refuses pre-spend). `AGY_REVIEW_ALLOW_ADDDIR` is a **retired** compatibility key: still recognized so an existing line never warns as unknown, but it arms nothing — the writer refuses to set it and `--unset` clears it. Allowed keys + value rules come from the bundled bridge manifests. **Previews by default**; refuses an unknown key, an out-of-range value, a model or effort the catalog does not offer, or a duplicate-carrying file; writes atomically (symlink/TOCTOU-safe). Never commits, never runs a model (only the read-only catalog commands). |
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: agent-workflow-kit
|
|
|
3
3
|
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`. A typed decision with a confidence from Jev? Run `/agent-workflow-kit jev`.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '14.
|
|
6
|
+
version: '14.36.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.36.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review-lens
|
|
3
3
|
description: Read-only ADDITIONAL review lens — an independent opinion additional to the orchestrator's own review and to whatever configured backends ran; never a substitute for either. Grants no shell, so it can never turn a review into a wave of approval prompts. Never for writing code or running gates.
|
|
4
|
-
model:
|
|
4
|
+
model: opus
|
|
5
5
|
effort: high
|
|
6
6
|
tools: Read, Grep, Glob
|
|
7
7
|
---
|
|
@@ -8,18 +8,18 @@ The opt-in **subagent-vehicle writer** — the family's second `.claude/` writer
|
|
|
8
8
|
**Every READ-ONLY vehicle grants READ-ONLY tools and NO `Bash`** — that is the load-bearing property, not a detail. A read-only fan-out on a full-tool vehicle shells out for facts it could have read, and each shelled command is an approval prompt the maintainer never needed to see; a vehicle with no shell structurally cannot do that. Two lanes ride on it:
|
|
9
9
|
|
|
10
10
|
- **cheap lane** (`model: haiku`, `effort: low`) — `mechanical-sweep`, `changelog-skeleton`, `gate-triage`: extraction sweeps, changelog fact-skeletons, gate-failure triage. Extraction/drafting only; the orchestrator applies judgment and verifies the output.
|
|
11
|
-
- **review lens** (`review-lens`,
|
|
11
|
+
- **review lens** (`review-lens`, `opus` / `high` unless `docs/ai/vehicles.json` sets another posture) — an ADDITIONAL independent read-only opinion on code the configured review backends have already seen. It exists because a third lens otherwise has **no vehicle at all**: the cheap vehicles are scoped away from judgment, and a review-capable full-tool subagent is the prompt-flood shape. It never replaces the configured review recipe of a plan or a diff — an epic's rendered brief is reviewed by the lens alone, whatever `epic.review` resolves to — and it is advisory like every other review.
|
|
12
12
|
|
|
13
13
|
Writing code, running gates, and user-facing copy never move to these four vehicles.
|
|
14
14
|
|
|
15
15
|
**The fifth vehicle, `executor`, is the ONE full-tool one** (model and effort from `docs/ai/vehicles.json` — `opus` / `high` by default — and every tool, a shell included). It is the instrument behind the **Subagent** recipe, dispatched ONLY for a **bounded, file-disjoint slice** the orchestrator verifies afterwards by running its suites: a slice of execution (`plan-execution.execute`), a plan/contract authoring brief (`plan-authoring.author`), an authoring-round fold (`plan-authoring.fold`), a bounded chore (`routine.carrier`), an epic brief (`epic.author`), or one prompt the orchestrator wrote from one ledger row, run inside its Files (`task.execute`); `task.author` dispatches no slice, since the orchestrator writes each row's prompt itself. It **never commits** (no git write at all), is **never a review backend**, is **never a bridge substitute**, and is **never dispatched for read-only work** — a sweep, an inventory or a review rides a read-only vehicle instead.
|
|
16
16
|
|
|
17
|
-
**Its readiness is the vehicle FILE.** `surveyExecutorVehicle` answers exactly one of four states: **`placed`** (bytes equal to the body derived from `docs/ai/vehicles.json` at `.claude/agents/executor.md`), **`customized`** (a readable regular file whose frontmatter names `executor` and whose tools are not read-only), **`unusable`** (a symlink, a non-regular or unreadable file, one failing that check, or an unreadable `docs/ai/vehicles.json` — with the reason), **`missing`**. `placed` and `customized` are dispatchable; on `unusable` or `missing` a `subagent` slot **degrades to Solo** with a reason naming this mode's apply command (and `docs/ai/vehicles.json` when that file is unreadable). `/agent-workflow-kit status` shows the state in its `agents` block and the deployment advisor raises it when a configured slot names `subagent` and the vehicle is `unusable` or `missing` — that item is **`executor-vehicle`** (attention-class: a configured declaration that is broken), a DIFFERENT capability from this mode's placement offer (`agents`), which converges the moment nothing is left to place and therefore can never observe a present-but-unusable vehicle. It is a claim about the FILE, never about the host: a Claude Code lane, like this mode itself.
|
|
17
|
+
**Its readiness is the vehicle FILE.** `surveyExecutorVehicle` answers exactly one of four states: **`placed`** (bytes equal to the body derived from `docs/ai/vehicles.json` at `.claude/agents/executor.md`), **`customized`** (a readable regular file whose frontmatter names `executor` and whose tools are not read-only), **`unusable`** (a symlink, a non-regular or unreadable file, one failing that check, or an unreadable `docs/ai/vehicles.json` — with the reason), **`missing`**. `placed` and `customized` are dispatchable; on `unusable` or `missing` a `subagent` slot **degrades to Solo** with a reason naming this mode's apply command (and `docs/ai/vehicles.json` when that file is unreadable). `/agent-workflow-kit status` shows the state in its `agents` block and the deployment advisor raises it when a configured slot names `subagent` and the vehicle is `unusable` or `missing` — that item is **`executor-vehicle`** (attention-class: a configured declaration that is broken), a DIFFERENT capability from this mode's placement offer (`agents`), which converges the moment nothing is left to place and the bundled review lens matches its derived body, and therefore can never observe a present-but-unusable vehicle. It is a claim about the FILE, never about the host: a Claude Code lane, like this mode itself.
|
|
18
18
|
|
|
19
19
|
Run `node ${CLAUDE_SKILL_DIR}/tools/cheap-agents.mjs [--dry-run | --apply] [--cwd <dir>]`:
|
|
20
20
|
|
|
21
|
-
1. **`--dry-run` first, always** (the default — changes nothing). It previews, per bundled vehicle, whether it **would place** the file, finds it **already current**, or finds a **customized** file (different content) that will be **preserved, never overwritten** (delete the file to reseed it from the bundle) — except the executor
|
|
21
|
+
1. **`--dry-run` first, always** (the default — changes nothing). It previews, per bundled vehicle, whether it **would place** the file, finds it **already current**, or finds a **customized** file (different content) that will be **preserved, never overwritten** (delete the file to reseed it from the bundle) — except the executor and the bundled `review-lens`, whose differing bytes **would re-derive** from `docs/ai/vehicles.json` and are rewritten on `--apply` (the earlier body is replaced, never preserved). The lens takes its model and effort from that file's optional `review-lens` section — `opus` / `high` by default — and the report prints its posture line beside the executor's.
|
|
22
22
|
2. **Only on an explicit yes**, re-run with `--apply`. It writes **only** under `.claude/agents/` — never `settings.json` / `settings.local.json`, never a commit. `--apply` is deployment-gated (the stamp must be at the lineage head) and symlink-safe (a symlinked project root / `.claude` / `.claude/agents` / target file is a STOP, in a `--dry-run` too).
|
|
23
23
|
3. **Hidden-mode deployments:** after apply, run the hide-footprint reconcile (`node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --reconcile`) so the placed files stay invisible to `git status` — `/.claude/agents/` is in the known-footprint registry; the apply report reminds you.
|
|
24
24
|
|
|
25
|
-
**Invariants:** writer (writes only `.claude/agents/`) · preview by default · a diverged existing file is reported and preserved, never clobbered — except the executor, re-derived from `docs/ai/vehicles.json` on apply · never touches settings · never commits · **no READ-ONLY vehicle grants `Bash`, and exactly one bundled vehicle, `executor`, grants a shell** · the cheap-lane vehicles are pinned to `model: haiku` + `effort: low`, and the review lens is pinned OFF the cheap model (all content-tested).
|
|
25
|
+
**Invariants:** writer (writes only `.claude/agents/`) · preview by default · a diverged existing file is reported and preserved, never clobbered — except the executor and the review lens, re-derived from `docs/ai/vehicles.json` on apply · never touches settings · never commits · **no READ-ONLY vehicle grants `Bash`, and exactly one bundled vehicle, `executor`, grants a shell** · the cheap-lane vehicles are pinned to `model: haiku` + `effort: low`, and the bundled review lens file is pinned OFF the cheap model (`opus` / `high`), its placed copy taking the posture `docs/ai/vehicles.json` sets — `opus` / `high` when unset (all content-tested).
|
|
@@ -23,7 +23,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
23
23
|
3. **Choose conversational language — answered in the step-2 batch.** Which language should the agent *talk to them* in — questions, explanations, summaries, status updates? Offer the language they're already writing in as the default. Carry the answer into the `{{COMM_LANGUAGE}}` slot of the *Communication language* block when `AGENTS.md` is created (step 5). See [Communication contract](${CLAUDE_SKILL_DIR}/references/contracts.md#communication-contract). This sets the **dialogue** language only — never the files.
|
|
24
24
|
4. **Choose agent attribution — answered in the step-2 batch.** May the agent attribute work to itself / to AI — `Co-Authored-By` trailers, "Generated with …" footers, "AI"/agent/model mentions in code, comments, commit messages, PR titles/bodies, or docs? **Default to `off`** (no agent/AI mention anywhere) unless they opt in — people are routinely surprised to find an AI listed as a repo contributor. Carry the answer into the `{{AGENT_ATTRIBUTION}}` slot of the *Attribution* block when `AGENTS.md` is created (step 5). **If `off`**, the advisor's `attribution` item of step 11 sets the harness setting in the project's `.claude/settings.json`, applied on the user's yes after its check — the trailer is added by the harness, so a doc directive alone won't stop it. See [Attribution contract](${CLAUDE_SKILL_DIR}/references/contracts.md#attribution-contract).
|
|
25
25
|
5. **Entry-point doc.** If `AGENTS.md` / `CLAUDE.md` already exist (step-1 recon), do **not** overwrite — show the user and ask whether to merge or replace. Otherwise create `AGENTS.md` (the cross-agent standard — Codex / Cursor / Devin Desktop / Copilot read it natively) from `${CLAUDE_SKILL_DIR}/references/templates/AGENTS.md`, and symlink `CLAUDE.md -> AGENTS.md` (`ln -s AGENTS.md CLAUDE.md`) for Claude Code — single source, no duplication. For nested context, add a subdir `AGENTS.md` (+ a `CLAUDE.md` symlink beside it for Claude Code).
|
|
26
|
-
6. **Deploy `docs/ai/`.** Create every `docs/ai/` file + `pages/` + the seed `adr/` store + the seed `specs/` store from `${CLAUDE_SKILL_DIR}/references/templates/` (the template loop deploys every template except `AGENTS.md`, `adr-record.md` and `SPEC_TEMPLATE.md` — the latter two are skill-home authoring references, never deployed). This deploys the HOT ADR window **`docs/ai/decisions.md`** and the seed navigator **`docs/ai/adr/log.md`** (its `adr/` subdir copies in wholesale), the spec store root **`docs/ai/specs/index.md`** (its `specs/` subdir copies in wholesale), the `.md` docs, **and** the four seeded, user-editable strict-JSON configs: **`docs/ai/orchestration.json`** (the per-project recipe defaults the `procedures` advisor reads), **`docs/ai/gates.json`** (the project's gate declaration — seeded as the no-consent EMPTY list; FILLING it is the consented `gates-init` preview, `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), **`docs/ai/autonomy.json`** (the per-project autonomy policy — seeded SPARSE with the onboarding note only, so it is exactly the computed defaults until the user declares levels via `/agent-workflow-kit set-autonomy` or by hand), and **`docs/ai/vehicles.json`** (the executor vehicle's model, effort and fallback as a setting the kit's readers resolve into one posture; the placed executor's body follows it — seeded once, hand-edited, never clobbered). Keep each `.md` file's frontmatter (`type / lastUpdated / scope / staleAfter / owner / maxLines`); the `.json` seeds carry no frontmatter (the docs cap-validator globs `*.md` only, so they are inherently skipped).
|
|
26
|
+
6. **Deploy `docs/ai/`.** Create every `docs/ai/` file + `pages/` + the seed `adr/` store + the seed `specs/` store from `${CLAUDE_SKILL_DIR}/references/templates/` (the template loop deploys every template except `AGENTS.md`, `adr-record.md` and `SPEC_TEMPLATE.md` — the latter two are skill-home authoring references, never deployed). This deploys the HOT ADR window **`docs/ai/decisions.md`** and the seed navigator **`docs/ai/adr/log.md`** (its `adr/` subdir copies in wholesale), the spec store root **`docs/ai/specs/index.md`** (its `specs/` subdir copies in wholesale), the `.md` docs, **and** the four seeded, user-editable strict-JSON configs: **`docs/ai/orchestration.json`** (the per-project recipe defaults the `procedures` advisor reads), **`docs/ai/gates.json`** (the project's gate declaration — seeded as the no-consent EMPTY list; FILLING it is the consented `gates-init` preview, `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), **`docs/ai/autonomy.json`** (the per-project autonomy policy — seeded SPARSE with the onboarding note only, so it is exactly the computed defaults until the user declares levels via `/agent-workflow-kit set-autonomy` or by hand), and **`docs/ai/vehicles.json`** (the executor vehicle's model, effort and fallback as a setting the kit's readers resolve into one posture; the placed executor's body follows it, and the bundled review lens's model and effort, `opus` / `high` by default, follow its optional `review-lens` section — seeded once, hand-edited, never clobbered). Keep each `.md` file's frontmatter (`type / lastUpdated / scope / staleAfter / owner / maxLines`); the `.json` seeds carry no frontmatter (the docs cap-validator globs `*.md` only, so they are inherently skipped).
|
|
27
27
|
7. **Fill templates** per the table below. **Then materialize the navigator, as the last `docs/ai` step:** the entry point declares `docs/ai/index.md` always-loaded and it is a GENERATED artifact with no template, so run `node ${CLAUDE_SKILL_DIR}/references/scripts/check-docs-size.mjs --ensure-index --root=<project>` and relay its one outcome line (`ensure-index: regenerated` / `already-current`, or a `write-refused` / `probe-failed` line naming the offending path — either is a STOP: resolve it before reporting the deploy, since the navigator the entry point declares does not exist yet). The generator runs from **this skill's own directory**, so a project with **no Node runtime** is covered too: the agent host runs it, and step 8's copy is not a precondition.
|
|
28
28
|
8. **Offer enforcement on every project, preview-first.** Explain in one line that the dependency-free Node scripts are available because the agent host already runs their step-7 navigator; the only skip condition is that the agent host cannot run them. Run the `enforcement` item's check line, `node ${CLAUDE_SKILL_DIR}/tools/apply-danger-check.mjs --variant enforcement --cwd <project>`: its result is the preview — each script the kit places into the project's `scripts/` with its bytes and the installer it runs with its bytes. Ask once on that result; on a yes copy them by its apply line with that digest, run by the consent route of `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`: the copy is create-only and the project's installer installs the pre-commit hook. On a no, skip the hook and say so; a check that refuses (exit 1 or 3) or an apply that exits 6 leaves no script the kit placed — say so with its output and fill the command slots as on a no. On a no the docs command slots name no project-local command — fill `env_commands.md` and the `Docs caps + index freshness` row of `AGENTS.md` with the same note: the caps + index gate run from the agent's own skill copy (the step-7 `--root=<project>` form) and the archive rotations need the scripts deployed. Never write the skill's absolute path into a deployed doc. They self-configure (project name from `package.json` or, without one, the directory basename; hierarchical/on-demand sections auto-discovered). The hook calls `node scripts/<x>.mjs` directly, so a committer without `node` on PATH gets the hook's own loud failure.
|
|
29
29
|
9. **Wire / hide** per visibility (see [Visibility contract](${CLAUDE_SKILL_DIR}/references/contracts.md#visibility-contract)). The pre-commit hook is the installer's decision at step 8's apply; over a pre-commit hook the installer did not write, that apply exits 6 with the installer's output naming the hook, and moving it aside is the user's own step. Once the gate declaration is final-run-capable, the advisor's `commit-guard` item binds commits to the green `run-gates --final` receipt; it is applied the same way, its check line first.
|
|
@@ -22,7 +22,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
22
22
|
|
|
23
23
|
The **consent route** for a harness item: run its `check:` line in the sandbox and show its result. On the check's `masked:` line, say so and run that same check line once more outside the sandbox, so it re-judges in the apply's own environment; the yes is taken on the outside result. Take the user's yes on that result and run its `apply:` line with that result's digest once, outside the sandbox at once, so the harness asks at the point of action (with the apply ask rule seeded, its own dialog); never another line, never unseen. When velocity-core, kit-tools-tier or bridge-tier is among the user's picks (velocity-core preferred), check, confirm and apply it, or leave it pending, before any other pick's check runs, so its apply seeds the ask rule before the other harness applies. When the harness's auto mode refuses the check or the apply, say so and give the one plain step: leave auto mode for this one action (Shift+Tab in the terminal, the mode indicator in an editor) and say yes, or run `npx @sabaiway/agent-workflow-kit@latest init` in a terminal of your own. On the user's word, run the same line once more when they left auto mode, never another, never unseen; when they ran `init` instead, run the advisor again and offer only what is still pending, each through this route from its check (after a restart `init` asked for, the next session's upgrade does it). When that refusal hits the velocity-core, kit-tools-tier or bridge-tier pick taken first, run no other pick's check until the user answers the plain step. A check that refuses (exit 1) leaves the item unapplied and pending, its result shown; a changed digest (exit 5) applies nothing, shows the new result and asks a new yes. A no, a harness refusal and any other failure of a harness apply leave the item pending with its output shown, and the agent continues. `init`, run in a console from the project's folder, is the route the user may take instead.
|
|
24
24
|
|
|
25
|
-
**The `executor-vehicle` item (the carrier the project already CONFIGURED — beside the `agents` offer, never the same item).** It fires on ONE pair of facts: a slot in `docs/ai/orchestration.json` names `subagent` while the executor vehicle (`.claude/agents/executor.md`) is `missing` or `unusable` — so every such slot silently degrades to solo. That is a configured declaration that is broken, hence **`attention`**, not an offer; its text names the state, its reason and how many configured slots are affected across the six activities (`plan-authoring`, `plan-execution`, `routine`, `feedback-triage`, `epic`, `task`), covering the subagent-capable slots `plan-authoring.author`, `plan-authoring.fold`, `plan-execution.execute`, `routine.carrier`, `epic.author`, `task.author` and `task.execute`. **`placed` renders NOTHING** (a working vehicle is not a recommendation); a differing executor body is the optional `executor-vehicle.differing` arm, re-derived on the chat yes after the check, a config naming no `subagent` slot renders no base item, and a config the validated reader refuses is a stated **skip**, never an item computed over a shape nothing accepted. It is deliberately DISTINCT from its two neighbours, neither of which can observe this state: the `agents` offer converges the moment nothing is left to PLACE — which a present-but-unusable executor also satisfies — and the review-recipe probe skips a slot configured `subagent` by construction. The agent applies it on the chat yes after the check; `init` previews the agents writer's own `cheap-agents.mjs` and applies it with `--apply` on the terminal yes as a route the user may take instead. Its `recipe:` line carries each precondition the advisor observes beside the apply slot, followed by the hidden-mode reconcile follow-up (`hide-footprint --reconcile`), for a hidden deployment only. Honest limit on convergence: the apply places an ABSENT vehicle, so it clears `missing`; it STOPs on a symlink, so on `unusable` the `recipe:` line names each refusal as a precondition; an executor whose frontmatter is invalid is re-derived by the apply itself. Risk profile: writes under `.claude/agents/` only — the executor's
|
|
25
|
+
**The `executor-vehicle` item (the carrier the project already CONFIGURED — beside the `agents` offer, never the same item).** It fires on ONE pair of facts: a slot in `docs/ai/orchestration.json` names `subagent` while the executor vehicle (`.claude/agents/executor.md`) is `missing` or `unusable` — so every such slot silently degrades to solo. That is a configured declaration that is broken, hence **`attention`**, not an offer; its text names the state, its reason and how many configured slots are affected across the six activities (`plan-authoring`, `plan-execution`, `routine`, `feedback-triage`, `epic`, `task`), covering the subagent-capable slots `plan-authoring.author`, `plan-authoring.fold`, `plan-execution.execute`, `routine.carrier`, `epic.author`, `task.author` and `task.execute`. **`placed` renders NOTHING** (a working vehicle is not a recommendation); a differing executor body is the optional `executor-vehicle.differing` arm, re-derived on the chat yes after the check, a config naming no `subagent` slot renders no base item, and a config the validated reader refuses is a stated **skip**, never an item computed over a shape nothing accepted. It is deliberately DISTINCT from its two neighbours, neither of which can observe this state: the `agents` offer converges the moment nothing is left to PLACE and the review lens matches its derived body — which a present-but-unusable executor also satisfies — and the review-recipe probe skips a slot configured `subagent` by construction. The agent applies it on the chat yes after the check; `init` previews the agents writer's own `cheap-agents.mjs` and applies it with `--apply` on the terminal yes as a route the user may take instead. Its `recipe:` line carries each precondition the advisor observes beside the apply slot, followed by the hidden-mode reconcile follow-up (`hide-footprint --reconcile`), for a hidden deployment only. Honest limit on convergence: the apply places an ABSENT vehicle, so it clears `missing`; it STOPs on a symlink, so on `unusable` the `recipe:` line names each refusal as a precondition; an executor whose frontmatter is invalid is re-derived by the apply itself. Risk profile: writes under `.claude/agents/` only — the executor's and the review lens's bodies re-derived from `docs/ai/vehicles.json` (the lens opus/high by default), every other vehicle create-only — never `settings.json`, never a commit.
|
|
26
26
|
|
|
27
27
|
**Per-item posture notes (the consent moment — surface BEFORE running or handing over the apply):**
|
|
28
28
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_README": "The executor vehicle's model, effort and fallback as a project setting, read by the kit's vehicle-settings readers into one posture, which the placed executor's body follows: the agents writer derives it from model and effort, the upgrade reports a differing body as pending, and fallback names the model of the one retry; seeded once and never clobbered, hand-edited as three bare tokens with fallback differing from model.",
|
|
2
|
+
"_README": "The executor vehicle's model, effort and fallback as a project setting, read by the kit's vehicle-settings readers into one posture, which the placed executor's body follows: the agents writer derives it from model and effort, the upgrade reports a differing body as pending, and fallback names the model of the one retry; seeded once and never clobbered, hand-edited as three bare tokens with fallback differing from model. The optional review-lens section holds the bundled review lens's model and effort, opus and high by default: the agents writer derives the placed review-lens.md from them and re-derives a differing copy.",
|
|
3
3
|
"executor": {
|
|
4
4
|
"model": "opus",
|
|
5
5
|
"effort": "high",
|
|
6
6
|
"fallback": "sonnet"
|
|
7
|
+
},
|
|
8
|
+
"review-lens": {
|
|
9
|
+
"model": "opus",
|
|
10
|
+
"effort": "high"
|
|
7
11
|
}
|
|
8
12
|
}
|
|
@@ -8,7 +8,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { refuseDirectRun } from './direct-run.mjs';
|
|
10
10
|
import { deriveLensTemplate } from './review-roster-resolve.mjs';
|
|
11
|
-
import { EXECUTOR_DEFAULTS, VEHICLES_REL, readVehicles, resolveExecutor } from './vehicle-settings.mjs';
|
|
11
|
+
import { EXECUTOR_DEFAULTS, VEHICLES_REL, readVehicles, resolveExecutor, resolveReviewLens } from './vehicle-settings.mjs';
|
|
12
12
|
|
|
13
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
|
|
@@ -70,6 +70,12 @@ export const readBundledAgents = (deps = {}) => {
|
|
|
70
70
|
return templates;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
const bundledTemplate = (name, deps) => {
|
|
74
|
+
const bundled = readBundledAgents(deps).find((item) => item.name === name);
|
|
75
|
+
if (!bundled) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${name} is missing from the bundle`);
|
|
76
|
+
return bundled;
|
|
77
|
+
};
|
|
78
|
+
|
|
73
79
|
// ── the preflight reads (velocity discipline: symlink-safe, stamp read, no writes) ────
|
|
74
80
|
|
|
75
81
|
export const readStamp = (absPath, fs) => {
|
|
@@ -130,11 +136,26 @@ export const deriveExecutorBody = (bundledContent, posture) =>
|
|
|
130
136
|
deriveLensTemplate(bundledContent, executorVehicleSpec(posture));
|
|
131
137
|
|
|
132
138
|
export const executorTemplate = (posture, deps = {}) => {
|
|
133
|
-
const bundled =
|
|
134
|
-
if (!bundled) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${EXECUTOR_VEHICLE} is missing from the bundle`);
|
|
139
|
+
const bundled = bundledTemplate(EXECUTOR_VEHICLE, deps);
|
|
135
140
|
return { name: EXECUTOR_VEHICLE, content: deriveExecutorBody(bundled.content, posture), rederive: true };
|
|
136
141
|
};
|
|
137
142
|
|
|
143
|
+
// ── the bundled review lens, derived from its posture like the executor ───────────────
|
|
144
|
+
|
|
145
|
+
export const REVIEW_LENS_VEHICLE = 'review-lens.md';
|
|
146
|
+
export const REVIEW_LENS_VEHICLE_REL = `${AGENTS_DIR}/${REVIEW_LENS_VEHICLE}`;
|
|
147
|
+
|
|
148
|
+
export const readReviewLensPosture = (projectDir, deps = {}) => {
|
|
149
|
+
const fs = readFsDeps(deps);
|
|
150
|
+
return resolveReviewLens(readVehicles(projectDir, fs.readFile, fs.lstat));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const reviewLensTemplate = (posture, deps = {}) => {
|
|
154
|
+
const bundled = bundledTemplate(REVIEW_LENS_VEHICLE, deps);
|
|
155
|
+
const spec = { stem: 'review-lens', model: posture.model, effort: posture.effort, derived: true };
|
|
156
|
+
return { name: REVIEW_LENS_VEHICLE, content: deriveLensTemplate(bundled.content, spec), rederive: true };
|
|
157
|
+
};
|
|
158
|
+
|
|
138
159
|
// The executor ensure's cell 6: a regular placed executor whose bytes differ from the body derived from
|
|
139
160
|
// the settings; the placed file's read failure throws.
|
|
140
161
|
export const executorBodyDiffers = (projectDir, deps = {}) => {
|
|
@@ -220,8 +241,7 @@ const lensFrontmatter = (frontmatter, stem) => {
|
|
|
220
241
|
|
|
221
242
|
const templateFor = (spec, deps) => {
|
|
222
243
|
if (spec.template == null) return null;
|
|
223
|
-
const bundled =
|
|
224
|
-
if (!bundled) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${spec.template}.md is missing from the bundle`);
|
|
244
|
+
const bundled = bundledTemplate(`${spec.template}.md`, deps);
|
|
225
245
|
return {
|
|
226
246
|
name: `${spec.stem}.md`,
|
|
227
247
|
content: spec.derived ? deriveLensTemplate(bundled.content, spec) : bundled.content,
|
package/tools/cheap-agents.mjs
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// STOPs both, since the derived lenses it names cannot be known);
|
|
15
15
|
// • symlink-safe — a symlinked project root, `.claude`, `.claude/agents` or target file is a
|
|
16
16
|
// STOP, in a dry-run too, never a write-through;
|
|
17
|
-
// • preserves differing .claude/agents/ files (`customized — preserved`), except the executor
|
|
18
|
-
// on apply
|
|
17
|
+
// • preserves differing .claude/agents/ files (`customized — preserved`), except the executor
|
|
18
|
+
// and the bundled review lens: on apply each body is re-derived from docs/ai/vehicles.json
|
|
19
|
+
// (the lens from its review-lens section, opus and high by default), replacing the earlier body;
|
|
19
20
|
// an identical file is `already current` (idempotent re-run);
|
|
20
21
|
// • writes ONLY under .claude/agents/ — never settings.json / settings.local.json, never
|
|
21
22
|
// commits.
|
|
@@ -38,7 +39,7 @@ import { shellQuoteArg } from './repo-lex.mjs';
|
|
|
38
39
|
import { loadConfig } from './orchestration-config.mjs';
|
|
39
40
|
import { lensMembersOf } from './review-roster.mjs';
|
|
40
41
|
import { deriveLensTemplate, lensVehicleSpec } from './review-roster-resolve.mjs';
|
|
41
|
-
import { loadVehicles, resolveExecutor } from './vehicle-settings.mjs';
|
|
42
|
+
import { loadVehicles, resolveExecutor, resolveReviewLens } from './vehicle-settings.mjs';
|
|
42
43
|
import { safeLine } from './carriers.mjs';
|
|
43
44
|
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
44
45
|
// The READ core, never a second copy: the bundle, the placement plan and the executor survey live
|
|
@@ -55,7 +56,9 @@ import {
|
|
|
55
56
|
CHEAP_AGENTS_CONFIG,
|
|
56
57
|
CHEAP_AGENTS_VEHICLES,
|
|
57
58
|
EXECUTOR_VEHICLE,
|
|
59
|
+
REVIEW_LENS_VEHICLE,
|
|
58
60
|
executorTemplate,
|
|
61
|
+
reviewLensTemplate,
|
|
59
62
|
makeCheapAgentsError,
|
|
60
63
|
readFsDeps,
|
|
61
64
|
readBundledAgents,
|
|
@@ -100,15 +103,16 @@ const USAGE = `usage: cheap-agents [--dry-run | --apply] [--cwd <dir>] [--help]
|
|
|
100
103
|
|
|
101
104
|
Places the bundled subagent definitions into the project's ${AGENTS_DIR}/. Four vehicles grant NO
|
|
102
105
|
shell: three ride a cheap model (haiku/low) for mechanical work — extraction sweeps, changelog
|
|
103
|
-
fact-skeletons, gate triage — and review-lens is a read-only REVIEW vehicle
|
|
104
|
-
|
|
106
|
+
fact-skeletons, gate triage — and review-lens is a read-only REVIEW vehicle, opus/high unless
|
|
107
|
+
docs/ai/vehicles.json sets another posture. The fifth, executor, is the ONE full-tool vehicle: dispatched only for a bounded execution,
|
|
105
108
|
authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and
|
|
106
109
|
it never commits.
|
|
107
110
|
Default is --dry-run (a preview; writes nothing). --apply writes.
|
|
108
111
|
Configured derived review lenses are planned and placed beside the bundled vehicles.
|
|
109
|
-
An existing file with DIFFERENT content is preserved and reported, never overwritten, except the executor
|
|
110
|
-
The executor's model and effort come from docs/ai/vehicles.json,
|
|
111
|
-
from
|
|
112
|
+
An existing file with DIFFERENT content is preserved and reported, never overwritten, except the executor
|
|
113
|
+
and the review lens. The executor's model and effort come from docs/ai/vehicles.json, the review lens's
|
|
114
|
+
from its review-lens section there (opus and high by default), and a placed executor or review-lens.md
|
|
115
|
+
whose bytes differ from its derived body is re-derived on --apply (the earlier body is replaced, never preserved).`;
|
|
112
116
|
|
|
113
117
|
export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
114
118
|
|
|
@@ -151,11 +155,12 @@ const loadConfigOrStop = (cwd, deps) => {
|
|
|
151
155
|
}
|
|
152
156
|
};
|
|
153
157
|
|
|
154
|
-
const
|
|
158
|
+
const loadPosturesOrStop = (cwd, deps) => {
|
|
155
159
|
try {
|
|
156
|
-
|
|
160
|
+
const answer = loadVehicles(cwd, deps.readFile, deps.lstat);
|
|
161
|
+
return { posture: resolveExecutor(answer).posture, lensPosture: resolveReviewLens(answer).posture };
|
|
157
162
|
} catch (err) {
|
|
158
|
-
throw makeCheapAgentsError(CHEAP_AGENTS_VEHICLES, `${err?.message ?? err} — the agents writer cannot derive the executor
|
|
163
|
+
throw makeCheapAgentsError(CHEAP_AGENTS_VEHICLES, `${err?.message ?? err} — the agents writer cannot derive the executor and review-lens vehicles — nothing is placed`);
|
|
159
164
|
}
|
|
160
165
|
};
|
|
161
166
|
|
|
@@ -182,10 +187,12 @@ export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
|
182
187
|
const fs = writeFsDeps(deps);
|
|
183
188
|
const projectDir = cwd ?? process.cwd();
|
|
184
189
|
refuseSymlinkedRoot(projectDir, deps);
|
|
185
|
-
const posture =
|
|
186
|
-
const derived = [
|
|
190
|
+
const { posture, lensPosture } = loadPosturesOrStop(projectDir, deps);
|
|
191
|
+
const derived = [
|
|
192
|
+
executorTemplate(posture, deps), reviewLensTemplate(lensPosture, deps), ...configuredDerivedTemplates(projectDir, deps),
|
|
193
|
+
];
|
|
187
194
|
const preflight = preflightCheapAgents({ cwd: projectDir, derived }, deps);
|
|
188
|
-
if (dryRun) return { wrote: false, dryRun: true, posture, ...preflight };
|
|
195
|
+
if (dryRun) return { wrote: false, dryRun: true, posture, lensPosture, ...preflight };
|
|
189
196
|
|
|
190
197
|
if (!preflight.stampOk) {
|
|
191
198
|
throw makeCheapAgentsError(
|
|
@@ -202,7 +209,7 @@ export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
|
202
209
|
});
|
|
203
210
|
} else fs.writeFile(item.abs, item.content, UTF8);
|
|
204
211
|
}
|
|
205
|
-
return { wrote: toWrite.length > 0, dryRun: false, posture, ...preflight };
|
|
212
|
+
return { wrote: toWrite.length > 0, dryRun: false, posture, lensPosture, ...preflight };
|
|
206
213
|
};
|
|
207
214
|
|
|
208
215
|
// ── report ────────────────────────────────────────────────────────────────────────────
|
|
@@ -213,9 +220,12 @@ const ACTION_LABEL = {
|
|
|
213
220
|
're-derive': 're-derived — a hand edit was replaced; set the model in docs/ai/vehicles.json',
|
|
214
221
|
'customized-preserved': 'customized — preserved (delete the file to reseed from the bundle)',
|
|
215
222
|
};
|
|
223
|
+
const LENS_REDERIVE_LABEL = 're-derived — the earlier body was replaced; set its model and effort in docs/ai/vehicles.json';
|
|
224
|
+
const labelOf = (item) =>
|
|
225
|
+
(item.action === 're-derive' && item.name === REVIEW_LENS_VEHICLE ? LENS_REDERIVE_LABEL : ACTION_LABEL[item.action]);
|
|
216
226
|
|
|
217
227
|
export const formatResult = (result) => {
|
|
218
|
-
const { posture } = result;
|
|
228
|
+
const { posture, lensPosture } = result;
|
|
219
229
|
const lines = [
|
|
220
230
|
result.dryRun
|
|
221
231
|
? 'agent-workflow subagent vehicles — DRY RUN (no changes)'
|
|
@@ -223,7 +233,7 @@ export const formatResult = (result) => {
|
|
|
223
233
|
];
|
|
224
234
|
for (const item of result.plan) {
|
|
225
235
|
const verb = result.dryRun && item.action === 'place' ? 'would place'
|
|
226
|
-
: result.dryRun && item.action === 're-derive' ? 'would re-derive' :
|
|
236
|
+
: result.dryRun && item.action === 're-derive' ? 'would re-derive' : labelOf(item);
|
|
227
237
|
lines.push(` - ${item.rel}: ${verb}`);
|
|
228
238
|
}
|
|
229
239
|
if (!result.stampOk) {
|
|
@@ -235,6 +245,7 @@ export const formatResult = (result) => {
|
|
|
235
245
|
`three of those ride the cheap lane (model: haiku, effort: low) for mechanical work; ${FALLBACK_LENS_ADDITIONAL_ONLY}`,
|
|
236
246
|
'executor is the one FULL-TOOL vehicle: dispatched only for a bounded execution, authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and it never commits.',
|
|
237
247
|
`executor posture: model=${safeLine(posture.model)} effort=${safeLine(posture.effort)} fallback=${safeLine(posture.fallback)} (source: ${safeLine(posture.source)}) — set in docs/ai/vehicles.json`,
|
|
248
|
+
`review-lens posture: model=${safeLine(lensPosture.model)} effort=${safeLine(lensPosture.effort)} (source: ${safeLine(lensPosture.source)}) — set in docs/ai/vehicles.json`,
|
|
238
249
|
);
|
|
239
250
|
// A preview must print the EXACT command that applies it. The advisor renders this dry-run as an
|
|
240
251
|
// item's one-liner, and that flow's contract is "run the printed command, no improvisation" — a
|
|
@@ -87,7 +87,8 @@ import { ATTRIBUTION_KEY, LEGACY_ATTRIBUTION_KEY, attributionHidesText, effectiv
|
|
|
87
87
|
import { planGlobalRemoval } from './hide-footprint.mjs';
|
|
88
88
|
import { NODE_EVIDENCE, NODE_EVIDENCE_SCRIPTS, probeNodeEvidence } from './node-evidence.mjs';
|
|
89
89
|
// The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
|
|
90
|
-
import { surveyExecutorVehicle, readExecutorPosture, executorBodyDiffers, executorTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
|
|
90
|
+
import { surveyExecutorVehicle, readExecutorPosture, executorBodyDiffers, executorTemplate, reviewLensTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION, REVIEW_LENS_VEHICLE } from './cheap-agents-read.mjs';
|
|
91
|
+
import { readVehicles, resolveExecutor, resolveReviewLens } from './vehicle-settings.mjs';
|
|
91
92
|
// The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
|
|
92
93
|
// (contract: kit/ack-store) — `status` reads the same store, and a second copy is what drifts.
|
|
93
94
|
import {
|
|
@@ -291,6 +292,7 @@ export const WHATS = Object.freeze({
|
|
|
291
292
|
'mcp-channel': "the kit's read-only MCP server is not registered here — path questions and literal searches stay shell strings",
|
|
292
293
|
'mcp-channel.differing': 'an "{server}" MCP entry is already declared here and DIFFERS from the registration this kit copy would write',
|
|
293
294
|
agents: '{n} bundled subagent vehicle(s) not placed (Claude Code) — {ro} read-only, {ex} the full-tool executor; the apply PREVIEWS first',
|
|
295
|
+
'agents.lens-rederive': 'the placed review-lens.md differs from its body derived from docs/ai/vehicles.json; {n} vehicle(s) to place ({ro} read-only, {ex} executor)',
|
|
294
296
|
'executor-vehicle': '{n} slot(s) configured subagent but the executor vehicle is {state}{reason} — every such slot runs solo until it is usable',
|
|
295
297
|
'executor-vehicle.differing': 'the placed executor vehicle differs from the body derived from docs/ai/vehicles.json — a hand edit stands until it is re-derived',
|
|
296
298
|
attribution: 'AGENTS.md says attribution off but .claude/settings.json does not hide the harness trailer and PR text',
|
|
@@ -431,8 +433,8 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
431
433
|
{ id: 'delegated-execution', mode: 'set-recipe', advisorKey: 'review-recipe' },
|
|
432
434
|
{ id: 'agents', mode: 'agents', advisorKey: 'agents' },
|
|
433
435
|
// A DISTINCT capability from the offer above: the offer converges the moment nothing is left to
|
|
434
|
-
// PLACE, which a customized-but-unusable executor
|
|
435
|
-
// state that makes a configured subagent carrier run solo.
|
|
436
|
+
// PLACE and the review lens matches its derived body, which a customized-but-unusable executor
|
|
437
|
+
// also satisfies — so it can never observe the state that makes a configured subagent carrier run solo.
|
|
436
438
|
{ id: 'executor-vehicle', mode: 'agents', advisorKey: 'executor-vehicle' },
|
|
437
439
|
// Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
|
|
438
440
|
// a TRUST-POSTURE change. Velocity presents its full posture at its own consent moment;
|
|
@@ -1081,16 +1083,19 @@ const probeStateBlockHook = ({ root, deps, add, skip }) => {
|
|
|
1081
1083
|
// surfaced its absence: `agents` is the family's SECOND `.claude/` writer, the `help` Tune tail
|
|
1082
1084
|
// advertises it, and the advisor had no entry for it — so a user who never runs `help` never learned
|
|
1083
1085
|
// it existed while the advisor reported the deployment optimal. Found by the guard, not by an incident.
|
|
1084
|
-
//
|
|
1086
|
+
// A PLACE action and the bundled review lens's `re-derive` (its placed body differs from the one derived from docs/ai/vehicles.json) count as gaps: `already-current` is converged; a customized read-only vehicle or derived lens is
|
|
1085
1087
|
// the user's own edit the writer never clobbers and the advisor never nags about; the executor is not preserved but re-derived from docs/ai/vehicles.json (its `re-derive` answer is not a gap of this offer).
|
|
1086
1088
|
const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
1087
1089
|
try {
|
|
1088
|
-
const
|
|
1090
|
+
const fs = readFsDeps(deps);
|
|
1091
|
+
const answer = readVehicles(root, fs.readFile, fs.lstat);
|
|
1092
|
+
const { posture, reason } = resolveExecutor(answer);
|
|
1089
1093
|
if (posture === null) {
|
|
1090
1094
|
skip('agents', new Error(`${reason} — fix that file first`));
|
|
1091
1095
|
return;
|
|
1092
1096
|
}
|
|
1093
|
-
const
|
|
1097
|
+
const derived = [executorTemplate(posture, deps), reviewLensTemplate(resolveReviewLens(answer).posture, deps)];
|
|
1098
|
+
const preflight = preflightCheapAgents({ cwd: root, derived }, deps);
|
|
1094
1099
|
// The writer refuses below the expected lineage, so offering its command would hand the user a
|
|
1095
1100
|
// guaranteed failure — the honest surface is a stated skip naming the recovery.
|
|
1096
1101
|
if (!preflight.stampOk) {
|
|
@@ -1098,7 +1103,8 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
1098
1103
|
return;
|
|
1099
1104
|
}
|
|
1100
1105
|
const toPlace = preflight.plan.filter((item) => item.action === 'place');
|
|
1101
|
-
|
|
1106
|
+
const lensRederive = preflight.plan.some((item) => item.name === REVIEW_LENS_VEHICLE && item.action === 're-derive');
|
|
1107
|
+
if (toPlace.length === 0 && !lensRederive) return; // converged
|
|
1102
1108
|
// The writer's contract is «--dry-run first, ALWAYS» (references/modes/agents.md, and its stated
|
|
1103
1109
|
// invariant «preview by default»), so the rendered line is the PREVIEW — this item joins the
|
|
1104
1110
|
// dry-run-preview class the mode doc already documents for the gates-declaration seeder, where the
|
|
@@ -1107,7 +1113,7 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
1107
1113
|
// The hidden-mode reconcile rides the detail, never the apply line: it is wrong to run on a
|
|
1108
1114
|
// VISIBLE deployment, and the apply slot must stay one pure executable command.
|
|
1109
1115
|
const executors = toPlace.filter((item) => item.name === EXECUTOR_VEHICLE).length;
|
|
1110
|
-
add('agents', fillTemplate(WHATS.agents, { n: toPlace.length, ex: executors, ro: toPlace.length - executors }),
|
|
1116
|
+
add('agents', fillTemplate(WHATS[lensRederive ? 'agents.lens-rederive' : 'agents'], { n: toPlace.length, ex: executors, ro: toPlace.length - executors }),
|
|
1111
1117
|
`node ${q(toolPath('cheap-agents.mjs'))} --cwd ${q(root)}`, 'agents',
|
|
1112
1118
|
`hidden-mode deployments only: after the --apply the preview prints, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
|
|
1113
1119
|
);
|
|
@@ -1118,8 +1124,8 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
1118
1124
|
|
|
1119
1125
|
// The subagent carrier's ONE instrument (contract: kit/carriers). Neither neighbour can report this
|
|
1120
1126
|
// state: `probeReviewRecipe` skips a slot configured `subagent` by construction, and the offer above
|
|
1121
|
-
// judges only what is left to PLACE — a customized-but-broken
|
|
1122
|
-
// project could declare the carrier and have every such slot run solo, unseen.
|
|
1127
|
+
// judges only what is left to PLACE and the bundled lens's re-derive — a customized-but-broken executor
|
|
1128
|
+
// gives it neither. So a project could declare the carrier and have every such slot run solo, unseen.
|
|
1123
1129
|
const VEHICLE_BROKEN_STATES = Object.freeze(['missing', 'unusable']);
|
|
1124
1130
|
|
|
1125
1131
|
const writerBlock = (root, deps) => {
|
|
@@ -22,8 +22,8 @@ const target = (activity, slot, candidates) => Object.freeze({ activity, slot, c
|
|
|
22
22
|
export const TIER_TARGETS = Object.freeze([
|
|
23
23
|
target('epic', 'author', ['subagent', 'solo']),
|
|
24
24
|
target('epic', 'review', ['reviewed', 'solo']),
|
|
25
|
-
target('task', 'author', ['
|
|
26
|
-
target('task', 'execute', ['
|
|
25
|
+
target('task', 'author', ['subagent', 'solo']),
|
|
26
|
+
target('task', 'execute', ['subagent', 'solo']),
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
29
|
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -3,11 +3,14 @@ import { join } from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
export const VEHICLES_REL = 'docs/ai/vehicles.json';
|
|
5
5
|
export const EXECUTOR_DEFAULTS = Object.freeze({ model: 'opus', effort: 'high', fallback: 'sonnet' });
|
|
6
|
+
export const REVIEW_LENS_DEFAULTS = Object.freeze({ model: 'opus', effort: 'high' });
|
|
6
7
|
|
|
7
8
|
const README_KEY = '_README';
|
|
8
9
|
const EXECUTOR_KEY = 'executor';
|
|
9
|
-
const
|
|
10
|
+
const LENS_KEY = 'review-lens';
|
|
11
|
+
const TOP_LEVEL_KEYS = Object.freeze([README_KEY, EXECUTOR_KEY, LENS_KEY]);
|
|
10
12
|
const EXECUTOR_KEYS = Object.freeze(Object.keys(EXECUTOR_DEFAULTS));
|
|
13
|
+
const LENS_KEYS = Object.freeze(Object.keys(REVIEW_LENS_DEFAULTS));
|
|
11
14
|
const OBJECT_TYPE = 'object';
|
|
12
15
|
const STRING_TYPE = 'string';
|
|
13
16
|
const EMPTY_STRING = '';
|
|
@@ -44,6 +47,25 @@ const unreadable = (reason) => ({ state: UNREADABLE, reason });
|
|
|
44
47
|
const describeError = (error) => String(error?.code ?? error?.message ?? error);
|
|
45
48
|
const formatReadError = (error) => `${VEHICLES_REL}: ${UNREADABLE} (${describeError(error)})`;
|
|
46
49
|
|
|
50
|
+
const validateSection = (value, rel, name, keys) => {
|
|
51
|
+
const section = value[name];
|
|
52
|
+
if (!isPlainObject(section)) return refuseValue(rel, name, OBJECT_REQUIRED);
|
|
53
|
+
for (const key of Object.keys(section)) {
|
|
54
|
+
if (!keys.includes(key)) return refuseValue(rel, `${name}.${key}`, UNKNOWN_KEY);
|
|
55
|
+
}
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const qualifiedKey = `${name}.${key}`;
|
|
58
|
+
if (!Object.hasOwn(section, key)) return refuseValue(rel, qualifiedKey, REQUIRED_KEY);
|
|
59
|
+
const token = section[key];
|
|
60
|
+
if (typeof token !== STRING_TYPE) return refuseValue(rel, qualifiedKey, STRING_REQUIRED);
|
|
61
|
+
if (token === EMPTY_STRING) return refuseValue(rel, qualifiedKey, NONEMPTY_REQUIRED);
|
|
62
|
+
if (!TOKEN_PATTERN.test(token) || token.trim() !== token) {
|
|
63
|
+
return refuseValue(rel, qualifiedKey, TOKEN_REQUIRED);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, values: Object.fromEntries(keys.map((key) => [key, section[key]])) };
|
|
67
|
+
};
|
|
68
|
+
|
|
47
69
|
export const validateVehicles = (value, rel = VEHICLES_REL) => {
|
|
48
70
|
if (!isPlainObject(value)) return refuseValue(rel, BODY_NAME, OBJECT_REQUIRED);
|
|
49
71
|
for (const key of Object.keys(value)) {
|
|
@@ -53,26 +75,15 @@ export const validateVehicles = (value, rel = VEHICLES_REL) => {
|
|
|
53
75
|
return refuseValue(rel, README_KEY, STRING_REQUIRED);
|
|
54
76
|
}
|
|
55
77
|
if (!Object.hasOwn(value, EXECUTOR_KEY)) return refuseValue(rel, EXECUTOR_KEY, REQUIRED_KEY);
|
|
56
|
-
const executor = value
|
|
57
|
-
if (!
|
|
58
|
-
|
|
59
|
-
if (!EXECUTOR_KEYS.includes(key)) return refuseValue(rel, `${EXECUTOR_KEY}.${key}`, UNKNOWN_KEY);
|
|
60
|
-
}
|
|
61
|
-
for (const key of EXECUTOR_KEYS) {
|
|
62
|
-
const qualifiedKey = `${EXECUTOR_KEY}.${key}`;
|
|
63
|
-
if (!Object.hasOwn(executor, key)) return refuseValue(rel, qualifiedKey, REQUIRED_KEY);
|
|
64
|
-
const token = executor[key];
|
|
65
|
-
if (typeof token !== STRING_TYPE) return refuseValue(rel, qualifiedKey, STRING_REQUIRED);
|
|
66
|
-
if (token === EMPTY_STRING) return refuseValue(rel, qualifiedKey, NONEMPTY_REQUIRED);
|
|
67
|
-
if (!TOKEN_PATTERN.test(token) || token.trim() !== token) {
|
|
68
|
-
return refuseValue(rel, qualifiedKey, TOKEN_REQUIRED);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (executor.fallback === executor.model) {
|
|
78
|
+
const executor = validateSection(value, rel, EXECUTOR_KEY, EXECUTOR_KEYS);
|
|
79
|
+
if (!executor.ok) return executor;
|
|
80
|
+
if (executor.values.fallback === executor.values.model) {
|
|
72
81
|
return refuseValue(rel, EXECUTOR_KEY, DISTINCT_FALLBACK_REQUIRED);
|
|
73
82
|
}
|
|
74
|
-
const {
|
|
75
|
-
return { ok: true, settings
|
|
83
|
+
const settings = { executor: executor.values };
|
|
84
|
+
if (!Object.hasOwn(value, LENS_KEY)) return { ok: true, settings };
|
|
85
|
+
const lens = validateSection(value, rel, LENS_KEY, LENS_KEYS);
|
|
86
|
+
return lens.ok ? { ok: true, settings: { ...settings, [LENS_KEY]: lens.values } } : lens;
|
|
76
87
|
};
|
|
77
88
|
|
|
78
89
|
const probeEntry = (path, lstat) => {
|
|
@@ -125,3 +136,14 @@ export const resolveExecutor = (answer) => {
|
|
|
125
136
|
if (answer?.state === UNREADABLE) return { posture: null, reason: answer.reason };
|
|
126
137
|
throw fail(CONFIG_EXIT_CODE, `${VEHICLES_REL}: ${UNKNOWN_STATE} ${String(answer?.state)}`);
|
|
127
138
|
};
|
|
139
|
+
|
|
140
|
+
export const resolveReviewLens = (answer) => {
|
|
141
|
+
if (answer?.state === PRESENT && Object.hasOwn(answer.settings, LENS_KEY)) {
|
|
142
|
+
return { posture: { ...answer.settings[LENS_KEY], source: FILE_SOURCE }, reason: null };
|
|
143
|
+
}
|
|
144
|
+
if (answer?.state === PRESENT || answer?.state === ABSENT) {
|
|
145
|
+
return { posture: { ...REVIEW_LENS_DEFAULTS, source: DEFAULT_SOURCE }, reason: null };
|
|
146
|
+
}
|
|
147
|
+
if (answer?.state === UNREADABLE) return { posture: null, reason: answer.reason };
|
|
148
|
+
throw fail(CONFIG_EXIT_CODE, `${VEHICLES_REL}: ${UNKNOWN_STATE} ${String(answer?.state)}`);
|
|
149
|
+
};
|