@sabaiway/agent-workflow-kit 14.34.0 → 14.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,7 +55,7 @@ import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-re
55
55
  import { probeSandboxMasks, needsMasksApply, patternToRel } from './sandbox-masks.mjs';
56
56
  import { shellQuoteArg } from './review-state.mjs';
57
57
  import { isFinalCapableDeclaration } from './run-gates.mjs';
58
- import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes, isKitOwnedCheckerGate, GATES_REL, LCOV_PRODUCER_KEY } from './gates-declaration.mjs';
58
+ import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes, isKitOwnedCheckerGate, LCOV_PRODUCER_KEY } from './gates-declaration.mjs';
59
59
  import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
60
60
  // The typed channel's READ-ONLY leaf only — never tools/mcp.mjs, which reaches the write core.
61
61
  import {
@@ -87,7 +87,8 @@ import { ATTRIBUTION_KEY, LEGACY_ATTRIBUTION_KEY, attributionHidesText, effectiv
87
87
  import { planGlobalRemoval } from './hide-footprint.mjs';
88
88
  import { NODE_EVIDENCE, NODE_EVIDENCE_SCRIPTS, probeNodeEvidence } from './node-evidence.mjs';
89
89
  // The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
90
- import { surveyExecutorVehicle, readExecutorPosture, executorBodyDiffers, executorTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
90
+ import { surveyExecutorVehicle, readExecutorPosture, executorBodyDiffers, executorTemplate, reviewLensTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION, REVIEW_LENS_VEHICLE } from './cheap-agents-read.mjs';
91
+ import { readVehicles, resolveExecutor, resolveReviewLens } from './vehicle-settings.mjs';
91
92
  // The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
92
93
  // (contract: kit/ack-store) — `status` reads the same store, and a second copy is what drifts.
93
94
  import {
@@ -119,6 +120,8 @@ export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_
119
120
  const HERE = dirname(fileURLToPath(import.meta.url));
120
121
  const toolPath = (rel) => join(HERE, rel);
121
122
  const q = shellQuoteArg;
123
+ // A user variant's own step, as the lane leaf words it: the rendered apply of every user item.
124
+ const userStepOf = (variant, root) => applySlot(variant, { toolsDir: HERE, root }).apply;
122
125
  const ABSENT_CODE = 'ENOENT';
123
126
  const ENFORCEMENT_KIT_ROOT = resolve(HERE, '..');
124
127
  const ENFORCEMENT_BUNDLE_ROOT = join(ENFORCEMENT_KIT_ROOT, 'references', 'scripts');
@@ -163,6 +166,7 @@ export const SEVERITIES = Object.freeze({
163
166
  'review-recipe': SEVERITY_ATTENTION,
164
167
  'gates-declaration': SEVERITY_OPTIONAL,
165
168
  'gates-inert': SEVERITY_ATTENTION,
169
+ 'gates-inert.unfillable': SEVERITY_ATTENTION,
166
170
  'gates-inert.no-verification': SEVERITY_ATTENTION,
167
171
  // Both third outcomes are attention, and both therefore block the flow-optimal line MECHANICALLY:
168
172
  // an added item makes the verdict non-null. One reports a pair that is dead on a tree the closed
@@ -255,6 +259,7 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
255
259
  // banned tokens), never a fixture-coverage gamble. A pure-placeholder template marks a WHAT whose
256
260
  // content is fully dynamic (capped at composition by truncation-with-count) — except `profile-gap`, whose
257
261
  // WHAT is never truncated: the leaf's suite pins every composed WHAT under the cap.
262
+ const GATES_INERT_WHAT = 'the declared coverage checker ({id}) has no producer before it — it certifies nothing this run, or reads a stale lcov';
258
263
  export const WHATS = Object.freeze({
259
264
  'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
260
265
  'velocity-core.rule': 'auto mode refuses a harness apply, so work cannot stay in auto mode — the apply ask rule is not seeded',
@@ -267,7 +272,8 @@ export const WHATS = Object.freeze({
267
272
  'sandbox-provision.installable': 'the OS sandbox is unavailable: {reason} — installable via the doctor (consent tuple {tuple})',
268
273
  'review-recipe': '{degraded}',
269
274
  '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',
270
- 'gates-inert': 'the declared coverage checker ({id}) has no producer before it — it certifies nothing this run, or reads a stale lcov',
275
+ 'gates-inert': GATES_INERT_WHAT,
276
+ 'gates-inert.unfillable': GATES_INERT_WHAT,
271
277
  'gates-inert.no-verification': "all {n} declared gate(s) are the kit's own checkers — the matrix runs no project-verification command",
272
278
  'gates-inert.producer-unrecognized': 'the coverage checker ({id}) has NO producer declared and none offerable, on a tree the changed-line domain barely reaches',
273
279
  'gates-inert.coverage-domain-narrow': 'certification covers the assessable minority only — {ext} dominate(s) the tracked tree, and the changed-line domain excludes them',
@@ -286,6 +292,7 @@ export const WHATS = Object.freeze({
286
292
  'mcp-channel': "the kit's read-only MCP server is not registered here — path questions and literal searches stay shell strings",
287
293
  'mcp-channel.differing': 'an "{server}" MCP entry is already declared here and DIFFERS from the registration this kit copy would write',
288
294
  agents: '{n} bundled subagent vehicle(s) not placed (Claude Code) — {ro} read-only, {ex} the full-tool executor; the apply PREVIEWS first',
295
+ 'agents.lens-rederive': 'the placed review-lens.md differs from its body derived from docs/ai/vehicles.json; {n} vehicle(s) to place ({ro} read-only, {ex} executor)',
289
296
  'executor-vehicle': '{n} slot(s) configured subagent but the executor vehicle is {state}{reason} — every such slot runs solo until it is usable',
290
297
  'executor-vehicle.differing': 'the placed executor vehicle differs from the body derived from docs/ai/vehicles.json — a hand edit stands until it is re-derived',
291
298
  attribution: 'AGENTS.md says attribution off but .claude/settings.json does not hide the harness trailer and PR text',
@@ -426,8 +433,8 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
426
433
  { id: 'delegated-execution', mode: 'set-recipe', advisorKey: 'review-recipe' },
427
434
  { id: 'agents', mode: 'agents', advisorKey: 'agents' },
428
435
  // A DISTINCT capability from the offer above: the offer converges the moment nothing is left to
429
- // PLACE, which a customized-but-unusable executor also satisfies — so it can never observe the
430
- // state that makes a configured subagent carrier run solo.
436
+ // PLACE and the review lens matches its derived body, which a customized-but-unusable executor
437
+ // also satisfies — so it can never observe the state that makes a configured subagent carrier run solo.
431
438
  { id: 'executor-vehicle', mode: 'agents', advisorKey: 'executor-vehicle' },
432
439
  // Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
433
440
  // a TRUST-POSTURE change. Velocity presents its full posture at its own consent moment;
@@ -588,7 +595,8 @@ const probeReviewRecipe = ({ root, deps, add, skip }) => {
588
595
  }
589
596
  }
590
597
  if (degraded.length > 0) {
591
- add('review-recipe', fillTemplate(WHATS['review-recipe'], { degraded: capList(degraded, templateBudget(WHATS['review-recipe'])) }), '/agent-workflow-kit backends');
598
+ add('review-recipe', fillTemplate(WHATS['review-recipe'], { degraded: capList(degraded, templateBudget(WHATS['review-recipe'])) }),
599
+ userStepOf('review-recipe', root));
592
600
  }
593
601
  } catch (err) {
594
602
  skip('review-recipe', err);
@@ -651,8 +659,8 @@ const probeGates = ({ root, deps, add, skip, shared }) => {
651
659
  // gate is not a kit checker. Cause A's apply follows what the FILL can actually do, which the
652
660
  // D-8 placement rule changed: when the checker is the declaration's LAST gate and the project's own
653
661
  // scripts yield an offerable producer, the fill now PLACES that producer before the checker, so the
654
- // remedy is the ordinary consent-gated preview. HAND-APPLY remains exactly where no offerable
655
- // producer exists, or where the checker is not last — there the edit really is the maintainer's,
662
+ // remedy is the ordinary consent-gated preview. Where no offerable producer exists, or the checker
663
+ // is not last, the arm is the user item `gates-inert.unfillable` — the edit is the project's own,
656
664
  // because the fill never reorders entries it did not write.
657
665
  //
658
666
  // An ABSENT or EMPTY declaration belongs to the gates-declaration item; a malformed one throws out
@@ -765,8 +773,9 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
765
773
  add(
766
774
  'gates-inert',
767
775
  fillTemplate(WHATS['gates-inert.producer-unrecognized'], { id }),
768
- `HAND-APPLY: mark the gate that actually writes the lcov with "lcovProducer": true in ${GATES_REL} (references/modes/gates.md names the key), or drop ${id} — and note the fill offers only terminating-class script NAMES, so a recognized body under another name (a "ci" script running node --test) is declared by hand, not marked`,
776
+ userStepOf('gates-inert.producer-unrecognized', root),
769
777
  'gates-inert.producer-unrecognized',
778
+ `${id} has no producer declared and none offerable — the fill offers only terminating-class script names, so a recognized body under another name (a "ci" script running node --test) is declared as its own gate entry, never marked`,
770
779
  );
771
780
  return;
772
781
  }
@@ -788,13 +797,12 @@ export const probeGatesInert = ({ root, deps, add, skip }) => {
788
797
  : colliding !== undefined
789
798
  ? `a producer IS offerable here, but its id "${colliding.id}" is already declared, so the fill refuses it as a collision — rename that gate, or repoint it at the producer form`
790
799
  : `no offerable producer exists here, and the fill never reorders entries it did not write`;
791
- add(
792
- 'gates-inert',
793
- fillTemplate(WHATS['gates-inert'], { id }),
794
- checkerIsLast && producer
795
- ? fillPreviewFor(root, [producer.id])
796
- : `HAND-APPLY: declare or MOVE a suite gate carrying the coverage reporters BEFORE ${id} in ${GATES_REL} (references/modes/gates.md names the exact form), or drop ${id} — ${blocked}`,
797
- );
800
+ if (checkerIsLast && producer) {
801
+ add('gates-inert', fillTemplate(WHATS['gates-inert'], { id }), fillPreviewFor(root, [producer.id]));
802
+ return;
803
+ }
804
+ add('gates-inert', fillTemplate(WHATS['gates-inert.unfillable'], { id }), userStepOf('gates-inert.unfillable', root),
805
+ 'gates-inert.unfillable', blocked);
798
806
  return;
799
807
  }
800
808
  // The source-size checker is one of the kit's OWN checkers, and it is deliberately NOT
@@ -887,15 +895,11 @@ const probeSourceSize = ({ root, deps, add, skip }) => {
887
895
  }
888
896
  // The id SQUATTER — the id says the practice is declared, the cmd is not this checker under any
889
897
  // reading. `--adopt` here mints the record and is then refused by the fill on the id collision:
890
- // correct against a squatter, and useless as a RENDERED remedy, so this arm names the two hand
891
- // edits that actually resolve it instead.
898
+ // correct against a squatter, and useless as a RENDERED remedy, so this arm is a user item whose
899
+ // step names the edits that actually resolve it.
892
900
  if (gates.some((gate) => gate.id === SOURCE_SIZE_GATE_ID)) {
893
- add(
894
- 'source-size',
895
- fillTemplate(WHATS['source-size.id-squatter'], {}),
896
- `HAND-APPLY: in ${GATES_REL}, either rename the "${SOURCE_SIZE_GATE_ID}" gate to an id of its own, or repoint its cmd at node ${q(tool)} --check — then re-run recommendations`,
897
- 'source-size.id-squatter',
898
- );
901
+ add('source-size', fillTemplate(WHATS['source-size.id-squatter'], {}), userStepOf('source-size.id-squatter', root),
902
+ 'source-size.id-squatter');
899
903
  return;
900
904
  }
901
905
  add('source-size', fillTemplate(WHATS['source-size'], {}), adoptLine);
@@ -1079,16 +1083,19 @@ const probeStateBlockHook = ({ root, deps, add, skip }) => {
1079
1083
  // surfaced its absence: `agents` is the family's SECOND `.claude/` writer, the `help` Tune tail
1080
1084
  // advertises it, and the advisor had no entry for it — so a user who never runs `help` never learned
1081
1085
  // it existed while the advisor reported the deployment optimal. Found by the guard, not by an incident.
1082
- // Only a PLACE action counts as a gap: `already-current` is converged; a customized read-only vehicle or lens is
1086
+ // A PLACE action and the bundled review lens's `re-derive` (its placed body differs from the one derived from docs/ai/vehicles.json) count as gaps: `already-current` is converged; a customized read-only vehicle or derived lens is
1083
1087
  // the user's own edit the writer never clobbers and the advisor never nags about; the executor is not preserved but re-derived from docs/ai/vehicles.json (its `re-derive` answer is not a gap of this offer).
1084
1088
  const probeCheapAgents = ({ root, deps, add, skip }) => {
1085
1089
  try {
1086
- const { posture, reason } = readExecutorPosture(root, deps);
1090
+ const fs = readFsDeps(deps);
1091
+ const answer = readVehicles(root, fs.readFile, fs.lstat);
1092
+ const { posture, reason } = resolveExecutor(answer);
1087
1093
  if (posture === null) {
1088
1094
  skip('agents', new Error(`${reason} — fix that file first`));
1089
1095
  return;
1090
1096
  }
1091
- const preflight = preflightCheapAgents({ cwd: root, derived: [executorTemplate(posture, deps)] }, deps);
1097
+ const derived = [executorTemplate(posture, deps), reviewLensTemplate(resolveReviewLens(answer).posture, deps)];
1098
+ const preflight = preflightCheapAgents({ cwd: root, derived }, deps);
1092
1099
  // The writer refuses below the expected lineage, so offering its command would hand the user a
1093
1100
  // guaranteed failure — the honest surface is a stated skip naming the recovery.
1094
1101
  if (!preflight.stampOk) {
@@ -1096,7 +1103,8 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
1096
1103
  return;
1097
1104
  }
1098
1105
  const toPlace = preflight.plan.filter((item) => item.action === 'place');
1099
- if (toPlace.length === 0) return; // converged
1106
+ const lensRederive = preflight.plan.some((item) => item.name === REVIEW_LENS_VEHICLE && item.action === 're-derive');
1107
+ if (toPlace.length === 0 && !lensRederive) return; // converged
1100
1108
  // The writer's contract is «--dry-run first, ALWAYS» (references/modes/agents.md, and its stated
1101
1109
  // invariant «preview by default»), so the rendered line is the PREVIEW — this item joins the
1102
1110
  // dry-run-preview class the mode doc already documents for the gates-declaration seeder, where the
@@ -1105,7 +1113,7 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
1105
1113
  // The hidden-mode reconcile rides the detail, never the apply line: it is wrong to run on a
1106
1114
  // VISIBLE deployment, and the apply slot must stay one pure executable command.
1107
1115
  const executors = toPlace.filter((item) => item.name === EXECUTOR_VEHICLE).length;
1108
- add('agents', fillTemplate(WHATS.agents, { n: toPlace.length, ex: executors, ro: toPlace.length - executors }),
1116
+ add('agents', fillTemplate(WHATS[lensRederive ? 'agents.lens-rederive' : 'agents'], { n: toPlace.length, ex: executors, ro: toPlace.length - executors }),
1109
1117
  `node ${q(toolPath('cheap-agents.mjs'))} --cwd ${q(root)}`, 'agents',
1110
1118
  `hidden-mode deployments only: after the --apply the preview prints, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
1111
1119
  );
@@ -1116,8 +1124,8 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
1116
1124
 
1117
1125
  // The subagent carrier's ONE instrument (contract: kit/carriers). Neither neighbour can report this
1118
1126
  // state: `probeReviewRecipe` skips a slot configured `subagent` by construction, and the offer above
1119
- // judges only what is left to PLACE — a customized-but-broken vehicle leaves nothing to place. So a
1120
- // project could declare the carrier and have every such slot run solo, unseen.
1127
+ // judges only what is left to PLACE and the bundled lens's re-derive — a customized-but-broken executor
1128
+ // gives it neither. So a project could declare the carrier and have every such slot run solo, unseen.
1121
1129
  const VEHICLE_BROKEN_STATES = Object.freeze(['missing', 'unusable']);
1122
1130
 
1123
1131
  const writerBlock = (root, deps) => {
@@ -1166,7 +1174,7 @@ const renderBrokenExecutor = ({ root, deps, add, skip }) => {
1166
1174
  const blocked = writerBlock(root, deps);
1167
1175
  if (blocked) preconditions.push(`${blocked} — fix that`);
1168
1176
  const room = templateBudget(WHATS['executor-vehicle']) - String(configured.length).length - survey.state.length - 2;
1169
- return add(
1177
+ add(
1170
1178
  'executor-vehicle',
1171
1179
  fillTemplate(WHATS['executor-vehicle'], {
1172
1180
  n: configured.length,
@@ -1179,9 +1187,10 @@ const renderBrokenExecutor = ({ root, deps, add, skip }) => {
1179
1187
  'executor-vehicle',
1180
1188
  `${preconditions.length ? `precondition: ${preconditions.join('; ')}; ` : ''}${HIDDEN_AGENTS_FOLLOW_UP(root)}`,
1181
1189
  );
1190
+ return true;
1182
1191
  } catch (err) {
1183
1192
  skip('executor-vehicle', err);
1184
- return false;
1193
+ return true;
1185
1194
  }
1186
1195
  };
1187
1196
 
@@ -1342,6 +1351,8 @@ export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'read-lane', 'workt
1342
1351
  // spec-check's own census; a recorded decline (the `spec-adoption` ack lane) is the fact that
1343
1352
  // silences the item, and an unreadable store is a stated skip so the flow-optimal line never renders
1344
1353
  // over it. The not-adopted apply is the spec-layer ensure; the decline preview rides the recipe line.
1354
+ // The one opening of every offer's decline alternative: a chat write the agent runs on the user's decline.
1355
+ export const DECLINE_ALTERNATIVE = 'decline alternative (asked apart, before the confirmation; on your decline the agent records it instead of the apply, never after it): ';
1345
1356
  export const probeSpecAdoption = ({ root, deps, add, skip }) => {
1346
1357
  try {
1347
1358
  const survey = surveySpecAdoption(root, deps);
@@ -1357,7 +1368,7 @@ export const probeSpecAdoption = ({ root, deps, add, skip }) => {
1357
1368
  fillTemplate(WHATS['spec-adoption'], {}),
1358
1369
  `node ${q(toolPath('ensure-configs.mjs'))} --reconcile --only ${SPEC_LAYER_ENSURE} --cwd ${q(root)}`,
1359
1370
  'spec-adoption',
1360
- `HAND-APPLY alternative (instead of the apply, never after it): decline the layer by recording it — ${decline}`,
1371
+ `${DECLINE_ALTERNATIVE}${decline}`,
1361
1372
  );
1362
1373
  return;
1363
1374
  }
@@ -1477,13 +1488,12 @@ export const probeAdrStore = ({ root, deps, add, skip }) => {
1477
1488
  try {
1478
1489
  const shape = ADR_LAYOUT_SHAPES[surveyAdrLayoutStrict(root, deps)];
1479
1490
  if (!shape) return; // migrated, or no ADR substrate at all — nothing to offer
1480
- // HAND-APPLY, not the standard lane: the consent flow executes the apply slot against the
1481
- // confirmation given BEFORE the preview, and this crossing requires informed consent AFTER its
1482
- // dry-run. A runnable one-liner here would auto-run a tree-mutating migration on stale consent.
1491
+ // The apply is the bare dry run: consent takes two steps, as profile-gap's does — the first yes
1492
+ // runs it and the plan is relayed, the same line with --apply runs only on a second yes over it.
1483
1493
  add(
1484
1494
  'adr-store-migration',
1485
1495
  fillTemplate(WHATS['adr-store-migration'], { shape }),
1486
- `HAND-APPLY: node ${q(toolPath('migrate-adr-store.mjs'))} --dry-run --cwd ${q(root)} — then re-run with --apply ONLY after showing the plan and getting fresh consent`,
1496
+ `node ${q(toolPath('migrate-adr-store.mjs'))} --dry-run --cwd ${q(root)}`,
1487
1497
  );
1488
1498
  } catch (err) {
1489
1499
  skip('adr-store-migration', err);
@@ -1534,7 +1544,7 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
1534
1544
  const place = placeOf({ env, platform: host.platform, hostname: host.hostname, container: host.container });
1535
1545
  const decline = `node ${q(toolPath('ack-write.mjs'))} --lane jev-connect --fingerprint ${JEV_CONNECT_DECLINE} --cwd ${q(root)}`;
1536
1546
  add('jev-connect', WHATS['jev-connect'], applySlot('jev-connect', { toolsDir: HERE }).apply, 'jev-connect',
1537
- `${place ? `run the apply in ${place}; ` : ''}HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${decline}`);
1547
+ `${place ? `run the apply in ${place}; ` : ''}${DECLINE_ALTERNATIVE}${decline}`);
1538
1548
  } catch (err) {
1539
1549
  skip('jev-connect', err);
1540
1550
  }
@@ -1568,7 +1578,7 @@ export const probeJevSkill = ({ root, deps, add, skip }) => {
1568
1578
  .map(({ path, reason }) => `left untouched: ${printable(path)} — ${printable(reason)}; `).join('');
1569
1579
  const record = `node ${q(toolPath('ack-write.mjs'))} --lane jev-skill --fingerprint ${decline} --cwd ${q(root)}`;
1570
1580
  add('jev-skill', what, line, behind.length ? 'jev-skill.earlier' : 'jev-skill',
1571
- `${place ? `run the apply in ${place}; ` : ''}${untouched}${note ? `${note}; ` : ''}HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${record}`);
1581
+ `${place ? `run the apply in ${place}; ` : ''}${untouched}${note ? `${note}; ` : ''}${DECLINE_ALTERNATIVE}${record}`);
1572
1582
  } catch (err) {
1573
1583
  skip('jev-skill', err);
1574
1584
  }
@@ -1664,10 +1674,9 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
1664
1674
  // which item fired, and only this field says which ARM of it did — so a consumer (the pre-publish
1665
1675
  // smoke asserting a specific false-green never returns) can assert the exact outcome instead of
1666
1676
  // pattern-matching prose that is free to be reworded.
1667
- // `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
1668
- // the capped WHAT and does NOT belong in the pure-command apply (the bridge-tier missing-entries
1669
- // line; the worktrees-dir ack fallback; the
1670
- // agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
1677
+ // `detail` (optional) is an extra rendered `recipe:` line — an item's own facts or its alternative,
1678
+ // TOO LONG for the capped WHAT and outside the pure-command apply (such as the bridge-tier
1679
+ // missing-entries line or the worktrees-dir ack fallback). Single-line like apply; absent otherwise.
1671
1680
  // Returns whether the item really RENDERED. One probe's disposition depends on another's having
1672
1681
  // spoken (the marker-stale ⟷ read-lane.stale precedence), and "the conditions still look right"
1673
1682
  // is not the same fact as "an item exists" — the shape backstop can refuse, and a condition read
@@ -1757,9 +1766,9 @@ Usage:
1757
1766
 
1758
1767
  Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
1759
1768
  one composed verdict line opens every non-optimal render, then per item {severity · what is
1760
- sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the bridge-tier
1761
- missing-entries line, the worktrees-dir ack fallback, or the agents hidden-mode reconcile
1762
- follow-up) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
1769
+ sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (an item's own
1770
+ facts or its alternative, such as the bridge-tier missing-entries line or
1771
+ the worktrees-dir ack fallback) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
1763
1772
  REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
1764
1773
  section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
1765
1774
  stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
@@ -1770,8 +1779,8 @@ allow rules, <wrapper> * exclusions (codex-exec * where an execute slot delegate
1770
1779
  sandbox.network.allowedDomains and state dirs in sandbox.filesystem.allowWrite. Where the host
1771
1780
  honors the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed
1772
1781
  command of the project can reach each host, and every sandboxed command can write each state dir;
1773
- plan/diff review modes and delegated execution keep their prompt. Items marked HAND-APPLY stay
1774
- the user's own step.
1782
+ plan/diff review modes and delegated execution keep their prompt.
1783
+ A user item stays the user's own step.
1775
1784
 
1776
1785
  Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
1777
1786
  rendered (items or empty); 1 error; 2 usage.`;
@@ -22,8 +22,8 @@ const target = (activity, slot, candidates) => Object.freeze({ activity, slot, c
22
22
  export const TIER_TARGETS = Object.freeze([
23
23
  target('epic', 'author', ['subagent', 'solo']),
24
24
  target('epic', 'review', ['reviewed', 'solo']),
25
- target('task', 'author', ['delegated', 'subagent', 'solo']),
26
- target('task', 'execute', ['delegated', 'subagent', 'solo']),
25
+ target('task', 'author', ['subagent', 'solo']),
26
+ target('task', 'execute', ['subagent', 'solo']),
27
27
  ]);
28
28
 
29
29
  const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -3,11 +3,14 @@ import { join } from 'node:path';
3
3
 
4
4
  export const VEHICLES_REL = 'docs/ai/vehicles.json';
5
5
  export const EXECUTOR_DEFAULTS = Object.freeze({ model: 'opus', effort: 'high', fallback: 'sonnet' });
6
+ export const REVIEW_LENS_DEFAULTS = Object.freeze({ model: 'opus', effort: 'high' });
6
7
 
7
8
  const README_KEY = '_README';
8
9
  const EXECUTOR_KEY = 'executor';
9
- const TOP_LEVEL_KEYS = Object.freeze([README_KEY, EXECUTOR_KEY]);
10
+ const LENS_KEY = 'review-lens';
11
+ const TOP_LEVEL_KEYS = Object.freeze([README_KEY, EXECUTOR_KEY, LENS_KEY]);
10
12
  const EXECUTOR_KEYS = Object.freeze(Object.keys(EXECUTOR_DEFAULTS));
13
+ const LENS_KEYS = Object.freeze(Object.keys(REVIEW_LENS_DEFAULTS));
11
14
  const OBJECT_TYPE = 'object';
12
15
  const STRING_TYPE = 'string';
13
16
  const EMPTY_STRING = '';
@@ -44,6 +47,25 @@ const unreadable = (reason) => ({ state: UNREADABLE, reason });
44
47
  const describeError = (error) => String(error?.code ?? error?.message ?? error);
45
48
  const formatReadError = (error) => `${VEHICLES_REL}: ${UNREADABLE} (${describeError(error)})`;
46
49
 
50
+ const validateSection = (value, rel, name, keys) => {
51
+ const section = value[name];
52
+ if (!isPlainObject(section)) return refuseValue(rel, name, OBJECT_REQUIRED);
53
+ for (const key of Object.keys(section)) {
54
+ if (!keys.includes(key)) return refuseValue(rel, `${name}.${key}`, UNKNOWN_KEY);
55
+ }
56
+ for (const key of keys) {
57
+ const qualifiedKey = `${name}.${key}`;
58
+ if (!Object.hasOwn(section, key)) return refuseValue(rel, qualifiedKey, REQUIRED_KEY);
59
+ const token = section[key];
60
+ if (typeof token !== STRING_TYPE) return refuseValue(rel, qualifiedKey, STRING_REQUIRED);
61
+ if (token === EMPTY_STRING) return refuseValue(rel, qualifiedKey, NONEMPTY_REQUIRED);
62
+ if (!TOKEN_PATTERN.test(token) || token.trim() !== token) {
63
+ return refuseValue(rel, qualifiedKey, TOKEN_REQUIRED);
64
+ }
65
+ }
66
+ return { ok: true, values: Object.fromEntries(keys.map((key) => [key, section[key]])) };
67
+ };
68
+
47
69
  export const validateVehicles = (value, rel = VEHICLES_REL) => {
48
70
  if (!isPlainObject(value)) return refuseValue(rel, BODY_NAME, OBJECT_REQUIRED);
49
71
  for (const key of Object.keys(value)) {
@@ -53,26 +75,15 @@ export const validateVehicles = (value, rel = VEHICLES_REL) => {
53
75
  return refuseValue(rel, README_KEY, STRING_REQUIRED);
54
76
  }
55
77
  if (!Object.hasOwn(value, EXECUTOR_KEY)) return refuseValue(rel, EXECUTOR_KEY, REQUIRED_KEY);
56
- const executor = value[EXECUTOR_KEY];
57
- if (!isPlainObject(executor)) return refuseValue(rel, EXECUTOR_KEY, OBJECT_REQUIRED);
58
- for (const key of Object.keys(executor)) {
59
- if (!EXECUTOR_KEYS.includes(key)) return refuseValue(rel, `${EXECUTOR_KEY}.${key}`, UNKNOWN_KEY);
60
- }
61
- for (const key of EXECUTOR_KEYS) {
62
- const qualifiedKey = `${EXECUTOR_KEY}.${key}`;
63
- if (!Object.hasOwn(executor, key)) return refuseValue(rel, qualifiedKey, REQUIRED_KEY);
64
- const token = executor[key];
65
- if (typeof token !== STRING_TYPE) return refuseValue(rel, qualifiedKey, STRING_REQUIRED);
66
- if (token === EMPTY_STRING) return refuseValue(rel, qualifiedKey, NONEMPTY_REQUIRED);
67
- if (!TOKEN_PATTERN.test(token) || token.trim() !== token) {
68
- return refuseValue(rel, qualifiedKey, TOKEN_REQUIRED);
69
- }
70
- }
71
- if (executor.fallback === executor.model) {
78
+ const executor = validateSection(value, rel, EXECUTOR_KEY, EXECUTOR_KEYS);
79
+ if (!executor.ok) return executor;
80
+ if (executor.values.fallback === executor.values.model) {
72
81
  return refuseValue(rel, EXECUTOR_KEY, DISTINCT_FALLBACK_REQUIRED);
73
82
  }
74
- const { model, effort, fallback } = executor;
75
- return { ok: true, settings: { executor: { model, effort, fallback } } };
83
+ const settings = { executor: executor.values };
84
+ if (!Object.hasOwn(value, LENS_KEY)) return { ok: true, settings };
85
+ const lens = validateSection(value, rel, LENS_KEY, LENS_KEYS);
86
+ return lens.ok ? { ok: true, settings: { ...settings, [LENS_KEY]: lens.values } } : lens;
76
87
  };
77
88
 
78
89
  const probeEntry = (path, lstat) => {
@@ -125,3 +136,14 @@ export const resolveExecutor = (answer) => {
125
136
  if (answer?.state === UNREADABLE) return { posture: null, reason: answer.reason };
126
137
  throw fail(CONFIG_EXIT_CODE, `${VEHICLES_REL}: ${UNKNOWN_STATE} ${String(answer?.state)}`);
127
138
  };
139
+
140
+ export const resolveReviewLens = (answer) => {
141
+ if (answer?.state === PRESENT && Object.hasOwn(answer.settings, LENS_KEY)) {
142
+ return { posture: { ...answer.settings[LENS_KEY], source: FILE_SOURCE }, reason: null };
143
+ }
144
+ if (answer?.state === PRESENT || answer?.state === ABSENT) {
145
+ return { posture: { ...REVIEW_LENS_DEFAULTS, source: DEFAULT_SOURCE }, reason: null };
146
+ }
147
+ if (answer?.state === UNREADABLE) return { posture: null, reason: answer.reason };
148
+ throw fail(CONFIG_EXIT_CODE, `${VEHICLES_REL}: ${UNKNOWN_STATE} ${String(answer?.state)}`);
149
+ };
@@ -3,6 +3,10 @@ import { dirCovers } from './declared-paths.mjs';
3
3
  import { claudeDirOf, quoteArg, skillTargets } from './jev-facts.mjs';
4
4
  import { targetsFor } from './jev-connect.mjs';
5
5
  import { BUNDLED_SCRIPT_NAMES, INSTALLER_REL, SCRIPTS_REL } from './git-hooks.mjs';
6
+ import { dqUnsafePath } from './checker-claim.mjs';
7
+ import { isLineUnsafe } from './source-size-refusal.mjs';
8
+ import { SOURCE_SIZE_GATE_ID } from './source-size-gate-cmd.mjs';
9
+ import { GATES_REL } from './gates-declaration.mjs';
6
10
 
7
11
  export const LANE_CHAT = 'chat';
8
12
  export const LANE_HARNESS = 'harness';
@@ -16,16 +20,17 @@ export const VARIANT_LANES = Object.freeze({
16
20
  'autonomy-render': LANE_HARNESS,
17
21
  'sandbox-provision': LANE_USER,
18
22
  'sandbox-provision.installable': LANE_USER,
19
- 'review-recipe': LANE_CHAT,
23
+ 'review-recipe': LANE_USER,
20
24
  'gates-declaration': LANE_CHAT,
21
25
  'gates-inert': LANE_CHAT,
26
+ 'gates-inert.unfillable': LANE_USER,
22
27
  'gates-inert.no-verification': LANE_CHAT,
23
- 'gates-inert.producer-unrecognized': LANE_CHAT,
28
+ 'gates-inert.producer-unrecognized': LANE_USER,
24
29
  'gates-inert.coverage-domain-narrow': LANE_CHAT,
25
30
  'source-size': LANE_CHAT,
26
31
  'source-size.unminted': LANE_CHAT,
27
32
  'source-size.adopted-elsewhere': LANE_CHAT,
28
- 'source-size.id-squatter': LANE_CHAT,
33
+ 'source-size.id-squatter': LANE_USER,
29
34
  'gate-hook': LANE_HARNESS,
30
35
  'gate-hook.marker-stale': LANE_HARNESS,
31
36
  'commit-guard': LANE_HARNESS,
@@ -109,6 +114,15 @@ const getGuardPaths = (facts) => [preCommitOf(facts), join(facts.root, INSTALLER
109
114
 
110
115
  const formatCommand = (argv) => argv.map(quoteArg).join(' ');
111
116
  const TERMINAL_STEP = 'run in a terminal of your own: ';
117
+ const OWN_STEP = 'your own step — ';
118
+ const RERUN_ADVISOR = ', then run /agent-workflow-kit recommendations';
119
+
120
+ const squatterStep = (facts) => {
121
+ const tool = join(facts.toolsDir, 'source-size-check.mjs');
122
+ const rename = `${OWN_STEP}in ${GATES_REL} rename the "${SOURCE_SIZE_GATE_ID}" gate to an id of its own`;
123
+ if (dqUnsafePath(tool) || isLineUnsafe(tool)) return `${rename}${RERUN_ADVISOR}`;
124
+ return `${rename}, or repoint its cmd at node "${tool}" --check${RERUN_ADVISOR}`;
125
+ };
112
126
 
113
127
  // rejudged: the written value derives from state init's record does not watch, so init re-runs its check before the apply.
114
128
  const createForm = (file, preview, apply, writes, envDependent = false, userStep = null, rejudged = false) => Object.freeze({
@@ -155,13 +169,13 @@ const FORMS = Object.freeze({
155
169
  'jev-skill': SKILL_FORM,
156
170
  'jev-skill.earlier': SKILL_FORM,
157
171
  'jev-connect': createForm('jev-connect.mjs', null, [], getStartupPaths, true,
158
- (facts, argv) => 'your own step — it asks for the key in your terminal and the key never goes into the chat — '
172
+ (facts, argv) => `${OWN_STEP}it asks for the key in your terminal and the key never goes into the chat — `
159
173
  + `${TERMINAL_STEP}${formatCommand(argv.apply)}`),
160
174
  'family-freshness': Object.freeze({
161
- userStep: () => `your own step — ${TERMINAL_STEP}${formatCommand(['npx', '@sabaiway/agent-workflow-kit@latest', 'init'])}`,
175
+ userStep: () => `${OWN_STEP}${TERMINAL_STEP}${formatCommand(['npx', '@sabaiway/agent-workflow-kit@latest', 'init'])}`,
162
176
  }),
163
177
  'sandbox-provision': Object.freeze({
164
- userStep: (facts) => 'your own step — the doctor has no install it can run for you here (the reason is this item\'s text) — '
178
+ userStep: (facts) => `${OWN_STEP}the doctor has no install it can run for you here (the reason is this item's text) — `
165
179
  + `${TERMINAL_STEP}cd ${quoteArg(facts.root)} && ${formatCommand(['node', join(facts.toolsDir, 'autonomy-doctor.mjs')])}`,
166
180
  }),
167
181
  enforcement: createForm('git-hooks.mjs',
@@ -171,7 +185,21 @@ const FORMS = Object.freeze({
171
185
  'worktrees-dir': createForm('velocity-profile.mjs',
172
186
  ['--worktrees', '--cwd', '<root>'], ['--apply', '--worktrees', '--cwd', '<root>'], LOCAL_SETTINGS_WRITES, false, null, true),
173
187
  'worktrees-dir.home': Object.freeze({
174
- userStep: () => 'your own step — point docs/ai/worktrees.json parentDir at a dedicated directory, then run /agent-workflow-kit recommendations',
188
+ userStep: () => `${OWN_STEP}point docs/ai/worktrees.json parentDir at a dedicated directory${RERUN_ADVISOR}`,
189
+ }),
190
+ 'gates-inert.unfillable': Object.freeze({
191
+ userStep: () => `${OWN_STEP}in ${GATES_REL} declare or move a suite gate carrying the coverage reporters before the coverage checker`
192
+ + ` (references/modes/gates.md names the exact form), or drop that checker${RERUN_ADVISOR}`,
193
+ }),
194
+ 'gates-inert.producer-unrecognized': Object.freeze({
195
+ userStep: () => `${OWN_STEP}in ${GATES_REL} mark the gate that writes the lcov with "lcovProducer": true`
196
+ + ` (references/modes/gates.md names the key), or drop the coverage checker${RERUN_ADVISOR}`,
197
+ }),
198
+ 'source-size.id-squatter': Object.freeze({ userStep: squatterStep }),
199
+ 'review-recipe': Object.freeze({
200
+ userStep: () => `${OWN_STEP}set up the backend the recipe names with /agent-workflow-kit setup (it places the bridge skill`
201
+ + ' and links its wrappers, and names the CLI install and sign-in that stay yours; /agent-workflow-kit backends shows what is'
202
+ + ` missing), or re-pick the recipe with /agent-workflow-kit set-recipe${RERUN_ADVISOR}`,
175
203
  }),
176
204
  });
177
205