@sabaiway/agent-workflow-kit 3.15.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -0
- package/README.md +3 -3
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +12 -8
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +735 -55
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +899 -51
- package/bridges/antigravity-cli-bridge/bin/agy.sh +4 -3
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +23 -0
- package/bridges/antigravity-cli-bridge/capability.json +14 -4
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +12 -4
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +4 -3
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +65 -2
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +2 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +63 -13
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +38 -0
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/review-lens.md +39 -0
- package/references/hooks/gate-approve.mjs +15 -0
- package/references/modes/agents.md +11 -2
- package/references/modes/autonomy-doctor.md +2 -0
- package/references/modes/backends.md +2 -0
- package/references/modes/bootstrap.md +2 -0
- package/references/modes/bridge-settings.md +4 -1
- package/references/modes/commit-guard.md +2 -0
- package/references/modes/core-evidence.md +2 -0
- package/references/modes/coverage-check.md +2 -0
- package/references/modes/doc-parity.md +2 -0
- package/references/modes/gates.md +2 -0
- package/references/modes/grounding.md +2 -0
- package/references/modes/help.md +2 -0
- package/references/modes/hook.md +4 -1
- package/references/modes/migrate-adr-store.md +2 -0
- package/references/modes/procedures.md +2 -0
- package/references/modes/recipes.md +2 -0
- package/references/modes/recommendations.md +4 -3
- package/references/modes/review-state.md +2 -0
- package/references/modes/sandbox-masks.md +2 -0
- package/references/modes/set-autonomy.md +2 -0
- package/references/modes/set-recipe.md +3 -0
- package/references/modes/setup.md +2 -0
- package/references/modes/state-block-guard.md +2 -0
- package/references/modes/status.md +4 -1
- package/references/modes/uninstall.md +2 -0
- package/references/modes/upgrade.md +2 -0
- package/references/modes/velocity.md +7 -0
- package/references/modes/worktrees.md +2 -0
- package/tools/bridge-settings-read.mjs +40 -10
- package/tools/bridge-settings.mjs +22 -7
- package/tools/cheap-agents.mjs +15 -5
- package/tools/commands.mjs +2 -2
- package/tools/core-evidence.mjs +29 -2
- package/tools/detect-backends.mjs +1 -1
- package/tools/manifest/schema.md +7 -0
- package/tools/manifest/validate.mjs +8 -0
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +9 -2
- package/tools/recipes.mjs +4 -1
- package/tools/recommendations.mjs +110 -59
- package/tools/renderers.mjs +10 -1
- package/tools/review-state.mjs +4 -0
- package/tools/view-model.mjs +3 -1
package/tools/core-evidence.mjs
CHANGED
|
@@ -452,8 +452,22 @@ export const REVIEW_RECEIPT_CLASS = Object.freeze({
|
|
|
452
452
|
MALFORMED_MARKER: 'malformed-marker',
|
|
453
453
|
POSTURE_UNMARKED: 'posture-unmarked',
|
|
454
454
|
MALFORMED_POSTURE: 'malformed-posture',
|
|
455
|
+
DELIVERY_UNMARKED: 'delivery-unmarked',
|
|
456
|
+
MALFORMED_DELIVERY: 'malformed-delivery',
|
|
455
457
|
});
|
|
456
458
|
|
|
459
|
+
// Backends whose `code` receipts must SELF-DECLARE how the change set reached the model (D8b).
|
|
460
|
+
// Scoped, not universal: agy's oversized lane was the one observed returning a confident
|
|
461
|
+
// fabrication, and codex's receipt semantics are deliberately untouched.
|
|
462
|
+
const DELIVERY_DECLARING_BACKENDS = new Set(['agy']);
|
|
463
|
+
|
|
464
|
+
// A delivery declaration is a lowercase token naming HOW delivery was established (`inline` when
|
|
465
|
+
// the whole change set rode one prompt, `fed` when a chunked feed proved it by echo). The gate
|
|
466
|
+
// requires PRESENT and WELL-FORMED, never a PARTICULAR value — which lane was used is the
|
|
467
|
+
// wrapper's business; that the receipt declares one at all is the gate's.
|
|
468
|
+
const isValidReceiptDelivery = (delivery) =>
|
|
469
|
+
typeof delivery === 'string' && delivery.length > 0 && delivery.length <= 32 && /^[a-z][a-z0-9-]*$/.test(delivery);
|
|
470
|
+
|
|
457
471
|
// The D5 posture declaration (strip Phase 4): an object whose `model` is a NON-EMPTY string;
|
|
458
472
|
// `effort` (when present) a non-empty string; `tier` (when present) a non-empty string or null.
|
|
459
473
|
// Backend-agnostic — the wrapper is the writer authority on WHICH keys it declares.
|
|
@@ -483,6 +497,15 @@ export const classifyReviewReceiptForTree = (receipt, fingerprint) => {
|
|
|
483
497
|
if (!isValidReceiptPosture(receipt.posture)) return REVIEW_RECEIPT_CLASS.MALFORMED_POSTURE;
|
|
484
498
|
if (!isRecognizedVerdict(receipt.verdict)) return REVIEW_RECEIPT_CLASS.UNRECOGNIZED_VERDICT;
|
|
485
499
|
if (receipt.grounded !== true) return REVIEW_RECEIPT_CLASS.UNGROUNDED;
|
|
500
|
+
// LAST on purpose. The delivery arm may only intercept a receipt that would otherwise ATTEST:
|
|
501
|
+
// placing it earlier pulled delivery-less `unrecognized-verdict` / `ungrounded` agy receipts out
|
|
502
|
+
// of summarize's latest-NORMAL selection, so an EARLIER ship survived a LATER bad receipt — the
|
|
503
|
+
// selection-first doctrine, broken silently. Those two classes stay byte-identical to before;
|
|
504
|
+
// the ONE class this arm changes is the previously-attesting one, which is the point.
|
|
505
|
+
if (DELIVERY_DECLARING_BACKENDS.has(receipt.backend)) {
|
|
506
|
+
if (!Object.hasOwn(receipt, 'delivery')) return REVIEW_RECEIPT_CLASS.DELIVERY_UNMARKED;
|
|
507
|
+
if (!isValidReceiptDelivery(receipt.delivery)) return REVIEW_RECEIPT_CLASS.MALFORMED_DELIVERY;
|
|
508
|
+
}
|
|
486
509
|
return REVIEW_RECEIPT_CLASS.ATTESTING;
|
|
487
510
|
};
|
|
488
511
|
|
|
@@ -504,6 +527,8 @@ export const summarizeReviewReceiptsForTree = (receipts, fingerprint) => {
|
|
|
504
527
|
const malformedMarker = rowsFor(REVIEW_RECEIPT_CLASS.MALFORMED_MARKER);
|
|
505
528
|
const postureUnmarked = rowsFor(REVIEW_RECEIPT_CLASS.POSTURE_UNMARKED);
|
|
506
529
|
const malformedPosture = rowsFor(REVIEW_RECEIPT_CLASS.MALFORMED_POSTURE);
|
|
530
|
+
const deliveryUnmarked = rowsFor(REVIEW_RECEIPT_CLASS.DELIVERY_UNMARKED);
|
|
531
|
+
const malformedDelivery = rowsFor(REVIEW_RECEIPT_CLASS.MALFORMED_DELIVERY);
|
|
507
532
|
const counts = {
|
|
508
533
|
currentCount: classified.length,
|
|
509
534
|
ungroundedCount: rowsFor(REVIEW_RECEIPT_CLASS.UNGROUNDED).length,
|
|
@@ -512,6 +537,7 @@ export const summarizeReviewReceiptsForTree = (receipts, fingerprint) => {
|
|
|
512
537
|
markerRejected: malformedMarker.length,
|
|
513
538
|
unmarkedRejected: unmarked.length,
|
|
514
539
|
postureRejected: postureUnmarked.length + malformedPosture.length,
|
|
540
|
+
deliveryRejected: deliveryUnmarked.length + malformedDelivery.length,
|
|
515
541
|
};
|
|
516
542
|
if (normal.length > 0) {
|
|
517
543
|
const latest = normal[normal.length - 1];
|
|
@@ -521,7 +547,7 @@ export const summarizeReviewReceiptsForTree = (receipts, fingerprint) => {
|
|
|
521
547
|
}
|
|
522
548
|
if (classified.length > 0) {
|
|
523
549
|
return {
|
|
524
|
-
state: malformedMarker.length > 0 || unmarked.length > 0 || counts.postureRejected > 0 ? 'rejected' : 'probe',
|
|
550
|
+
state: malformedMarker.length > 0 || unmarked.length > 0 || counts.postureRejected > 0 || counts.deliveryRejected > 0 ? 'rejected' : 'probe',
|
|
525
551
|
receipt: null,
|
|
526
552
|
...counts,
|
|
527
553
|
};
|
|
@@ -537,12 +563,13 @@ export const describeMissingReviewAttestation = (summary) => {
|
|
|
537
563
|
summary.markerRejected > 0 ? `${summary.markerRejected} receipt(s) with a malformed probe marker` : null,
|
|
538
564
|
summary.unmarkedRejected > 0 ? `${summary.unmarkedRejected} receipt(s) with no probe marker` : null,
|
|
539
565
|
(summary.postureRejected ?? 0) > 0 ? `${summary.postureRejected} receipt(s) with an absent/invalid run posture (a pre-D5 wrapper — re-run the review on the current bridge)` : null,
|
|
566
|
+
(summary.deliveryRejected ?? 0) > 0 ? `${summary.deliveryRejected} agy receipt(s) with an absent/invalid delivery declaration (minted before the change set was PROVEN delivered — re-run the review on the current bridge)` : null,
|
|
540
567
|
].filter(Boolean);
|
|
541
568
|
const exclusionSuffix = exclusions.length > 0 ? `; excluded ${exclusions.join(', ')}` : '';
|
|
542
569
|
if (summary.state === 'ungrounded') return `the latest normal receipt for the current tree is ungrounded${exclusionSuffix}`;
|
|
543
570
|
if (summary.state === 'unrecognized-verdict') return `the latest normal receipt carries an unrecognized verdict (${JSON.stringify(summary.receipt?.verdict ?? null)}) — an unknown verdict never attests${exclusionSuffix}`;
|
|
544
571
|
if (summary.state === 'probe') return 'only probe receipts exist for the current tree — a probe review never attests';
|
|
545
|
-
if (summary.state === 'rejected') return `current-tree receipts have an untrustworthy probe marker or
|
|
572
|
+
if (summary.state === 'rejected') return `current-tree receipts have an untrustworthy probe marker, run posture or delivery declaration${exclusionSuffix}`;
|
|
546
573
|
return 'no fresh code receipt exists for the current tree';
|
|
547
574
|
};
|
|
548
575
|
|
|
@@ -133,7 +133,7 @@ const RAW_BACKENDS = [
|
|
|
133
133
|
'agy-review --continue [--decided @f] [--focus "…"]',
|
|
134
134
|
'agy-review --conversation <id> [--decided @f] [--focus "…"]',
|
|
135
135
|
],
|
|
136
|
-
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write failure warns, never fails the review",
|
|
136
|
+
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; delivery = how the change set REACHED the model, currently emitted as 'inline' (the whole set rode one prompt — proven by construction) or 'fed' (a chunked feed whose per-part echo proof verified); REQUIRED on every agy code receipt and its ABSENCE is what stops a pre-fed-lane receipt attesting, while the gate accepts any well-formed declaration rather than a particular value; absent by construction on plan/diff/continuation receipts, which carry no change set; a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write failure warns, never fails the review",
|
|
137
137
|
notes: [
|
|
138
138
|
'pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt',
|
|
139
139
|
'the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration agy-run hands to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it never enters the receipt posture or the D5 banner↔receipt parity',
|
package/tools/manifest/schema.md
CHANGED
|
@@ -83,6 +83,13 @@ this block). Each entry:
|
|
|
83
83
|
union of both bridges' `settings` keys) and skips other wrappers' keys silently.
|
|
84
84
|
- `effect` (string, required) — what the knob does, incl. built-in defaults and any spend/risk
|
|
85
85
|
caveat (the credit-rate caveat rides here for the tier knob).
|
|
86
|
+
- `retired` (string, optional; **>= 20 chars when present**) — the STATED reason a key is kept only
|
|
87
|
+
for RECOGNITION. Recognition alone is not enough: a retired key stays in the registry so an
|
|
88
|
+
existing settings line never starts warning as unknown, but it **arms nothing**. Runtime semantics
|
|
89
|
+
the field switches on: the writer REFUSES a new `--set` (and still permits `--unset`, which is the
|
|
90
|
+
stated recovery), the reader/`--json`/status/`procedures` render it as retired rather than active,
|
|
91
|
+
and the init/upgrade survival check reports it as flagged rather than "all current". A bare flag is
|
|
92
|
+
rejected by validation — a dead knob must explain itself wherever it surfaces.
|
|
86
93
|
|
|
87
94
|
Wrappers never parse JSON at run time: each carries its own shell registry/validation constants,
|
|
88
95
|
drift-guarded set-equal to this block by the bridge `bin/*.test.mjs` suites (help section keys,
|
|
@@ -596,6 +596,14 @@ export const validateManifest = (skillDir) => {
|
|
|
596
596
|
seenKeys.add(entry.key);
|
|
597
597
|
}
|
|
598
598
|
if (typeof entry.effect !== 'string' || !entry.effect) errors.push(`${at}.effect must be a non-empty string`);
|
|
599
|
+
// RETIRED metadata (D3): recognition alone is not enough. A key kept only so an existing
|
|
600
|
+
// settings line never warns as unknown carries a STATED reason; the writer refuses a new
|
|
601
|
+
// `--set` of it and every reader surface renders it as retired. Optional, but never a bare
|
|
602
|
+
// flag — an unvalidated field must not be able to ship.
|
|
603
|
+
if (Object.hasOwn(entry, 'retired')
|
|
604
|
+
&& (typeof entry.retired !== 'string' || entry.retired.trim().length < 20)) {
|
|
605
|
+
errors.push(`${at}.retired must be a string stating WHY the key is retired (>= 20 chars)`);
|
|
606
|
+
}
|
|
599
607
|
if (!Array.isArray(entry.appliesTo) || entry.appliesTo.length === 0
|
|
600
608
|
|| !entry.appliesTo.every((c) => typeof c === 'string' && c)) {
|
|
601
609
|
errors.push(`${at}.appliesTo must be a non-empty array of wrapper cmd names`);
|
package/tools/presentation.mjs
CHANGED
package/tools/procedures.mjs
CHANGED
|
@@ -160,7 +160,10 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) => {
|
|
|
160
160
|
const contracts = dispatch
|
|
161
161
|
.map((d) => ({ backend: d.backend, role: d.role, cmd: wrapperCmdFor(d.backend, d.role), contract: wrapperContractFor(d.backend, d.role) }))
|
|
162
162
|
.filter((c) => c.cmd && c.contract)
|
|
163
|
-
|
|
163
|
+
// `retired` rides along: without it this surface advertised a RETIRED key as an ordinary
|
|
164
|
+
// settable knob, while the writer refuses to set it — a driving contract that contradicts the
|
|
165
|
+
// tool it points at.
|
|
166
|
+
.map((c) => ({ ...c, settings: knobsFor(c.cmd).map((k) => ({ key: k.key, allowed: allowedLabel(k), retired: k.retired ?? null })) }));
|
|
164
167
|
return { slot, ...resolved, backends, contracts };
|
|
165
168
|
});
|
|
166
169
|
};
|
|
@@ -329,7 +332,11 @@ const contractLines = ({ cmd, contract, settings }) => {
|
|
|
329
332
|
// branch — contractLines drops any contract key it does not name, so this must be enumerated here).
|
|
330
333
|
if ((settings ?? []).length) {
|
|
331
334
|
lines.push(' host settings (survive kit upgrades — set via /agent-workflow-kit bridge-settings):');
|
|
332
|
-
for (const s of settings)
|
|
335
|
+
for (const s of settings) {
|
|
336
|
+
lines.push(s.retired
|
|
337
|
+
? ` ${s.key} — RETIRED: recognized but arms nothing; the writer refuses --set, --unset clears an existing line`
|
|
338
|
+
: ` ${s.key} — ${s.allowed}`);
|
|
339
|
+
}
|
|
333
340
|
}
|
|
334
341
|
return lines;
|
|
335
342
|
};
|
package/tools/recipes.mjs
CHANGED
|
@@ -352,7 +352,10 @@ export const composeStatusLine = (detection, recommendation, settings = null, au
|
|
|
352
352
|
// chars — collapse them to a single space so the "exactly one line" backend-status contract holds.
|
|
353
353
|
const oneLine = (s) => String(s).replace(/[\s]+/g, ' ').trim();
|
|
354
354
|
const active = settings?.active ?? [];
|
|
355
|
-
|
|
355
|
+
// A RETIRED knob is rendered as retired, never as an armed capability (D3 Invariant E): the line
|
|
356
|
+
// exists in the user's file, so hiding it would be a silent deletion — but reading it as active
|
|
357
|
+
// would claim a capability the wrapper no longer has.
|
|
358
|
+
const suffix = active.length ? ` · settings: ${active.map((s) => `${oneLine(s.key)}=${oneLine(s.value)}${s.retired ? ' (RETIRED — arms nothing)' : ''}`).join(' · ')}` : '';
|
|
356
359
|
// The autonomy segment (AD-044 Plan 4): rendered ONLY when the caller supplies the computed
|
|
357
360
|
// facts (composeAutonomyFacts) — an omitted param keeps the line byte-identical (the settings-
|
|
358
361
|
// suffix precedent). Fact-only: effective per-activity levels + the render-sync state; an absent
|
|
@@ -57,9 +57,10 @@ import { shellQuoteArg } from './review-state.mjs';
|
|
|
57
57
|
import { isFinalCapableDeclaration } from './run-gates.mjs';
|
|
58
58
|
import { resolveGitHooksPath } from './commit-guard.mjs';
|
|
59
59
|
import { loadConfig } from './orchestration-config.mjs';
|
|
60
|
-
import { DEFAULT_BUNDLE_ROOT
|
|
60
|
+
import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
61
61
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
62
62
|
import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
|
|
63
|
+
import { preflightCheapAgents } from './cheap-agents.mjs';
|
|
63
64
|
|
|
64
65
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
65
66
|
const toolPath = (rel) => join(HERE, rel);
|
|
@@ -105,10 +106,9 @@ export const SEVERITIES = Object.freeze({
|
|
|
105
106
|
'read-lane.stale': SEVERITY_ATTENTION,
|
|
106
107
|
'read-lane.missing': SEVERITY_ATTENTION,
|
|
107
108
|
'state-block': SEVERITY_OPTIONAL,
|
|
109
|
+
agents: SEVERITY_OPTIONAL,
|
|
108
110
|
'family-freshness': SEVERITY_ATTENTION,
|
|
109
111
|
'sandbox-masks': SEVERITY_OPTIONAL,
|
|
110
|
-
'agy-adddir': SEVERITY_OPTIONAL,
|
|
111
|
-
'agy-adddir.invalid-env': SEVERITY_ATTENTION,
|
|
112
112
|
'sandbox-lane': SEVERITY_OPTIONAL,
|
|
113
113
|
'worktrees-dir': SEVERITY_OPTIONAL,
|
|
114
114
|
});
|
|
@@ -163,11 +163,10 @@ export const WHATS = Object.freeze({
|
|
|
163
163
|
'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',
|
|
164
164
|
'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',
|
|
165
165
|
'state-block': 'nothing checks the closing state block — a turn that ends on «nothing needed from you», or on a promise it never started, passes unseen',
|
|
166
|
+
agents: '{n} read-only subagent(s) not placed (Claude Code) — no shell-free vehicle for that work; the apply PREVIEWS first',
|
|
166
167
|
'family-freshness': '{parts}',
|
|
167
168
|
'sandbox-masks': '{n} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale',
|
|
168
169
|
'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)',
|
|
169
|
-
'agy-adddir': 'agy-review is placed but AGY_REVIEW_ALLOW_ADDDIR is not set ({file}) — an oversized code review refuses instead of offloading',
|
|
170
|
-
'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',
|
|
171
170
|
'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
|
|
172
171
|
'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
|
|
173
172
|
});
|
|
@@ -211,19 +210,73 @@ export const BENEFITS = Object.freeze({
|
|
|
211
210
|
'autonomy-policy': 'clarity — the per-activity autonomy policy becomes an explicit, versioned declaration instead of implicit computed defaults',
|
|
212
211
|
'autonomy-render': `velocity — confined commands auto-allow per your declared policy; ${DUAL_SECURITY_BENEFIT}`,
|
|
213
212
|
'sandbox-provision': `velocity — confined ad-hoc commands stop prompting; ${DUAL_SECURITY_BENEFIT}`,
|
|
214
|
-
'review-recipe': '
|
|
213
|
+
'review-recipe': 'recipe coverage — the review AND execution recipes you configured actually run instead of silently degrading',
|
|
215
214
|
'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
|
|
216
215
|
'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
|
|
217
216
|
'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
|
|
218
217
|
'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
|
|
219
218
|
'state-block': 'no silent stalls — a turn ending on «you are not needed», or on work it never started, warns at once instead of waiting to be spotted',
|
|
219
|
+
agents: 'cost and quiet — mechanical work runs on a cheap model, and no vehicle has a shell, so a read-only fan-out cannot flood you with prompts',
|
|
220
220
|
'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
|
|
221
221
|
'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
|
|
222
|
-
'agy-adddir': 'large reviews — an oversized agy code review offloads to a staging dir instead of refusing',
|
|
223
222
|
'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
|
|
224
223
|
'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
|
|
225
224
|
});
|
|
226
225
|
|
|
226
|
+
// ── the CLOSED opt-in capability registry (OPT-IN-SHIPS-INVISIBLE) ──────────────────────────────
|
|
227
|
+
// 3.14.0 shipped a capability with every surface an AGENT reads and NO advisor entry, so `upgrade`
|
|
228
|
+
// told a user their setup was optimal while the thing that had just shipped sat unwired. Every other
|
|
229
|
+
// surface of a mode is drift-guarded; the advisor was the one that was not, and it is the only
|
|
230
|
+
// surface a user receives PASSIVELY.
|
|
231
|
+
//
|
|
232
|
+
// Keyed by CAPABILITY, not by mode: a per-mode registry cannot detect a new opt-in added INSIDE an
|
|
233
|
+
// already-registered mode (the read-lane belongs to mode `hook`), because the mode set does not move.
|
|
234
|
+
// Each id is DECLARED at its point of use in references/modes/<mode>.md and this registry is asserted
|
|
235
|
+
// set-equal to those declarations (test/advisor-coverage.test.mjs), mirroring the catalog ⟷ SKILL.md
|
|
236
|
+
// ⟷ mode-docs triangle commands.test.mjs already enforces.
|
|
237
|
+
//
|
|
238
|
+
// Every row carries EXACTLY ONE of `advisorKey` (the offer that observes its unconfigured state) or
|
|
239
|
+
// `exempt` (a stated reason it legitimately has none). Adding a capability ADDS a checked row; it
|
|
240
|
+
// never widens a blocklist.
|
|
241
|
+
export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
242
|
+
{ id: 'velocity-core', mode: 'velocity', advisorKey: 'velocity-core' },
|
|
243
|
+
{ id: 'kit-tools-tier', mode: 'velocity', advisorKey: 'kit-tools-tier' },
|
|
244
|
+
{ id: 'bridge-tier', mode: 'velocity', advisorKey: 'bridge-tier' },
|
|
245
|
+
{ id: 'autonomy-render', mode: 'velocity', advisorKey: 'autonomy-render' },
|
|
246
|
+
{ id: 'sandbox-lane', mode: 'velocity', advisorKey: 'sandbox-lane' },
|
|
247
|
+
{ id: 'autonomy-policy', mode: 'set-autonomy', advisorKey: 'autonomy-policy' },
|
|
248
|
+
{ id: 'sandbox-provision', mode: 'autonomy-doctor', advisorKey: 'sandbox-provision' },
|
|
249
|
+
{ id: 'gates-declaration', mode: 'gates', advisorKey: 'gates-declaration' },
|
|
250
|
+
{ id: 'gate-hook', mode: 'hook', advisorKey: 'gate-hook' },
|
|
251
|
+
{ id: 'read-lane', mode: 'hook', advisorKey: 'read-lane' },
|
|
252
|
+
{ id: 'commit-guard', mode: 'commit-guard', advisorKey: 'commit-guard' },
|
|
253
|
+
{ id: 'state-block', mode: 'state-block-guard', advisorKey: 'state-block' },
|
|
254
|
+
{ id: 'sandbox-masks', mode: 'sandbox-masks', advisorKey: 'sandbox-masks' },
|
|
255
|
+
{ id: 'worktrees-dir', mode: 'worktrees', advisorKey: 'worktrees-dir' },
|
|
256
|
+
{ id: 'family-freshness', mode: 'upgrade', advisorKey: 'family-freshness' },
|
|
257
|
+
{ id: 'review-recipe', mode: 'set-recipe', advisorKey: 'review-recipe' },
|
|
258
|
+
// The execute slot is a DISTINCT opt-in from the review slot, and the same probe reports both —
|
|
259
|
+
// which is why the review-recipe benefit is worded for either slot rather than for review alone.
|
|
260
|
+
{ id: 'delegated-execution', mode: 'set-recipe', advisorKey: 'review-recipe' },
|
|
261
|
+
{ id: 'agents', mode: 'agents', advisorKey: 'agents' },
|
|
262
|
+
// Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
|
|
263
|
+
// a TRUST-POSTURE change. The kit never nudges a user toward weakening their approval posture (the
|
|
264
|
+
// same doctrine that keeps sandbox network/filesystem allowances HAND-APPLY); velocity presents the
|
|
265
|
+
// full honest posture at its own consent moment, where the user is already deciding.
|
|
266
|
+
{
|
|
267
|
+
id: 'accept-edits',
|
|
268
|
+
mode: 'velocity',
|
|
269
|
+
exempt: 'a trust-posture change (auto-applied edits, auto-run mkdir/touch/mv/cp) — the kit never nudges a user toward weakening their own approval posture; velocity states the full posture at its own consent moment',
|
|
270
|
+
},
|
|
271
|
+
// Exempt for the mirror-image reason: the Fast tier bills at a higher credit rate, so an unprompted
|
|
272
|
+
// offer would be the kit nudging the user to spend money.
|
|
273
|
+
{
|
|
274
|
+
id: 'codex-fast',
|
|
275
|
+
mode: 'bridge-settings',
|
|
276
|
+
exempt: 'a paid SPEND knob (the priority tier bills at a higher credit rate) — the kit never nudges a user toward spending money; bridge-settings surfaces it with its cost caveat when asked',
|
|
277
|
+
},
|
|
278
|
+
].map((c) => Object.freeze(c)));
|
|
279
|
+
|
|
227
280
|
// A typed usage failure (exit 2) — the codebase's typed-error idiom (no classes).
|
|
228
281
|
const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
|
|
229
282
|
|
|
@@ -462,6 +515,42 @@ const probeStateBlockHook = ({ root, deps, add, skip }) => {
|
|
|
462
515
|
}
|
|
463
516
|
};
|
|
464
517
|
|
|
518
|
+
// The cheap-agents offer (OPT-IN-SHIPS-INVISIBLE). This item exists because the coverage registry
|
|
519
|
+
// surfaced its absence: `agents` is the family's SECOND `.claude/` writer, the `help` Tune tail
|
|
520
|
+
// advertises it, and the advisor had no entry for it — so a user who never runs `help` never learned
|
|
521
|
+
// it existed while the advisor reported the deployment optimal. Found by the guard, not by an incident.
|
|
522
|
+
// Only a PLACE action counts as a gap: `already-current` is converged and `customized-preserved` is
|
|
523
|
+
// the user's own edit, which the writer never clobbers and the advisor must never nag about.
|
|
524
|
+
const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
525
|
+
try {
|
|
526
|
+
const preflight = preflightCheapAgents({ cwd: root }, deps);
|
|
527
|
+
// The writer refuses below the expected lineage, so offering its command would hand the user a
|
|
528
|
+
// guaranteed failure — the honest surface is a stated skip naming the recovery.
|
|
529
|
+
if (!preflight.stampOk) {
|
|
530
|
+
skip('agents', new Error(`not a deployed agent-workflow project at the current lineage (found ${preflight.stamp ?? 'none'}) — run upgrade first`));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const toPlace = preflight.plan.filter((item) => item.action === 'place');
|
|
534
|
+
if (toPlace.length === 0) return; // converged
|
|
535
|
+
// The writer's contract is «--dry-run first, ALWAYS» (references/modes/agents.md, and its stated
|
|
536
|
+
// invariant «preview by default»), so the rendered line is the PREVIEW — this item joins the
|
|
537
|
+
// dry-run-preview class the mode doc already documents for the gates-declaration seeder, where the
|
|
538
|
+
// same confirmation then runs the follow-up the preview prints. Rendering --apply here would have
|
|
539
|
+
// skipped the per-vehicle plan the user is supposed to see before consenting.
|
|
540
|
+
// The hidden-mode reconcile rides the detail, never the apply line: it is wrong to run on a
|
|
541
|
+
// VISIBLE deployment, and the apply slot must stay one pure executable command.
|
|
542
|
+
add(
|
|
543
|
+
'agents',
|
|
544
|
+
fillTemplate(WHATS.agents, { n: toPlace.length }),
|
|
545
|
+
`node ${q(toolPath('cheap-agents.mjs'))} --cwd ${q(root)}`,
|
|
546
|
+
'agents',
|
|
547
|
+
`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`,
|
|
548
|
+
);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
skip('agents', err);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
465
554
|
const probeFamilyFreshness = ({ deps, add, skip }) => {
|
|
466
555
|
try {
|
|
467
556
|
const survey = deps.surveyFamily ?? surveyFamily;
|
|
@@ -502,53 +591,13 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
|
|
|
502
591
|
}
|
|
503
592
|
};
|
|
504
593
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const isValidBool = (v) => v === '0' || v === '1';
|
|
513
|
-
const env = deps.getenv ?? process.env;
|
|
514
|
-
if (env.AGY_REVIEW_ALLOW_ADDDIR != null) {
|
|
515
|
-
if (isValidBool(env.AGY_REVIEW_ALLOW_ADDDIR)) return; // an explicit valid env choice — respected
|
|
516
|
-
// A SET-BUT-EMPTY env var is the wrapper's opt-out shape (${!key+x}: it shadows the file
|
|
517
|
-
// and falls back to the built-in refuse default) — a user CHOICE, never nagged (codex).
|
|
518
|
-
if (env.AGY_REVIEW_ALLOW_ADDDIR === '') return;
|
|
519
|
-
// env > file: while ANY env value is set the wrapper ignores the settings file, so the file
|
|
520
|
-
// writer cannot fix an invalid env — the honest apply is to fix/unset the env var (codex).
|
|
521
|
-
const value = truncatedTo(oneLineOf(JSON.stringify(env.AGY_REVIEW_ALLOW_ADDDIR)), templateBudget(WHATS['agy-adddir.invalid-env']));
|
|
522
|
-
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');
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
const confPath = settingsPath({ getenv: env, home: deps.home });
|
|
526
|
-
const readFile = deps.readFile ?? readFileSync;
|
|
527
|
-
const text = (() => {
|
|
528
|
-
try {
|
|
529
|
-
return readFile(confPath, 'utf8');
|
|
530
|
-
} catch (err) {
|
|
531
|
-
if (err?.code === 'ENOENT') return '';
|
|
532
|
-
throw err;
|
|
533
|
-
}
|
|
534
|
-
})();
|
|
535
|
-
const parsed = parseSettings(text);
|
|
536
|
-
const fileEntries = parsed.byKey.get('AGY_REVIEW_ALLOW_ADDDIR');
|
|
537
|
-
const fileValue = fileEntries?.length ? fileEntries[fileEntries.length - 1].value : null;
|
|
538
|
-
if (fileValue != null && isValidBool(fileValue)) return; // env is absent here — a valid file value governs
|
|
539
|
-
// The settings writer REFUSES a duplicate-carrying file — rendering its command would hand
|
|
540
|
-
// the user a guaranteed failure; the honest apply is fix-duplicates-first (codex terminal).
|
|
541
|
-
const dups = duplicateKeys(parsed);
|
|
542
|
-
const what = fillTemplate(WHATS['agy-adddir'], { file: SETTINGS_FILENAME });
|
|
543
|
-
if (dups.length > 0) {
|
|
544
|
-
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`);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
add('agy-adddir', what, `node ${q(toolPath('bridge-settings.mjs'))} --set AGY_REVIEW_ALLOW_ADDDIR=1 --apply`);
|
|
548
|
-
} catch (err) {
|
|
549
|
-
skip('agy-adddir', err);
|
|
550
|
-
}
|
|
551
|
-
};
|
|
594
|
+
// probeAgyAdddir is GONE, not silenced. It offered to arm AGY_REVIEW_ALLOW_ADDDIR with the benefit
|
|
595
|
+
// "large reviews — an oversized agy code review offloads to a staging dir instead of refusing".
|
|
596
|
+
// Headless agy AUTO-DENIES its own read_file tool, so that lane could return a confident fabrication
|
|
597
|
+
// (two BLOCKING findings citing lines of a file that has none, observed) or an empty SHIP, with no
|
|
598
|
+
// way to tell — the advisor was recommending the one lane whose failure mode is undetectable. The
|
|
599
|
+
// knob is retired in the wrappers (recognized, arms nothing) and an oversized code review is now a
|
|
600
|
+
// chunked feed with a per-part delivery proof, so there is nothing left to offer.
|
|
552
601
|
|
|
553
602
|
// The manifest-declared session-sandbox recipe surfaces of every BUNDLED bridge whose review
|
|
554
603
|
// wrapper is in the wired set — networkHosts ∪ writableDirs, derived from the manifests (the
|
|
@@ -713,7 +762,7 @@ const readAckValue = (root, deps, ackKey) => {
|
|
|
713
762
|
// false (the lane is off — offer it). `readLane === true` → enabled (converged). A parse/IO error on
|
|
714
763
|
// an EXISTING file, a symlinked ancestor/leaf, an escape, or a non-object root THROWS — the probe
|
|
715
764
|
// turns it into a stated skip (a BROKEN toggle the writer would refuse to overwrite is not "off").
|
|
716
|
-
// A present-but-non-boolean
|
|
765
|
+
// A present-but-non-boolean value is a valid store the writer merges → false (offer), never a skip.
|
|
717
766
|
const readReadLaneToggle = (root, deps) => {
|
|
718
767
|
const readFile = deps.readFile ?? readFileSync;
|
|
719
768
|
const lstat = deps.lstat ?? lstatSync;
|
|
@@ -739,7 +788,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
739
788
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
740
789
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
741
790
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
742
|
-
export const RISK_NOTED_KEYS = Object.freeze(['
|
|
791
|
+
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir']);
|
|
743
792
|
|
|
744
793
|
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
745
794
|
try {
|
|
@@ -926,9 +975,9 @@ const PROBES = Object.freeze([
|
|
|
926
975
|
probeCommitGuard,
|
|
927
976
|
probeReadLane,
|
|
928
977
|
probeStateBlockHook,
|
|
978
|
+
probeCheapAgents,
|
|
929
979
|
probeFamilyFreshness,
|
|
930
980
|
probeMasksItem,
|
|
931
|
-
probeAgyAdddir,
|
|
932
981
|
probeSandboxLane,
|
|
933
982
|
probeWorktreesDir,
|
|
934
983
|
]);
|
|
@@ -946,7 +995,8 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
946
995
|
// its class differs from the base (the invalid-env attention arm).
|
|
947
996
|
// `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
|
|
948
997
|
// the capped WHAT and does NOT belong in the pure-command apply (the sandbox-lane live recipe:
|
|
949
|
-
// egress hosts + resolved writable dirs
|
|
998
|
+
// egress hosts + resolved writable dirs; the worktrees-dir hand-apply-first grant advice; the
|
|
999
|
+
// agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
|
|
950
1000
|
const add = (key, what, apply, severityKey = key, detail = null) => {
|
|
951
1001
|
const problems = [];
|
|
952
1002
|
if (!(key in BENEFITS)) problems.push(`unregistered item key ${JSON.stringify(key)}`);
|
|
@@ -1005,7 +1055,8 @@ Usage:
|
|
|
1005
1055
|
Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
|
|
1006
1056
|
one composed verdict line opens every non-optimal render, then per item {severity · what is
|
|
1007
1057
|
sub-optimal · the benefit in one plain line · an optional \`recipe:\` line (the sandbox-lane live
|
|
1008
|
-
recipe,
|
|
1058
|
+
recipe, the worktrees-dir hand-apply-first grant advice, or the agents hidden-mode reconcile
|
|
1059
|
+
follow-up) · the exact consent-gated apply one-liner}. --cwd is
|
|
1009
1060
|
REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
|
|
1010
1061
|
section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
|
|
1011
1062
|
stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
|
package/tools/renderers.mjs
CHANGED
|
@@ -60,8 +60,17 @@ const renderBridges = (vm, { glyph, color }) => {
|
|
|
60
60
|
// Absent when no knob is active, so the block stays byte-identical to before when nothing is set.
|
|
61
61
|
if (b.settings?.error) lines.push(` ${pad('', MEMBER_COL)}${glyph.note} couldn't read bridge settings (${b.settings.error})`);
|
|
62
62
|
else if (b.settings?.active?.length) {
|
|
63
|
-
|
|
63
|
+
// A RETIRED knob is CONFIGURED but arms nothing. It must still SHOW — the line really is in
|
|
64
|
+
// the user's file — but rendering it like any other active setting would claim a capability
|
|
65
|
+
// the wrapper no longer has, which is the exact confusion the retirement exists to prevent.
|
|
66
|
+
const active = b.settings.active
|
|
67
|
+
.map((s) => `${s.key}=${s.value} [${s.source}]${s.retired ? ' RETIRED — arms nothing' : ''}`)
|
|
68
|
+
.join(' · ');
|
|
64
69
|
lines.push(` ${pad('', MEMBER_COL)}settings: ${active}`);
|
|
70
|
+
const retired = b.settings.active.filter((s) => s.retired);
|
|
71
|
+
for (const s of retired) {
|
|
72
|
+
lines.push(` ${pad('', MEMBER_COL)}${glyph.note} ${s.key} is retired — clear it with: bridge-settings --unset ${s.key} --apply`);
|
|
73
|
+
}
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
return lines;
|
package/tools/review-state.mjs
CHANGED
|
@@ -244,6 +244,7 @@ export const backendReceiptStatus = (receipts, backend, fingerprint) => {
|
|
|
244
244
|
markerRejected: summary.markerRejected,
|
|
245
245
|
unmarkedRejected: summary.unmarkedRejected,
|
|
246
246
|
postureRejected: summary.postureRejected,
|
|
247
|
+
deliveryRejected: summary.deliveryRejected,
|
|
247
248
|
};
|
|
248
249
|
if (summary.state === 'current') {
|
|
249
250
|
return { state: 'current', verdict: summary.receipt.verdict ?? 'unknown', shipClass: isShipVerdict(summary.receipt.verdict), grounded: true, timestamp: summary.receipt.timestamp ?? null, ...counts };
|
|
@@ -373,6 +374,9 @@ const rejectionCause = (b) => {
|
|
|
373
374
|
if ((b.postureRejected ?? 0) > 0) {
|
|
374
375
|
parts.push(`${b.postureRejected} with an absent/invalid run posture (D5) — a pre-posture wrapper minted it; re-run the review on the current bridge`);
|
|
375
376
|
}
|
|
377
|
+
if ((b.deliveryRejected ?? 0) > 0) {
|
|
378
|
+
parts.push(`${b.deliveryRejected} with an absent/invalid delivery declaration (D8b) — the receipt never declared HOW the change set reached the model, so delivery was not proven; re-run the review on the current bridge`);
|
|
379
|
+
}
|
|
376
380
|
return parts.join(' + ');
|
|
377
381
|
};
|
|
378
382
|
|
package/tools/view-model.mjs
CHANGED
|
@@ -37,7 +37,9 @@ const bridgeVm = (b) => ({
|
|
|
37
37
|
settings: b.settings?.error
|
|
38
38
|
? { error: b.settings.error }
|
|
39
39
|
: b.settings?.active?.length
|
|
40
|
-
|
|
40
|
+
// `retired` rides along: a retired knob is CONFIGURED but arms nothing, and dropping the flag
|
|
41
|
+
// here rendered a dead key as an active setting on the one surface a user reads by default.
|
|
42
|
+
? { active: b.settings.active.map((a) => ({ key: a.key, value: a.value, source: a.source, retired: a.retired ?? null })) }
|
|
41
43
|
: null,
|
|
42
44
|
});
|
|
43
45
|
|