@sabaiway/agent-workflow-kit 1.45.0 → 1.46.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.
@@ -6,19 +6,23 @@
6
6
  // bridge allowlist, autonomy render drifted, sandbox not provisioned, gates undeclared) — every
7
7
  // `upgrade` run therefore ends with a mandatory, deterministic Recommendations section: what is
8
8
  // sub-optimal · the benefit in ONE plain line · the exact consent-gated apply one-liner. The agent
9
- // pastes the section VERBATIM (the composeStatusLine precedent) and then OFFERS to apply; the user
10
- // picks items in plain language; the agent runs EXACTLY the rendered one-liners no improvisation,
11
- // each writer's own consent semantics intact.
9
+ // PRESENTS the section in the user's conversational language every fact, count and item from
10
+ // the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; the raw
11
+ // tool block shown on request (the AD-032 report-contract lane). The user picks items in plain
12
+ // language; after the per-item consent moment the agent runs EXACTLY the rendered one-liners — no
13
+ // improvisation, each writer's own consent semantics intact.
12
14
  //
13
15
  // Contract:
14
16
  // node recommendations.mjs --cwd <project-root> [--json]
15
17
  // --cwd is REQUIRED (subdir-proof: the target project is explicit, never inferred from the shell's
16
18
  // current directory). The section renders PRESENT-EVEN-WHEN-EMPTY (the exact empty-state line
17
- // below). Benefit lines are frozen tool DATA, fact-true: the dual velocity+security wording rides
18
- // ONLY items with a real security delta (sandbox/autonomy render); the bridge-wrappers item claims
19
- // velocity only. A probe failure is a stated skipped-item line never a crash, never a fabricated
20
- // item. The network-allowlist item is HAND-APPLY by design (bridge council 2026-07-11, both
21
- // backends concur): the kit never seeds sandbox.network.allowedDomains / filesystem.allowWrite.
19
+ // below) and VERDICT-FIRST (D1): every non-optimal state opens with ONE composed verdict line.
20
+ // Registry strings are frozen tool DATA, fact-true, one line under the shape cap (D2); posture/
21
+ // risk prose lives in the mode doc at the consent moment (D3). A probe failure is a stated
22
+ // skipped-item line — never a crash, never a fabricated item. The sandbox-lane item is HAND-APPLY
23
+ // by design (bridge council 2026-07-11, both backends concur): the kit never seeds
24
+ // sandbox.network.allowedDomains / filesystem.allowWrite; its convergence is a NEUTRAL
25
+ // fingerprint-bound acknowledgement, never a security key (D4).
22
26
  //
23
27
  // Read-only: never writes, never commits, never runs a subscription CLI. The reused probes are all
24
28
  // exported read-only surfaces of their owning tools (velocity/autonomy/doctor/backends/recipes/
@@ -26,6 +30,8 @@
26
30
  // Dependency-free, Node >= 18. No side effects on import (the isDirectRun idiom).
27
31
 
28
32
  import { readFileSync, readdirSync, lstatSync } from 'node:fs';
33
+ import { createHash } from 'node:crypto';
34
+ import { homedir } from 'node:os';
29
35
  import { dirname, join, resolve } from 'node:path';
30
36
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
37
  import {
@@ -60,24 +66,141 @@ export const RECOMMENDATIONS_EMPTY_LINE = 'no recommendations — flow optimal.'
60
66
  // The one dual-wording security clause — rides ONLY the items with a real security delta.
61
67
  export const DUAL_SECURITY_BENEFIT = 'safer — blast radius bounded by the OS sandbox, not human attention';
62
68
 
69
+ // ── the verdict-first contract (D1, REC-UX-REWORK) ──────────────────────────────────────────────
70
+ // The optimal state (no items, no skips) renders the frozen empty-state line ALONE — byte-identical
71
+ // to the pre-verdict contract. Every other state opens the body with ONE verdict line composed from
72
+ // these frozen templates ({X}-style placeholders; the "(s)" invariant form IS the pinned
73
+ // pluralization rule — no singular/plural branching). The templates are English tool DATA
74
+ // (doc-parity-bound in both mode docs); user-language rendering is the agent's presentation layer.
75
+ export const VERDICT_ATTENTION_TEMPLATE = '{K} item(s) need attention';
76
+ export const VERDICT_NOTHING_BROKEN = 'nothing is broken';
77
+ export const VERDICT_OPTIONAL_TEMPLATE = '{N} optional recommendation(s), apply any you want';
78
+ export const VERDICT_SKIPS_TEMPLATE = 'optimality NOT attested — {M} probe check(s) skipped';
79
+
80
+ // ── the frozen severity registry (D1; pinned by tests) ─────────────────────────────────────────
81
+ // `attention` — the item reports a CONFIGURED declaration that is broken, drifted, degrading or
82
+ // invalid (the deployment needs review); `optional` — an offer to enable an unconfigured
83
+ // capability. One class per key, frozen registry data; a `<key>.<variant>` entry classes a
84
+ // per-site arm whose semantics differ from its base (the invalid-env arm reports an INVALID
85
+ // configured value — attention — while the unset arm stays an offer).
86
+ export const SEVERITY_ATTENTION = 'attention';
87
+ export const SEVERITY_OPTIONAL = 'optional';
88
+ export const SEVERITIES = Object.freeze({
89
+ 'velocity-core': SEVERITY_OPTIONAL,
90
+ 'kit-tools-tier': SEVERITY_OPTIONAL,
91
+ 'bridge-tier': SEVERITY_OPTIONAL,
92
+ 'autonomy-policy': SEVERITY_OPTIONAL,
93
+ 'autonomy-render': SEVERITY_ATTENTION,
94
+ 'sandbox-provision': SEVERITY_OPTIONAL,
95
+ 'review-recipe': SEVERITY_ATTENTION,
96
+ 'gates-declaration': SEVERITY_OPTIONAL,
97
+ 'gate-hook': SEVERITY_OPTIONAL,
98
+ 'family-freshness': SEVERITY_ATTENTION,
99
+ 'sandbox-masks': SEVERITY_OPTIONAL,
100
+ 'agy-adddir': SEVERITY_OPTIONAL,
101
+ 'agy-adddir.invalid-env': SEVERITY_ATTENTION,
102
+ 'sandbox-lane': SEVERITY_OPTIONAL,
103
+ });
104
+ // The per-item render tags (frozen presentation data, same language contract as the templates).
105
+ export const SEVERITY_LABELS = Object.freeze({
106
+ [SEVERITY_ATTENTION]: 'needs attention',
107
+ [SEVERITY_OPTIONAL]: 'optional',
108
+ });
109
+
110
+ // {X}-style template fill (D1/D2): every placeholder must be supplied — a miss is a programming
111
+ // error that surfaces through the probe's stated-skip lane, never a rendered "{K}".
112
+ const fillTemplate = (template, values) => template.replace(/\{([A-Za-z]+)\}/g, (_, name) => {
113
+ if (!(name in values)) throw new Error(`unfilled template placeholder {${name}}`);
114
+ return String(values[name]);
115
+ });
116
+
117
+ // composeVerdict(counts) → the ONE verdict line, or null for the optimal state (D1 state matrix).
118
+ // attention>0 leads; the "nothing is broken" wording renders ONLY when attention==0 AND skipped==0
119
+ // (a skipped probe could hide an attention-class problem, so the claim would overreach; it renders
120
+ // only as the lead-in to the optional offer, never in a skips-only state); the skips part is
121
+ // appended last.
122
+ export const composeVerdict = ({ attention, optional, skipped }) => {
123
+ if (attention === 0 && optional === 0 && skipped === 0) return null;
124
+ const parts = [];
125
+ if (attention > 0) parts.push(fillTemplate(VERDICT_ATTENTION_TEMPLATE, { K: attention }));
126
+ if (optional > 0) {
127
+ const offer = fillTemplate(VERDICT_OPTIONAL_TEMPLATE, { N: optional });
128
+ parts.push(attention === 0 && skipped === 0 ? `${VERDICT_NOTHING_BROKEN} — ${offer}` : offer);
129
+ }
130
+ if (skipped > 0) parts.push(fillTemplate(VERDICT_SKIPS_TEMPLATE, { M: skipped }));
131
+ return parts.join('; ');
132
+ };
133
+
134
+ // ── the frozen WHAT-template registry (D2; pinned by the static shape test) ─────────────────────
135
+ // Every static WHAT template lives here — `<key>` is the item key, `<key>.<variant>` a per-site
136
+ // variant of the same item — so ALL variants are assertable at build time (single line, char cap,
137
+ // banned tokens), never a fixture-coverage gamble. A pure-placeholder template marks a WHAT whose
138
+ // content is fully dynamic (capped at composition by truncation-with-count).
139
+ export const WHATS = Object.freeze({
140
+ 'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
141
+ 'kit-tools-tier': "the kit's own read-only tools still prompt — {n} kit-tools tier entr(ies) not seeded",
142
+ 'bridge-tier': 'council review runs prompt per bridge invocation — {n} bridge-wrappers tier entr(ies) not seeded (placed bridges only, code mode only)',
143
+ 'autonomy-policy': 'no {path} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)',
144
+ 'autonomy-render': 'the declared autonomy policy is not rendered into .claude/settings.json — drift: {drift}',
145
+ 'sandbox-provision': 'the OS sandbox is unavailable: {reason}',
146
+ 'sandbox-provision.installable': 'the OS sandbox is unavailable: {reason} — installable via the doctor (consent tuple {tuple})',
147
+ 'review-recipe': '{degraded}',
148
+ 'gates-declaration': 'no declared gate matrix (docs/ai/gates.json absent or empty) — gates prompt one by one; the apply PREVIEWS its --apply line, writes nothing',
149
+ 'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
150
+ 'family-freshness': '{parts}',
151
+ 'sandbox-masks': '{n} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale',
152
+ 'sandbox-masks.stale-real': '{n} sandbox device mask(s) clutter git status — the exclude block is stale; {m} fenced entr(ies) are REAL paths (a fresh apply drops them)',
153
+ 'agy-adddir': 'agy-review is placed but AGY_REVIEW_ALLOW_ADDDIR is not set ({file}) — an oversized code review refuses instead of offloading',
154
+ 'agy-adddir.invalid-env': 'AGY_REVIEW_ALLOW_ADDDIR is set to an INVALID value ({value}) — refuse-mode applies and the settings file is shadowed while it is set',
155
+ 'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
156
+ });
157
+
158
+ // ── the shape contract (D2): registry strings AND composed items stay one line under the cap ────
159
+ export const ITEM_LINE_CAP = 140;
160
+ export const SKIP_REASON_CAP = 200;
161
+
162
+ const oneLineOf = (text) => String(text).replace(/\s*[\r\n]+\s*/g, ' ').trim();
163
+ // Scalar truncation-with-count — a capped value states what it dropped, never a silent cut.
164
+ // GUARANTEED result.length <= cap for every input/budget: when even the count note cannot fit
165
+ // the budget, the tail arm hard-slices to a bare ellipsis instead of overflowing.
166
+ const truncatedTo = (text, cap) => {
167
+ if (text.length <= cap) return text;
168
+ const note = (dropped) => `… (+${dropped} more chars)`;
169
+ let keep = cap;
170
+ while (keep > 0 && keep + note(text.length - keep).length > cap) keep -= 1;
171
+ if (keep === 0) return cap <= 0 ? '' : `${text.slice(0, cap - 1)}…`;
172
+ return text.slice(0, keep) + note(text.length - keep);
173
+ };
174
+ // List truncation-with-count: whole leading entries + " (+N more)" for the dropped tail; if even
175
+ // the first entry overflows, it is scalar-truncated so the count survives.
176
+ const capList = (entries, budget, sep = '; ') => {
177
+ for (let take = entries.length; take >= 1; take -= 1) {
178
+ const joined = entries.slice(0, take).join(sep);
179
+ const tail = take < entries.length ? ` (+${entries.length - take} more)` : '';
180
+ if (joined.length + tail.length <= budget) return joined + tail;
181
+ }
182
+ const tail = entries.length > 1 ? ` (+${entries.length - 1} more)` : '';
183
+ return truncatedTo(entries[0], Math.max(0, budget - tail.length)) + tail;
184
+ };
185
+ // The char budget a template leaves for its placeholder values.
186
+ const templateBudget = (template) => ITEM_LINE_CAP - template.replace(/\{[A-Za-z]+\}/g, '').length;
187
+
63
188
  // ── the frozen benefit registry (fact-true; pinned by tests) ────────────────────────────────────
64
189
  export const BENEFITS = Object.freeze({
65
190
  'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away',
66
191
  'kit-tools-tier': "velocity — the kit's own read-only tools run promptless (audited, resolved-absolute tier)",
67
192
  'bridge-tier':
68
- 'velocity — unattended council review runs: the placed review wrappers code mode stops prompting (delegated execution and the plan/diff modes keep their prompt)',
193
+ 'velocity — placed review wrappers run code-mode council reviews promptless (plan/diff modes and delegated execution keep their prompt)',
69
194
  'autonomy-policy': 'clarity — the per-activity autonomy policy becomes an explicit, versioned declaration instead of implicit computed defaults',
70
- 'autonomy-render': `velocity — the sandbox auto-allows confined commands per your declared policy; ${DUAL_SECURITY_BENEFIT}`,
71
- 'sandbox-provision': `velocity — confined ad-hoc commands stop prompting once the OS sandbox is available; ${DUAL_SECURITY_BENEFIT}`,
195
+ 'autonomy-render': `velocity — confined commands auto-allow per your declared policy; ${DUAL_SECURITY_BENEFIT}`,
196
+ 'sandbox-provision': `velocity — confined ad-hoc commands stop prompting; ${DUAL_SECURITY_BENEFIT}`,
72
197
  'review-recipe': 'review coverage — the review recipe you configured actually runs instead of silently degrading',
73
198
  'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
74
199
  'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
75
200
  'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
76
201
  'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
77
- 'agy-adddir':
78
- 'large reviewsan oversized agy code review offloads to a staging dir instead of refusing; CAVEAT: re-enables the Issue-001 stall risk (the hard timeout bounds it)',
79
- 'network-allowlist':
80
- 'unblocks the NETWORK half of in-sandbox bridge runs where the sandbox honors settings keys (settings-native harnesses; the network gate ONLY — no filesystem allowance is recommended); RISK stated plainly: pre-allows egress to these hosts for EVERY sandboxed command (informed hand-consent only). Live-observed 2026-07-12: an IDE-managed session sandbox ignores these settings keys too — there the durable lanes are the harness’s own per-host network consents / session sandbox config, or the per-run consented bypass; codex additionally needs a writable HOME (EROFS ~/.codex)',
202
+ 'agy-adddir': 'large reviews — an oversized agy code review offloads to a staging dir instead of refusing',
203
+ 'sandbox-lane': 'discoverabilitythe manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
81
204
  });
82
205
 
83
206
  // A typed usage failure (exit 2) — the codebase's typed-error idiom (no classes).
@@ -103,12 +226,12 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
103
226
  // above owns the real failure modes).
104
227
  const core = planVelocityProfile(preflight, {});
105
228
  if (core.toAdd.length > 0) {
106
- add('velocity-core', `routine read-only commands still prompt — ${core.toAdd.length} audited read-only allowlist entr(ies) not seeded`, applyLine(''));
229
+ add('velocity-core', fillTemplate(WHATS['velocity-core'], { n: core.toAdd.length }), applyLine(''));
107
230
  }
108
231
  try {
109
232
  const kt = planVelocityProfile(preflight, { kitTools: true });
110
233
  if (kt.tierToAdd.length > 0) {
111
- add('kit-tools-tier', `the kit's own read-only tools still prompt — ${kt.tierToAdd.length} kit-tools tier entr(ies) not seeded`, applyLine(' --kit-tools'));
234
+ add('kit-tools-tier', fillTemplate(WHATS['kit-tools-tier'], { n: kt.tierToAdd.length }), applyLine(' --kit-tools'));
112
235
  }
113
236
  } catch (err) {
114
237
  skip('kit-tools-tier', err);
@@ -117,7 +240,7 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
117
240
  const bt = planVelocityProfile(preflight, { bridgeTier: true, findWrapper: deps.findWrapper });
118
241
  const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length;
119
242
  if (delta > 0) {
120
- add('bridge-tier', `council review runs prompt per bridge invocation ${delta} bridge-wrappers tier entr(ies) not seeded (placed bridges only, code mode only)`, applyLine(' --bridge-tier'));
243
+ add('bridge-tier', fillTemplate(WHATS['bridge-tier'], { n: delta }), applyLine(' --bridge-tier'));
121
244
  }
122
245
  } catch (err) {
123
246
  skip('bridge-tier', err);
@@ -134,7 +257,7 @@ const probeAutonomyItems = ({ root, deps, add, skip }) => {
134
257
  // declaration: its render check still runs below (codex, Segment B closing).
135
258
  if (source !== 'none' && isSparseSeedConfig(config)) return;
136
259
  if (source === 'none') {
137
- add('autonomy-policy', `no ${AUTONOMY_REL} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)`, '/agent-workflow-kit set-autonomy (run IN the target project — the conversational writer previews, then writes its docs/ai/autonomy.json)');
260
+ add('autonomy-policy', fillTemplate(WHATS['autonomy-policy'], { path: AUTONOMY_REL }), '/agent-workflow-kit set-autonomy (run IN the target project — the conversational writer previews, then writes its docs/ai/autonomy.json)');
138
261
  }
139
262
  } catch (err) {
140
263
  skip('autonomy-policy', err);
@@ -144,7 +267,8 @@ const probeAutonomyItems = ({ root, deps, add, skip }) => {
144
267
  try {
145
268
  const check = checkAutonomyProfile({ cwd: root }, deps);
146
269
  if (!check.inSync) {
147
- add('autonomy-render', `the declared autonomy policy is not rendered into .claude/settings.json — drift: ${check.drift[0]}${check.drift.length > 1 ? ` (+${check.drift.length - 1} more)` : ''}`, `node ${q(toolPath('velocity-profile.mjs'))} --autonomy --apply --cwd ${q(root)}`);
270
+ const drift = capList(check.drift, templateBudget(WHATS['autonomy-render']));
271
+ add('autonomy-render', fillTemplate(WHATS['autonomy-render'], { drift }), `node ${q(toolPath('velocity-profile.mjs'))} --autonomy --apply --cwd ${q(root)}`);
148
272
  }
149
273
  } catch (err) {
150
274
  skip('autonomy-render', err);
@@ -156,10 +280,11 @@ const probeSandboxProvision = ({ root, deps, add, skip }) => {
156
280
  const p = probeSandboxAvailability(deps);
157
281
  if (p.available) return;
158
282
  const plan = deriveDoctorPlan({ probeResult: p, env: deps.env ?? process.env, isExec: deps.isExecutable ?? isExecutableFile });
159
- const installNote = plan.tuple ? ` — installable via the doctor (consent tuple ${plan.tuple})` : '';
283
+ const variant = plan.tuple ? 'sandbox-provision.installable' : 'sandbox-provision';
284
+ const reason = truncatedTo(oneLineOf(p.reason), templateBudget(WHATS[variant]) - (plan.tuple ? String(plan.tuple).length : 0));
160
285
  // The doctor reads process.cwd() (deployment-gated) and takes no --cwd flag — the one-liner
161
286
  // pins the target project via a cd prefix (codex R2, Segment B).
162
- add('sandbox-provision', `the OS sandbox is unavailable: ${p.reason}${installNote}`, `cd ${q(root)} && node ${q(toolPath('autonomy-doctor.mjs'))}`);
287
+ add('sandbox-provision', fillTemplate(WHATS[variant], { reason, tuple: plan.tuple }), `cd ${q(root)} && node ${q(toolPath('autonomy-doctor.mjs'))}`);
163
288
  } catch (err) {
164
289
  skip('sandbox-provision', err);
165
290
  }
@@ -179,7 +304,7 @@ const probeReviewRecipe = ({ root, deps, add, skip }) => {
179
304
  }
180
305
  }
181
306
  if (degraded.length > 0) {
182
- add('review-recipe', degraded.join('; '), '/agent-workflow-kit backends');
307
+ add('review-recipe', fillTemplate(WHATS['review-recipe'], { degraded: capList(degraded, templateBudget(WHATS['review-recipe'])) }), '/agent-workflow-kit backends');
183
308
  }
184
309
  } catch (err) {
185
310
  skip('review-recipe', err);
@@ -196,13 +321,13 @@ const probeGates = ({ root, deps, add, skip }) => {
196
321
  // scripts and writes only on an explicit yes) — never the runner.
197
322
  if (!sg.declarationPresent || sg.declaredGates === 0) {
198
323
  // The seeder writes ONLY with --apply and consent is per-entry (--only) — the apply field
199
- // stays a PURE executable command (run-exactly-verbatim feeds it to the shell); the
324
+ // stays a PURE executable command (run-exactly-as-rendered feeds it to the shell); the
200
325
  // two-step preview semantics live in WHAT, never as prose appended to the command.
201
- add('gates-declaration', 'no declared gate matrix (docs/ai/gates.json absent or empty) — gate commands run ad hoc and prompt one by one; the apply line is the PREVIEW (writes nothing) — it prints the exact consent-gated --apply [--only <id>] line to run next', `node ${q(toolPath('seed-gates.mjs'))} --cwd ${q(root)}`);
326
+ add('gates-declaration', fillTemplate(WHATS['gates-declaration'], {}), `node ${q(toolPath('seed-gates.mjs'))} --cwd ${q(root)}`);
202
327
  return;
203
328
  }
204
329
  if (sg.declaredGates > 0 && !sg.wired) {
205
- add('gate-hook', `${sg.declaredGates} declared gate(s) prompt per run — the gate-approval hook is not wired`, `node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`);
330
+ add('gate-hook', fillTemplate(WHATS['gate-hook'], { n: sg.declaredGates }), `node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`);
206
331
  }
207
332
  } catch (err) {
208
333
  skip('gate-hook', err);
@@ -228,7 +353,7 @@ const probeFamilyFreshness = ({ deps, add, skip }) => {
228
353
  // ALL caveats per row — a memory missing BOTH templates must not drop the second (codex).
229
354
  ...caveated.map((r) => `${r.name}: ${r.caveats.join('; ')}`),
230
355
  ];
231
- add('family-freshness', parts.join('; '), 'npx @sabaiway/agent-workflow-kit@latest init');
356
+ add('family-freshness', fillTemplate(WHATS['family-freshness'], { parts: capList(parts, templateBudget(WHATS['family-freshness'])) }), 'npx @sabaiway/agent-workflow-kit@latest init');
232
357
  } catch (err) {
233
358
  skip('family-freshness', err);
234
359
  }
@@ -239,11 +364,11 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
239
364
  const p = probeSandboxMasks({ cwd: root, ...deps });
240
365
  if (p == null) return; // not a git work tree — the lane is N/A, not sub-optimal
241
366
  if (!needsMasksApply(p)) return;
242
- const stale = p.staleReal.length ? `; ${p.staleReal.length} fenced entr(ies) became REAL paths (a fresh apply drops them by construction)` : '';
367
+ const variant = p.staleReal.length > 0 ? 'sandbox-masks.stale-real' : 'sandbox-masks';
243
368
  // A stale-real-only fence (EMPTY derivation over a non-empty block) makes the plain --apply
244
369
  // REFUSE — the exact one-liner must carry --clear there (codex R1, Segment B).
245
370
  const apply = p.masks.length === 0 && p.staleReal.length > 0 ? `${p.applyCmd} --clear` : p.applyCmd;
246
- add('sandbox-masks', `${p.masks.length} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale${stale}`, apply);
371
+ add('sandbox-masks', fillTemplate(WHATS[variant], { n: p.masks.length, m: p.staleReal.length }), apply);
247
372
  } catch (err) {
248
373
  skip('sandbox-masks', err);
249
374
  }
@@ -265,7 +390,8 @@ const probeAgyAdddir = ({ deps, add, skip }) => {
265
390
  if (env.AGY_REVIEW_ALLOW_ADDDIR === '') return;
266
391
  // env > file: while ANY env value is set the wrapper ignores the settings file, so the file
267
392
  // writer cannot fix an invalid env — the honest apply is to fix/unset the env var (codex).
268
- add('agy-adddir', `AGY_REVIEW_ALLOW_ADDDIR is set to an INVALID value (${JSON.stringify(env.AGY_REVIEW_ALLOW_ADDDIR)}) — the wrapper falls back to refuse-mode and the settings file is shadowed while the env var is set`, 'HAND-APPLY: unset AGY_REVIEW_ALLOW_ADDDIR in the environment (or export it as 1), THEN configure it durably via the bridge-settings writer');
393
+ const value = truncatedTo(oneLineOf(JSON.stringify(env.AGY_REVIEW_ALLOW_ADDDIR)), templateBudget(WHATS['agy-adddir.invalid-env']));
394
+ add('agy-adddir', fillTemplate(WHATS['agy-adddir.invalid-env'], { value }), 'HAND-APPLY: unset AGY_REVIEW_ALLOW_ADDDIR in the environment (or export it as 1), THEN configure it durably via the bridge-settings writer', 'agy-adddir.invalid-env');
269
395
  return;
270
396
  }
271
397
  const confPath = settingsPath({ getenv: env, home: deps.home });
@@ -285,7 +411,7 @@ const probeAgyAdddir = ({ deps, add, skip }) => {
285
411
  // The settings writer REFUSES a duplicate-carrying file — rendering its command would hand
286
412
  // the user a guaranteed failure; the honest apply is fix-duplicates-first (codex terminal).
287
413
  const dups = duplicateKeys(parsed);
288
- const what = `agy-review is placed but AGY_REVIEW_ALLOW_ADDDIR is not set (${SETTINGS_FILENAME}) — an oversized code review refuses instead of offloading`;
414
+ const what = fillTemplate(WHATS['agy-adddir'], { file: SETTINGS_FILENAME });
289
415
  if (dups.length > 0) {
290
416
  add('agy-adddir', what, `HAND-APPLY: ${SETTINGS_FILENAME} carries duplicate key(s) (${dups.join(', ')}) and the settings writer refuses to edit it — remove the duplicate lines by hand, THEN run: node ${q(toolPath('bridge-settings.mjs'))} --set AGY_REVIEW_ALLOW_ADDDIR=1 --apply`);
291
417
  return;
@@ -296,16 +422,18 @@ const probeAgyAdddir = ({ deps, add, skip }) => {
296
422
  }
297
423
  };
298
424
 
299
- // networkHosts of every BUNDLED bridge whose review wrapper is in the placed set — derived from the
300
- // manifests (the single documentation source), never a hardcoded host list here.
301
- const bundledNetworkHosts = (placedWrappers, deps) => {
425
+ // The manifest-declared session-sandbox recipe surfaces of every BUNDLED bridge whose review
426
+ // wrapper is in the wired set — networkHosts ∪ writableDirs, derived from the manifests (the
427
+ // single documentation source), never hardcoded here.
428
+ const bundledSandboxRecipe = (placedWrappers, deps) => {
302
429
  const readFile = deps.readFile ?? readFileSync;
303
430
  const readDir = deps.readdir ?? readdirSync;
304
431
  const bundleRoot = deps.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
305
432
  const hosts = [];
433
+ const dirEntries = [];
306
434
  for (const dir of readDir(bundleRoot)) {
307
- // An unreadable/unparsable bundled manifest must NOT thin the paste list silently — a partial
308
- // allowlist pasted as complete is worse than no item. The throw reaches the probe's catch and
435
+ // An unreadable/unparsable bundled manifest must NOT thin the recipe silently — a partial
436
+ // recipe rendered as complete is worse than no item. The throw reaches the probe's catch and
309
437
  // becomes a stated skip.
310
438
  let manifest;
311
439
  try {
@@ -316,14 +444,58 @@ const bundledNetworkHosts = (placedWrappers, deps) => {
316
444
  throw new Error(`bundled manifest unreadable: ${join(dir, 'capability.json')} — ${err?.message ?? err}`);
317
445
  }
318
446
  const reviewCmd = manifest?.roles?.review?.cmd;
319
- if (reviewCmd && placedWrappers.includes(reviewCmd) && Array.isArray(manifest.networkHosts)) {
447
+ if (!reviewCmd || !placedWrappers.includes(reviewCmd)) continue;
448
+ if (Array.isArray(manifest.networkHosts)) {
320
449
  for (const h of manifest.networkHosts) if (!hosts.includes(h)) hosts.push(h);
321
450
  }
451
+ if (Array.isArray(manifest.writableDirs)) dirEntries.push(...manifest.writableDirs);
322
452
  }
323
- return hosts;
453
+ return { hosts, dirEntries };
454
+ };
455
+
456
+ // D6 resolution, mirroring the wrappers' byte-semantics (`${VAR:-default}` + the exact case-arms:
457
+ // `~` / `~/…` / `/…` ride as-given; EVERY other form — including `~user/…`, which the wrappers
458
+ // never resolve as a home path — anchors like a relative path). The advisor anchors to the TARGET
459
+ // PROJECT ROOT (the pinned --cwd), matching what a wrapper invoked from the project root resolves
460
+ // (the documented dispatch form; the wrapper itself anchors to its invocation $PWD).
461
+ const resolveWritableDir = (entry, { env, root }) => {
462
+ const value = entry.env == null ? '' : (env[entry.env] ?? '');
463
+ if (value === '') return entry.default;
464
+ if (value === '~' || value.startsWith('~/') || value.startsWith('/')) return value;
465
+ return resolve(root, value);
324
466
  };
325
467
 
326
- const probeNetworkAllowlist = ({ root, deps, add, skip }) => {
468
+ // The NEUTRAL recipe fingerprint (D4): a hash over the resolved hosts ∪ dirs data — an
469
+ // acknowledgement token, never a security key. Canonical form is HOME-SYMBOLIC: an
470
+ // absolute dir under the resolved home canonicalizes BACK to its `~/…` form and tilde forms stay
471
+ // symbolic — so `~/.codex` and its absolute expansion acknowledge the SAME recipe AND the default
472
+ // recipe's fingerprint is identical across machines/users (a committed project-scope ack never
473
+ // churns between them); only a genuinely-outside-home absolute override stays absolute
474
+ // (machine-specific by nature). Any change to the recipe re-fires the item.
475
+ export const recipeFingerprint = ({ hosts, dirs, home }) => {
476
+ const homeAbs = resolve(home);
477
+ const norm = (d) => {
478
+ if (d === '~') return '~';
479
+ if (d.startsWith('~/')) return `~/${d.slice(2)}`;
480
+ const abs = resolve(d);
481
+ if (abs === homeAbs) return '~';
482
+ return abs.startsWith(`${homeAbs}/`) ? `~/${abs.slice(homeAbs.length + 1)}` : abs;
483
+ };
484
+ const canonical = JSON.stringify({ hosts: [...hosts].sort(), dirs: [...new Set(dirs.map(norm))].sort() });
485
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
486
+ };
487
+
488
+ // The kit-owned neutral ack namespace (D4): project-scoped, hand-applicable as one line, read from
489
+ // BOTH settings scopes; the sandbox/permissions security keys are NEVER consulted as an ack.
490
+ export const SANDBOX_LANE_ACK_PARENT = 'agentWorkflow';
491
+ export const SANDBOX_LANE_ACK_KEY = 'sandboxLaneAck';
492
+
493
+ // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
494
+ // at the consent moment; the static contract test asserts EXACT bidirectional coverage
495
+ // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
496
+ export const RISK_NOTED_KEYS = Object.freeze(['agy-adddir', 'sandbox-lane']);
497
+
498
+ const probeSandboxLane = ({ root, deps, add, skip }) => {
327
499
  try {
328
500
  const settings = readSettingsFile(join(root, SETTINGS_FILE), { ...deps, cwd: root });
329
501
  const localSettings = readSettingsFile(join(root, SETTINGS_LOCAL_FILE), { ...deps, cwd: root });
@@ -331,8 +503,8 @@ const probeNetworkAllowlist = ({ root, deps, add, skip }) => {
331
503
  const excluded = Array.isArray(sandbox?.excludedCommands) ? sandbox.excludedCommands : [];
332
504
  const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
333
505
  // Wired = the FULL two-surface tier proof (excludedCommands + the code-mode allow rule, either
334
- // scope) — surfacing the risky egress hand-apply while the tier is half-configured would
335
- // front-run the bridge-tier item (codex terminal). Byte-form from the tier's own constants.
506
+ // scope) — surfacing the recipe while the tier is half-configured would front-run the
507
+ // bridge-tier item (codex terminal). Byte-form from the tier's own constants.
336
508
  const allowRules = [
337
509
  ...(Array.isArray(settings.data?.permissions?.allow) ? settings.data.permissions.allow : []),
338
510
  ...(Array.isArray(localSettings.data?.permissions?.allow) ? localSettings.data.permissions.allow : []),
@@ -341,27 +513,26 @@ const probeNetworkAllowlist = ({ root, deps, add, skip }) => {
341
513
  (w) => excluded.includes(w) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
342
514
  );
343
515
  if (wired.length === 0) return; // the tier is not (fully) wired — the bridge-tier item covers first
344
- // Convergence (codex R3): a hand-applied list must silence the item — compare the LIVE
345
- // allowedDomains (project + local scope) against the manifests and render only what is missing.
346
- const projectApplied = Array.isArray(sandbox?.network?.allowedDomains) ? sandbox.network.allowedDomains : [];
347
- const localApplied = Array.isArray(localSettings.data?.sandbox?.network?.allowedDomains) ? localSettings.data.sandbox.network.allowedDomains : [];
348
- // Local scope counts toward COVERAGE only — the paste targets the COMMITTED project file, so a
349
- // local-only allowance must never be widened to the whole project (codex terminal).
350
- const applied = [...projectApplied, ...localApplied];
351
- const manifestHosts = bundledNetworkHosts(wired, deps);
352
- const missing = manifestHosts.filter((h) => !applied.includes(h));
353
- if (missing.length === 0 && applied.length > 0) return; // every manifest host is already hand-applied
354
- // The pasted value is the FULL desired final list for the PROJECT scope (project ∪ missing)
355
- // a missing-only snippet pasted verbatim would DROP the already-applied domains and oscillate.
356
- const finalList = [...projectApplied, ...missing];
357
- const hostsJson = finalList.map((h) => JSON.stringify(h)).join(', ');
516
+ const { hosts, dirEntries } = bundledSandboxRecipe(wired, deps);
517
+ const env = deps.getenv ?? process.env;
518
+ const home = deps.home ?? homedir();
519
+ const dirs = [];
520
+ for (const entry of dirEntries) {
521
+ const resolved = resolveWritableDir(entry, { env, root });
522
+ if (!dirs.includes(resolved)) dirs.push(resolved);
523
+ }
524
+ const fingerprint = recipeFingerprint({ hosts, dirs, home });
525
+ // Convergence is the NEUTRAL fingerprint-bound acknowledgement, read from BOTH scopes a
526
+ // changed recipe (hosts, dirs, or an env override) re-fires the item (D4).
527
+ const acks = [settings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY], localSettings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY]];
528
+ if (acks.includes(fingerprint)) return; // the acknowledged recipe — the item converged
358
529
  add(
359
- 'network-allowlist',
360
- 'IF plain wrapper runs still hit sandbox network prompts, this session’s sandbox is HARNESS-MANAGED — settings-level exclusions are inert there (live-observed 2026-07-11)',
361
- `HAND-APPLY (the kit never writes this): in .claude/settings.json set the key sandbox.network.allowedDomains to [${hostsJson}] a MERGE into the existing sandbox object (keep excludedCommands and every other sandbox key); hosts from the bridges' capability.json networkHosts (observed-minimal; a blocked host names itself at run time)`,
530
+ 'sandbox-lane',
531
+ fillTemplate(WHATS['sandbox-lane'], {}),
532
+ `HAND-APPLY (a neutral acknowledgement, never a security key): recipe — egress hosts [${hosts.join(', ')}]; writable state dirs [${dirs.join(', ')}] (observed-minimal; a blocked host names itself at run time); what to DO with it per host class: the mode doc's sandbox-lanes section; once handled, record the ack: set "${SANDBOX_LANE_ACK_PARENT}"."${SANDBOX_LANE_ACK_KEY}" to "${fingerprint}" in .claude/settings.json or settings.local.json a MERGE into the existing ${SANDBOX_LANE_ACK_PARENT} object (keep its other keys; create it only if absent)`,
362
533
  );
363
534
  } catch (err) {
364
- skip('network-allowlist', err);
535
+ skip('sandbox-lane', err);
365
536
  }
366
537
  };
367
538
 
@@ -375,31 +546,58 @@ const PROBES = Object.freeze([
375
546
  probeFamilyFreshness,
376
547
  probeMasksItem,
377
548
  probeAgyAdddir,
378
- probeNetworkAllowlist,
549
+ probeSandboxLane,
379
550
  ]);
380
551
 
381
552
  export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
382
553
  const root = resolve(cwd);
383
554
  const items = [];
384
555
  const skips = [];
385
- const add = (key, what, apply) => items.push({ key, what, benefit: BENEFITS[key], apply });
386
- const skip = (key, err) => skips.push({ key, reason: err?.message ?? String(err) });
387
- for (const probe of PROBES) probe({ root, deps, add, skip });
556
+ // Skip reasons ride arbitrary Error.messages normalized to ONE line and length-capped so a
557
+ // multiline or oversized message can never rebuild a prose wall (D2).
558
+ const skip = (key, err) => skips.push({ key, reason: truncatedTo(oneLineOf(err?.message ?? String(err)), SKIP_REASON_CAP) });
559
+ // The runtime shape backstop (D2): every COMPOSED item is validated at construction — a
560
+ // violation surfaces through the stated-skip lane, never a crash, never a rendered violation.
561
+ // severityKey defaults to the item key; a per-site arm passes its `<key>.<variant>` entry when
562
+ // its class differs from the base (the invalid-env attention arm).
563
+ const add = (key, what, apply, severityKey = key) => {
564
+ const problems = [];
565
+ if (!(key in BENEFITS)) problems.push(`unregistered item key ${JSON.stringify(key)}`);
566
+ if (!(severityKey in SEVERITIES)) problems.push(`unregistered severity key ${JSON.stringify(severityKey)}`);
567
+ if (/[\r\n]/.test(what)) problems.push('WHAT is not a single line');
568
+ else if (what.length > ITEM_LINE_CAP) problems.push(`WHAT exceeds the ${ITEM_LINE_CAP}-char cap (${what.length})`);
569
+ if (/[\r\n]/.test(apply)) problems.push('apply is not a single line');
570
+ if (problems.length > 0) {
571
+ skip(key, new Error(`item shape violation — ${problems.join('; ')}`));
572
+ return;
573
+ }
574
+ items.push({ key, severity: SEVERITIES[severityKey], what, benefit: BENEFITS[key], apply });
575
+ };
576
+ for (const probe of deps.probes ?? PROBES) probe({ root, deps, add, skip });
388
577
  return { root, items, skips };
389
578
  };
390
579
 
391
- // ── rendering (the agent pastes this section VERBATIM) ──────────────────────────────────────────
580
+ // ── rendering (English tool DATA — the agent presents it in the user's conversational language,
581
+ // facts/counts complete, commands byte-exact; the raw block on request) ─────────────────────────
392
582
  export const formatRecommendations = ({ items, skips }) => {
393
583
  const lines = [RECOMMENDATIONS_SECTION_HEADER, ''];
394
- if (items.length === 0 && skips.length === 0) {
584
+ const attention = items.filter((i) => i.severity === SEVERITY_ATTENTION).length;
585
+ const verdict = composeVerdict({ attention, optional: items.length - attention, skipped: skips.length });
586
+ if (verdict == null) {
395
587
  // The flow-optimal claim renders ONLY when every probe ran and none fired — an empty item
396
588
  // list beside skipped checks would falsely attest optimality (codex R1, Segment B).
397
589
  lines.push(RECOMMENDATIONS_EMPTY_LINE);
398
- } else if (items.length === 0) {
399
- lines.push(`no applicable items, but ${skips.length} probe check(s) were skipped — the flow is NOT attested optimal:`);
400
- } else {
401
- items.forEach((item, i) => {
402
- lines.push(`${i + 1}. ${item.what}`);
590
+ return lines.join('\n');
591
+ }
592
+ lines.push(verdict);
593
+ if (items.length > 0) {
594
+ lines.push('');
595
+ // Attention items lead (stable within each class — the frozen probe order).
596
+ const ordered = [...items].sort(
597
+ (a, b) => (a.severity === SEVERITY_ATTENTION ? 0 : 1) - (b.severity === SEVERITY_ATTENTION ? 0 : 1),
598
+ );
599
+ ordered.forEach((item, i) => {
600
+ lines.push(`${i + 1}. ${SEVERITY_LABELS[item.severity] ?? SEVERITY_LABELS[SEVERITY_OPTIONAL]}: ${item.what}`);
403
601
  lines.push(` benefit: ${item.benefit}`);
404
602
  lines.push(` apply: ${item.apply}`);
405
603
  });
@@ -415,15 +613,17 @@ const HELP = `recommendations — the read-only upgrade Recommendations advisor
415
613
  Usage:
416
614
  node recommendations.mjs --cwd <project-root> [--json]
417
615
 
418
- Computes the deterministic Recommendations section every kit upgrade ends with: what is
419
- sub-optimal in THIS deployment · the benefit in one plain line · the exact consent-gated apply
420
- one-liner. --cwd is REQUIRED (the target project is explicit, never inferred from the shell's
421
- current directory). The section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}");
422
- a probe failure is a stated skipped-item line. Apply lines are cwd-independent (absolute tool
423
- paths, a pinned --cwd; the doctor item pins via a cd prefix; the ONE exception is the set-autonomy
424
- item a conversational skill invocation labeled "run IN the target project") and preserve each
425
- writer's own consent semantics; the network-allowlist item is HAND-APPLY by design this tool and
426
- the kit writers never seed sandbox network/filesystem allowances.
616
+ Computes the deterministic Recommendations section every kit upgrade ends with VERDICT-FIRST:
617
+ one composed verdict line opens every non-optimal render, then per item {severity · what is
618
+ sub-optimal · the benefit in one plain line · the exact consent-gated apply one-liner}. --cwd is
619
+ REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
620
+ section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
621
+ stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
622
+ the doctor item pins via a cd prefix; the ONE exception is the set-autonomy item a
623
+ conversational skill invocation labeled "run IN the target project") and preserve each writer's
624
+ own consent semantics; the sandbox-lane item is HAND-APPLY by design — this tool and the kit
625
+ writers never seed sandbox network/filesystem allowances, and its convergence is a neutral
626
+ fingerprint acknowledgement, never a security key.
427
627
 
428
628
  Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
429
629
  rendered (items or empty); 1 error; 2 usage.`;