@sabaiway/agent-workflow-kit 1.47.0 → 1.48.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 +42 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/changelog-skeleton.md +1 -0
- package/references/agents/gate-triage.md +1 -0
- package/references/agents/mechanical-sweep.md +1 -0
- package/references/hooks/gate-approve.mjs +120 -18
- package/references/modes/hook.md +6 -1
- package/references/modes/recommendations.md +8 -6
- package/references/modes/velocity.md +4 -2
- package/tools/ack-write.mjs +186 -0
- package/tools/doc-parity.mjs +7 -0
- package/tools/gate-hook.mjs +193 -4
- package/tools/recommendations.mjs +171 -17
- package/tools/velocity-profile.mjs +13 -2
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
// below) and VERDICT-FIRST (D1): every non-optimal state opens with ONE composed verdict line.
|
|
20
20
|
// Registry strings are frozen tool DATA, fact-true, one line under the shape cap (D2); posture/
|
|
21
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
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// fingerprint-bound acknowledgement
|
|
22
|
+
// skipped-item line — never a crash, never a fabricated item. The kit never seeds
|
|
23
|
+
// sandbox.network.allowedDomains / filesystem.allowWrite (HAND-APPLY territory; bridge council
|
|
24
|
+
// 2026-07-11, both backends concur); the sandbox-lane item's convergence is a NEUTRAL
|
|
25
|
+
// fingerprint-bound acknowledgement recorded by the consent-gated ack writer into the family-owned
|
|
26
|
+
// docs/ai/acks.json (AD-055 relocated it off the host settings schema), never a security key (D4).
|
|
26
27
|
//
|
|
27
28
|
// Read-only: never writes, never commits, never runs a subscription CLI. The reused probes are all
|
|
28
29
|
// exported read-only surfaces of their owning tools (velocity/autonomy/doctor/backends/recipes/
|
|
@@ -55,6 +56,7 @@ import { probeSandboxMasks, needsMasksApply } from './sandbox-masks.mjs';
|
|
|
55
56
|
import { shellQuoteArg } from './review-state.mjs';
|
|
56
57
|
import { loadConfig } from './orchestration-config.mjs';
|
|
57
58
|
import { DEFAULT_BUNDLE_ROOT, SETTINGS_FILENAME, settingsPath, parseSettings, duplicateKeys } from './bridge-settings-read.mjs';
|
|
59
|
+
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
58
60
|
|
|
59
61
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
60
62
|
const toolPath = (rel) => join(HERE, rel);
|
|
@@ -95,6 +97,9 @@ export const SEVERITIES = Object.freeze({
|
|
|
95
97
|
'review-recipe': SEVERITY_ATTENTION,
|
|
96
98
|
'gates-declaration': SEVERITY_OPTIONAL,
|
|
97
99
|
'gate-hook': SEVERITY_OPTIONAL,
|
|
100
|
+
'read-lane': SEVERITY_OPTIONAL,
|
|
101
|
+
'read-lane.stale': SEVERITY_ATTENTION,
|
|
102
|
+
'read-lane.missing': SEVERITY_ATTENTION,
|
|
98
103
|
'family-freshness': SEVERITY_ATTENTION,
|
|
99
104
|
'sandbox-masks': SEVERITY_OPTIONAL,
|
|
100
105
|
'agy-adddir': SEVERITY_OPTIONAL,
|
|
@@ -147,6 +152,9 @@ export const WHATS = Object.freeze({
|
|
|
147
152
|
'review-recipe': '{degraded}',
|
|
148
153
|
'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
154
|
'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
|
|
155
|
+
'read-lane': 'the gate hook is wired but the read-only compound lane is off — pipes/chains of seeded reads still prompt one by one',
|
|
156
|
+
'read-lane.stale': 'the read-lane is ON but the placed gate hook is stale — an old hook never reads lanes.json, so the lane is silently dark; reseed it',
|
|
157
|
+
'read-lane.missing': 'the gate hook is wired but its placed file is missing — every Bash call errors and the read-lane is dark; re-place it',
|
|
150
158
|
'family-freshness': '{parts}',
|
|
151
159
|
'sandbox-masks': '{n} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale',
|
|
152
160
|
'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)',
|
|
@@ -197,6 +205,7 @@ export const BENEFITS = Object.freeze({
|
|
|
197
205
|
'review-recipe': 'review coverage — the review recipe you configured actually runs instead of silently degrading',
|
|
198
206
|
'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
|
|
199
207
|
'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
|
|
208
|
+
'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
|
|
200
209
|
'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
|
|
201
210
|
'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
|
|
202
211
|
'agy-adddir': 'large reviews — an oversized agy code review offloads to a staging dir instead of refusing',
|
|
@@ -334,6 +343,43 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
334
343
|
}
|
|
335
344
|
};
|
|
336
345
|
|
|
346
|
+
// The read-lane offer (AD-055 Part II, Help-through-Recommendations): once the gate hook is PLACED
|
|
347
|
+
// and WIRED, offer to enable the opt-in read-only compound lane. Mutually exclusive with the
|
|
348
|
+
// gate-hook item BY CONSTRUCTION — that item keys on `!wired`, this one on `wired` — so an unplaced
|
|
349
|
+
// or un-wired hook is covered there, never double-offered here. The apply is the gate-hook
|
|
350
|
+
// --read-lane PREVIEW one-liner (its own currency check + posture note fire at the writer; it may
|
|
351
|
+
// prompt once — it IS a consent flow). Converges once lanes.json enables the lane.
|
|
352
|
+
const probeReadLane = ({ root, deps, add, skip }) => {
|
|
353
|
+
try {
|
|
354
|
+
const sg = surveyGateHook(root, deps);
|
|
355
|
+
if (sg.error) throw new Error(sg.error);
|
|
356
|
+
if (!sg.wired) return; // not wired → the gate-hook item covers (no double-fire)
|
|
357
|
+
if (!sg.filePlaced) {
|
|
358
|
+
// Wired but the placed hook FILE is missing — the hook errors on every Bash call and the lane
|
|
359
|
+
// is silently dark; surface it as attention with a place-first recovery (council R2-M2).
|
|
360
|
+
add('read-lane', fillTemplate(WHATS['read-lane.missing'], {}), `node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`, 'read-lane.missing');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (readReadLaneToggle(root, deps)) {
|
|
364
|
+
// The lane is ON: converged IFF the placed hook is byte-current — a stale (pre-1.48) hook never
|
|
365
|
+
// reads lanes.json, so the enabled lane is a silent no-op the user must reseed (council B7). The
|
|
366
|
+
// rm target is ABSOLUTE (council R2-M3) so running the recovery from any cwd can only delete this
|
|
367
|
+
// repo's hook.
|
|
368
|
+
if (isPlacedHookCurrent(root, deps)) return; // converged
|
|
369
|
+
add(
|
|
370
|
+
'read-lane',
|
|
371
|
+
fillTemplate(WHATS['read-lane.stale'], {}),
|
|
372
|
+
`HAND-APPLY: rm ${q(join(root, GATE_HOOK_REL))}, then node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`,
|
|
373
|
+
'read-lane.stale',
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
add('read-lane', fillTemplate(WHATS['read-lane'], {}), `node ${q(toolPath('gate-hook.mjs'))} --read-lane --cwd ${q(root)}`);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
skip('read-lane', err);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
337
383
|
const probeFamilyFreshness = ({ deps, add, skip }) => {
|
|
338
384
|
try {
|
|
339
385
|
const survey = deps.surveyFamily ?? surveyFamily;
|
|
@@ -485,15 +531,102 @@ export const recipeFingerprint = ({ hosts, dirs, home }) => {
|
|
|
485
531
|
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
486
532
|
};
|
|
487
533
|
|
|
488
|
-
// The kit-owned neutral ack
|
|
489
|
-
//
|
|
534
|
+
// The kit-owned neutral ack store (D4; AD-055 Part I): a FAMILY-OWNED strict-JSON file no host
|
|
535
|
+
// validator guards — top-level key `sandboxLaneAck` (+ optional `_README`), unknown keys tolerated
|
|
536
|
+
// on read (future acks are siblings). This is the PRIMARY ack channel; the legacy settings-scope
|
|
537
|
+
// keys below are read for one deprecation window. The sandbox/permissions security keys are NEVER
|
|
538
|
+
// consulted as an ack.
|
|
539
|
+
export const ACKS_FILE = 'docs/ai/acks.json';
|
|
540
|
+
export const ACKS_LANE_KEY = 'sandboxLaneAck';
|
|
541
|
+
|
|
542
|
+
// The opt-in read-lane toggle file (AD-055 Part II) — the SAME kit-owned docs/ai/lanes.json the
|
|
543
|
+
// placed hook reads live. The read-lane item offers to enable it once the hook is placed+wired.
|
|
544
|
+
export const LANES_FILE = 'docs/ai/lanes.json';
|
|
545
|
+
export const READ_LANE_KEY = 'readLane';
|
|
546
|
+
|
|
547
|
+
// The placed gate hook + the bundled runtime the read-lane item byte-compares for currency (AD-055
|
|
548
|
+
// Part II, council B7): an enabled lane over a STALE hook is a silent no-op — the pre-1.48 hook never
|
|
549
|
+
// reads lanes.json — so it must surface as an ATTENTION reseed, not a silent convergence.
|
|
550
|
+
const GATE_HOOK_REL = '.claude/hooks/agent-workflow-gates.mjs';
|
|
551
|
+
const BUNDLED_HOOK_ABS = join(HERE, '..', 'references', 'hooks', 'gate-approve.mjs');
|
|
552
|
+
|
|
553
|
+
// Byte-compare the placed gate hook against the bundled runtime. A read error (an unreadable placed
|
|
554
|
+
// hook, a broken kit bundle) propagates → the probe states a skip, never a wrong currency verdict.
|
|
555
|
+
const isPlacedHookCurrent = (root, deps) => {
|
|
556
|
+
const readFile = deps.readFile ?? readFileSync;
|
|
557
|
+
const placed = readFile(join(root, GATE_HOOK_REL), 'utf8');
|
|
558
|
+
const bundle = readFile(deps.bundledHookPath ?? BUNDLED_HOOK_ABS, 'utf8');
|
|
559
|
+
return placed === bundle;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// The LEGACY neutral ack namespace (pre-AD-055): read from BOTH settings scopes until the next kit
|
|
563
|
+
// MAJOR (2.0.0) so a never-migrated host stays converged across the deprecation window (Decisions 3).
|
|
490
564
|
export const SANDBOX_LANE_ACK_PARENT = 'agentWorkflow';
|
|
491
565
|
export const SANDBOX_LANE_ACK_KEY = 'sandboxLaneAck';
|
|
492
566
|
|
|
567
|
+
// Read the family-owned ack store. An ABSENT file (or absent docs/ai) is the NORMAL not-yet-acked
|
|
568
|
+
// state → null (plain fall-through, never a skip). A parse/IO error on an EXISTING file THROWS — the
|
|
569
|
+
// probe's catch turns it into a stated skip line (Decisions 2). A non-object root is a malformed
|
|
570
|
+
// store (fail-closed skip); a non-string value at the key is tolerated → null (the item re-fires).
|
|
571
|
+
// The WHOLE path chain (root / docs / ai / acks.json) is guarded WITHOUT following symlinks
|
|
572
|
+
// BEFORE any read: a symlinked ANCESTOR could otherwise read an ack from OUTSIDE the project (the
|
|
573
|
+
// writer refuses such a deployment — the reader must too), a symlinked/dangling LEAF must not read as
|
|
574
|
+
// not-yet-acked, and a non-regular target (FIFO/dir/device) is a fail-closed SKIP — never read it (a
|
|
575
|
+
// FIFO would BLOCK the advisor). ENOENT-safe: an absent file/dir is the NORMAL not-yet-acked null.
|
|
576
|
+
const readAcksLane = (root, deps) => {
|
|
577
|
+
const readFile = deps.readFile ?? readFileSync;
|
|
578
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
579
|
+
const absPath = join(root, ACKS_FILE);
|
|
580
|
+
let st;
|
|
581
|
+
try {
|
|
582
|
+
assertContainedRealPath(root, absPath, { lstat }); // symlinked root/ancestor/leaf or escape → throws
|
|
583
|
+
st = lstat(absPath);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
if (err?.code === 'ENOENT') return null; // genuinely absent (file or docs/ai) — normal not-yet-acked
|
|
586
|
+
throw err; // a symlinked ancestor/leaf, an escape, or a real IO error — stated skip
|
|
587
|
+
}
|
|
588
|
+
if (!st.isFile()) {
|
|
589
|
+
throw new Error(`${ACKS_FILE} is not a regular file — refusing to read it`);
|
|
590
|
+
}
|
|
591
|
+
const parsed = JSON.parse(readFile(absPath, 'utf8'));
|
|
592
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
593
|
+
throw new Error(`${ACKS_FILE}: expected a JSON object`);
|
|
594
|
+
}
|
|
595
|
+
const value = parsed[ACKS_LANE_KEY];
|
|
596
|
+
return typeof value === 'string' ? value : null;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Read the opt-in read-lane toggle for the read-lane item. An ABSENT file (or absent docs/ai) →
|
|
600
|
+
// false (the lane is off — offer it). `readLane === true` → enabled (converged). A parse/IO error on
|
|
601
|
+
// an EXISTING file, a symlinked ancestor/leaf, an escape, or a non-object root THROWS — the probe
|
|
602
|
+
// turns it into a stated skip (a BROKEN toggle the writer would refuse to overwrite is not "off").
|
|
603
|
+
// A present-but-non-boolean `readLane` is a valid store the writer merges → false (offer), never a skip.
|
|
604
|
+
const readReadLaneToggle = (root, deps) => {
|
|
605
|
+
const readFile = deps.readFile ?? readFileSync;
|
|
606
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
607
|
+
const absPath = join(root, LANES_FILE);
|
|
608
|
+
let st;
|
|
609
|
+
try {
|
|
610
|
+
assertContainedRealPath(root, absPath, { lstat }); // symlinked root/ancestor/leaf or escape → throws
|
|
611
|
+
st = lstat(absPath);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
if (err?.code === 'ENOENT') return false; // genuinely absent — the lane is off, offer it
|
|
614
|
+
throw err; // a symlinked ancestor/leaf, an escape, or a real IO error — stated skip
|
|
615
|
+
}
|
|
616
|
+
if (!st.isFile()) {
|
|
617
|
+
throw new Error(`${LANES_FILE} is not a regular file — refusing to read it`);
|
|
618
|
+
}
|
|
619
|
+
const parsed = JSON.parse(readFile(absPath, 'utf8'));
|
|
620
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
621
|
+
throw new Error(`${LANES_FILE}: expected a JSON object`);
|
|
622
|
+
}
|
|
623
|
+
return parsed[READ_LANE_KEY] === true;
|
|
624
|
+
};
|
|
625
|
+
|
|
493
626
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
494
627
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
495
628
|
// (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']);
|
|
629
|
+
export const RISK_NOTED_KEYS = Object.freeze(['agy-adddir', 'sandbox-lane', 'read-lane']);
|
|
497
630
|
|
|
498
631
|
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
499
632
|
try {
|
|
@@ -522,14 +655,28 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
|
522
655
|
if (!dirs.includes(resolved)) dirs.push(resolved);
|
|
523
656
|
}
|
|
524
657
|
const fingerprint = recipeFingerprint({ hosts, dirs, home });
|
|
525
|
-
// Convergence is the NEUTRAL fingerprint-bound acknowledgement
|
|
526
|
-
//
|
|
527
|
-
|
|
658
|
+
// Convergence is the NEUTRAL fingerprint-bound acknowledgement: the item converges iff the
|
|
659
|
+
// CURRENT fingerprint equals the ack in ANY consulted store — the family-owned acks.json FIRST,
|
|
660
|
+
// then the legacy settings scopes; a stale value in one store is ignored when another matches
|
|
661
|
+
// (Decisions 2). A changed recipe (hosts, dirs, or an env override) re-fires the item (D4).
|
|
662
|
+
const acks = [
|
|
663
|
+
readAcksLane(root, deps),
|
|
664
|
+
settings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
|
|
665
|
+
localSettings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
|
|
666
|
+
];
|
|
528
667
|
if (acks.includes(fingerprint)) return; // the acknowledged recipe — the item converged
|
|
668
|
+
// The item joins the CONSENT-GATED WRITER class (Decisions 4): the apply is the ack writer's
|
|
669
|
+
// PREVIEW one-liner (pure executable, cwd-independent), carrying the neutral fingerprint — never
|
|
670
|
+
// a security key. The LIVE recipe (egress hosts + resolved writable dirs) rides a separate
|
|
671
|
+
// rendered `recipe:` line (the fill source for the mode doc's lane-(2) hand-apply block); the
|
|
672
|
+
// fingerprint encodes it, so a changed recipe re-fires with a fresh command.
|
|
673
|
+
const recipe = `egress hosts [${hosts.join(', ')}]; writable state dirs [${dirs.join(', ')}] (observed-minimal; a blocked host names itself at run time)`;
|
|
529
674
|
add(
|
|
530
675
|
'sandbox-lane',
|
|
531
676
|
fillTemplate(WHATS['sandbox-lane'], {}),
|
|
532
|
-
`
|
|
677
|
+
`node ${q(toolPath('ack-write.mjs'))} --fingerprint ${fingerprint} --cwd ${q(root)}`,
|
|
678
|
+
'sandbox-lane',
|
|
679
|
+
recipe,
|
|
533
680
|
);
|
|
534
681
|
} catch (err) {
|
|
535
682
|
skip('sandbox-lane', err);
|
|
@@ -543,6 +690,7 @@ const PROBES = Object.freeze([
|
|
|
543
690
|
probeSandboxProvision,
|
|
544
691
|
probeReviewRecipe,
|
|
545
692
|
probeGates,
|
|
693
|
+
probeReadLane,
|
|
546
694
|
probeFamilyFreshness,
|
|
547
695
|
probeMasksItem,
|
|
548
696
|
probeAgyAdddir,
|
|
@@ -560,18 +708,22 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
560
708
|
// violation surfaces through the stated-skip lane, never a crash, never a rendered violation.
|
|
561
709
|
// severityKey defaults to the item key; a per-site arm passes its `<key>.<variant>` entry when
|
|
562
710
|
// its class differs from the base (the invalid-env attention arm).
|
|
563
|
-
|
|
711
|
+
// `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
|
|
712
|
+
// the capped WHAT and does NOT belong in the pure-command apply (the sandbox-lane live recipe:
|
|
713
|
+
// egress hosts + resolved writable dirs). Single-line like apply; absent for every other item.
|
|
714
|
+
const add = (key, what, apply, severityKey = key, detail = null) => {
|
|
564
715
|
const problems = [];
|
|
565
716
|
if (!(key in BENEFITS)) problems.push(`unregistered item key ${JSON.stringify(key)}`);
|
|
566
717
|
if (!(severityKey in SEVERITIES)) problems.push(`unregistered severity key ${JSON.stringify(severityKey)}`);
|
|
567
718
|
if (/[\r\n]/.test(what)) problems.push('WHAT is not a single line');
|
|
568
719
|
else if (what.length > ITEM_LINE_CAP) problems.push(`WHAT exceeds the ${ITEM_LINE_CAP}-char cap (${what.length})`);
|
|
569
720
|
if (/[\r\n]/.test(apply)) problems.push('apply is not a single line');
|
|
721
|
+
if (detail != null && /[\r\n]/.test(detail)) problems.push('recipe detail is not a single line');
|
|
570
722
|
if (problems.length > 0) {
|
|
571
723
|
skip(key, new Error(`item shape violation — ${problems.join('; ')}`));
|
|
572
724
|
return;
|
|
573
725
|
}
|
|
574
|
-
items.push({ key, severity: SEVERITIES[severityKey], what, benefit: BENEFITS[key], apply });
|
|
726
|
+
items.push({ key, severity: SEVERITIES[severityKey], what, benefit: BENEFITS[key], apply, detail });
|
|
575
727
|
};
|
|
576
728
|
for (const probe of deps.probes ?? PROBES) probe({ root, deps, add, skip });
|
|
577
729
|
return { root, items, skips };
|
|
@@ -599,6 +751,7 @@ export const formatRecommendations = ({ items, skips }) => {
|
|
|
599
751
|
ordered.forEach((item, i) => {
|
|
600
752
|
lines.push(`${i + 1}. ${SEVERITY_LABELS[item.severity] ?? SEVERITY_LABELS[SEVERITY_OPTIONAL]}: ${item.what}`);
|
|
601
753
|
lines.push(` benefit: ${item.benefit}`);
|
|
754
|
+
if (item.detail) lines.push(` recipe: ${item.detail}`);
|
|
602
755
|
lines.push(` apply: ${item.apply}`);
|
|
603
756
|
});
|
|
604
757
|
}
|
|
@@ -615,15 +768,16 @@ Usage:
|
|
|
615
768
|
|
|
616
769
|
Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
|
|
617
770
|
one composed verdict line opens every non-optimal render, then per item {severity · what is
|
|
618
|
-
sub-optimal · the benefit in one plain line ·
|
|
771
|
+
sub-optimal · the benefit in one plain line · an optional \`recipe:\` line (the sandbox-lane live
|
|
772
|
+
recipe only) · the exact consent-gated apply one-liner}. --cwd is
|
|
619
773
|
REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
|
|
620
774
|
section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
|
|
621
775
|
stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
|
|
622
776
|
the doctor item pins via a cd prefix; the ONE exception is the set-autonomy item — a
|
|
623
777
|
conversational skill invocation labeled "run IN the target project") and preserve each writer's
|
|
624
|
-
own consent semantics; the sandbox
|
|
625
|
-
|
|
626
|
-
|
|
778
|
+
own consent semantics; the kit never seeds sandbox network/filesystem allowances (HAND-APPLY
|
|
779
|
+
territory), and the sandbox-lane convergence is a neutral fingerprint acknowledgement recorded by
|
|
780
|
+
the consent-gated ack writer into docs/ai/acks.json (never a security key).
|
|
627
781
|
|
|
628
782
|
Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
|
|
629
783
|
rendered (items or empty); 1 error; 2 usage.`;
|
|
@@ -255,8 +255,19 @@ export const RUNTIME_RESIDUAL_FORMS = Object.freeze({
|
|
|
255
255
|
writeRedirections: Object.freeze(['>', '>>', '1>', '2>', '&>', '>|']),
|
|
256
256
|
// `$(…)` + backtick + process substitution `<(…)` all RUN a nested command (`>(…)` is caught by
|
|
257
257
|
// the `>` redirection scan). Bare `<` is input redirection (reads a file — read-only commands may
|
|
258
|
-
// already do that), so it is deliberately NOT here.
|
|
259
|
-
|
|
258
|
+
// already do that), so it is deliberately NOT here. The bash-5.3 function substitutions `${ cmd; }`
|
|
259
|
+
// (a blank — space/tab/newline/CR — right after `${`) and `${| cmd; }` (runs a command, assigns
|
|
260
|
+
// REPLY) also execute a nested command — matched as the literal openers `${ ` / `${\t` / `${\n` /
|
|
261
|
+
// `${\r` / `${|` (AD-055 Part II; the `\r` opener guards CRLF payloads — council agy nit). An
|
|
262
|
+
// ordinary `${VAR}` parameter expansion has NO blank after `${`, so it trips none of these — kept
|
|
263
|
+
// rung-(b)-silent on a settings-allowed single (rung (c) excludes all `$`).
|
|
264
|
+
commandSubstitutions: Object.freeze(['$(', '`', '<(', '${ ', '${\t', '${\n', '${\r', '${|']),
|
|
265
|
+
// A backslash immediately before a newline/CR is a bash LINE CONTINUATION: bash removes it and
|
|
266
|
+
// splices the two lines into ONE word, which can reconstruct a residual token (`--output`, `$(`,
|
|
267
|
+
// `${ …; }`) that a raw substring scan on the pre-splice string misses (`--outp\<newline>ut=f` →
|
|
268
|
+
// `--output=f`). Guards a settings-allowed SINGLE (rung c already forbids backslash in every
|
|
269
|
+
// segment). AD-055 Part II council fold (codex B3).
|
|
270
|
+
lineContinuations: Object.freeze(['\\\n', '\\\r']),
|
|
260
271
|
// The `--output` write-flag family, matched as a raw SUBSTRING of the whole command (never a
|
|
261
272
|
// whitespace-token check): the hook sees the pre-shell command string, so `--output=f`,
|
|
262
273
|
// `"--output=f"`, `'--output' f`, and `\--output` must all trip it — over-asking on a benign
|