@sabaiway/agent-workflow-kit 10.4.0 → 10.6.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 +109 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +7 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +69 -17
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +73 -2
- package/bridges/antigravity-cli-bridge/capability.json +2 -2
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +3 -0
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +89 -18
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +55 -2
- package/bridges/codex-cli-bridge/capability.json +2 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/review-lens.md +5 -3
- package/references/modes/agents.md +1 -1
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/procedures.md +11 -5
- package/references/modes/recipes.md +2 -2
- package/references/modes/review-state.md +3 -3
- package/references/modes/set-recipe.md +4 -4
- package/references/modes/status.md +1 -1
- package/references/modes/velocity.md +1 -0
- package/references/templates/orchestration.json +1 -1
- package/tools/bridge-posture.mjs +48 -0
- package/tools/carriers.mjs +21 -9
- package/tools/cheap-agents-read.mjs +86 -24
- package/tools/cheap-agents.mjs +47 -7
- package/tools/commit-guard.mjs +4 -3
- package/tools/detect-backends.mjs +2 -2
- package/tools/direct-run.mjs +3 -0
- package/tools/dispatch-store-read.mjs +269 -0
- package/tools/dispatch-store.mjs +21 -357
- package/tools/fold-scope.mjs +5 -60
- package/tools/grounding.mjs +2 -2
- package/tools/held-session.mjs +165 -0
- package/tools/orchestration-config.mjs +19 -78
- package/tools/orchestration-readme.mjs +70 -0
- package/tools/plan-shape-cli.mjs +112 -0
- package/tools/plan-shape-facts.mjs +204 -0
- package/tools/plan-shape.mjs +348 -0
- package/tools/procedures.mjs +161 -31
- package/tools/recipes.mjs +60 -79
- package/tools/repo-lex.mjs +40 -0
- package/tools/review-roster-resolve.mjs +104 -0
- package/tools/review-roster.mjs +128 -0
- package/tools/review-rounds-cli.mjs +92 -0
- package/tools/review-rounds.mjs +115 -0
- package/tools/review-state.mjs +85 -6
- package/tools/set-recipe-roster.mjs +167 -0
- package/tools/set-recipe.mjs +80 -23
- package/tools/velocity-profile.mjs +8 -22
|
@@ -13,7 +13,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/family-registry.mjs --json [--dir <project>]
|
|
|
13
13
|
> **Status reads `refresh`; the shared version block + the bootstrap/upgrade footers stay `notes`-based (unchanged this release).** `${CLAUDE_SKILL_DIR}/references/modes/status.md` has its OWN status-only render (above), keyed on `installed[].refresh.behind` / `refresh.recommend`. The shared **version block** (under *The version block + welcome mat* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`) and the bootstrap (step 11) + every upgrade (steps 4 / 8) report footer still consume `installed[].notes` verbatim — that wiring is deliberately **untouched** here (their migration onto `refresh` is deferred). Do not rewrite those footers onto `refresh`.
|
|
14
14
|
2. **Deployment (`--dir`)** (from `project`): whether `docs/ai/` is deployed + the deploy stamps by `display`; and **visibility** — render `project.visibility.state` in **user-safe words only**: *visible (tracked)* / *hidden (git-ignored, local-only)* / *unclear (uncommitted or partially set up)* — **never** the words "hidden fence" or any marker term. A `visibility.error` → surface it plainly. When `project.adrLayout` is **`old`** or **`old-unrotated`**, add a plain-language note that the project still uses an **older ADR layout** and should run the opt-in **`/agent-workflow-kit migrate-adr-store`** to move to the one-file-per-ADR store (preview first; it never commits) — the note is the SAME for both, they differ only in how the older layout was detected (`old` = a retired archive file is still on disk; `old-unrotated` = the project's deployed rotation script predates the store); `migrated` / `none` need no note. Then **one `specs` line, always** (from `project.specs` — the feature-spec layer's adoption state, judged from the store alone): `not adopted` (no `docs/ai/specs/` — the layer was never seeded), `adopting (N draft)` (a store with no live contract yet), `adopted (N live, M draft)`, or `could not be read — <reason>` (the store or a document in it could not be observed; never counted as any of the other three). A recorded decline (`project.specs.declined`) appends ` — declined` to the first two. The internal tokens are `not-adopted` / `adopting` / `adopted` / `unreadable`; render the plain phrases above, never the tokens. An envelope without the field says the installed kit predates it — say so rather than inventing a state.
|
|
15
15
|
3. **Settings (`--dir`, one line each)** (from `project.settings`):
|
|
16
|
-
- **recipes** — the effective recipe per slot, **one line per slot** over the three activities (`plan-authoring.author` / `.review`, `plan-execution.execute` / `.review`, `routine.carrier` / `.parallel`); each line also carries **where the value came from** (`source` — the project's config, an explicit override, or a computed default) and, when the resolved recipe is not the one that was asked for, **what it degraded from** (`degradedFrom`). Say both in plain words (never the field names): an effective recipe alone cannot tell a deliberate choice from a default, nor a degrade from a config that really names the resolved recipe — and a slot showing `solo` after a degrade from `subagent` is exactly the case the **subagents** line below explains. Detail → `/agent-workflow-kit procedures` / `recipes`; a `recipes.detectError` → say the backends couldn't be checked, so bridge-backed recipes floored at solo; the executor vehicle is unaffected.
|
|
16
|
+
- **recipes** — the effective recipe per slot, **one line per slot** over the three activities (`plan-authoring.author` / `.fold` / `.review`, `plan-execution.execute` / `.review`, `routine.carrier` / `.parallel`); each line also carries **where the value came from** (`source` — the project's config, an explicit override, or a computed default) and, when the resolved recipe is not the one that was asked for, **what it degraded from** (`degradedFrom`). Say both in plain words (never the field names): an effective recipe alone cannot tell a deliberate choice from a default, nor a degrade from a config that really names the resolved recipe — and a slot showing `solo` after a degrade from `subagent` is exactly the case the **subagents** line below explains. Detail → `/agent-workflow-kit procedures` / `recipes`; a `recipes.detectError` → say the backends couldn't be checked, so bridge-backed recipes floored at solo; the executor vehicle is unaffected.
|
|
17
17
|
- **attribution** — `includeCoAuthoredBy` effective; call out a **local override** only when `local` is non-null **and** differs from `project` (a `null` `local` means the key is absent there, so the project value stands — that is not an override).
|
|
18
18
|
- **velocity** — the effective `permissions.defaultMode` + whether an allowlist is seeded (detail → `/agent-workflow-kit velocity`).
|
|
19
19
|
- **subagents** — how many of the kit's subagent vehicles are placed (`agents.placed` of `agents.bundled`; zero placed → the optional `/agent-workflow-kit agents` opt-in), then the **split the line always states separately**: the **read-only vehicles** (`agents.readOnly` — four; none of them grants a shell, three ride a cheap model for mechanical work and one is a read-only review lens) and the **ONE full-tool `executor`**, whose own state is `agents.executor`: *placed* / *customized* (both dispatchable) / *unusable* (`agents.executorReason` carries the reason — say it) / *missing*. That state is what decides whether a slot configured `subagent` can ride it: on **unusable** or **missing** every such slot runs solo instead, so surface it plainly beside the recipes lines and point at `/agent-workflow-kit agents` to place it. It is a claim about the FILE, never about the host. An envelope with no executor state says the installed kit predates the field — say so rather than inventing a state.
|
|
@@ -35,6 +35,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [
|
|
|
35
35
|
- `node ${CLAUDE_SKILL_DIR}/tools/release-scan.mjs <path>` (wildcard)
|
|
36
36
|
- `node ${CLAUDE_SKILL_DIR}/tools/repo-search.mjs --pattern <literal> --path <p>` (wildcard — the LITERAL search lane; for a pattern carrying a shell-significant byte use `--pattern-file <p>` instead, and for a TARGET PATH carrying one use `--paths-file <p>` — either way the bytes never enter the command string. one of the TWO tier tools that carry hook residual coverage — see the honesty note below)
|
|
37
37
|
- `node ${CLAUDE_SKILL_DIR}/tools/path-inventory.mjs --path <p>` (wildcard — the read-only inventory lane: exists / type / size / `wc -l` lines / one-level listing / small-file contents for N paths in ONE call, so small path questions never need a composed shell. `--paths-file <p>` is its out-of-band target lane; hook residual coverage as with repo-search)
|
|
38
|
+
- `node ${CLAUDE_SKILL_DIR}/tools/review-rounds-cli.mjs --artifact <plan>` (wildcard — the read-only round table over the review receipts, the verdict half of the per-round emission the `procedures` advisor names every round; the path argument is resolved and compared, never opened as a pattern, so it rides the settings-level posture like the rest)
|
|
38
39
|
- `node ${CLAUDE_SKILL_DIR}/tools/run-gates.mjs --cwd ${PROJECT_ROOT}` — **EXACT byte-string only**, and honestly **project-exec, not read-only**: it runs YOUR declared `docs/ai/gates.json` commands — the same trust boundary the opt-in hook grants byte-exact per-cmd. A wildcard would be BROADER than that boundary (`--cwd <dir>` executes another project's declared gates), so the bare cwd-defaulting form, any other `--cwd`, `--only`, and **`--final`** forms all still prompt (`--final` WRITES the final-run receipt into the core-evidence store — a recording run is never auto-approved).
|
|
39
40
|
- `node ${CLAUDE_SKILL_DIR}/tools/source-size-check.mjs --check` — **EXACT byte-string only**, and only this READ-ONLY mode: the tool's `--write-baseline` and `--adopt` modes WRITE (the recorded ratchet, and a gate declaration) and are deliberately uncovered — they still prompt, always. Every other form, `--cwd` included, keeps prompting. **What it covers, precisely:** the agent's own direct invocation. It is **not** the byte-string a DECLARED gate carries — the fill emits that path double-quoted (a kit path with a space must survive), and a seedable allow rule may carry no quotes at all, so the two spellings cannot be one string; a declared gate is auto-approved by the opt-in hook instead, byte-exact against `docs/ai/gates.json`.
|
|
40
41
|
- Writer previews, **exact arg-free dry-run byte-strings only** (the SEEDED tier byte-string is the arg-free preview of each): `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/cheap-agents.mjs` · `node ${CLAUDE_SKILL_DIR}/tools/gate-hook.mjs` — every `--apply`/`--write`/`--yes` still prompts, always. (`gate-hook` also has a **`--read-lane`** flagged preview — the opt-in read-only compound lane, `${CLAUDE_SKILL_DIR}/references/modes/hook.md`; that flagged form is NOT the seeded arg-free byte-string, so it may **prompt once** — it IS a consent flow, stated, no silent cap.)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_README": "Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option never goes away. Three activities are configured independently, and so is each slot within them: 'plan-authoring' (slots author, review), 'plan-execution' (slots execute, review) and 'routine' (slots carrier, parallel). A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize); an 'execute' slot accepts solo | delegated | subagent (you implement / a backend runs a bounded sub-task / a full-tool frontier subagent carries a bounded slice you verify); the carrier slots 'plan-authoring.author' and 'routine.carrier' accept solo | subagent. 'routine.parallel' is a flag rather than a recipe: it accepts on | off and decides whether file-disjoint subagent slices dispatch concurrently. The default below is 'solo' for every recipe and carrier slot, and 'on' for the parallel switch — no execution backend required. Raise a slot to reviewed or council for a second opinion, or to delegated to hand off execution; those need an execution backend set up first. 'subagent' needs the executor vehicle placed in this project — the composition root's `agents` writer places it; without it the slot resolves to solo with the reason stated. Remove a slot's line, or a whole activity block (or run `set-recipe --unset <activity>.<slot>`), to fall back to the computed default: reviewed when a review backend is ready and otherwise solo for a review slot, solo for author, execute and carrier, on for parallel. Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your environment. Strict JSON — no comments.",
|
|
2
|
+
"_README": "Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option never goes away. Three activities are configured independently, and so is each slot within them: 'plan-authoring' (slots author, fold, review), 'plan-execution' (slots execute, review) and 'routine' (slots carrier, parallel). A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize), or an explicit roster array such as [\"codex-review\", \"agy-review\", \"review-lens\"] in hand-edit form; an 'execute' slot accepts solo | delegated | subagent (you implement / a backend runs a bounded sub-task / a full-tool frontier subagent carries a bounded slice you verify); the carrier slots 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier' accept solo | subagent. 'routine.parallel' is a flag rather than a recipe: it accepts on | off and decides whether file-disjoint subagent slices dispatch concurrently. The default below is 'solo' for every recipe and carrier slot, and 'on' for the parallel switch — no execution backend required. Raise a slot to reviewed or council for a second opinion, or to delegated to hand off execution; those need an execution backend set up first. 'subagent' needs the executor vehicle placed in this project — the composition root's `agents` writer places it; without it the slot resolves to solo with the reason stated. Remove a slot's line, or a whole activity block (or run `set-recipe --unset <activity>.<slot>`), to fall back to the computed default: reviewed when a review backend is ready and otherwise solo for a review slot, solo for author, fold, execute and carrier, on for parallel. Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your environment. Strict JSON — no comments.",
|
|
3
3
|
"plan-authoring": {
|
|
4
4
|
"review": "solo"
|
|
5
5
|
},
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
4
|
+
import { KNOWN_BACKENDS } from './detect-backends.mjs';
|
|
5
|
+
import { receiptIdOfCmd } from './carriers.mjs';
|
|
6
|
+
|
|
7
|
+
const oneLine = (value) => String(value).replace(/[\s]+/gu, ' ').trim();
|
|
8
|
+
|
|
9
|
+
const validPosture = (posture) => posture !== null && typeof posture === 'object' && !Array.isArray(posture)
|
|
10
|
+
&& typeof posture.model === 'string' && posture.model.length > 0
|
|
11
|
+
&& (!Object.hasOwn(posture, 'effort') || (typeof posture.effort === 'string' && posture.effort.length > 0))
|
|
12
|
+
&& (!Object.hasOwn(posture, 'tier') || posture.tier === null || (typeof posture.tier === 'string' && posture.tier.length > 0))
|
|
13
|
+
&& Object.keys(posture).every((key) => ['model', 'effort', 'tier'].includes(key));
|
|
14
|
+
|
|
15
|
+
const postureString = (posture, backend, settings) => {
|
|
16
|
+
const parts = [`model=${oneLine(posture.model)}`];
|
|
17
|
+
if (Object.hasOwn(posture, 'effort')) parts.push(`effort=${oneLine(posture.effort)}`);
|
|
18
|
+
if (Object.hasOwn(posture, 'tier')) {
|
|
19
|
+
const knob = (settings?.active ?? []).find((row) => row.key === 'CODEX_SERVICE_TIER' && row.bridge === backend.name);
|
|
20
|
+
parts.push(knob ? `tier=${oneLine(knob.value)} (bridge-settings)` : `tier=${posture.tier ?? 'standard'}`);
|
|
21
|
+
}
|
|
22
|
+
return parts.join(' ');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const posturesByBackend = (ctx = {}) => {
|
|
26
|
+
const bundleRoot = ctx.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
|
|
27
|
+
const read = ctx.readFile ?? readFileSync;
|
|
28
|
+
return Object.fromEntries(KNOWN_BACKENDS.flatMap((backend) => {
|
|
29
|
+
const cmd = backend.roleCmds?.review;
|
|
30
|
+
const receiptId = receiptIdOfCmd(cmd);
|
|
31
|
+
if (receiptId === null) return [];
|
|
32
|
+
const path = join(bundleRoot, backend.name, 'capability.json');
|
|
33
|
+
try {
|
|
34
|
+
const manifest = JSON.parse(String(read(path, 'utf8')));
|
|
35
|
+
if (!Object.hasOwn(manifest, 'posture')) {
|
|
36
|
+
return [[receiptId, { state: 'none', posture: null, path }]];
|
|
37
|
+
}
|
|
38
|
+
if (!validPosture(manifest.posture)) throw new Error('invalid posture block');
|
|
39
|
+
return [[receiptId, {
|
|
40
|
+
state: 'valid',
|
|
41
|
+
posture: postureString(manifest.posture, backend, ctx.settings ?? { active: [] }),
|
|
42
|
+
path,
|
|
43
|
+
}]];
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return [[receiptId, { state: 'unreadable', posture: null, path, reason: oneLine(error?.message ?? error) }]];
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
};
|
package/tools/carriers.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { refuseDirectRun } from './direct-run.mjs';
|
|
|
12
12
|
// `policy` marks an activity that is a SESSION with an autonomy level of its own; a routine chore
|
|
13
13
|
// runs inside such a session and carries none.
|
|
14
14
|
export const ACTIVITIES = {
|
|
15
|
-
'plan-authoring': { slots: { author: 'carrier', review: 'review' }, policy: true },
|
|
15
|
+
'plan-authoring': { slots: { author: 'carrier', fold: 'carrier', review: 'review' }, policy: true },
|
|
16
16
|
'plan-execution': { slots: { execute: 'execute', review: 'review' }, policy: true },
|
|
17
17
|
routine: { slots: { carrier: 'carrier', parallel: 'switch' }, policy: false },
|
|
18
18
|
};
|
|
@@ -31,6 +31,18 @@ export const SLOT_RECIPES = {
|
|
|
31
31
|
export const SWITCH_SLOT = 'switch';
|
|
32
32
|
export const SWITCH_DEFAULT = 'on';
|
|
33
33
|
|
|
34
|
+
const CODEX = 'codex-cli-bridge';
|
|
35
|
+
const AGY = 'antigravity-cli-bridge';
|
|
36
|
+
|
|
37
|
+
export const DISPLAY_ALIASES = Object.freeze({ [CODEX]: 'codex', [AGY]: 'agy' });
|
|
38
|
+
export const BACKEND_PRIORITY = Object.freeze([CODEX, AGY]);
|
|
39
|
+
export const REVIEW_CMD_ALIASES = Object.freeze({
|
|
40
|
+
'codex-review': Object.freeze({ backend: CODEX, receiptId: 'codex' }),
|
|
41
|
+
'agy-review': Object.freeze({ backend: AGY, receiptId: 'agy' }),
|
|
42
|
+
});
|
|
43
|
+
export const receiptIdOfCmd = (cmd) => REVIEW_CMD_ALIASES[cmd]?.receiptId ?? null;
|
|
44
|
+
export const LENS_VERDICTS = Object.freeze(['ship', 'ship with nits', 'revise', 'rethink']);
|
|
45
|
+
|
|
34
46
|
export const isSwitchSlot = (slotType) => slotType === SWITCH_SLOT;
|
|
35
47
|
|
|
36
48
|
export const CARRY_ROLE = 'carry';
|
|
@@ -90,12 +102,12 @@ export const vehicleDegradeReason = (survey, applyHint = EXECUTOR_APPLY) => {
|
|
|
90
102
|
// Pure constants. `procedures.mjs` prints them for a slot resolved to `subagent`; the wording is a
|
|
91
103
|
// red line, so a render composes these strings and never re-words one.
|
|
92
104
|
|
|
93
|
-
// The slice noun is per
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
'plan-authoring':
|
|
97
|
-
'plan-execution': 'a slice is a set of file-disjoint ledger rows; wording is copied verbatim where wording is a red line',
|
|
98
|
-
routine: "a slice is a bounded mechanical task; a read-only one (a sweep, gate triage) rides its placed read-only vehicle, or is carried solo with a stated reason when that vehicle is absent; a write-capable one (a regeneration, a fixture build) rides the executor; the changelog stays the orchestrator's",
|
|
105
|
+
// The slice noun is per SLOT — authoring and folding share an activity but carry different work.
|
|
106
|
+
export const SLICE_BY_SLOT = {
|
|
107
|
+
'plan-authoring.author': 'a slice is a brief naming the goal, the governing spec(s) and the ledger constraints; the subagent drafts the plan or the contract from it, and the orchestrator reviews the draft as its own',
|
|
108
|
+
'plan-authoring.fold': "a slice is the round's findings with their dispositions; the subagent edits the plan or the contract in place and returns; the orchestrator runs the self-consistency read itself",
|
|
109
|
+
'plan-execution.execute': 'a slice is a set of file-disjoint ledger rows; wording is copied verbatim where wording is a red line',
|
|
110
|
+
'routine.carrier': "a slice is a bounded mechanical task; a read-only one (a sweep, gate triage) rides its placed read-only vehicle, or is carried solo with a stated reason when that vehicle is absent; a write-capable one (a regeneration, a fixture build) rides the executor; the changelog stays the orchestrator's",
|
|
99
111
|
};
|
|
100
112
|
|
|
101
113
|
export const VEHICLE_STATE_TOKEN = '<state>';
|
|
@@ -120,10 +132,10 @@ export const SUBAGENT_SLOT_TYPES = Object.entries(SLOT_RECIPES)
|
|
|
120
132
|
.map(([slotType]) => slotType);
|
|
121
133
|
|
|
122
134
|
// dispatchForm({ activity, slot, state }) → the lines a `subagent`-resolved slot renders: the
|
|
123
|
-
//
|
|
135
|
+
// slot's slice sentence, then the four shared lines with the surveyed vehicle state filled in.
|
|
124
136
|
// A slot whose type cannot hold `subagent` (a review slot) and an unknown activity render nothing.
|
|
125
137
|
export const dispatchForm = ({ activity, slot, state } = {}) => {
|
|
126
|
-
const slice =
|
|
138
|
+
const slice = SLICE_BY_SLOT[`${activity}.${slot}`];
|
|
127
139
|
const slotType = ACTIVITIES[activity]?.slots?.[slot];
|
|
128
140
|
if (!slice || !SUBAGENT_SLOT_TYPES.includes(slotType)) return [];
|
|
129
141
|
return [slice, ...DISPATCH_LINES.map((line) => line.replace(VEHICLE_STATE_TOKEN, state ?? MISSING))];
|
|
@@ -7,6 +7,7 @@ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
7
7
|
import { dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { refuseDirectRun } from './direct-run.mjs';
|
|
10
|
+
import { deriveLensTemplate } from './review-roster-resolve.mjs';
|
|
10
11
|
|
|
11
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
|
|
@@ -23,6 +24,7 @@ const EXIT_PRECONDITION = 1;
|
|
|
23
24
|
export const CHEAP_AGENTS_STAMP = 'CHEAP_AGENTS_STAMP';
|
|
24
25
|
export const CHEAP_AGENTS_SYMLINK = 'CHEAP_AGENTS_SYMLINK';
|
|
25
26
|
export const CHEAP_AGENTS_BUNDLE = 'CHEAP_AGENTS_BUNDLE';
|
|
27
|
+
export const CHEAP_AGENTS_CONFIG = 'CHEAP_AGENTS_CONFIG';
|
|
26
28
|
|
|
27
29
|
export const makeCheapAgentsError = (code, message) =>
|
|
28
30
|
Object.assign(new Error(`${ERROR_PREFIX} ${message}`), { name: 'CheapAgentsError', code, exitCode: EXIT_PRECONDITION });
|
|
@@ -107,13 +109,23 @@ export const planPlacement = (templates, projectDir, deps = {}) => {
|
|
|
107
109
|
|
|
108
110
|
export const EXECUTOR_VEHICLE = 'executor.md';
|
|
109
111
|
export const EXECUTOR_VEHICLE_REL = `${AGENTS_DIR}/${EXECUTOR_VEHICLE}`;
|
|
112
|
+
export const EXECUTOR_VEHICLE_SPEC = Object.freeze({
|
|
113
|
+
stem: 'executor', template: 'executor', model: null, effort: null, tools: 'full', derived: false,
|
|
114
|
+
});
|
|
115
|
+
const READ_ONLY_TOOLS = new Set(['Read', 'Grep', 'Glob']);
|
|
110
116
|
|
|
111
117
|
// The YAML subset a vehicle's frontmatter is read with: a bare scalar, a single- or double-quoted
|
|
112
118
|
// scalar, or a flow sequence; an unquoted ` #comment` and surrounding whitespace are dropped first.
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
const stripComment = (raw) => String(raw ?? '').replace(/^((?:[^"'#]|"[^"]*"|'[^']*')*?)\s+#.*$/u, '$1').trim();
|
|
120
|
+
const cleanValue = (raw) => stripComment(raw).replace(/^(["'])(.*)\1$/u, '$2').replace(/^\[(.*)\]$/u, '$1').trim();
|
|
121
|
+
|
|
122
|
+
const scalarValue = (raw) => {
|
|
123
|
+
const value = stripComment(raw);
|
|
124
|
+
const quoted = value.match(/^(["'])(.*)\1$/u);
|
|
125
|
+
if (quoted) return quoted[2].trim();
|
|
126
|
+
return /^[[{|>]|["']/u.test(value) ? null : value;
|
|
116
127
|
};
|
|
128
|
+
const scalarOf = (frontmatter, key) => scalarValue(frontmatter.match(new RegExp(`^${key}:(.*)$`, 'mu'))?.[1]);
|
|
117
129
|
|
|
118
130
|
// The block-sequence items under the `tools:` key: `- item` lines indented deeper than the key,
|
|
119
131
|
// with blank and comment lines allowed between them; the first other line ends the list.
|
|
@@ -133,40 +145,90 @@ const blockItems = (frontmatter) => {
|
|
|
133
145
|
return items;
|
|
134
146
|
};
|
|
135
147
|
|
|
136
|
-
const
|
|
137
|
-
|
|
148
|
+
const frontmatterOf = (content) => String(content).replace(/\r\n/gu, '\n').match(/^---\n([\s\S]*?)\n---(?:\n|$)/u)?.[1] ?? '';
|
|
149
|
+
|
|
150
|
+
const listedTools = (frontmatter) => {
|
|
151
|
+
const inline = cleanValue(frontmatter.match(/^tools:(.*)$/mu)?.[1]);
|
|
152
|
+
return (inline || blockItems(frontmatter).join(', ')).split(',').map((tool) => cleanValue(tool)).filter(Boolean);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const executorFrontmatterRefusal = (frontmatter, stem) => {
|
|
138
156
|
for (const key of ['name', 'tools']) {
|
|
139
157
|
if ((frontmatter.match(new RegExp(`^${key}:`, 'gmu')) ?? []).length > 1) return `duplicate \`${key}:\` key in the frontmatter`;
|
|
140
158
|
}
|
|
141
|
-
if (
|
|
159
|
+
if (scalarOf(frontmatter, 'name') !== stem) return `frontmatter does not declare \`name: ${stem}\``;
|
|
142
160
|
if (!/^tools:/mu.test(frontmatter)) return null;
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const granted = listed.split(',').map((tool) => cleanValue(tool));
|
|
147
|
-
return granted.includes('Bash') ? null : `tools: ${listed} is read-only`;
|
|
161
|
+
const granted = listedTools(frontmatter);
|
|
162
|
+
if (granted.length === 0) return 'tools: is empty — grant a list that includes Bash, or drop the line';
|
|
163
|
+
return granted.includes('Bash') ? null : `tools: ${granted.join(', ')} is read-only`;
|
|
148
164
|
};
|
|
149
165
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
const lensFrontmatter = (frontmatter, stem) => {
|
|
167
|
+
for (const key of ['name', 'model', 'effort', 'tools']) {
|
|
168
|
+
const count = (frontmatter.match(new RegExp(`^${key}:`, 'gmu')) ?? []).length;
|
|
169
|
+
if (count !== 1) return { refusal: count === 0 ? `frontmatter has no \`${key}:\` key` : `duplicate \`${key}:\` key in the frontmatter` };
|
|
170
|
+
}
|
|
171
|
+
if (scalarOf(frontmatter, 'name') !== stem) {
|
|
172
|
+
return { refusal: `frontmatter does not declare \`name: ${stem}\`` };
|
|
173
|
+
}
|
|
174
|
+
const scalars = Object.fromEntries(['model', 'effort'].map((key) => [key, scalarOf(frontmatter, key)]));
|
|
175
|
+
for (const [key, value] of Object.entries(scalars)) {
|
|
176
|
+
if (value === null) return { refusal: `${key}: is not a scalar` };
|
|
177
|
+
if (value === '') return { refusal: `${key}: is empty` };
|
|
178
|
+
}
|
|
179
|
+
const tools = listedTools(frontmatter);
|
|
180
|
+
if (tools.length === 0) return { refusal: 'tools: must grant a non-empty read-only list' };
|
|
181
|
+
const unsafe = tools.find((tool) => !READ_ONLY_TOOLS.has(tool));
|
|
182
|
+
if (unsafe) return { refusal: `tools: grants non-read-only tool ${unsafe}` };
|
|
183
|
+
return { refusal: null, ...scalars };
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const templateFor = (spec, deps) => {
|
|
187
|
+
if (spec.template == null) return null;
|
|
188
|
+
const bundled = readBundledAgents(deps).find((item) => item.name === `${spec.template}.md`);
|
|
189
|
+
if (!bundled) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${spec.template}.md is missing from the bundle`);
|
|
190
|
+
return {
|
|
191
|
+
name: `${spec.stem}.md`,
|
|
192
|
+
content: spec.derived ? deriveLensTemplate(bundled.content, spec) : bundled.content,
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const surveyVehicle = (projectDir, spec, deps = {}) => {
|
|
197
|
+
const rel = `${AGENTS_DIR}/${spec.stem}.md`;
|
|
154
198
|
try {
|
|
155
|
-
const template =
|
|
156
|
-
if (!template) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${EXECUTOR_VEHICLE} is missing from the bundle`);
|
|
199
|
+
const template = templateFor(spec, deps);
|
|
157
200
|
const fs = readFsDeps(deps);
|
|
158
201
|
assertDirSafe(join(projectDir, CLAUDE_DIR), CLAUDE_DIR, fs);
|
|
159
202
|
assertDirSafe(join(projectDir, AGENTS_DIR), AGENTS_DIR, fs);
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
if (placement.action === '
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
203
|
+
const placeholder = template ?? { name: `${spec.stem}.md`, content: null };
|
|
204
|
+
const [placement] = planPlacement([placeholder], projectDir, deps);
|
|
205
|
+
if (placement.action === 'place') {
|
|
206
|
+
const reason = template === null ? 'no bundled template to derive it from' : null;
|
|
207
|
+
return { state: 'missing', reason, rel };
|
|
208
|
+
}
|
|
209
|
+
const frontmatter = frontmatterOf(placement.existing ?? template?.content);
|
|
210
|
+
if (placement.action === 'already-current') {
|
|
211
|
+
if (spec.tools !== 'read-only') return { state: 'placed', reason: null, rel };
|
|
212
|
+
const lens = lensFrontmatter(frontmatter, spec.stem);
|
|
213
|
+
return lens.refusal === null
|
|
214
|
+
? { state: 'placed', reason: null, rel, model: lens.model, effort: lens.effort }
|
|
215
|
+
: { state: 'unusable', reason: lens.refusal, rel };
|
|
216
|
+
}
|
|
217
|
+
if (spec.tools !== 'read-only') {
|
|
218
|
+
const refusal = executorFrontmatterRefusal(frontmatter, spec.stem);
|
|
219
|
+
return refusal === null
|
|
220
|
+
? { state: 'customized', reason: null, rel }
|
|
221
|
+
: { state: 'unusable', reason: refusal, rel };
|
|
222
|
+
}
|
|
223
|
+
const lens = lensFrontmatter(frontmatter, spec.stem);
|
|
224
|
+
return lens.refusal === null
|
|
225
|
+
? { state: 'customized', reason: null, rel, model: lens.model, effort: lens.effort }
|
|
226
|
+
: { state: 'unusable', reason: lens.refusal, rel };
|
|
167
227
|
} catch (err) {
|
|
168
228
|
return { state: 'unusable', reason: err?.message ?? String(err), rel };
|
|
169
229
|
}
|
|
170
230
|
};
|
|
171
231
|
|
|
232
|
+
export const surveyExecutorVehicle = (projectDir, deps = {}) => surveyVehicle(projectDir, EXECUTOR_VEHICLE_SPEC, deps);
|
|
233
|
+
|
|
172
234
|
refuseDirectRun(import.meta.url);
|
package/tools/cheap-agents.mjs
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// The family's second `.claude/` writer, the velocity-profile.mjs writer discipline verbatim:
|
|
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
|
-
// head (a dry-run stays usable
|
|
13
|
+
// head (a dry-run stays usable whatever the stamp says; an unreadable orchestration config
|
|
14
|
+
// STOPs both, since the derived lenses it names cannot be known);
|
|
14
15
|
// • symlink-safe — a symlinked `.claude` / `.claude/agents` / target file is a STOP, never a
|
|
15
16
|
// write-through;
|
|
16
17
|
// • NEVER overwrites an existing .claude/agents/ file whose content differs from the bundled
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
// apply report reminds you.
|
|
26
27
|
//
|
|
27
28
|
// Exit codes: 0 done / dry-run (incl. preserved customizations — a user's file is a legitimate
|
|
28
|
-
// state, not an error); 1 precondition STOP (stamp, symlink, missing bundle
|
|
29
|
+
// state, not an error); 1 precondition STOP (stamp, symlink, missing bundle, an unreadable
|
|
30
|
+
// orchestration config — the derived lenses it names cannot be known); 2 usage.
|
|
29
31
|
// Dependency-free, Node >= 22. No side effects on import.
|
|
30
32
|
|
|
31
33
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
@@ -33,6 +35,9 @@ import { join } from 'node:path';
|
|
|
33
35
|
import { fileURLToPath } from 'node:url';
|
|
34
36
|
import { isDirectRun } from './direct-run.mjs';
|
|
35
37
|
import { shellQuoteArg } from './repo-lex.mjs';
|
|
38
|
+
import { loadConfig } from './orchestration-config.mjs';
|
|
39
|
+
import { lensMembersOf } from './review-roster.mjs';
|
|
40
|
+
import { deriveLensTemplate, lensVehicleSpec } from './review-roster-resolve.mjs';
|
|
36
41
|
// The READ core, never a second copy: the bundle, the placement plan and the executor survey live
|
|
37
42
|
// there so the read-only advisor graph can reach them without reaching this writer.
|
|
38
43
|
import {
|
|
@@ -42,6 +47,9 @@ import {
|
|
|
42
47
|
EXPECTED_WORKFLOW_VERSION,
|
|
43
48
|
UTF8,
|
|
44
49
|
CHEAP_AGENTS_STAMP,
|
|
50
|
+
CHEAP_AGENTS_BUNDLE,
|
|
51
|
+
CHEAP_AGENTS_CONFIG,
|
|
52
|
+
EXECUTOR_VEHICLE,
|
|
45
53
|
makeCheapAgentsError,
|
|
46
54
|
readFsDeps,
|
|
47
55
|
readBundledAgents,
|
|
@@ -59,6 +67,7 @@ export {
|
|
|
59
67
|
CHEAP_AGENTS_STAMP,
|
|
60
68
|
CHEAP_AGENTS_SYMLINK,
|
|
61
69
|
CHEAP_AGENTS_BUNDLE,
|
|
70
|
+
CHEAP_AGENTS_CONFIG,
|
|
62
71
|
makeCheapAgentsError,
|
|
63
72
|
readBundledAgents,
|
|
64
73
|
planPlacement,
|
|
@@ -86,6 +95,7 @@ model. The fifth, executor, is the ONE full-tool vehicle: dispatched only for a
|
|
|
86
95
|
authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and
|
|
87
96
|
it never commits.
|
|
88
97
|
Default is --dry-run (a preview; writes nothing). --apply writes.
|
|
98
|
+
Configured derived review lenses are planned and placed beside the bundled vehicles.
|
|
89
99
|
An existing file with DIFFERENT content is preserved and reported, never overwritten.`;
|
|
90
100
|
|
|
91
101
|
export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
@@ -95,10 +105,12 @@ const writeFsDeps = (deps = {}) => ({
|
|
|
95
105
|
writeFile: deps.writeFile ?? writeFileSync,
|
|
96
106
|
});
|
|
97
107
|
|
|
98
|
-
export const preflightCheapAgents = ({ cwd }, deps = {}) => {
|
|
108
|
+
export const preflightCheapAgents = ({ cwd, derived = [] }, deps = {}) => {
|
|
99
109
|
const fs = readFsDeps(deps);
|
|
100
110
|
const projectDir = cwd ?? process.cwd();
|
|
101
|
-
const
|
|
111
|
+
const templatesByName = new Map(readBundledAgents(deps).map((template) => [template.name, template]));
|
|
112
|
+
for (const template of derived) templatesByName.set(template.name, template);
|
|
113
|
+
const templates = [...templatesByName.values()];
|
|
102
114
|
const stamp = readStamp(join(projectDir, WORKFLOW_STAMP), fs);
|
|
103
115
|
const stampOk = stamp === EXPECTED_WORKFLOW_VERSION;
|
|
104
116
|
assertDirSafe(join(projectDir, CLAUDE_DIR), CLAUDE_DIR, fs);
|
|
@@ -107,11 +119,38 @@ export const preflightCheapAgents = ({ cwd }, deps = {}) => {
|
|
|
107
119
|
return { projectDir, stamp, stampOk, plan };
|
|
108
120
|
};
|
|
109
121
|
|
|
122
|
+
const loadConfigOrStop = (cwd, deps) => {
|
|
123
|
+
try {
|
|
124
|
+
return loadConfig(cwd, deps.readFile, deps.lstat).config;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
throw makeCheapAgentsError(CHEAP_AGENTS_CONFIG, `${err?.message ?? err} — the agents writer cannot read the configured review lenses — nothing is placed`);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const configuredDerivedTemplates = (cwd, deps) => {
|
|
131
|
+
const config = loadConfigOrStop(cwd, deps);
|
|
132
|
+
const bundle = readBundledAgents(deps);
|
|
133
|
+
const templates = new Map();
|
|
134
|
+
for (const member of lensMembersOf(config)) {
|
|
135
|
+
const spec = lensVehicleSpec(member);
|
|
136
|
+
if (!spec.derived) continue;
|
|
137
|
+
const base = bundle.find((template) => template.name === `${spec.template}.md`);
|
|
138
|
+
if (!base) throw makeCheapAgentsError(CHEAP_AGENTS_BUNDLE, `${spec.template}.md is missing from the bundle`);
|
|
139
|
+
templates.set(`${spec.stem}.md`, { name: `${spec.stem}.md`, content: deriveLensTemplate(base.content, spec) });
|
|
140
|
+
}
|
|
141
|
+
return [...templates.values()];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const applyCheapAgentsCommand = (root) =>
|
|
145
|
+
`node ${shellQuoteArg(fileURLToPath(import.meta.url))} --apply --cwd ${shellQuoteArg(root)}`;
|
|
146
|
+
|
|
110
147
|
// ── the writer ────────────────────────────────────────────────────────────────────────
|
|
111
148
|
|
|
112
149
|
export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
113
150
|
const fs = writeFsDeps(deps);
|
|
114
|
-
const
|
|
151
|
+
const projectDir = cwd ?? process.cwd();
|
|
152
|
+
const derived = configuredDerivedTemplates(projectDir, deps);
|
|
153
|
+
const preflight = preflightCheapAgents({ cwd: projectDir, derived }, deps);
|
|
115
154
|
if (dryRun) return { wrote: false, dryRun: true, ...preflight };
|
|
116
155
|
|
|
117
156
|
if (!preflight.stampOk) {
|
|
@@ -147,8 +186,9 @@ export const formatResult = (result) => {
|
|
|
147
186
|
if (!result.stampOk) {
|
|
148
187
|
lines.push(`note: no current deployment stamp found (${result.stamp ?? 'none'}) — --apply will refuse until init/upgrade runs.`);
|
|
149
188
|
}
|
|
189
|
+
const readOnlyCount = result.plan.filter((item) => item.name !== EXECUTOR_VEHICLE).length;
|
|
150
190
|
lines.push(
|
|
151
|
-
|
|
191
|
+
`${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.`,
|
|
152
192
|
`three of those ride the cheap lane (model: haiku, effort: low) for mechanical work; ${FALLBACK_LENS_ADDITIONAL_ONLY}`,
|
|
153
193
|
'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.',
|
|
154
194
|
);
|
|
@@ -156,7 +196,7 @@ export const formatResult = (result) => {
|
|
|
156
196
|
// item's one-liner, and that flow's contract is "run the printed command, no improvisation" — a
|
|
157
197
|
// bare "re-run with --apply" would leave the caller to reconstruct --cwd and its quoting.
|
|
158
198
|
if (result.dryRun && result.plan.some((item) => item.action === 'place')) {
|
|
159
|
-
lines.push(`to apply, run exactly:
|
|
199
|
+
lines.push(`to apply, run exactly: ${applyCheapAgentsCommand(result.projectDir)}`);
|
|
160
200
|
}
|
|
161
201
|
if (!result.dryRun && result.wrote) {
|
|
162
202
|
lines.push('hidden-mode note: if this deployment is hidden, run the hide-footprint reconcile so the placed files stay out of `git status`.');
|
package/tools/commit-guard.mjs
CHANGED
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
// reason verbatim — a foreign worktree's chain stays advisory; the armed state extends the
|
|
35
35
|
// PASS line;
|
|
36
36
|
// 4. re-computes the review-state decision (the ship-receipt arm) — a missing/vetoed ship
|
|
37
|
-
// receipt refuses. The env it hands over is SANITIZED
|
|
38
|
-
//
|
|
37
|
+
// receipt refuses. The env it hands over is SANITIZED; four producer seams are stripped:
|
|
38
|
+
// receipts/evidence/flow-store/delegation-store. A poisoned override can neither redirect nor mask any store this guard
|
|
39
39
|
// reads.
|
|
40
40
|
// `git commit --no-verify` stays the stated residual (a self-discipline mechanism, not a security
|
|
41
41
|
// boundary). Read-only; dependency-free; Node >= 22. No side effects on import.
|
|
@@ -323,6 +323,7 @@ export const runGuard = ({ cwd = process.cwd(), env = process.env } = {}) => {
|
|
|
323
323
|
delete reviewEnv.AW_REVIEW_RECEIPTS;
|
|
324
324
|
delete reviewEnv.AW_CORE_EVIDENCE;
|
|
325
325
|
delete reviewEnv.AW_FLOW_STORE;
|
|
326
|
+
delete reviewEnv.AW_DELEGATION_STORE;
|
|
326
327
|
const review = decideCheck(buildState({ cwd, env: reviewEnv }));
|
|
327
328
|
if (review.code !== 0) {
|
|
328
329
|
return { code: 1, lines: [`commit-guard: REFUSED — the review obligations are not satisfied: ${review.reason}`] };
|
|
@@ -354,7 +355,7 @@ the LATEST completed run-gates --final receipt — refusing on { no receipt for
|
|
|
354
355
|
latest attempt · before≠after · declaration content drift · evidence-hash drift · lcov drift ·
|
|
355
356
|
a flow-store refusal (a PRESENT store's open own chain / base motion / coverage — verbatim; no
|
|
356
357
|
store file = byte-exact pre-flow behavior) · unsatisfied review obligations (the review-state
|
|
357
|
-
decision, over a sanitized env — receipts/evidence/flow-store seams stripped) }. Wire it into
|
|
358
|
+
decision, over a sanitized env — receipts/evidence/flow-store/delegation-store seams stripped) }. Wire it into
|
|
358
359
|
pre-commit; \`git commit --no-verify\` stays the stated residual (self-discipline, not a security
|
|
359
360
|
boundary).
|
|
360
361
|
|
|
@@ -97,7 +97,7 @@ const RAW_BACKENDS = [
|
|
|
97
97
|
],
|
|
98
98
|
grounding: 'automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags',
|
|
99
99
|
continue: [],
|
|
100
|
-
receipt: 'side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit\'s review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized \'Verdict: <ship|revise|rethink>\' line — empty or missing output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review',
|
|
100
|
+
receipt: 'side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; durationS = integer wall-clock seconds from CLI start through verdict parsing and the wrapper prints review duration: <n>s; blocking = the count of [blocker] and [major] lines (schema mode: findings with either severity; a payload whose findings cannot be counted fails the run, no receipt); artifactPath = the normalized realpath on plan receipts only (repo-relative inside the work tree, absolute otherwise), while a double quote, backslash or control byte refuses pre-spend because the receipt encoder cannot carry it; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit\'s review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized \'Verdict: <ship|revise|rethink>\' line — empty or missing output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review',
|
|
101
101
|
notes: [
|
|
102
102
|
'the review posture banner appends a banner-only timeout=<duration> field — exactly the duration handed to timeout(1); the hard-timeout preflight fails CLOSED when no timeout/gtimeout binary exists (the wrapper refuses by name before any CLI run, so an uncapped review run can no longer happen), and the field never enters the receipt posture or the D5 banner↔receipt parity',
|
|
103
103
|
'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
|
|
@@ -139,7 +139,7 @@ const RAW_BACKENDS = [
|
|
|
139
139
|
'agy-review --continue [--decided @f] [--focus "…"] [--nonce <n>]',
|
|
140
140
|
'agy-review --conversation <id> [--decided @f] [--focus "…"] [--nonce <n>]',
|
|
141
141
|
],
|
|
142
|
-
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review",
|
|
142
|
+
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; durationS = integer wall-clock seconds from CLI start through verdict parsing and the wrapper prints review duration: <n>s; blocking = the count of numbered items in the first ### Blocking section; artifactPath = the normalized realpath on plan/diff receipts only (repo-relative inside the work tree, absolute otherwise), while a double quote, backslash or control byte refuses pre-spend because the receipt encoder cannot carry it; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review",
|
|
143
143
|
notes: [
|
|
144
144
|
'transport: every review dispatch drives the CLI in --output-format json (plus --disable-slash-commands) and the returned envelope is parsed in node (bin/agy-envelope.mjs) — the operator-facing invocations and flags above do NOT change, and on a ZERO exit the wrapper still PRINTS the review text, never JSON. A missing or unreadable envelope on a zero exit is a loud failure with NO receipt, never a downgraded verdict and never a fallback to raw-stdout parsing; a non-zero CLI exit keeps its own code and message, and publishes the captured stdout unchanged from the SINGLE dispatch or the FINAL fed turn (which may therefore be a JSON or partial payload — the envelope is parsed only on a zero exit); an INTERMEDIATE feed turn is the exception, its output stays private (Invariant E) and its failure prints only a named error. Enforced by a PRE-SPEND capability probe, not a version floor: agy --help must advertise --output-format and --disable-slash-commands, node must be >= 22, and bin/agy-envelope.mjs must be present — otherwise the review refuses before any run is spent and names the missing capability',
|
|
145
145
|
'pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt',
|
package/tools/direct-run.mjs
CHANGED
|
@@ -62,6 +62,9 @@ export const LIBRARY_ONLY_MODULES = Object.freeze({
|
|
|
62
62
|
// The READ core of the subagent-vehicle surface: it sits one name away from the writer
|
|
63
63
|
// references/modes/agents.md DOES name, and reaching for it is reaching for the agents mode.
|
|
64
64
|
'cheap-agents-read.mjs': '/agent-workflow-kit agents',
|
|
65
|
+
'review-roster.mjs': '/agent-workflow-kit recipes',
|
|
66
|
+
'review-roster-resolve.mjs': '/agent-workflow-kit recipes',
|
|
67
|
+
'set-recipe-roster.mjs': '/agent-workflow-kit set-recipe',
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
// The frozen refusal line. One line, names the module, names the command.
|