@sabaiway/agent-workflow-kit 13.2.0 → 14.0.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 +101 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +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/modes/upgrade.md +7 -5
- package/references/templates/vehicles.json +8 -0
- package/tools/cheap-agents-read.mjs +37 -3
- package/tools/cheap-agents.mjs +44 -13
- package/tools/ensure-configs.mjs +16 -11
- package/tools/ensure-executor.mjs +87 -0
- package/tools/ensure-ops.mjs +16 -8
- package/tools/ensure-vocabulary.mjs +10 -3
- package/tools/recommendations.mjs +15 -9
- package/tools/upgrade-runlist.mjs +2 -0
- package/tools/vehicle-settings.mjs +127 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,107 @@ 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.0.0 — the placed executor's body is derived from `docs/ai/vehicles.json`: the agents writer and a new eighth config ensure, `executor`, re-derive it instead of preserving a hand edit, the readiness survey reports an unreadable setting as an unusable vehicle, and the advisor reads the posture (AD-143)
|
|
8
|
+
|
|
9
|
+
**MAJOR: the never-clobber rule for the placed executor is withdrawn.** Until 13.3.0 the agents writer reported a
|
|
10
|
+
`.claude/agents/executor.md` whose bytes differed from the bundle as `customized — preserved` and never touched it;
|
|
11
|
+
from 14.0.0 its body is DERIVED from the executor section of `docs/ai/vehicles.json`, and a differing file is
|
|
12
|
+
REWRITTEN — by `cheap-agents --apply` and by the consent-free configs step of every upgrade run. A model you set by
|
|
13
|
+
editing the placed file is replaced; set it in `docs/ai/vehicles.json` instead (`model`, `effort`, `fallback`), and
|
|
14
|
+
every later upgrade keeps it. No migration file is owed and the lineage stamp does not move. Every other vehicle —
|
|
15
|
+
the four read-only ones and the derived review lenses — keeps `customized — preserved`. Story S2 of the epic
|
|
16
|
+
`EXECUTOR-VEHICLE-PINS-ONE-MODEL-AND-DIES-ON-ITS-QUOTA`; the contract is `docs/ai/specs/kit/executor-vehicle.md`
|
|
17
|
+
(live, revision 1, S1–S7 bound).
|
|
18
|
+
|
|
19
|
+
- **The derived body (`tools/cheap-agents-read.mjs`, 268 lines).** `readExecutorPosture(cwd, deps)` resolves the
|
|
20
|
+
setting through the never-throwing reader into `{ posture, reason }`; `executorVehicleSpec`, `deriveExecutorBody`
|
|
21
|
+
and `executorTemplate` build the body as the bundled `references/agents/executor.md` with exactly its `model:` and
|
|
22
|
+
`effort:` lines replaced — the same `deriveLensTemplate` substitution the review lenses use, never a second copy.
|
|
23
|
+
With the setting absent the derived body IS the bundled bytes; `fallback` never appears in a placed file.
|
|
24
|
+
- **The readiness survey reads the setting.** `surveyExecutorVehicle` runs the `.claude` and `.claude/agents` STOPs,
|
|
25
|
+
then the setting, then the bytes: `placed` now means bytes equal to the DERIVED body, and an unreadable
|
|
26
|
+
`docs/ai/vehicles.json` answers `unusable` with the reader's reason and `rel` naming that file — never `missing`,
|
|
27
|
+
`customized` or a healthy default, so `status`, the advisor and the recipe resolver name the file to fix.
|
|
28
|
+
- **The agents writer (`tools/cheap-agents.mjs`, 278 lines).** A differing executor takes the new action
|
|
29
|
+
`re-derive`: `--apply` replaces it through the shared contained atomic write (a temp sibling and a rename, so a
|
|
30
|
+
hard-linked file elsewhere is never written through) and reports `re-derived — a hand edit was replaced; set the
|
|
31
|
+
model in docs/ai/vehicles.json`; the preview says `would re-derive`. An unreadable setting STOPs the whole run by the
|
|
32
|
+
new code `CHEAP_AGENTS_VEHICLES`, exit 1, nothing placed, in a dry-run and an apply alike. The report gains one line,
|
|
33
|
+
`executor posture: model=… effort=… fallback=… (source: file|default)`.
|
|
34
|
+
- **The eighth config ensure, `executor`, fifth in the fixed order after `vehicles` (`tools/ensure-executor.mjs`, new,
|
|
35
|
+
87 lines).** Composed into the op table by `tools/ensure-configs.mjs`, which now runs and documents eight. It never
|
|
36
|
+
places the vehicle: no entry at the path is `not-placed` (checked BEFORE the setting is read, so a project without a
|
|
37
|
+
placed executor is never failed by a broken setting); bytes equal to the derived body are `already-current`;
|
|
38
|
+
differing bytes are `re-derived` through the contained atomic write, `would-re-derive` under `--dry-run`. A failure
|
|
39
|
+
opens with one cause: `wrong-node-kind` (a symlinked or non-directory ancestor, a non-regular node at the path), the
|
|
40
|
+
NEW `vehicle-settings-unreadable`, `bundle-unreadable` or `write-refused`; any other lstat or read error reaches the
|
|
41
|
+
catch-all `unexpected-error`, and the write is the last step. `tools/ensure-vocabulary.mjs` gains the op, the
|
|
42
|
+
`re-derived` / `would-re-derive` pair, `not-placed` and the cause; `tools/upgrade-runlist.mjs`'s copy of the relayed
|
|
43
|
+
tokens gains `re-derived` and `not-placed`.
|
|
44
|
+
- **The advisor (`tools/recommendations.mjs`).** The `agents` offer reads the posture and is a stated skip with the
|
|
45
|
+
reader's reason on an unreadable setting; the `executor-vehicle` item's `HAND-APPLY:` precondition names the setting
|
|
46
|
+
and each writer refusal the advisor observes.
|
|
47
|
+
- **The docs.** `references/modes/upgrade.md` counts eight ensures and relays the tokens and cause;
|
|
48
|
+
`references/modes/agents.md`, `bootstrap.md`, `recommendations.md` and the README's agents row name the executor
|
|
49
|
+
exception; the seed `references/templates/vehicles.json` says the placed body follows the setting and `fallback`
|
|
50
|
+
names the model of the one retry (a present settings file keeps its earlier note, create-only).
|
|
51
|
+
- **Tests.** Two new suites, `tools/cheap-agents-executor.test.mjs` (S1–S4) and `tools/ensure-executor.test.mjs` (S5,
|
|
52
|
+
one case per state-table cell); `ensure-configs.test.mjs` pins eight ops with `executor` fifth (S6);
|
|
53
|
+
`package-content.test.mjs` names `tools/ensure-executor.mjs` and pins 312 packed files.
|
|
54
|
+
|
|
55
|
+
## 13.3.0 — the executor vehicle's model becomes a project setting: `vehicle-settings.mjs` reads `docs/ai/vehicles.json` through two readers over one three-state table and resolves it to one posture, the seed `vehicles.json` ships, and the config ensure gains a seventh op, `vehicles`, fourth in the fixed order (AD-142)
|
|
56
|
+
|
|
57
|
+
**MINOR: one new tool, one new seed template, one new ensure op; no existing verb, token, cause or order of the six
|
|
58
|
+
prior ops moves.** The placed executor vehicle carried one model hard-wired in the bundled template — a project that
|
|
59
|
+
wanted another edited the placed file, which the placer then reported as a customization it never refreshed, and
|
|
60
|
+
when that model's quota ran out the slice died with it. This release ships the setting and its readers (story S1 of
|
|
61
|
+
the epic `EXECUTOR-VEHICLE-PINS-ONE-MODEL-AND-DIES-ON-ITS-QUOTA`); deriving the placed body from it and retrying a
|
|
62
|
+
refused slice on the fallback are the epic's second story. The contract is `docs/ai/specs/kit/vehicle-settings.md`
|
|
63
|
+
(live, revision 1, S1–S5 bound).
|
|
64
|
+
|
|
65
|
+
- **`tools/vehicle-settings.mjs` (127 lines, imports only `node:fs` and `node:path`).** The setting is ONE
|
|
66
|
+
strict-JSON file, `docs/ai/vehicles.json`, beside `orchestration.json` and `autonomy.json` — never a key of
|
|
67
|
+
`orchestration.json`. Its shape is closed: an optional `_README` string, then exactly one `executor` section of
|
|
68
|
+
exactly three non-empty bare-token string keys `model`, `effort` and `fallback` (`fallback` differing from `model`,
|
|
69
|
+
since an equal fallback retries into the same quota); every other form is MALFORMED, the reason naming the rel
|
|
70
|
+
path and the offending key or value. `readVehicles(cwd, readFile, lstat)` probes with lstat FIRST and never throws:
|
|
71
|
+
ENOENT alone answers `{ state: 'absent' }`; a directory, a symlink dangling or not, any other node kind, a
|
|
72
|
+
permission or read error, a parse error and a malformed body answer `{ state: 'unreadable', reason }` — the door
|
|
73
|
+
the read-only surfaces walk through; a well-formed regular file answers `{ state: 'present', settings }`.
|
|
74
|
+
`loadVehicles` is the same table with `unreadable` raised as the module-local `fail(1, reason)` (the
|
|
75
|
+
orchestration-config idiom, defined locally as every sibling reader does, so the module stays a leaf the read-only
|
|
76
|
+
surfaces can import). `resolveExecutor(answer)` is pure and answers `{ posture, reason }`: `present` the file's
|
|
77
|
+
three values with `source: 'file'`; `absent` the ONE frozen `EXECUTOR_DEFAULTS` — `model: 'opus'`, `effort: 'high'`,
|
|
78
|
+
`fallback: 'sonnet'` — with `source: 'default'`; `unreadable` a `null` posture and the reader's reason, so a broken
|
|
79
|
+
file is never mistaken for a healthy default. Exactly six exports: `VEHICLES_REL`, `EXECUTOR_DEFAULTS`,
|
|
80
|
+
`validateVehicles`, `readVehicles`, `loadVehicles`, `resolveExecutor`; no side effects on import.
|
|
81
|
+
- **`references/templates/vehicles.json` (new).** The seed: a `_README` sentence, then `executor` at the three
|
|
82
|
+
defaults; the suite pins its parsed `executor` equal to `EXECUTOR_DEFAULTS`, so no second hand-copy exists.
|
|
83
|
+
- **The seventh config ensure, `vehicles`, fourth in the fixed order after `autonomy`.** `ENSURE_OPS` in
|
|
84
|
+
`tools/ensure-vocabulary.mjs` reads `orchestration, gates, autonomy, vehicles, scripts, specs, index`;
|
|
85
|
+
`ensureVehicles` in `tools/ensure-ops.mjs` rides the existing `seedFromTemplate` door — create-only from the
|
|
86
|
+
bundled template, a present file preserved byte for byte and never read (a malformed present file is preserved,
|
|
87
|
+
not repaired: the readers are what name it), `wrong-node-kind` loud, `would-seed` under `--dry-run`, the outcome
|
|
88
|
+
tokens the seed family already had. `tools/ensure-configs.mjs` composes the table as before and says seven; the
|
|
89
|
+
op counts and banner ordinals inside `ensure-ops.mjs` were removed rather than renumbered, so the order lives in
|
|
90
|
+
`ENSURE_OPS` alone.
|
|
91
|
+
- **The mode docs count what runs.** `references/modes/upgrade.md`'s `configs` step performs all seven ensures and
|
|
92
|
+
names the vehicles declaration after the autonomy declaration with the shared `seeded` / `already-present` tokens
|
|
93
|
+
(both report clauses say seven); `references/modes/bootstrap.md` step 6 names the fourth seeded strict-JSON
|
|
94
|
+
config, `docs/ai/vehicles.json`, which the template loop now deploys on a fresh kit-path deployment. A deployment
|
|
95
|
+
on the memory package's own templates gets the file at its first upgrade run's ensure.
|
|
96
|
+
- **Tests.** `tools/vehicle-settings.test.mjs` (315 lines): one case per scenario S1–S5, the module and the ensure op
|
|
97
|
+
imported dynamically with throwing fallbacks so the suite loaded red on the tree before either existed; S3 walks
|
|
98
|
+
the whole malformed-form table, S4 six node kinds and IO errors with the lstat/readFile call order recorded, S5
|
|
99
|
+
the ensure's six outcomes plus the registry and the order. `ensure-configs.test.mjs`'s three outcome sequences
|
|
100
|
+
carry `vehicles: seeded` fourth; `composed-lines-ux.test.mjs`'s slot count reads `ENSURE_OPS.length`; the shipped
|
|
101
|
+
tarball pin is 311 (309 + the tool + the seed).
|
|
102
|
+
- **How it was built.** The story ran on the task tier as the pilot's measurement: ten ledger rows became ten
|
|
103
|
+
briefed tasks on wave `p1` — an author dispatch wrote each `TASK-` brief (kit-stamped against a fresh checkpoint),
|
|
104
|
+
an execute dispatch landed it — twenty codex-exec threads on one held session, every one folded first pass; the
|
|
105
|
+
diff council's findings were folded before the release; the plan council took six rounds. Figures:
|
|
106
|
+
`docs/ai/history/pilot-measure-2026-09-13.md`.
|
|
107
|
+
|
|
7
108
|
## 13.2.0 — the task tier lands: `checkpoint.mjs` mints, orders, verifies and prunes tree snapshots of a plan's sequence, `checkpoint-restore.mjs` restores one in three proven steps, `task-brief.mjs` stamps and checks a brief against the newest checkpoint, and `TASK-` is a scratch name (AD-141)
|
|
8
109
|
|
|
9
110
|
**MINOR: three new tools and one added arm of the scratch-name predicate; no existing verb, record, transition or
|
package/README.md
CHANGED
|
@@ -249,7 +249,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
249
249
|
| `/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 degrades to printed maintainer-pasted commands, and the one-time host consent that makes it promptless surfaces via `recommendations`. Never commits, never pushes, never runs a subscription CLI. |
|
|
250
250
|
| `/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. |
|
|
251
251
|
| `/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; opt-in `acceptEdits`; plus a **read-only advisory** of likely project gate commands to add by hand. Writes **only** `.claude/settings.json` — **never** allowlists commit/push/publish, never writes `settings.local.json`, 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. |
|
|
252
|
-
| `/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 review recipe, advisory like every review). The fifth, **`executor`** (
|
|
252
|
+
| `/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 review recipe, advisory like every review). 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 is re-derived from `docs/ai/vehicles.json` (a hand edit is replaced); never touches `settings*.json`, never commits. |
|
|
253
253
|
| `/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 (delete-to-reseed otherwise). Preview by default; never writes `settings.local.json`; never commits. |
|
|
254
254
|
| `/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. This mode is **read-only and has no writer** — it ships the self-contained hook and a paste-ready wiring block you merge into `.claude/settings.json` yourself. |
|
|
255
255
|
| `/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). First 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; **model/effort are never settable** (the quality guard is untouched). **Previews by default**; refuses an unknown key, an out-of-range value, or a duplicate-carrying file; writes atomically (symlink/TOCTOU-safe). Never commits, never runs a subscription CLI. |
|
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".
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '
|
|
6
|
+
version: '14.0.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": "
|
|
3
|
+
"version": "14.0.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",
|
|
@@ -12,14 +12,14 @@ The opt-in **subagent-vehicle writer** — the family's second `.claude/` writer
|
|
|
12
12
|
|
|
13
13
|
Writing code, running gates, and user-facing copy never move to these four vehicles.
|
|
14
14
|
|
|
15
|
-
**The fifth vehicle, `executor`, is the ONE full-tool one** (
|
|
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`), or a bounded chore (`routine.carrier`). 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`** (the
|
|
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.
|
|
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).
|
|
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, whose differing bytes **would re-derive** from `docs/ai/vehicles.json` and are rewritten on `--apply` (a hand edit is replaced, never preserved).
|
|
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 `.claude` / `.claude/agents` / target file is a STOP).
|
|
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 · 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, 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).
|
|
@@ -21,7 +21,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
21
21
|
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.
|
|
22
22
|
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` and the project uses Claude Code**, also set `"includeCoAuthoredBy": false` in the project's `.claude/settings.json` (create it if absent) — 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).
|
|
23
23
|
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).
|
|
24
|
-
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
|
|
24
|
+
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).
|
|
25
25
|
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.
|
|
26
26
|
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. Preview the source and destination list and ask; on a yes copy `${CLAUDE_SKILL_DIR}/references/scripts/*.mjs` (+ `*.test.mjs`) into the project's `scripts/`. On a no, skip the hook in step 9 and say so; the docs command slots then 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.
|
|
27
27
|
9. **Wire / hide** per visibility (see [Visibility contract](${CLAUDE_SKILL_DIR}/references/contracts.md#visibility-contract)). When the user accepts the enforcement offer, install the pre-commit hook: `node scripts/install-git-hooks.mjs`. If the installer reports a pre-existing non-marker hook, stop and ask the user to merge it manually rather than overwriting. Once the gate declaration is final-run-capable, the installer's optional consented `--commit-guard <kit commit-guard.mjs path>` arm binds commits to the green `run-gates --final` receipt (the Recommendations advisor surfaces the exact one-liner).
|
|
@@ -20,7 +20,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
20
20
|
3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** after presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys; for `sandbox-lane` the note INCLUDES the sandbox-lanes ladder — present the whole ladder inline at the consent moment, never as a bare pointer). The user **explicitly confirms**, and **only then** — **no command runs before confirmation** — do you run the rendered command. Do NOT infer safety from the presence or absence of an `--apply` flag: most items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `family-freshness`'s `npx … init`. Some items instead render a **dry-run preview** that changes nothing and prints an explicit follow-up `--apply`/mutating command to run NEXT (e.g. `sandbox-lane`'s ack-write — a NEUTRAL recipe fingerprint into the family-owned `docs/ai/acks.json`, never a security key — and the `gates-declaration` seeder): after the SAME confirmation you run that printed follow-up command — no second ask, no improvisation (each writer keeps its own consent semantics: previews, `--apply` flags, refusals). An item marked **HAND-APPLY** (e.g. the `read-lane` stale/missing arms) is **never run by you and never written by the kit** — hand the user its rendered line together with its posture note; HAND-APPLY is **maintainer territory wherever it renders** — the apply slot or the `recipe:` line. For `worktrees-dir` the two classes COMPOSE in one item and the ORDER is part of the contract: its `recipe:` line is the HAND-APPLY grant advice, its apply one-liner is the dry-run ack preview, and you **wait for the maintainer to confirm the grant is applied (or the terminal fallback chosen)** before running that preview and the exact `--apply` command it prints — the ack RECORDS a choice that must already have been made, and against a trusted host NO the item renders no ack at all.
|
|
21
21
|
4. Registry strings (benefits + item texts) are frozen tool data, fact-true, ONE line under the pinned shape cap — posture/risk detail lives in the notes below at the consent moment, never inline in the overview. The dual velocity+security wording (`safer — blast radius bounded by the OS sandbox, not human attention`) rides ONLY the items with a real security delta (the autonomy render, the sandbox provisioning); the bridge-wrappers item claims **velocity only**.
|
|
22
22
|
|
|
23
|
-
**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 four activities. **`placed` and `customized` render NOTHING** (a working vehicle is not a recommendation), a config naming no `subagent` slot renders nothing, 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. Its apply is the agents writer's own `cheap-agents.mjs --apply` (the same writer the `agents` item previews; here the rendered line IS the mutation, so read the item's state before confirming), and its `recipe:` line is 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
|
|
23
|
+
**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 four activities. **`placed` and `customized` render NOTHING** (a working vehicle is not a recommendation), a config naming no `subagent` slot renders nothing, 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. Its apply is the agents writer's own `cheap-agents.mjs --apply` (the same writer the `agents` item previews; here the rendered line IS the mutation, so read the item's state before confirming), and its `recipe:` line is 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 item's apply line carries a `HAND-APPLY:` precondition naming each refusal the advisor observes; an executor whose frontmatter is invalid is re-derived by the apply itself. Risk profile: writes under `.claude/agents/` only — the executor's body re-derived from `docs/ai/vehicles.json`, every other vehicle create-only — never `settings.json`, never a commit.
|
|
24
24
|
|
|
25
25
|
**Per-item posture notes (the consent moment — surface BEFORE running or handing over the apply):**
|
|
26
26
|
|
|
@@ -11,7 +11,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
11
11
|
|
|
12
12
|
1. `pointers` — `node ${CLAUDE_SKILL_DIR}/tools/inject-methodology.mjs reconcile <project>/AGENTS.md` → per pointer: added · already present · skipped (reported) · a hard STOP.
|
|
13
13
|
2. `footprint` — `node ${CLAUDE_SKILL_DIR}/tools/hide-footprint.mjs --dir <project> --reconcile --dry-run` → visibility: visible · ambiguous · hidden — consent (conditional): ambiguous → ask which it is BEFORE anything; hidden → the conditional re-run without `--dry-run` (its surfaced paths ask per bootstrap step 9).
|
|
14
|
-
3. `configs` — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>` → one line per ensure: `seeded` / `note-refreshed` / `refreshed` / `regenerated` / `already-current` / `customized-preserved` / `malformed-preserved` / `already-present` / `skipped-no-node-evidence` / `old-adr-layout-migration-instructed` / `failed`.
|
|
14
|
+
3. `configs` — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --cwd <project>` → one line per ensure: `seeded` / `note-refreshed` / `refreshed` / `regenerated` / `re-derived` / `already-current` / `customized-preserved` / `malformed-preserved` / `already-present` / `not-placed` / `skipped-no-node-evidence` / `old-adr-layout-migration-instructed` / `failed`.
|
|
15
15
|
4. `gates-migration` — `node ${CLAUDE_SKILL_DIR}/references/scripts/migrate-gates.mjs --kit-tools ${CLAUDE_SKILL_DIR}/tools --cwd <project>` → the preview plan · INERT checker · CUSTOMIZED entries, each named — consent: apply only on an explicit yes, re-run with `--apply`.
|
|
16
16
|
5. `bridges` — `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed` → per-bridge lines: refreshed · already current · skipped — with its stated reason (not placed / newer than the bundle / unsupported host) · `skipped-readonly` · could not refresh.
|
|
17
17
|
6. `lens` — `node ${CLAUDE_SKILL_DIR}/tools/lens-region.mjs reconcile <project>/docs/ai/agent_rules.md` → per section: refreshed · already current · custom edit preserved · file absent / engine too old — skipped · over the line cap — refused · section absent — noted · a fully absent/invalid engine → hard STOP.
|
|
@@ -31,7 +31,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
31
31
|
|
|
32
32
|
**`footprint` — hidden-mode footprint reconcile (D9 / AD-014).** A deployment does not record whether it chose `hidden`, so first **infer visibility** — the dry-run writes **zero bytes** and reports one of — **visible** (the entry point is tracked) → nothing to do; **ambiguous** (untracked but not ignored — could be a fresh uncommitted repo, or a hide that broke) → **ASK** the user which it is, never guess; **hidden** → re-run without `--dry-run` to migrate any older **machine-global** hide to the **project-local** `.git/info/exclude` (one managed block; folds in the legacy `.claude/skills/` line), idempotently (a clean re-run is zero-diff). Handle its surfaced paths exactly as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`) (already-committed → show `git rm --cached`, ask before `--include`; generic-name present file → ask; **leftover machine-wide ignore block → ASK before `--remove-global`**, default keep + report). No Node on the agent host / Windows → as bootstrap step 9 (`${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`). This runs on **every** hidden upgrade, like the methodology slot — no lineage-head bump, no migration file.
|
|
33
33
|
|
|
34
|
-
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all
|
|
34
|
+
**`configs` — the project-configuration ensures, ONE run.** The ONE command performs **all eight** ensures — orchestration config · gate declaration · autonomy declaration · vehicles declaration · executor re-derivation · enforcement scripts · spec layer · navigator index — in a fixed order, and prints **one outcome line per ensure**: paste those lines into the step 4 / step 8 success report. Every SEED is **create-only** (an existing file is preserved byte-for-byte); a refresh-class op rewrites only what it owns — the outcome tokens by ensure, below, name each arm, the executor's re-derivation among them. One ensure failing **never** skips the others: each reports its own outcome and the run exits non-zero when any of them `failed`. The outcome tokens, by ensure: orchestration → `seeded` / `note-refreshed` / `already-current` / `customized-preserved` / `malformed-preserved`; gates, autonomy and vehicles → `seeded` / `already-present`; executor → `not-placed` / `already-current` / `re-derived`; scripts → `seeded` / `already-present` / `old-adr-layout-migration-instructed` / `skipped-no-node-evidence`; specs → `seeded` / `refreshed` / `already-present` / `customized-preserved` / `skipped-no-node-evidence`; index → `regenerated` / `already-current`; and any ensure may report `failed`, whose line OPENS with the cause (relay it with that cause — never soften it into a skip; an op that copies file by file also states when it stopped partway). The cause vocabulary is CLOSED — one of `race-unresolved`, `template-unreadable`, `bundle-unreadable`, `adr-layout-unverifiable`, `node-evidence-unverifiable`, `wrong-node-kind`, `vehicle-settings-unreadable`, `write-refused`, `unexpected-error`, `generator-unlaunchable`, `generator-failed`, `index-probe-failed`, `index-stale-after-write` — and every cause that can only arise AFTER the generator ran (`generator-failed`, and `index-probe-failed` / `index-stale-after-write` when they follow a reported regeneration) DISCLOSES in its own line that a write may already have landed. **A non-zero exit STOPs this upgrade** — report the failed line and stop there, before the equal-head exit, the migrations and the re-stamp. Add `--dry-run` to preview without writing a byte. Like the pointer slots + the footprint reconcile, all eight reach an equal-head deployment **without a lineage-head bump or a migration file** (all inherently outside the docs cap-validator).
|
|
35
35
|
|
|
36
36
|
**What the orchestration-config ensure does.** `docs/ai/orchestration.json` must exist **and its onboarding note must be current**: created from the canonical seed if missing; if it already exists, **every activity/slot the user set is preserved** and ONLY the `_README` note is refreshed, and only when the existing one still matches a known prior canonical — the tested `refreshIfCanonical` / `refreshReadme` in `tools/orchestration-config.mjs` is the source of truth for that decision (it normalizes CRLF/whitespace before comparing; a *customized* `_README` is preserved verbatim → `customized-preserved`; a *malformed* existing config is **preserved untouched + LOUD** → `malformed-preserved`, never clobbered and never silently skipped). The current note points at `/agent-workflow-kit set-recipe`. **Kit-owned:** in the **delegated** path memory only seeds/preserves the file (memory upgrade step 2) and this ensure applies the `_README` refresh; in the **fallback** path it does both. (Memory stays standalone.)
|
|
37
37
|
|
|
@@ -39,7 +39,9 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
39
39
|
|
|
40
40
|
**What the autonomy-declaration ensure does.** `docs/ai/autonomy.json` must exist: created from `${CLAUDE_SKILL_DIR}/references/templates/autonomy.json` if missing (the kit's own template twin — a stale memory never silently loses the seed); **an existing file is preserved byte-for-byte** (a declared policy is authored content). The seed is SPARSE (the onboarding note only) and **defaults-equivalent** — deploying it never changes behavior (the computed defaults stay the policy until the user declares levels with `/agent-workflow-kit set-autonomy` or by hand).
|
|
41
41
|
|
|
42
|
-
**What the
|
|
42
|
+
**What the executor ensure does.** A placed `.claude/agents/executor.md` is kept byte-equal to the body derived from `docs/ai/vehicles.json`: a differing regular file is rewritten (`re-derived` — a hand edit is replaced, never preserved), and an absent one is `not-placed` (the agents mode places it on consent; this ensure never does).
|
|
43
|
+
|
|
44
|
+
**What the enforcement-script ensure does.** A deployment older than the ADR-cascade feature has no `scripts/archive-decisions.mjs`, and an equal-head exit would otherwise never deliver it. The pairs must exist in the project's `scripts/`: `archive-decisions.mjs` + `archive-decisions.test.mjs` and `markdown-blocks.mjs` + `markdown-blocks.test.mjs`, copied from `${CLAUDE_SKILL_DIR}/references/scripts/` if missing; **an existing file is preserved, never overwritten** (drift repair belongs to a lineage migration). Nothing else is seeded — the other tokenizer-era tests red beside OLD archivers. **OLD ADR-store layout — DETECTED FIRST, never auto-migrated (AD-051, Decision 13):** on a `docs/ai/history/decisions-archive*.md` monolith (`old`), or no monolith but a deployed rotator predating the store (`old-unrotated`), the project is on the RETIRED 3-tier cascade, so the ensure writes NOTHING and reports `old-adr-layout-migration-instructed` (the new `archive-decisions.mjs` beside un-migrated monoliths would red their ADR gate). Relay it as the LOUD instruct it is: the fix is the opt-in **`/agent-workflow-kit migrate-adr-store`** (consent-gated; previews first, never commits), and the seed lands on the next upgrade. A layout the ensure cannot READ is `failed`, not a seed — it never writes on an unverifiable tree. The seed applies ONLY to a clean layout (neither signal). **Whether Node runs here is PROVEN, never proxied:** the ensure seeds on a regular `package.json` at the root OR on any kit-seeded `scripts/*.mjs` already deployed (a bootstrap places them into projects that never carry a `package.json`); only when every probe answers absent does it report `skipped-no-node-evidence`, a line naming every probe it checked, while the config ensures still run; a probe that cannot be read is `failed` with the cause `node-evidence-unverifiable`, nothing written. **A `skipped-*` outcome whose stated reason this tool could itself disprove may not exist** — the retired `skipped-no-node` was exactly that, printed beside twenty deployed Node scripts. The deployed pre-commit hook gains the `archive-decisions.mjs --check` line only when the hook itself is next refreshed (re-run `node scripts/install-git-hooks.mjs` after the ensure and it will refuse a non-marker hook as always); an OLD hook without the line stays consistent-safe — the decisions gate is simply not enforced yet, never a broken hook.
|
|
43
45
|
|
|
44
46
|
**What the navigator ensure does.** `docs/ai/index.md` is the always-loaded navigator the entry point declares, and it is GENERATED — no template ships it, so a deployment that never ran the generator boots from a broken entry point (and, on a Node project, carries a pre-commit hook that fails its own index check). The ensure runs the bundled generator's finalizer and reports `regenerated` (it was missing or stale — it was written) or `already-current` (nothing written). It never skips a No-Node project: the generator runs from `${CLAUDE_SKILL_DIR}/references/scripts/` on the agent host, not from the project's `scripts/`. **Its position in the run-list is EARLY and therefore NOT authoritative** — `lens` (and, on the migrated path, steps 6–7) still change `docs/ai` afterwards — so the authoritative run is the LATE `--only index` rung documented at both exits; the early one is idempotent and costs at most an `already-current` line.
|
|
45
47
|
|
|
@@ -90,7 +92,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
90
92
|
**The LATE navigator finalizer — the AUTHORITATIVE run, after the last `docs/ai` mutation.** `lens` above rewrites `docs/ai/agent_rules.md`, so the step-3 index ensure is already behind by the time the run-list ends. Re-run that ONE op here, before the step-4 report: `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>`. Relay **this** line in the report (it supersedes the early one; an untouched tree reports `already-current`, a failure STOPs the upgrade like any other ensure). On the migrated path the same rung runs again at the END of step 7 — after the migrations, before the step-8 re-stamp.
|
|
91
93
|
4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
|
|
92
94
|
- **A skip line that contradicts the observed tree is a FINDING, never pasted as neutral.** Every ensure skip names the fact it proved (the probes that answered absent, the layout it read); if the tree you can see disproves that reason — a "no Node" skip beside deployed Node scripts, a "not deployed" skip beside a stamped `docs/ai` — do not relay the line as an outcome: raise it in the report as a defect of the tool, with the contradicting fact named, and stop short of any step that would build on the skip.
|
|
93
|
-
- **Report step 3's outcome in plain language** — for **each** `pointers` slot (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); the **
|
|
95
|
+
- **Report step 3's outcome in plain language** — for **each** `pointers` slot (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); the **eight project-configuration ensure** (`configs`) lines exactly as the one ensure run composed them (the ensures and their outcome tokens are enumerated in step 3), each rendered in plain language: what was created, what was left exactly as the user wrote it, and — for a `failed` line — what stopped it; for the navigator, relay the **late** `--only index` line, not the early one; the **`gates-migration`** result — *nothing to migrate*, the shown plan *applied* on your explicit yes, or the plan *left unapplied* (consent not given), with any INERT checker or CUSTOMIZED entries named; the **placed-bridge refresh** (`bridges`) outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` with its re-scan verdict / *could not refresh* + recovery); the **agent-rules lens** (`lens`) outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused); the **bridge-settings reconcile** (`bridge-settings`) outcome (paste the tool's line(s) verbatim); and, for a hidden deployment, whether the hidden-mode footprint (`footprint`) was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
94
96
|
- **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
|
|
95
97
|
- **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
|
|
96
98
|
- **Live host/session facts are tool-composed only.** Any claim this report makes about the current host or session state — prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts — must trace to **live tool output** from **this session** (the lines you just composed, or a probe you ran this run); a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection. Full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
|
|
@@ -99,4 +101,4 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
99
101
|
5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
|
|
100
102
|
6. **Collect the migration answers FIRST, then apply.** If `AGENTS.md` is missing BOTH the *Communication language* and *Attribution* blocks — i.e. both blocks are missing (a pre-1.1.0 deployment) — ask the two questions as ONE structured multi-question prompt; record each answer individually, write nothing until ALL are answered, and carry the answers into the migrations below: a migration whose answer was already collected never re-asks (its own "Ask the user" step is the standalone fallback); a single missing block keeps its single ask (step 7). Then apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** — safe to re-run.
|
|
101
103
|
7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`. (An answer already collected by the step-6 batched prompt is carried in — never re-asked here.) **Then, before the stamp, re-run the LATE navigator finalizer:** the migrations of steps 6–7 may have added or changed `docs/ai` files, so run the one op again — `node ${CLAUDE_SKILL_DIR}/tools/ensure-configs.mjs --reconcile --only index --cwd <project>` — and relay THAT line in the step-8 report (idempotent: an untouched tree reports `already-current`; a failure STOPs this upgrade like any other ensure).
|
|
102
|
-
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 run-list outcomes, each as step 4 words it: the `pointers` slots, the `footprint` visibility verdict (and what a consented re-run migrated), the **
|
|
104
|
+
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 run-list outcomes, each as step 4 words it: the `pointers` slots, the `footprint` visibility verdict (and what a consented re-run migrated), the **eight project-configuration ensure** (`configs`) lines (enumerated in step 3) — the navigator one being the late `--only index` run at the end of step 7, the **`gates-migration`** result, the **placed-bridge refresh** (`bridges`) lines (pasted verbatim), the **agent-rules lens** (`lens`) + **Communication-section** outcomes (same outcome sets as step 4), and the **bridge-settings reconcile** (`bridge-settings`) outcome (line(s) pasted verbatim) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Every current host/session claim in this report is tool-composed only** — prompts fired, sandbox scope, whether a bypass was needed, network reachability and approval counts must trace to **live tool output** from **this session**, a memory/handover snapshot is **context, never report facts**, and an unbacked claim is **omitted or explicitly marked unverified** (full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
|
|
@@ -0,0 +1,8 @@
|
|
|
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 and the upgrade run derive it from model and effort, 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.",
|
|
3
|
+
"executor": {
|
|
4
|
+
"model": "opus",
|
|
5
|
+
"effort": "high",
|
|
6
|
+
"fallback": "sonnet"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -8,6 +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
12
|
|
|
12
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
|
|
@@ -25,6 +26,7 @@ export const CHEAP_AGENTS_STAMP = 'CHEAP_AGENTS_STAMP';
|
|
|
25
26
|
export const CHEAP_AGENTS_SYMLINK = 'CHEAP_AGENTS_SYMLINK';
|
|
26
27
|
export const CHEAP_AGENTS_BUNDLE = 'CHEAP_AGENTS_BUNDLE';
|
|
27
28
|
export const CHEAP_AGENTS_CONFIG = 'CHEAP_AGENTS_CONFIG';
|
|
29
|
+
export const CHEAP_AGENTS_VEHICLES = 'CHEAP_AGENTS_VEHICLES';
|
|
28
30
|
|
|
29
31
|
export const makeCheapAgentsError = (code, message) =>
|
|
30
32
|
Object.assign(new Error(`${ERROR_PREFIX} ${message}`), { name: 'CheapAgentsError', code, exitCode: EXIT_PRECONDITION });
|
|
@@ -88,7 +90,7 @@ export const assertDirSafe = (absPath, relPath, fs) => {
|
|
|
88
90
|
return { absent: false };
|
|
89
91
|
};
|
|
90
92
|
|
|
91
|
-
// Per-template placement plan: place | already-current | customized-preserved
|
|
93
|
+
// Per-template placement plan: place | already-current | re-derive | customized-preserved.
|
|
92
94
|
export const planPlacement = (templates, projectDir, deps = {}) => {
|
|
93
95
|
const fs = readFsDeps(deps);
|
|
94
96
|
return templates.map((template) => {
|
|
@@ -101,6 +103,7 @@ export const planPlacement = (templates, projectDir, deps = {}) => {
|
|
|
101
103
|
}
|
|
102
104
|
const existing = fs.readFile(abs, UTF8);
|
|
103
105
|
if (existing === template.content) return { ...template, rel, abs, action: 'already-current' };
|
|
106
|
+
if (template.rederive === true) return { ...template, rel, abs, action: 're-derive', existing };
|
|
104
107
|
return { ...template, rel, abs, action: 'customized-preserved', existing };
|
|
105
108
|
});
|
|
106
109
|
};
|
|
@@ -110,8 +113,28 @@ export const planPlacement = (templates, projectDir, deps = {}) => {
|
|
|
110
113
|
export const EXECUTOR_VEHICLE = 'executor.md';
|
|
111
114
|
export const EXECUTOR_VEHICLE_REL = `${AGENTS_DIR}/${EXECUTOR_VEHICLE}`;
|
|
112
115
|
export const EXECUTOR_VEHICLE_SPEC = Object.freeze({
|
|
113
|
-
stem: 'executor', template: 'executor', model:
|
|
116
|
+
stem: 'executor', template: 'executor', model: EXECUTOR_DEFAULTS.model,
|
|
117
|
+
effort: EXECUTOR_DEFAULTS.effort, tools: 'full', derived: true,
|
|
114
118
|
});
|
|
119
|
+
|
|
120
|
+
export const readExecutorPosture = (projectDir, deps = {}) => {
|
|
121
|
+
const fs = readFsDeps(deps);
|
|
122
|
+
return resolveExecutor(readVehicles(projectDir, fs.readFile, fs.lstat));
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const executorVehicleSpec = (posture) => ({
|
|
126
|
+
...EXECUTOR_VEHICLE_SPEC, model: posture.model, effort: posture.effort,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export const deriveExecutorBody = (bundledContent, posture) =>
|
|
130
|
+
deriveLensTemplate(bundledContent, executorVehicleSpec(posture));
|
|
131
|
+
|
|
132
|
+
export const executorTemplate = (posture, deps = {}) => {
|
|
133
|
+
const bundled = readBundledAgents(deps).find((item) => item.name === EXECUTOR_VEHICLE);
|
|
134
|
+
if (!bundled) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${EXECUTOR_VEHICLE} is missing from the bundle`);
|
|
135
|
+
return { name: EXECUTOR_VEHICLE, content: deriveExecutorBody(bundled.content, posture), rederive: true };
|
|
136
|
+
};
|
|
137
|
+
|
|
115
138
|
const READ_ONLY_TOOLS = new Set(['Read', 'Grep', 'Glob']);
|
|
116
139
|
|
|
117
140
|
// The YAML subset a vehicle's frontmatter is read with: a bare scalar, a single- or double-quoted
|
|
@@ -229,6 +252,17 @@ export const surveyVehicle = (projectDir, spec, deps = {}) => {
|
|
|
229
252
|
}
|
|
230
253
|
};
|
|
231
254
|
|
|
232
|
-
export const surveyExecutorVehicle = (projectDir, deps = {}) =>
|
|
255
|
+
export const surveyExecutorVehicle = (projectDir, deps = {}) => {
|
|
256
|
+
const fs = readFsDeps(deps);
|
|
257
|
+
try {
|
|
258
|
+
assertDirSafe(join(projectDir, CLAUDE_DIR), CLAUDE_DIR, fs);
|
|
259
|
+
assertDirSafe(join(projectDir, AGENTS_DIR), AGENTS_DIR, fs);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
return { state: 'unusable', reason: err.message, rel: EXECUTOR_VEHICLE_REL };
|
|
262
|
+
}
|
|
263
|
+
const { posture, reason } = readExecutorPosture(projectDir, deps);
|
|
264
|
+
if (posture === null) return { state: 'unusable', reason, rel: VEHICLES_REL };
|
|
265
|
+
return surveyVehicle(projectDir, executorVehicleSpec(posture), deps);
|
|
266
|
+
};
|
|
233
267
|
|
|
234
268
|
refuseDirectRun(import.meta.url);
|
package/tools/cheap-agents.mjs
CHANGED
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
// only for a bounded execution, authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and it
|
|
8
8
|
// never commits. `surveyExecutorVehicle` is that vehicle's readiness, for the subagent carrier.
|
|
9
9
|
//
|
|
10
|
-
// The family's second `.claude/` writer, the velocity-profile.mjs
|
|
10
|
+
// The family's second `.claude/` writer, the velocity-profile.mjs discipline with an executor exception:
|
|
11
11
|
// • preview-then-mutate — `--dry-run` is the DEFAULT and writes nothing; `--apply` writes;
|
|
12
12
|
// • deployment-gated — `--apply` STOPs unless docs/ai/.workflow-version equals the lineage
|
|
13
13
|
// head (a dry-run stays usable whatever the stamp says; an unreadable orchestration config
|
|
14
14
|
// STOPs both, since the derived lenses it names cannot be known);
|
|
15
15
|
// • symlink-safe — a symlinked `.claude` / `.claude/agents` / target file is a STOP, never a
|
|
16
16
|
// write-through;
|
|
17
|
-
// •
|
|
18
|
-
//
|
|
17
|
+
// • preserves differing .claude/agents/ files (`customized — preserved`), except the executor:
|
|
18
|
+
// on apply its body is re-derived from docs/ai/vehicles.json, replacing a hand edit;
|
|
19
19
|
// an identical file is `already current` (idempotent re-run);
|
|
20
20
|
// • writes ONLY under .claude/agents/ — never settings.json / settings.local.json, never
|
|
21
21
|
// commits.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
//
|
|
28
28
|
// Exit codes: 0 done / dry-run (incl. preserved customizations — a user's file is a legitimate
|
|
29
29
|
// state, not an error); 1 precondition STOP (stamp, symlink, missing bundle, an unreadable
|
|
30
|
-
// orchestration config —
|
|
30
|
+
// orchestration config or docs/ai/vehicles.json — their derived bodies cannot be known); 2 usage.
|
|
31
31
|
// Dependency-free, Node >= 22. No side effects on import.
|
|
32
32
|
|
|
33
33
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
@@ -38,6 +38,9 @@ import { shellQuoteArg } from './repo-lex.mjs';
|
|
|
38
38
|
import { loadConfig } from './orchestration-config.mjs';
|
|
39
39
|
import { lensMembersOf } from './review-roster.mjs';
|
|
40
40
|
import { deriveLensTemplate, lensVehicleSpec } from './review-roster-resolve.mjs';
|
|
41
|
+
import { loadVehicles, resolveExecutor } from './vehicle-settings.mjs';
|
|
42
|
+
import { safeLine } from './carriers.mjs';
|
|
43
|
+
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
41
44
|
// The READ core, never a second copy: the bundle, the placement plan and the executor survey live
|
|
42
45
|
// there so the read-only advisor graph can reach them without reaching this writer.
|
|
43
46
|
import {
|
|
@@ -47,9 +50,12 @@ import {
|
|
|
47
50
|
EXPECTED_WORKFLOW_VERSION,
|
|
48
51
|
UTF8,
|
|
49
52
|
CHEAP_AGENTS_STAMP,
|
|
53
|
+
CHEAP_AGENTS_SYMLINK,
|
|
50
54
|
CHEAP_AGENTS_BUNDLE,
|
|
51
55
|
CHEAP_AGENTS_CONFIG,
|
|
56
|
+
CHEAP_AGENTS_VEHICLES,
|
|
52
57
|
EXECUTOR_VEHICLE,
|
|
58
|
+
executorTemplate,
|
|
53
59
|
makeCheapAgentsError,
|
|
54
60
|
readFsDeps,
|
|
55
61
|
readBundledAgents,
|
|
@@ -68,6 +74,10 @@ export {
|
|
|
68
74
|
CHEAP_AGENTS_SYMLINK,
|
|
69
75
|
CHEAP_AGENTS_BUNDLE,
|
|
70
76
|
CHEAP_AGENTS_CONFIG,
|
|
77
|
+
CHEAP_AGENTS_VEHICLES,
|
|
78
|
+
readExecutorPosture,
|
|
79
|
+
executorTemplate,
|
|
80
|
+
deriveExecutorBody,
|
|
71
81
|
makeCheapAgentsError,
|
|
72
82
|
readBundledAgents,
|
|
73
83
|
planPlacement,
|
|
@@ -96,7 +106,9 @@ authoring, or write-capable routine slice the orchestrator verifies, never for r
|
|
|
96
106
|
it never commits.
|
|
97
107
|
Default is --dry-run (a preview; writes nothing). --apply writes.
|
|
98
108
|
Configured derived review lenses are planned and placed beside the bundled vehicles.
|
|
99
|
-
An existing file with DIFFERENT content is preserved and reported, never overwritten
|
|
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, and a placed executor whose bytes differ
|
|
111
|
+
from the derived body is re-derived on --apply (a hand edit is replaced, never preserved).`;
|
|
100
112
|
|
|
101
113
|
export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
102
114
|
|
|
@@ -127,6 +139,14 @@ const loadConfigOrStop = (cwd, deps) => {
|
|
|
127
139
|
}
|
|
128
140
|
};
|
|
129
141
|
|
|
142
|
+
const loadExecutorPostureOrStop = (cwd, deps) => {
|
|
143
|
+
try {
|
|
144
|
+
return resolveExecutor(loadVehicles(cwd, deps.readFile, deps.lstat)).posture;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw makeCheapAgentsError(CHEAP_AGENTS_VEHICLES, `${err?.message ?? err} — the agents writer cannot derive the executor vehicle — nothing is placed`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
130
150
|
const configuredDerivedTemplates = (cwd, deps) => {
|
|
131
151
|
const config = loadConfigOrStop(cwd, deps);
|
|
132
152
|
const bundle = readBundledAgents(deps);
|
|
@@ -149,9 +169,10 @@ export const applyCheapAgentsCommand = (root) =>
|
|
|
149
169
|
export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
150
170
|
const fs = writeFsDeps(deps);
|
|
151
171
|
const projectDir = cwd ?? process.cwd();
|
|
152
|
-
const
|
|
172
|
+
const posture = loadExecutorPostureOrStop(projectDir, deps);
|
|
173
|
+
const derived = [executorTemplate(posture, deps), ...configuredDerivedTemplates(projectDir, deps)];
|
|
153
174
|
const preflight = preflightCheapAgents({ cwd: projectDir, derived }, deps);
|
|
154
|
-
if (dryRun) return { wrote: false, dryRun: true, ...preflight };
|
|
175
|
+
if (dryRun) return { wrote: false, dryRun: true, posture, ...preflight };
|
|
155
176
|
|
|
156
177
|
if (!preflight.stampOk) {
|
|
157
178
|
throw makeCheapAgentsError(
|
|
@@ -159,10 +180,16 @@ export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
|
159
180
|
`not a deployed agent-workflow project at lineage ${EXPECTED_WORKFLOW_VERSION} (found ${preflight.stamp ?? 'none'}) — run init/upgrade first`,
|
|
160
181
|
);
|
|
161
182
|
}
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
for (const item of
|
|
165
|
-
|
|
183
|
+
const toWrite = preflight.plan.filter((item) => item.action === 'place' || item.action === 're-derive');
|
|
184
|
+
if (toWrite.length > 0) fs.mkdir(join(preflight.projectDir, AGENTS_DIR), { recursive: true });
|
|
185
|
+
for (const item of toWrite) {
|
|
186
|
+
if (item.action === 're-derive') {
|
|
187
|
+
writeContainedFileAtomic(preflight.projectDir, item.abs, item.content, deps, {
|
|
188
|
+
label: item.rel, stop: (message) => makeCheapAgentsError(CHEAP_AGENTS_SYMLINK, message),
|
|
189
|
+
});
|
|
190
|
+
} else fs.writeFile(item.abs, item.content, UTF8);
|
|
191
|
+
}
|
|
192
|
+
return { wrote: toWrite.length > 0, dryRun: false, posture, ...preflight };
|
|
166
193
|
};
|
|
167
194
|
|
|
168
195
|
// ── report ────────────────────────────────────────────────────────────────────────────
|
|
@@ -170,17 +197,20 @@ export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
|
170
197
|
const ACTION_LABEL = {
|
|
171
198
|
place: 'place',
|
|
172
199
|
'already-current': 'already current',
|
|
200
|
+
're-derive': 're-derived — a hand edit was replaced; set the model in docs/ai/vehicles.json',
|
|
173
201
|
'customized-preserved': 'customized — preserved (delete the file to reseed from the bundle)',
|
|
174
202
|
};
|
|
175
203
|
|
|
176
204
|
export const formatResult = (result) => {
|
|
205
|
+
const { posture } = result;
|
|
177
206
|
const lines = [
|
|
178
207
|
result.dryRun
|
|
179
208
|
? 'agent-workflow subagent vehicles — DRY RUN (no changes)'
|
|
180
209
|
: 'agent-workflow subagent vehicles — APPLY',
|
|
181
210
|
];
|
|
182
211
|
for (const item of result.plan) {
|
|
183
|
-
const verb = result.dryRun && item.action === 'place' ? 'would place'
|
|
212
|
+
const verb = result.dryRun && item.action === 'place' ? 'would place'
|
|
213
|
+
: result.dryRun && item.action === 're-derive' ? 'would re-derive' : ACTION_LABEL[item.action];
|
|
184
214
|
lines.push(` - ${item.rel}: ${verb}`);
|
|
185
215
|
}
|
|
186
216
|
if (!result.stampOk) {
|
|
@@ -191,11 +221,12 @@ export const formatResult = (result) => {
|
|
|
191
221
|
`${readOnlyCount} vehicles are Claude Code subagents with READ-ONLY tools and NO shell as bundled or derived (a customized file keeps whatever it grants) — so a fan-out on the shipped templates can never turn into a wave of approval prompts.`,
|
|
192
222
|
`three of those ride the cheap lane (model: haiku, effort: low) for mechanical work; ${FALLBACK_LENS_ADDITIONAL_ONLY}`,
|
|
193
223
|
'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.',
|
|
224
|
+
`executor posture: model=${safeLine(posture.model)} effort=${safeLine(posture.effort)} fallback=${safeLine(posture.fallback)} (source: ${safeLine(posture.source)}) — set in docs/ai/vehicles.json`,
|
|
194
225
|
);
|
|
195
226
|
// A preview must print the EXACT command that applies it. The advisor renders this dry-run as an
|
|
196
227
|
// item's one-liner, and that flow's contract is "run the printed command, no improvisation" — a
|
|
197
228
|
// bare "re-run with --apply" would leave the caller to reconstruct --cwd and its quoting.
|
|
198
|
-
if (result.dryRun && result.plan.some((item) => item.action === 'place')) {
|
|
229
|
+
if (result.dryRun && result.plan.some((item) => item.action === 'place' || item.action === 're-derive')) {
|
|
199
230
|
lines.push(`to apply, run exactly: ${applyCheapAgentsCommand(result.projectDir)}`);
|
|
200
231
|
}
|
|
201
232
|
if (!result.dryRun && result.wrote) {
|
package/tools/ensure-configs.mjs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// ensure-configs.mjs — ONE runnable command for the
|
|
2
|
+
// ensure-configs.mjs — ONE runnable command for the stamp-independent upgrade ensures:
|
|
3
3
|
//
|
|
4
4
|
// orchestration docs/ai/orchestration.json seed, or refresh a still-canonical onboarding note
|
|
5
5
|
// gates docs/ai/gates.json seed-if-missing (an existing declaration is authored content)
|
|
6
6
|
// autonomy docs/ai/autonomy.json seed-if-missing (same)
|
|
7
|
+
// vehicles docs/ai/vehicles.json seed-if-missing (same)
|
|
8
|
+
// executor .claude/agents/executor.md re-derived from docs/ai/vehicles.json when its bytes differ; absent stays absent
|
|
7
9
|
// scripts scripts/<ADR enforcement> seed-if-missing, ADR-layout detect FIRST
|
|
8
10
|
// specs scripts/<spec layer> reader + checker pairs seeded when absent, refreshed only on
|
|
9
11
|
// + docs/ai/specs/index.md a shipped prior body, store root seeded behind a current checker
|
|
10
12
|
// index docs/ai/index.md regenerate-if-missing-or-stale (a GENERATED artifact)
|
|
11
13
|
//
|
|
12
14
|
// Each was prose in references/modes/upgrade.md that an agent performed by hand. One command instead
|
|
13
|
-
// of
|
|
14
|
-
// has a single invocation point whose
|
|
15
|
+
// of one per ensure is deliberate: independent runs would be as many chances to skip one, and the mode
|
|
16
|
+
// doc now has a single invocation point whose outcome lines it relays.
|
|
15
17
|
//
|
|
16
18
|
// The contract (pinned by this module's tests):
|
|
17
19
|
// • --reconcile is REQUIRED. A bare run is a usage error, so nothing writes by accident.
|
|
@@ -19,7 +21,7 @@
|
|
|
19
21
|
// • The ops run in a FIXED order and one op's failure NEVER skips the rest: every op reports its own
|
|
20
22
|
// token, and the exit is non-zero when any of them failed.
|
|
21
23
|
// • The deployment gate runs ONCE, before any op: an absent/symlinked docs/ai stops the whole run
|
|
22
|
-
// with the gate's own message rather than
|
|
24
|
+
// with the gate's own message rather than one copy of it per op.
|
|
23
25
|
//
|
|
24
26
|
// Output is ENGLISH/structured (repo-artifact Hard Constraint); the agent localizes when narrating.
|
|
25
27
|
// Exit codes: 0 every op fine · 1 an op failed, or the deployment gate stopped the run · 2 usage.
|
|
@@ -31,14 +33,15 @@ import { dirname, resolve } from 'node:path';
|
|
|
31
33
|
import { fileURLToPath } from 'node:url';
|
|
32
34
|
import { assertDocsAiDeployment } from './atomic-write.mjs';
|
|
33
35
|
import { isDirectRun } from './direct-run.mjs';
|
|
36
|
+
import { ensureExecutor } from './ensure-executor.mjs';
|
|
34
37
|
import { ENSURE_OPS, OWN_IMPLEMENTATIONS, failedOutcome } from './ensure-ops.mjs';
|
|
35
38
|
import { ensureSpecs } from './ensure-specs.mjs';
|
|
36
39
|
|
|
37
40
|
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
38
41
|
|
|
39
42
|
// The op table this CLI walks — name → implementation, in ENSURE_OPS order. Composed HERE (the one
|
|
40
|
-
// consumer) because the spec-layer
|
|
41
|
-
export const ENSURE_IMPLEMENTATIONS = Object.freeze({ ...OWN_IMPLEMENTATIONS, specs: ensureSpecs });
|
|
43
|
+
// consumer) because the executor and spec-layer ensures import ensure-ops.mjs for their outcome door and probes.
|
|
44
|
+
export const ENSURE_IMPLEMENTATIONS = Object.freeze({ ...OWN_IMPLEMENTATIONS, executor: ensureExecutor, specs: ensureSpecs });
|
|
42
45
|
|
|
43
46
|
const EXIT_OK = 0;
|
|
44
47
|
const EXIT_FAILED = 1;
|
|
@@ -51,23 +54,25 @@ const EMPTY_ONLY = `--only needs one operation name (${ENSURE_OPS.join(' | ')})
|
|
|
51
54
|
const REPEATED_ONLY = '--only was passed more than once — this selector names exactly ONE operation';
|
|
52
55
|
const unknownOp = (value) => `--only ${value}: no such operation (${ENSURE_OPS.join(' | ')}) — nothing was run`;
|
|
53
56
|
|
|
54
|
-
const HELP = `ensure-configs — the
|
|
57
|
+
const HELP = `ensure-configs — the eight stamp-independent upgrade ensures, as ONE command.
|
|
55
58
|
|
|
56
59
|
Usage:
|
|
57
60
|
node ensure-configs.mjs --reconcile [--dry-run] [--only <op>] [--cwd <project>]
|
|
58
61
|
|
|
59
|
-
--reconcile required — run the
|
|
62
|
+
--reconcile required — run the eight ensures (${ENSURE_OPS.join(', ')})
|
|
60
63
|
--dry-run report what each ensure WOULD do; write nothing
|
|
61
64
|
--only <op> run EXACTLY ONE of them (an unknown, missing or repeated value is a usage error)
|
|
62
65
|
--cwd <dir> the target project (default: the current directory)
|
|
63
66
|
--help, -h this help
|
|
64
67
|
|
|
65
68
|
Every SEED is CREATE-ONLY: an existing file is preserved byte-for-byte, never clobbered and never
|
|
66
|
-
refreshed in place.
|
|
69
|
+
refreshed in place. Four ops refresh instead: the orchestration onboarding note, only while it still
|
|
67
70
|
matches a canonical the kit shipped (your own wording is preserved verbatim); the spec-layer reader
|
|
68
71
|
and checker pairs, only while a file's bytes are a body a release shipped (an edited file is
|
|
69
72
|
preserved, and the writes that depend on it — the spec store root included — are withheld behind
|
|
70
|
-
it);
|
|
73
|
+
it); the placed executor vehicle (.claude/agents/executor.md), re-derived from docs/ai/vehicles.json
|
|
74
|
+
whenever its bytes differ from the derived body (a hand edit is replaced, never preserved; an absent
|
|
75
|
+
file is never placed here); and the navigator index — a GENERATED artifact, regenerated
|
|
71
76
|
whenever it is missing or stale. The enforcement-script ensure detects an older ADR-store layout
|
|
72
77
|
FIRST and instructs the opt-in migration instead of seeding.
|
|
73
78
|
|
|
@@ -148,7 +153,7 @@ export const main = (argv = [], ctx = {}) => {
|
|
|
148
153
|
const cwd = resolve(args.cwd ?? ctx.cwd ?? process.cwd());
|
|
149
154
|
const deps = ctx.deps ?? {};
|
|
150
155
|
// ONE deployment gate for the whole run (see the header): with no docs/ai there is nothing to
|
|
151
|
-
// reconcile, and
|
|
156
|
+
// reconcile, and one identical STOP per op would read as that many separate problems.
|
|
152
157
|
assertDocsAiDeployment(cwd, deps, { noun: 'the project configuration', rel: 'under docs/ai' });
|
|
153
158
|
const outcomes = runEnsures({ cwd, kitRoot: ctx.kitRoot ?? KIT_ROOT, dryRun: args.dryRun, deps, only: args.only });
|
|
154
159
|
return {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// ensure-executor.mjs — the EIGHTH upgrade ensure, fifth in the fixed order right after `vehicles`:
|
|
2
|
+
// the placed executor vehicle (.claude/agents/executor.md) re-derived from docs/ai/vehicles.json.
|
|
3
|
+
// The agents writer PLACES the vehicle on consent; this ensure never does — it only keeps a placed
|
|
4
|
+
// body equal to the one derived from the settings, and a hand edit is replaced, never preserved.
|
|
5
|
+
//
|
|
6
|
+
// The state table, in the order the cells are proven (the write is the LAST step):
|
|
7
|
+
// | cell | .claude, .claude/agents | placed path | docs/ai/vehicles.json | answer | read before answering |
|
|
8
|
+
// |------|-------------------------|-------------|-----------------------|--------|-----------------------|
|
|
9
|
+
// | 0 | a symlink or not a directory | any | any | failed / wrong-node-kind | nothing |
|
|
10
|
+
// | 1 | absent or directories | a symlink, a directory, another non-regular node | any | failed / wrong-node-kind | nothing |
|
|
11
|
+
// | 2 | absent or directories | no entry | any, malformed included | not-placed | nothing |
|
|
12
|
+
// | 3 | directories | regular file | unreadable (malformed, read or lstat error) | failed / vehicle-settings-unreadable | the settings only |
|
|
13
|
+
// | 4 | directories | regular file | present or absent | failed / bundle-unreadable when the bundled template cannot be read | settings, bundle |
|
|
14
|
+
// | 5 | directories | regular file, bytes equal to the derived body | present or absent | already-current | settings, bundle, placed file |
|
|
15
|
+
// | 6 | directories | regular file, bytes differ | present or absent | dry-run would-re-derive; apply re-derived, or failed / write-refused | settings, bundle, placed file |
|
|
16
|
+
// | 7 | an lstat of `.claude`, `.claude/agents` or the placed path throwing other than ENOENT, or the read of the placed file throwing | | | NOT caught: the error propagates to the CLI's catch-all (`unexpected-error`) | |
|
|
17
|
+
//
|
|
18
|
+
// Dependency-free, Node >= 22. Every fs primitive is injectable (deps.*). No side effects on import.
|
|
19
|
+
|
|
20
|
+
import { readFileSync, lstatSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
23
|
+
import { composeFailure, composeOutcome, probeSeedTarget, tmpNote } from './ensure-ops.mjs';
|
|
24
|
+
import { assertDirSafe, AGENTS_DIR, CLAUDE_DIR, CHEAP_AGENTS_SYMLINK, EXECUTOR_VEHICLE_REL, executorTemplate, readExecutorPosture } from './cheap-agents-read.mjs';
|
|
25
|
+
import { VEHICLES_REL } from './vehicle-settings.mjs';
|
|
26
|
+
|
|
27
|
+
export const EXECUTOR_OP = 'executor';
|
|
28
|
+
|
|
29
|
+
const BUNDLE_DIR = ['references', 'agents'];
|
|
30
|
+
const ENCODING = 'utf8';
|
|
31
|
+
|
|
32
|
+
const ok = (token, lines) => composeOutcome(EXECUTOR_OP, token, lines, false);
|
|
33
|
+
const loud = (cause, ...lines) => composeFailure(EXECUTOR_OP, cause, ...lines);
|
|
34
|
+
const causeOf = (err) => String((err && err.message) || err);
|
|
35
|
+
|
|
36
|
+
export const ensureExecutor = ({ cwd, kitRoot, dryRun = false, deps = {} }) => {
|
|
37
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
38
|
+
const read = deps.readFile ?? readFileSync;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
assertDirSafe(join(cwd, CLAUDE_DIR), CLAUDE_DIR, { lstat });
|
|
42
|
+
assertDirSafe(join(cwd, AGENTS_DIR), AGENTS_DIR, { lstat });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err?.code !== CHEAP_AGENTS_SYMLINK) throw err;
|
|
45
|
+
return loud('wrong-node-kind', causeOf(err));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const target = join(cwd, EXECUTOR_VEHICLE_REL);
|
|
49
|
+
const probe = probeSeedTarget(target, lstat);
|
|
50
|
+
if (probe.wrongKind) {
|
|
51
|
+
return loud('wrong-node-kind', `${EXECUTOR_VEHICLE_REL}: exists but is ${probe.wrongKind} — nothing was read or written; resolve it by hand, then re-run`);
|
|
52
|
+
}
|
|
53
|
+
if (!probe.present) {
|
|
54
|
+
return ok('not-placed', [`${EXECUTOR_VEHICLE_REL}: not placed — the agents mode places it on consent; nothing written`]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { posture, reason } = readExecutorPosture(cwd, deps);
|
|
58
|
+
if (posture === null) {
|
|
59
|
+
return loud('vehicle-settings-unreadable', `${EXECUTOR_VEHICLE_REL}: ${reason} — nothing written; fix ${VEHICLES_REL} by hand, then re-run`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let body;
|
|
63
|
+
try {
|
|
64
|
+
body = executorTemplate(posture, { ...deps, bundleDir: join(kitRoot, ...BUNDLE_DIR) }).content;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return loud('bundle-unreadable', `${EXECUTOR_VEHICLE_REL}: the bundled executor template could not be read, so nothing was written — reinstall the kit. ${causeOf(err)}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const existing = read(target, ENCODING);
|
|
70
|
+
if (existing === body) {
|
|
71
|
+
return ok('already-current', [`${EXECUTOR_VEHICLE_REL}: the body derived from ${VEHICLES_REL} (model=${posture.model}, effort=${posture.effort}, source ${posture.source}) — nothing written`]);
|
|
72
|
+
}
|
|
73
|
+
if (dryRun) {
|
|
74
|
+
return ok('would-re-derive', [`${EXECUTOR_VEHICLE_REL}: differs from the body derived from ${VEHICLES_REL} — would be rewritten (a hand edit is replaced, never preserved)`]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = writeContainedFileAtomic(cwd, target, body, deps, { label: EXECUTOR_VEHICLE_REL });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return loud('write-refused', `${EXECUTOR_VEHICLE_REL}: ${causeOf(err)}`);
|
|
82
|
+
}
|
|
83
|
+
return ok('re-derived', [
|
|
84
|
+
`${EXECUTOR_VEHICLE_REL}: rewritten to the body derived from ${VEHICLES_REL} (model=${posture.model}, effort=${posture.effort}) — a hand edit was replaced; set the model in ${VEHICLES_REL}`,
|
|
85
|
+
...tmpNote(EXECUTOR_VEHICLE_REL, result.tmpLeftBehind),
|
|
86
|
+
]);
|
|
87
|
+
};
|
package/tools/ensure-ops.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// ensure-ops.mjs —
|
|
2
|
-
// outcome shape; the
|
|
1
|
+
// ensure-ops.mjs — the upgrade ensure operations this module owns, one function each, behind one shared
|
|
2
|
+
// outcome shape; the leaves that import this module compose their outcomes through
|
|
3
3
|
// the same door and probes exported below. The CLI that orders and runs them is ensure-configs.mjs —
|
|
4
4
|
// it owns the op table, so the import graph stays acyclic; this module owns what each ensure DOES
|
|
5
5
|
// and, more importantly, what it is allowed to CLAIM.
|
|
@@ -32,6 +32,7 @@ import { seedConfig, writeConfig } from './orchestration-write.mjs';
|
|
|
32
32
|
import { lstatNoFollow, writeDocsAiFileAtomic, writeProjectFileCreateOnly } from './atomic-write.mjs';
|
|
33
33
|
import { GATES_REL } from './gates-declaration.mjs';
|
|
34
34
|
import { AUTONOMY_REL } from './autonomy-config.mjs';
|
|
35
|
+
import { VEHICLES_REL } from './vehicle-settings.mjs';
|
|
35
36
|
import { surveyAdrLayoutStrict } from './family-registry.mjs';
|
|
36
37
|
import { ENSURE_TOKENS, FAILURE_CAUSES, SEED_SCRIPTS } from './ensure-vocabulary.mjs';
|
|
37
38
|
import { NODE_EVIDENCE, describeNodeProbes, probeNodeEvidence } from './node-evidence.mjs';
|
|
@@ -51,6 +52,9 @@ export {
|
|
|
51
52
|
} from './ensure-vocabulary.mjs';
|
|
52
53
|
|
|
53
54
|
const SCRIPTS_DIR = 'scripts';
|
|
55
|
+
const VEHICLES_OP = 'vehicles';
|
|
56
|
+
const VEHICLES_TEMPLATE = 'vehicles.json';
|
|
57
|
+
const VEHICLES_NOUN = 'a vehicle settings declaration';
|
|
54
58
|
|
|
55
59
|
const outcome = (op, token, lines, failed = false) => {
|
|
56
60
|
if (!ENSURE_TOKENS.includes(token)) {
|
|
@@ -98,7 +102,7 @@ export const probeSeedTarget = (abs, lstat) => {
|
|
|
98
102
|
export const tmpNote = (rel, tmpLeftBehind) =>
|
|
99
103
|
(tmpLeftBehind ? [`${rel}: the write stands, but its temp file could not be removed — delete it by hand: ${tmpLeftBehind}`] : []);
|
|
100
104
|
|
|
101
|
-
// ──
|
|
105
|
+
// ── orchestration.json — seed, or refresh ONLY a still-canonical onboarding note ────────────────
|
|
102
106
|
|
|
103
107
|
// Which no-change outcome is it? refreshReadme returns `changed: false` for two very different trees:
|
|
104
108
|
// a note that already IS the current canonical, and a note the user rewrote. Reporting both as
|
|
@@ -173,7 +177,7 @@ export const ensureOrchestration = ({ cwd, dryRun = false, deps = {} }) => {
|
|
|
173
177
|
return { ...refreshed, lines: [...refreshed.lines, ...seedNote] };
|
|
174
178
|
};
|
|
175
179
|
|
|
176
|
-
// ──
|
|
180
|
+
// ── gates.json + autonomy.json + vehicles.json — seed-if-missing, existing file preserved byte-for-byte ───────
|
|
177
181
|
|
|
178
182
|
const seedFromTemplate = ({ op, rel, template, noun, cwd, kitRoot, dryRun, deps }) => {
|
|
179
183
|
const lstat = deps.lstat ?? lstatSync;
|
|
@@ -202,7 +206,10 @@ export const ensureGates = ({ cwd, kitRoot, dryRun = false, deps = {} }) =>
|
|
|
202
206
|
export const ensureAutonomy = ({ cwd, kitRoot, dryRun = false, deps = {} }) =>
|
|
203
207
|
seedFromTemplate({ op: 'autonomy', rel: AUTONOMY_REL, template: 'autonomy.json', noun: 'an autonomy policy', cwd, kitRoot, dryRun, deps });
|
|
204
208
|
|
|
205
|
-
|
|
209
|
+
export const ensureVehicles = ({ cwd, kitRoot, dryRun = false, deps = {} }) =>
|
|
210
|
+
seedFromTemplate({ op: VEHICLES_OP, rel: VEHICLES_REL, template: VEHICLES_TEMPLATE, noun: VEHICLES_NOUN, cwd, kitRoot, dryRun, deps });
|
|
211
|
+
|
|
212
|
+
// ── scripts/ — the ADR-cascade enforcement pairs, detect-first ──────────────────────────────────
|
|
206
213
|
|
|
207
214
|
// The Node-evidence refusal every ensure that places Node scripts shares (contract: kit/node-evidence):
|
|
208
215
|
// null when Node provably runs here, else the ONE outcome the caller returns — a stated skip naming the
|
|
@@ -288,7 +295,7 @@ export const ensureScripts = ({ cwd, kitRoot, dryRun = false, deps = {} }) => {
|
|
|
288
295
|
return outcome('scripts', anyCreated ? 'seeded' : 'already-present', lines, false);
|
|
289
296
|
};
|
|
290
297
|
|
|
291
|
-
// ──
|
|
298
|
+
// ── docs/ai/index.md — the GENERATED navigator, regenerated when missing or stale ───────────────
|
|
292
299
|
|
|
293
300
|
// The only ensure whose target is generated rather than authored: there is nothing to preserve, and
|
|
294
301
|
// nothing to seed from either — the bundled generator IS the writer, driven through its idempotent
|
|
@@ -363,12 +370,13 @@ export const ensureIndex = ({ cwd, kitRoot, dryRun = false, deps = {} }) => {
|
|
|
363
370
|
return loud('index', 'index-probe-failed', `${INDEX_REL}: the generator reported a regeneration, but the verifying probe answered neither fresh nor stale — ${MAY_HAVE_WRITTEN}. ${verify.error ? causeOf(verify.error) : verdict}`);
|
|
364
371
|
};
|
|
365
372
|
|
|
366
|
-
// The
|
|
367
|
-
// the
|
|
373
|
+
// The ops this module owns, by name. The CLI composes the full ENSURE_OPS table from these plus
|
|
374
|
+
// the leaves that import THIS module, which join the table in ensure-configs.mjs (so it cannot live here).
|
|
368
375
|
export const OWN_IMPLEMENTATIONS = Object.freeze({
|
|
369
376
|
orchestration: ensureOrchestration,
|
|
370
377
|
gates: ensureGates,
|
|
371
378
|
autonomy: ensureAutonomy,
|
|
379
|
+
vehicles: ensureVehicles,
|
|
372
380
|
scripts: ensureScripts,
|
|
373
381
|
index: ensureIndex,
|
|
374
382
|
});
|
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
// The FIXED order the CLI runs them in — the order references/modes/upgrade.md already prescribed.
|
|
11
11
|
// `specs` sits after `scripts` (its reader pair is a scripts/ seed too) and BEFORE `index`: the store
|
|
12
12
|
// root it seeds is a docs/ai file the navigator must count.
|
|
13
|
-
|
|
13
|
+
// `executor` follows `vehicles`: its body is derived from the settings file that op seeds.
|
|
14
|
+
export const ENSURE_OPS = Object.freeze(['orchestration', 'gates', 'autonomy', 'vehicles', 'executor', 'scripts', 'specs', 'index']);
|
|
14
15
|
|
|
15
16
|
// Tokens that assert a WRITE happened. --dry-run may never emit one of these (the CLI's contract test
|
|
16
17
|
// walks this set), and each has exactly one `would-` counterpart below. `refreshed` is the spec-layer
|
|
17
18
|
// ensure's pair refresh (reader or checker) — a deployed script on a body a release shipped,
|
|
18
19
|
// rewritten to the bundled one (a custom body is never refreshed).
|
|
19
|
-
|
|
20
|
-
export const
|
|
20
|
+
// `re-derived` is the executor ensure rewriting a placed executor whose bytes differ from the body derived from docs/ai/vehicles.json.
|
|
21
|
+
export const WRITE_TOKENS = Object.freeze(['seeded', 'note-refreshed', 'refreshed', 'regenerated', 're-derived']);
|
|
22
|
+
export const DRY_RUN_TOKENS = Object.freeze(['would-seed', 'would-refresh-note', 'would-refresh', 'would-regenerate', 'would-re-derive']);
|
|
21
23
|
|
|
22
24
|
// The CLOSED outcome vocabulary. Closed at RUNTIME, not by convention: composing an outcome with a
|
|
23
25
|
// token outside this list throws, so an op cannot quietly invent a word the mode doc has never heard
|
|
@@ -29,6 +31,7 @@ export const ENSURE_TOKENS = Object.freeze([
|
|
|
29
31
|
...DRY_RUN_TOKENS,
|
|
30
32
|
'already-current',
|
|
31
33
|
'already-present',
|
|
34
|
+
'not-placed',
|
|
32
35
|
'customized-preserved',
|
|
33
36
|
'malformed-preserved',
|
|
34
37
|
'skipped-no-node-evidence',
|
|
@@ -48,6 +51,8 @@ export const FAILURE_CAUSES = Object.freeze([
|
|
|
48
51
|
// here is unproven, so the ensures that place Node scripts write nothing (contract: kit/node-evidence).
|
|
49
52
|
'node-evidence-unverifiable',
|
|
50
53
|
'wrong-node-kind',
|
|
54
|
+
// The settings file could not be read, so no body can be derived and nothing is written.
|
|
55
|
+
'vehicle-settings-unreadable',
|
|
51
56
|
'write-refused',
|
|
52
57
|
'unexpected-error',
|
|
53
58
|
// The navigator ensure drives a SEPARATE PROCESS (the bundled generator), so its failures split by
|
|
@@ -74,10 +79,12 @@ export const RELAYED_ENSURE_TOKENS = Object.freeze([
|
|
|
74
79
|
'note-refreshed',
|
|
75
80
|
'refreshed',
|
|
76
81
|
'regenerated',
|
|
82
|
+
're-derived',
|
|
77
83
|
'already-current',
|
|
78
84
|
'customized-preserved',
|
|
79
85
|
'malformed-preserved',
|
|
80
86
|
'already-present',
|
|
87
|
+
'not-placed',
|
|
81
88
|
'skipped-no-node-evidence',
|
|
82
89
|
'old-adr-layout-migration-instructed',
|
|
83
90
|
'failed',
|
|
@@ -85,7 +85,7 @@ import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
|
|
|
85
85
|
import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
|
|
86
86
|
import { NODE_EVIDENCE, NODE_EVIDENCE_SCRIPTS, probeNodeEvidence } from './node-evidence.mjs';
|
|
87
87
|
// The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
|
|
88
|
-
import { surveyExecutorVehicle, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
|
|
88
|
+
import { surveyExecutorVehicle, readExecutorPosture, executorTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
|
|
89
89
|
// The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
|
|
90
90
|
// (contract: kit/ack-store) — `status` reads the same store, and a second copy is what drifts.
|
|
91
91
|
import {
|
|
@@ -994,11 +994,16 @@ const probeStateBlockHook = ({ root, deps, add, skip }) => {
|
|
|
994
994
|
// surfaced its absence: `agents` is the family's SECOND `.claude/` writer, the `help` Tune tail
|
|
995
995
|
// advertises it, and the advisor had no entry for it — so a user who never runs `help` never learned
|
|
996
996
|
// it existed while the advisor reported the deployment optimal. Found by the guard, not by an incident.
|
|
997
|
-
// Only a PLACE action counts as a gap: `already-current` is converged
|
|
998
|
-
// the user's own edit
|
|
997
|
+
// Only a PLACE action counts as a gap: `already-current` is converged; a customized read-only vehicle or lens is
|
|
998
|
+
// 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).
|
|
999
999
|
const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
1000
1000
|
try {
|
|
1001
|
-
const
|
|
1001
|
+
const { posture, reason } = readExecutorPosture(root, deps);
|
|
1002
|
+
if (posture === null) {
|
|
1003
|
+
skip('agents', new Error(`${reason} — fix that file first`));
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const preflight = preflightCheapAgents({ cwd: root, derived: [executorTemplate(posture, deps)] }, deps);
|
|
1002
1007
|
// The writer refuses below the expected lineage, so offering its command would hand the user a
|
|
1003
1008
|
// guaranteed failure — the honest surface is a stated skip naming the recovery.
|
|
1004
1009
|
if (!preflight.stampOk) {
|
|
@@ -1050,15 +1055,16 @@ const probeExecutorVehicle = ({ root, deps, add, skip }) => {
|
|
|
1050
1055
|
const survey = (deps.surveyVehicle ?? surveyExecutorVehicle)(root, deps);
|
|
1051
1056
|
if (!VEHICLE_BROKEN_STATES.includes(survey.state)) return;
|
|
1052
1057
|
const reason = safeLine(survey.reason ?? '');
|
|
1058
|
+
const { posture, reason: settingsReason } = readExecutorPosture(root, deps);
|
|
1053
1059
|
const stamp = readStamp(join(root, WORKFLOW_STAMP), readFsDeps(deps));
|
|
1054
1060
|
const preconditions = [
|
|
1055
1061
|
...(stamp === EXPECTED_WORKFLOW_VERSION ? [] : [`run /agent-workflow-kit upgrade first (deployment stamp ${stamp ?? 'none'}, expected ${EXPECTED_WORKFLOW_VERSION})`]),
|
|
1056
|
-
...(
|
|
1062
|
+
...(posture === null ? [`${safeLine(settingsReason)} — fix that`] : []),
|
|
1057
1063
|
];
|
|
1058
1064
|
// The writer refuses on ANY vehicle path it cannot touch (a symlinked read-only vehicle blocks a
|
|
1059
|
-
// missing executor's placement too), so its own preflight is the last precondition
|
|
1065
|
+
// missing executor's placement too), so its own preflight is the last precondition.
|
|
1060
1066
|
const blocked = writerBlock(root, deps);
|
|
1061
|
-
if (blocked
|
|
1067
|
+
if (blocked) preconditions.push(`${blocked} — fix that`);
|
|
1062
1068
|
const room = templateBudget(WHATS['executor-vehicle']) - String(configured.length).length - survey.state.length - 2;
|
|
1063
1069
|
add(
|
|
1064
1070
|
'executor-vehicle',
|
|
@@ -1067,8 +1073,8 @@ const probeExecutorVehicle = ({ root, deps, add, skip }) => {
|
|
|
1067
1073
|
state: survey.state,
|
|
1068
1074
|
reason: reason && room > 0 ? `: ${truncatedTo(reason, room)}` : '',
|
|
1069
1075
|
}),
|
|
1070
|
-
// The writer places a
|
|
1071
|
-
//
|
|
1076
|
+
// The writer places a missing vehicle and re-derives a placed executor whose bytes differ from the derived body (an invalid
|
|
1077
|
+
// frontmatter or read-only tools included), so those states need no precondition; every precondition above is a refusal the apply would meet.
|
|
1072
1078
|
`${preconditions.length ? `HAND-APPLY: ${preconditions.join('; ')}, then run: ` : ''}node ${q(toolPath('cheap-agents.mjs'))} --apply --cwd ${q(root)}`,
|
|
1073
1079
|
'executor-vehicle',
|
|
1074
1080
|
`hidden-mode deployments only: after the apply, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
|
|
@@ -40,10 +40,12 @@ export const UPGRADE_RUNLIST = Object.freeze([
|
|
|
40
40
|
'note-refreshed',
|
|
41
41
|
'refreshed',
|
|
42
42
|
'regenerated',
|
|
43
|
+
're-derived',
|
|
43
44
|
'already-current',
|
|
44
45
|
'customized-preserved',
|
|
45
46
|
'malformed-preserved',
|
|
46
47
|
'already-present',
|
|
48
|
+
'not-placed',
|
|
47
49
|
'skipped-no-node-evidence',
|
|
48
50
|
'old-adr-layout-migration-instructed',
|
|
49
51
|
'failed',
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFileSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const VEHICLES_REL = 'docs/ai/vehicles.json';
|
|
5
|
+
export const EXECUTOR_DEFAULTS = Object.freeze({ model: 'opus', effort: 'high', fallback: 'sonnet' });
|
|
6
|
+
|
|
7
|
+
const README_KEY = '_README';
|
|
8
|
+
const EXECUTOR_KEY = 'executor';
|
|
9
|
+
const TOP_LEVEL_KEYS = Object.freeze([README_KEY, EXECUTOR_KEY]);
|
|
10
|
+
const EXECUTOR_KEYS = Object.freeze(Object.keys(EXECUTOR_DEFAULTS));
|
|
11
|
+
const OBJECT_TYPE = 'object';
|
|
12
|
+
const STRING_TYPE = 'string';
|
|
13
|
+
const EMPTY_STRING = '';
|
|
14
|
+
const TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
15
|
+
const ABSENT = 'absent';
|
|
16
|
+
const PRESENT = 'present';
|
|
17
|
+
const UNREADABLE = 'unreadable';
|
|
18
|
+
const FILE_SOURCE = 'file';
|
|
19
|
+
const DEFAULT_SOURCE = 'default';
|
|
20
|
+
const MISSING_ENTRY_CODE = 'ENOENT';
|
|
21
|
+
const ENCODING = 'utf8';
|
|
22
|
+
const CONFIG_EXIT_CODE = 1;
|
|
23
|
+
const BODY_NAME = 'body';
|
|
24
|
+
const UNKNOWN_KEY = 'unknown key';
|
|
25
|
+
const REQUIRED_KEY = 'required key is missing';
|
|
26
|
+
const OBJECT_REQUIRED = 'must be a plain object';
|
|
27
|
+
const STRING_REQUIRED = 'must be a string';
|
|
28
|
+
const NONEMPTY_REQUIRED = 'must not be empty';
|
|
29
|
+
const TOKEN_REQUIRED = 'must be a bare token';
|
|
30
|
+
const DISTINCT_FALLBACK_REQUIRED = 'fallback must differ from model';
|
|
31
|
+
const DIRECTORY_KIND = 'directory';
|
|
32
|
+
const SYMLINK_KIND = 'symlink';
|
|
33
|
+
const OTHER_KIND = 'not a regular file';
|
|
34
|
+
const UNKNOWN_STATE = 'unknown state';
|
|
35
|
+
|
|
36
|
+
const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
37
|
+
const isPlainObject = (value) => {
|
|
38
|
+
if (value === null || typeof value !== OBJECT_TYPE || Array.isArray(value)) return false;
|
|
39
|
+
const prototype = Object.getPrototypeOf(value);
|
|
40
|
+
return prototype === Object.prototype || prototype === null;
|
|
41
|
+
};
|
|
42
|
+
const refuseValue = (rel, key, problem) => ({ ok: false, reason: `${rel}: ${key} ${problem}` });
|
|
43
|
+
const unreadable = (reason) => ({ state: UNREADABLE, reason });
|
|
44
|
+
const describeError = (error) => String(error?.code ?? error?.message ?? error);
|
|
45
|
+
const formatReadError = (error) => `${VEHICLES_REL}: ${UNREADABLE} (${describeError(error)})`;
|
|
46
|
+
|
|
47
|
+
export const validateVehicles = (value, rel = VEHICLES_REL) => {
|
|
48
|
+
if (!isPlainObject(value)) return refuseValue(rel, BODY_NAME, OBJECT_REQUIRED);
|
|
49
|
+
for (const key of Object.keys(value)) {
|
|
50
|
+
if (!TOP_LEVEL_KEYS.includes(key)) return refuseValue(rel, key, UNKNOWN_KEY);
|
|
51
|
+
}
|
|
52
|
+
if (Object.hasOwn(value, README_KEY) && typeof value[README_KEY] !== STRING_TYPE) {
|
|
53
|
+
return refuseValue(rel, README_KEY, STRING_REQUIRED);
|
|
54
|
+
}
|
|
55
|
+
if (!Object.hasOwn(value, EXECUTOR_KEY)) return refuseValue(rel, EXECUTOR_KEY, REQUIRED_KEY);
|
|
56
|
+
const executor = value[EXECUTOR_KEY];
|
|
57
|
+
if (!isPlainObject(executor)) return refuseValue(rel, EXECUTOR_KEY, OBJECT_REQUIRED);
|
|
58
|
+
for (const key of Object.keys(executor)) {
|
|
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) {
|
|
72
|
+
return refuseValue(rel, EXECUTOR_KEY, DISTINCT_FALLBACK_REQUIRED);
|
|
73
|
+
}
|
|
74
|
+
const { model, effort, fallback } = executor;
|
|
75
|
+
return { ok: true, settings: { executor: { model, effort, fallback } } };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const probeEntry = (path, lstat) => {
|
|
79
|
+
try {
|
|
80
|
+
return { entry: lstat(path) };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return error?.code === MISSING_ENTRY_CODE
|
|
83
|
+
? { state: ABSENT }
|
|
84
|
+
: unreadable(formatReadError(error));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const describeNodeKind = (entry) => {
|
|
88
|
+
if (entry.isSymbolicLink()) return SYMLINK_KIND;
|
|
89
|
+
if (entry.isDirectory()) return DIRECTORY_KIND;
|
|
90
|
+
return OTHER_KIND;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const readVehicles = (cwd, readFile = readFileSync, lstat = lstatSync) => {
|
|
94
|
+
try {
|
|
95
|
+
const path = join(cwd, VEHICLES_REL);
|
|
96
|
+
const probe = probeEntry(path, lstat);
|
|
97
|
+
if (probe.state) return probe;
|
|
98
|
+
const { entry } = probe;
|
|
99
|
+
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
100
|
+
return unreadable(`${VEHICLES_REL}: ${describeNodeKind(entry)}`);
|
|
101
|
+
}
|
|
102
|
+
const value = JSON.parse(readFile(path, ENCODING));
|
|
103
|
+
const validation = validateVehicles(value);
|
|
104
|
+
return validation.ok
|
|
105
|
+
? { state: PRESENT, settings: validation.settings }
|
|
106
|
+
: unreadable(validation.reason);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return unreadable(formatReadError(error));
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const loadVehicles = (cwd, readFile = readFileSync, lstat = lstatSync) => {
|
|
113
|
+
const answer = readVehicles(cwd, readFile, lstat);
|
|
114
|
+
if (answer.state === UNREADABLE) throw fail(CONFIG_EXIT_CODE, answer.reason);
|
|
115
|
+
return answer;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const resolveExecutor = (answer) => {
|
|
119
|
+
if (answer?.state === PRESENT) {
|
|
120
|
+
return { posture: { ...answer.settings.executor, source: FILE_SOURCE }, reason: null };
|
|
121
|
+
}
|
|
122
|
+
if (answer?.state === ABSENT) {
|
|
123
|
+
return { posture: { ...EXECUTOR_DEFAULTS, source: DEFAULT_SOURCE }, reason: null };
|
|
124
|
+
}
|
|
125
|
+
if (answer?.state === UNREADABLE) return { posture: null, reason: answer.reason };
|
|
126
|
+
throw fail(CONFIG_EXIT_CODE, `${VEHICLES_REL}: ${UNKNOWN_STATE} ${String(answer?.state)}`);
|
|
127
|
+
};
|