@sabaiway/agent-workflow-kit 5.2.0 → 5.4.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 +110 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +11 -3
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +154 -35
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +257 -4
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/references/driving-codex.md +5 -3
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +33 -15
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/coverage-check.md +1 -1
- package/references/modes/gates.md +7 -2
- package/references/modes/recommendations.md +3 -1
- package/references/modes/upgrade.md +1 -1
- package/references/modes/velocity.md +5 -1
- package/references/scripts/archive-decisions.mjs +340 -15
- package/references/scripts/archive-decisions.test.mjs +522 -2
- package/references/scripts/migrate-gates.mjs +102 -10
- package/references/scripts/migrate-gates.test.mjs +37 -0
- package/tools/core-evidence.mjs +42 -2
- package/tools/coverage-check.mjs +23 -7
- package/tools/coverage-producer.mjs +68 -0
- package/tools/coverage-state.mjs +24 -0
- package/tools/declared-paths.mjs +32 -0
- package/tools/detect-backends.mjs +1 -0
- package/tools/dispatch-record.mjs +926 -0
- package/tools/doc-parity.mjs +19 -4
- package/tools/flow-check.mjs +48 -12
- package/tools/gates-declaration.mjs +49 -0
- package/tools/gates-init.mjs +83 -6
- package/tools/recommendations.mjs +63 -19
- package/tools/run-gates.mjs +111 -32
- package/tools/velocity-profile.mjs +102 -23
package/tools/doc-parity.mjs
CHANGED
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
ACKS_FILE,
|
|
34
34
|
} from './recommendations.mjs';
|
|
35
35
|
import { SKIPPED_READONLY } from './setup-backends.mjs';
|
|
36
|
+
// The host-conditional qualifier every settings-derived runtime claim carries (Decision 11).
|
|
37
|
+
import { HOST_HONORS_QUALIFIER } from './velocity-profile.mjs';
|
|
36
38
|
import { LATENT_ARM_NOTICE } from './review-state.mjs';
|
|
37
39
|
import { QUEUE_SHARED_RULE, LANDING_FROM_MAIN, NO_DEPENDENCIES_POSTURE, CLEANUP_OWNERSHIP_RULE, INCLUDE_IDENTITY_RULE, RESUME_VERIFY_RULE } from './worktrees.mjs';
|
|
38
40
|
// The flow contract constants: the accepted schema version + the honest lagging-kit sentence
|
|
@@ -42,6 +44,8 @@ import { FLOW_SCHEMA_VERSION, FLOW_LAGGING_KIT_CONTRACT } from './orchestration-
|
|
|
42
44
|
import { FLOW_BOOKKEEPING_FLOOR_RESIDUAL } from './set-flow.mjs';
|
|
43
45
|
import { FLOW_ARMED_HALVES_HEADER } from './procedures.mjs';
|
|
44
46
|
import { RECEIPT_DEADLINE_CONTRACT } from './receipt-deadline.mjs';
|
|
47
|
+
// The coverage vocabulary leaf: a CLOSED value set the gates contract doc must enumerate.
|
|
48
|
+
import { COVERAGE } from './coverage-state.mjs';
|
|
45
49
|
|
|
46
50
|
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
47
51
|
|
|
@@ -55,6 +59,7 @@ const WORKTREES_DOC = 'references/modes/worktrees.md';
|
|
|
55
59
|
const PROCEDURES_DOC = 'references/modes/procedures.md';
|
|
56
60
|
const SET_FLOW_DOC = 'references/modes/set-flow.md';
|
|
57
61
|
const RECEIPT_DEADLINE_DOC = 'references/modes/receipt-deadline.md';
|
|
62
|
+
const GATES_DOC = 'references/modes/gates.md';
|
|
58
63
|
|
|
59
64
|
// A typed usage failure (exit 2) for the CLI parser — the codebase's typed-error idiom (no classes).
|
|
60
65
|
const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
|
|
@@ -99,6 +104,11 @@ export const BINDINGS = Object.freeze([
|
|
|
99
104
|
// (the incident's "mode-doc apply text stays in lockstep" acceptance as a mechanism, not prose).
|
|
100
105
|
// Bound in BOTH docs that name the path (recommendations.md + velocity.md).
|
|
101
106
|
valueBinding('acks-file', ACKS_FILE, ACKS_FILE, [RECOMMENDATIONS_DOC, VELOCITY_DOC]),
|
|
107
|
+
// The host-conditional qualifier (Decision 11): the tier notice, the USAGE text and the autonomy
|
|
108
|
+
// render all state a settings key's RUNTIME effect through this one phrase, so the mode doc that
|
|
109
|
+
// documents those surfaces must carry it too — a reworded doc that quietly re-promises the effect
|
|
110
|
+
// fails here instead of shipping.
|
|
111
|
+
valueBinding('host-honors-qualifier', HOST_HONORS_QUALIFIER, HOST_HONORS_QUALIFIER, [VELOCITY_DOC]),
|
|
102
112
|
// The refresh read-only degrade outcome (REFRESH-EROFS-HONESTY / AD-056): the new skipped-readonly
|
|
103
113
|
// token must render in BOTH mode contracts that enumerate the placed-bridge refresh outcomes
|
|
104
114
|
// (setup.md owns --refresh-placed; upgrade.md pastes its lines) — a reworded doc dropping the
|
|
@@ -142,6 +152,10 @@ export const BINDINGS = Object.freeze([
|
|
|
142
152
|
// satisfaction — is the tool's identity; a mode doc silently drifting off it would re-open the
|
|
143
153
|
// #50 misclassification this runner exists to close.
|
|
144
154
|
valueBinding('receipt-deadline-contract', RECEIPT_DEADLINE_CONTRACT, RECEIPT_DEADLINE_CONTRACT, [RECEIPT_DEADLINE_DOC]),
|
|
155
|
+
// The runner's `coverage=` summary vocabulary (Decision 8): the gates contract doc enumerates the
|
|
156
|
+
// CLOSED value set, so a renamed or added value fails here instead of leaving the doc describing
|
|
157
|
+
// a vocabulary the runner no longer speaks. One binding per value — the set is small and closed.
|
|
158
|
+
...Object.values(COVERAGE).map((value) => valueBinding(`coverage-state:${value}`, value, `\`coverage=${value}\``, [GATES_DOC])),
|
|
145
159
|
].map((b) => Object.freeze(b)));
|
|
146
160
|
|
|
147
161
|
// ── the pure checker (readText is injectable for hermetic tests) ────────────────────────
|
|
@@ -187,12 +201,13 @@ Usage:
|
|
|
187
201
|
|
|
188
202
|
A CLOSED, exported registry binds each live code constant — the autonomy-doctor contract (the EXIT
|
|
189
203
|
table, the status tokens, the trusted-dir allowlist), the recommendations/upgrade presentation
|
|
190
|
-
contract (section header, empty line, verdict templates), the acks-store path, the
|
|
191
|
-
degrade token, the review-state clean-tree latent-arm notice, the worktrees provision-record
|
|
204
|
+
contract (section header, empty line, verdict templates), the acks-store path, the host-conditional
|
|
205
|
+
qualifier every settings-derived runtime claim carries, the setup refresh degrade token, the review-state clean-tree latent-arm notice, the worktrees provision-record
|
|
192
206
|
orientation contract (shared-queue rule, landing-from-main, no-dependencies install posture), the
|
|
193
207
|
worktrees cleanup-ownership rule, the worktrees include-identity rule, the worktrees
|
|
194
|
-
resume-verify rule,
|
|
195
|
-
lagging-kit sentence, procedures.md)
|
|
208
|
+
resume-verify rule, the flow tolerate contract (the accepted flow schema version + the
|
|
209
|
+
lagging-kit sentence, procedures.md), and the runner's closed coverage= summary vocabulary
|
|
210
|
+
(gates.md) — to
|
|
196
211
|
the exact token its references/modes/*.md contract must carry, and
|
|
197
212
|
asserts the CURRENT value renders into every bound file. A drifted doc, an unreadable bound file,
|
|
198
213
|
or an absent token FAILS CLOSED.
|
package/tools/flow-check.mjs
CHANGED
|
@@ -281,7 +281,33 @@ export const evaluateVetoOverride = ({ records, vetoReceipt, tree }) => {
|
|
|
281
281
|
// and their records remain forgeable (each store's own header says so) — a backdated or
|
|
282
282
|
// backwards-clock instant can move a red across this boundary; the cross-store arming-fence
|
|
283
283
|
// hardening is queued (FLOW-ARMING-FENCE-CROSS-STORE), never pretended here.
|
|
284
|
-
|
|
284
|
+
//
|
|
285
|
+
// The lane split (FLOW-FINAL-RED-DEADLOCK) — the same reasoning the D10 arm already applies one arm
|
|
286
|
+
// away (see the `consumer` paragraph in computeFlowDecision's own header; a line reference there
|
|
287
|
+
// would rot on the next insertion). The strict rule demands a receipt the gate's OWN run has not
|
|
288
|
+
// written yet: run-gates appends the final receipt only AFTER every gate has run, so an in-matrix
|
|
289
|
+
// flow-check can never see the completed retry that would answer the newest red, and each --final
|
|
290
|
+
// mints red N+1 — no number of rerun-causes converges. On the 'gate' lane a current-base red is
|
|
291
|
+
// therefore ALSO answered by a provable IN-PROGRESS retry: an authoritative rerun-cause naming its
|
|
292
|
+
// attempt AND binding the CURRENT fingerprint, plus a final-start at that fingerprint ordered
|
|
293
|
+
// STRICTLY AFTER that red whose attempt carries no completed final — the shape run-gates creates
|
|
294
|
+
// before any gate runs (run-gates.mjs:636-658), so inside a real final run the conjunction holds by
|
|
295
|
+
// construction while a standalone check on a quiet tree still refuses. The relaxation carries the
|
|
296
|
+
// SAME base correlation the strict rule demands of the retry tree: a fingerprint is not unique to a
|
|
297
|
+
// base (a clean tree hashes identically under every HEAD), so without it a cause minted at another
|
|
298
|
+
// base would answer the gate and leave a green final the commit boundary is guaranteed to reject —
|
|
299
|
+
// the relaxation must only ever admit a state a completed retry could actually clear. It is opt-in
|
|
300
|
+
// by EXACT match: any other consumer — unknown, misspelled, absent — evaluates the strict rule, and
|
|
301
|
+
// a tree whose fingerprint is unresolvable or ambiguously correlated never relaxes. The
|
|
302
|
+
// 'commit-guard' lane is unchanged; the commit boundary still demands a real completed retry.
|
|
303
|
+
// Stated residual: an INTERRUPTED final run leaves exactly that record shape with no live run
|
|
304
|
+
// behind it, so a standalone check reads PASS in that window. It authorizes nothing — commit-guard
|
|
305
|
+
// refuses the dangling start independently (commit-guard.mjs:213-223) and consults the STRICT lane
|
|
306
|
+
// (commit-guard.mjs:253) — and the window closes on the next completed final run. The real fix is a
|
|
307
|
+
// runner-attested capability (a one-time unpublished nonce over stdin or an inherited FD, verified
|
|
308
|
+
// against a one-way commitment recorded in the final-start); it needs its own IPC contract and is
|
|
309
|
+
// QUEUED, never pretended here.
|
|
310
|
+
export const collectUnansweredRedRefusals = ({ flowRecords, coreRecords, currentBase, owner, consumer = 'commit-guard', currentFingerprint = null }) => {
|
|
285
311
|
if (!hasOwnAdoption(flowRecords, owner)) return [];
|
|
286
312
|
const adoptionInstants = flowRecords
|
|
287
313
|
.filter((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption' && r.owner === owner)
|
|
@@ -302,6 +328,13 @@ export const collectUnansweredRedRefusals = ({ flowRecords, coreRecords, current
|
|
|
302
328
|
&& basesAt(g.fingerprintBefore).length === 1 && basesAt(g.fingerprintBefore)[0] === currentBase
|
|
303
329
|
&& firstFinalByAttempt.get(g.attempt) === gi
|
|
304
330
|
&& c.fingerprint === g.fingerprintBefore));
|
|
331
|
+
const completedAttempts = new Set(finals.map(({ r }) => r.attempt));
|
|
332
|
+
const relaxes = consumer === 'gate' && typeof currentFingerprint === 'string' && currentFingerprint !== ''
|
|
333
|
+
&& basesAt(currentFingerprint).length === 1 && basesAt(currentFingerprint)[0] === currentBase;
|
|
334
|
+
const inProgressRetryFor = (red, redAt) => rerunCauses.some((c) => c.attempt === red.attempt
|
|
335
|
+
&& c.fingerprint === currentFingerprint
|
|
336
|
+
&& coreRecords.some((s, si) => si > redAt && s.kind === 'final-start'
|
|
337
|
+
&& s.fingerprint === currentFingerprint && !completedAttempts.has(s.attempt)));
|
|
305
338
|
for (const { r, i } of finals) {
|
|
306
339
|
if (r.status !== 'red') continue;
|
|
307
340
|
if (armingInstant !== null && isCanonicalInstant(r.timestamp) && Date.parse(r.timestamp) < armingInstant) continue;
|
|
@@ -315,7 +348,7 @@ export const collectUnansweredRedRefusals = ({ flowRecords, coreRecords, current
|
|
|
315
348
|
continue;
|
|
316
349
|
}
|
|
317
350
|
if (bases[0] !== currentBase) continue;
|
|
318
|
-
if (!answeredBy(r, i)) {
|
|
351
|
+
if (!answeredBy(r, i) && !(relaxes && inProgressRetryFor(r, i))) {
|
|
319
352
|
refusals.push(`a red final (attempt "${r.attempt}") on the CURRENT base (${bases[0] == null ? 'null' : short(bases[0])}) has no later completed retry cleared by a rerun-cause — an unanswered red never passes an armed flow (#65). recovery (edit the quoted cause, then paste; mint on the RETRY tree): ${writerCommand(`rerun-cause --attempt=${shellQuote(r.attempt)} --cause='<the stated cause>'`)}`);
|
|
320
353
|
}
|
|
321
354
|
}
|
|
@@ -568,13 +601,14 @@ export const computeAllPathWorktreeSurface = (cwd) => {
|
|
|
568
601
|
return { ok: true, paths: [...new Set([...tracked, ...untracked])] };
|
|
569
602
|
};
|
|
570
603
|
|
|
571
|
-
// decideFlowCheck({ flowRead, coreRead, owner, motion?, evidence? }) → { refusals,
|
|
572
|
-
// Pure — consumes the FULL read-results of both stores; store health fails closed
|
|
573
|
-
// content judgment. `motion` ({ currentBase, resolveBaseDelta, resolveChangedSurface })
|
|
574
|
-
// Step-1.4 base-motion refusals; `evidence` ({ receipts, tree, backends }) arms the three
|
|
575
|
-
// rungs (#65/#25/#42 — each self-gates on an OWN adoption). Absent inputs keep the decision
|
|
576
|
-
// byte-identical to the Plan-2 checker.
|
|
577
|
-
|
|
604
|
+
// decideFlowCheck({ flowRead, coreRead, owner, motion?, evidence?, consumer? }) → { refusals,
|
|
605
|
+
// advisories }. Pure — consumes the FULL read-results of both stores; store health fails closed
|
|
606
|
+
// BEFORE any content judgment. `motion` ({ currentBase, resolveBaseDelta, resolveChangedSurface })
|
|
607
|
+
// arms the Step-1.4 base-motion refusals; `evidence` ({ receipts, tree, backends }) arms the three
|
|
608
|
+
// Phase-1 rungs (#65/#25/#42 — each self-gates on an OWN adoption). Absent inputs keep the decision
|
|
609
|
+
// byte-identical to the Plan-2 checker. `consumer` rides through to the #65 lane split and defaults
|
|
610
|
+
// to the STRICT lane, so a caller that forgets to thread it inherits strictness.
|
|
611
|
+
export const decideFlowCheck = ({ flowRead, coreRead, owner, flowPath = 'the flow store', corePath = 'the core evidence store', motion = null, evidence = null, consumer = 'commit-guard' }) => {
|
|
578
612
|
const refusals = [];
|
|
579
613
|
const advisories = [];
|
|
580
614
|
if (flowRead.readError) refusals.push(`the flow store is unreadable (${flowRead.readError}) — the checker consumes the FULL read-result; inspect ${flowPath} (fail closed)`);
|
|
@@ -594,7 +628,7 @@ export const decideFlowCheck = ({ flowRead, coreRead, owner, flowPath = 'the flo
|
|
|
594
628
|
refusals.push(...deltaRefusals(records, owner));
|
|
595
629
|
refusals.push(...degradeOrderingRefusals(coreRead.records));
|
|
596
630
|
if (evidence != null) {
|
|
597
|
-
refusals.push(...collectUnansweredRedRefusals({ flowRecords: records, coreRecords: coreRead.records, currentBase: evidence.tree.base, owner }));
|
|
631
|
+
refusals.push(...collectUnansweredRedRefusals({ flowRecords: records, coreRecords: coreRead.records, currentBase: evidence.tree.base, owner, consumer, currentFingerprint: evidence.tree.fingerprint }));
|
|
598
632
|
refusals.push(...collectDegradeCoverageRefusals({ flowRecords: records, coreRecords: coreRead.records, tree: evidence.tree, owner, backends: evidence.degradeBackends }));
|
|
599
633
|
refusals.push(...collectReceiptCoverageRefusals({ flowRecords: records, receipts: evidence.receipts, tree: evidence.tree, owner, backends: evidence.receiptBackends, declaredPaths: evidence.declaredPaths, refreshCap: evidence.refreshCap }));
|
|
600
634
|
}
|
|
@@ -611,7 +645,9 @@ export const decideFlowCheck = ({ flowRead, coreRead, owner, flowPath = 'the flo
|
|
|
611
645
|
// #42 never falls open). `consumer` (Plan 4 Decision 2): the D10 flow→final comparison runs ONLY
|
|
612
646
|
// on the 'commit-guard' lane — the default 'gate' lane (the in-matrix flow-check --check gate)
|
|
613
647
|
// stays inert on it, because during a final run the "latest final" is by construction the
|
|
614
|
-
// PREVIOUS one and an in-matrix comparison would make a new green final unreachable.
|
|
648
|
+
// PREVIOUS one and an in-matrix comparison would make a new green final unreachable. The SAME
|
|
649
|
+
// distinction reaches the #65 unanswered-red rung (its own header states the split): the 'gate'
|
|
650
|
+
// lane also answers a red under a provable in-progress retry, for the same reason.
|
|
615
651
|
export const computeFlowDecision = ({ cwd = process.cwd(), consumer = 'gate', probes = {} } = {}) => {
|
|
616
652
|
const fingerprintProbe = probes.fingerprint ?? computeTreeFingerprint;
|
|
617
653
|
const owner = deriveFlowOwner(cwd);
|
|
@@ -745,7 +781,7 @@ export const computeFlowDecision = ({ cwd = process.cwd(), consumer = 'gate', pr
|
|
|
745
781
|
}
|
|
746
782
|
}
|
|
747
783
|
}
|
|
748
|
-
const { refusals, advisories } = decideFlowCheck({ flowRead, coreRead, owner, flowPath, corePath, motion, evidence });
|
|
784
|
+
const { refusals, advisories } = decideFlowCheck({ flowRead, coreRead, owner, flowPath, corePath, motion, evidence, consumer });
|
|
749
785
|
// Semantic refusals bind only an ARMED store; the D10 binding refusals ride the commit-guard
|
|
750
786
|
// lane UNCONDITIONALLY — a deleted or truncated store must never un-arm the binding.
|
|
751
787
|
const effectiveRefusals = healthBroken ? refusals : [...(armed ? [...refusals, ...evidenceRefusals] : []), ...bindingRefusals];
|
|
@@ -10,6 +10,7 @@ import { readFileSync, lstatSync, realpathSync } from 'node:fs';
|
|
|
10
10
|
import { join, isAbsolute } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { fail, loadConfig, CONFIG_REL } from './orchestration-config.mjs';
|
|
13
|
+
import { matchesCoverageProducer } from './coverage-producer.mjs';
|
|
13
14
|
|
|
14
15
|
// The per-project declaration (strict JSON, hand-editable). cwd-relative — errors show a path the
|
|
15
16
|
// user can open (the orchestration-config CONFIG_REL idiom).
|
|
@@ -140,6 +141,54 @@ export const isFinalCapableDeclaration = (gates, projectDir) => {
|
|
|
140
141
|
return matchesCanonicalCheck(FINAL_CORE_CHECKS[1], gates[gates.length - 1].cmd, projectDir);
|
|
141
142
|
};
|
|
142
143
|
|
|
144
|
+
// coverageProducerPrecedes(gates, checkerIndex) → whether a producer runs BEFORE the checker at that
|
|
145
|
+
// index. ORDER is the whole question: a producer declared AFTER the checker writes the lcov too late,
|
|
146
|
+
// so the checker reads nothing — or, worse, stale bytes an earlier run left behind — and still
|
|
147
|
+
// passes. ONE home for the rule: the written-declaration defects below and the advisor's
|
|
148
|
+
// inert-declaration item both decide through it, so they cannot drift apart.
|
|
149
|
+
export const coverageProducerPrecedes = (gates, checkerIndex) =>
|
|
150
|
+
gates.slice(0, checkerIndex).some((gate) => matchesCoverageProducer(gate.cmd));
|
|
151
|
+
|
|
152
|
+
// coverageDeclarationDefects(gates, projectDir) → the WRITTEN-declaration coverage rule, as a list
|
|
153
|
+
// of named defects (empty = satisfied): at most ONE canonical coverage checker; if one is present
|
|
154
|
+
// it is LAST, and a producer precedes it. Consumers turn a defect into their own refusal — an
|
|
155
|
+
// offer-level check alone is not enough, because the declaration that gets WRITTEN is a merge of an
|
|
156
|
+
// existing file with a consented subset, and every one of these three shapes is reachable that way.
|
|
157
|
+
// Order is deliberate: a duplicate must be resolved before order or production can even be read.
|
|
158
|
+
export const coverageDeclarationDefects = (gates, projectDir) => {
|
|
159
|
+
const checkers = canonicalCheckerGates(gates, projectDir);
|
|
160
|
+
if (checkers.length > 1) {
|
|
161
|
+
return [{
|
|
162
|
+
kind: 'duplicate-checker',
|
|
163
|
+
message:
|
|
164
|
+
`${GATES_REL}: ${checkers.length} gates are the canonical coverage checker (${checkers.map((g) => g.id).join(', ')}) — ` +
|
|
165
|
+
'--final accepts exactly one; keep a single checker and remove the rest',
|
|
166
|
+
}];
|
|
167
|
+
}
|
|
168
|
+
if (checkers.length === 0) return []; // no checker declared — coverage is optional, nothing to enforce
|
|
169
|
+
const index = gates.indexOf(checkers[0]);
|
|
170
|
+
const after = gates.slice(index + 1);
|
|
171
|
+
if (after.length > 0) {
|
|
172
|
+
return [{
|
|
173
|
+
kind: 'checker-not-last',
|
|
174
|
+
message:
|
|
175
|
+
`${GATES_REL}: the canonical coverage checker (${checkers[0].id}) must be the LAST declared gate — ` +
|
|
176
|
+
`${after.map((g) => g.id).join(', ')} would run after it consumed the lcov. REORDER the declaration by hand ` +
|
|
177
|
+
'(the gate itself is fine — this is an ORDERING refusal, and the fill is append-only, so it cannot reorder for you)',
|
|
178
|
+
}];
|
|
179
|
+
}
|
|
180
|
+
if (!coverageProducerPrecedes(gates, index)) {
|
|
181
|
+
return [{
|
|
182
|
+
kind: 'no-producer',
|
|
183
|
+
message:
|
|
184
|
+
`${GATES_REL}: the canonical coverage checker (${checkers[0].id}) is declared but NO gate would produce the lcov it reads — ` +
|
|
185
|
+
'the checker would pass while verifying nothing. Declare a suite gate carrying the coverage reporters ' +
|
|
186
|
+
'(references/modes/gates.md names the exact form), or drop the checker',
|
|
187
|
+
}];
|
|
188
|
+
}
|
|
189
|
+
return [];
|
|
190
|
+
};
|
|
191
|
+
|
|
143
192
|
// The review-dependent predicate (#66/P14): a gate is review-dependent iff its cmd IS the plain
|
|
144
193
|
// canonical `--check` invocation of one of the kit's OWN checkers, resolved by realpath — never a
|
|
145
194
|
// project-authored id. A project abstracting the invocation behind its own script declares it in
|
package/tools/gates-init.mjs
CHANGED
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
// • the safety claim is scoped to the OFFER DERIVATION, not a runtime sandbox: a gate still
|
|
31
31
|
// executes project-controlled tooling (a node_modules/.bin PATH shim intercepts under every
|
|
32
32
|
// form) — the documented residual, disclosed in the preview, bounded by the two consents;
|
|
33
|
+
// • a `node --test` suite body — the ONE allowlist member that produces lcov unaided — is
|
|
34
|
+
// emitted WITH the canonical coverage reporters, so the offered checker has a producer; every
|
|
35
|
+
// other body is emitted verbatim. The coverage-check candidate is WITHHELD (loud note) when
|
|
36
|
+
// neither the offer nor the existing declaration carries a producer, and --apply refuses to
|
|
37
|
+
// WRITE a declaration whose checker has no producer, is not last, or has a canonical twin:
|
|
38
|
+
// the checker READS an lcov, and with nothing writing it the gate passes certifying nothing;
|
|
33
39
|
// • ids derive kebab-case from script names (build:prod → build-prod) and every offered entry
|
|
34
40
|
// passes the runner's validateDeclaration (this module imports the validator — NEVER the
|
|
35
41
|
// reverse: run-gates.mjs stays a runner that writes nothing);
|
|
@@ -54,6 +60,8 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
54
60
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
55
61
|
import { discoverGateCandidates, EXPECTED_WORKFLOW_VERSION } from './velocity-profile.mjs';
|
|
56
62
|
import { GATES_REL, validateDeclaration } from './run-gates.mjs';
|
|
63
|
+
import { coverageDeclarationDefects, isReviewDependentGate } from './gates-declaration.mjs';
|
|
64
|
+
import { COVERAGE_PRODUCER_BODY, matchesCoverageProducer } from './coverage-producer.mjs';
|
|
57
65
|
import { loadConfig } from './orchestration-config.mjs';
|
|
58
66
|
import { assertDocsAiDeployment, writeDocsAiFileAtomic, lstatNoFollow } from './atomic-write.mjs';
|
|
59
67
|
|
|
@@ -95,7 +103,10 @@ never watch/serve, never a write-mode variant) whose BODY is a member of the lit
|
|
|
95
103
|
allowlist is offered, as the hook-free \`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <body>\` form for
|
|
96
104
|
the detected package manager. A gate-class script whose body is NOT in the allowlist is screened
|
|
97
105
|
out with a note naming it (a command you trust can still be declared by hand); non-gate-class
|
|
98
|
-
names are excluded silently, as always.
|
|
106
|
+
names are excluded silently, as always. A \`node --test\` body carries the canonical coverage
|
|
107
|
+
reporters; the coverage-check candidate is withheld when nothing would produce the lcov it reads,
|
|
108
|
+
and --apply refuses to write a checker with no producer, a checker that is not last, or a second
|
|
109
|
+
canonical checker.
|
|
99
110
|
${TRUST_CHAIN_DISCLOSURE}`;
|
|
100
111
|
|
|
101
112
|
// ── candidate classification: the NAME screens (the LOCKED derivation invariants) ──────
|
|
@@ -195,6 +206,13 @@ export const execCmdFor = (pm, body) => {
|
|
|
195
206
|
};
|
|
196
207
|
};
|
|
197
208
|
|
|
209
|
+
// The coverage PRODUCER wiring (Decision 1 — closed-world here too). `node --test` is the only
|
|
210
|
+
// allowlist body that produces lcov with no extra dependency, so it is the only body the offer
|
|
211
|
+
// extends with the reporter flags; every other body is emitted verbatim and produces none (an
|
|
212
|
+
// optional-dependency coverage flag set would be speculation about the project's own tooling).
|
|
213
|
+
const COVERAGE_PRODUCING_BODY = 'node --test';
|
|
214
|
+
const emittedBodyOf = (body) => (body === COVERAGE_PRODUCING_BODY ? COVERAGE_PRODUCER_BODY : body);
|
|
215
|
+
|
|
198
216
|
export const kebabIdOf = (name) =>
|
|
199
217
|
String(name)
|
|
200
218
|
.toLowerCase()
|
|
@@ -253,12 +271,14 @@ const deriveScripts = (cwd, deps = {}) => {
|
|
|
253
271
|
screenedIds.push(id);
|
|
254
272
|
continue;
|
|
255
273
|
}
|
|
256
|
-
const
|
|
274
|
+
const emitted = emittedBodyOf(body);
|
|
275
|
+
const { cmd, note } = execCmdFor(pm, emitted);
|
|
257
276
|
if (cmd === null) {
|
|
258
277
|
withheld.push({ id, note });
|
|
259
278
|
continue;
|
|
260
279
|
}
|
|
261
|
-
|
|
280
|
+
const coverage = emitted === body ? '' : ' (+ the lcov reporters the coverage checker reads)';
|
|
281
|
+
entries.push({ id, title: `Project script ${c.scriptName}: ${body}${coverage}`, cmd });
|
|
262
282
|
}
|
|
263
283
|
const notes = [];
|
|
264
284
|
if (screenedIds.length) {
|
|
@@ -397,7 +417,10 @@ const assertOnlyIdsOffered = (offer, onlyIds = []) => {
|
|
|
397
417
|
const offered = new Set(offer.entries.map((e) => e.id));
|
|
398
418
|
const unknown = onlyIds.filter((id) => !offered.has(id));
|
|
399
419
|
if (unknown.length) {
|
|
400
|
-
|
|
420
|
+
// The offer's notes carry WHY a candidate is absent (a withheld checker, a screened body); an
|
|
421
|
+
// id-not-offered error without them reads as a typo the user did not make.
|
|
422
|
+
const why = offer.notes.length ? ` — why: ${offer.notes.join(' | ')}` : '';
|
|
423
|
+
throw usageFail(`--only names ids not in the offer: ${unknown.join(', ')} (offered: ${[...offered].join(', ') || 'none'})${why}`);
|
|
401
424
|
}
|
|
402
425
|
};
|
|
403
426
|
|
|
@@ -411,10 +434,47 @@ export const buildOffer = (cwd, deps = {}) => {
|
|
|
411
434
|
const rs = reviewStateCandidate(cwd, deps);
|
|
412
435
|
const fc = flowCheckCandidate(cwd, deps);
|
|
413
436
|
const cc = coverageCheckCandidate(cwd, deps);
|
|
414
|
-
|
|
437
|
+
// Decision 3 — the checker is never offered DEAD. The producer may come from this offer or from
|
|
438
|
+
// a declaration the user already wrote by hand, so the rule reads the merged picture; a
|
|
439
|
+
// declaration this preview cannot read degrades to a stated note, never to a silent withhold.
|
|
440
|
+
const existing = declaredGatesBestEffort(cwd, deps);
|
|
441
|
+
const producerPresent =
|
|
442
|
+
scripts.entries.some((entry) => matchesCoverageProducer(entry.cmd)) ||
|
|
443
|
+
existing.gates.some((gate) => matchesCoverageProducer(gate.cmd));
|
|
444
|
+
const withholdCoverage = cc.candidate !== null && !producerPresent;
|
|
445
|
+
const coverageNote = withholdCoverage
|
|
446
|
+
? `the coverage-check candidate was withheld: nothing would PRODUCE the lcov it reads — no offered or ` +
|
|
447
|
+
`declared gate carries the canonical coverage reporters, and a checker with no producer passes while ` +
|
|
448
|
+
`verifying nothing (declare a suite gate per references/modes/gates.md, then re-run)`
|
|
449
|
+
: cc.note;
|
|
450
|
+
// ALWAYS stated, not only when it changed the withhold: the preview would otherwise promise an
|
|
451
|
+
// applicable offer over a declaration --apply is certain to refuse.
|
|
452
|
+
const unreadableNote =
|
|
453
|
+
existing.unreadable === null
|
|
454
|
+
? null
|
|
455
|
+
: `the existing ${GATES_REL} could not be read (${existing.unreadable}) — this preview treated it as EMPTY, and --apply will refuse until it is fixed`;
|
|
456
|
+
// An offer that lists nothing but the kit's own checkers reads like a clean offer; say the
|
|
457
|
+
// consequence out loud (no command bodies are echoed — an unvetted body must never become one
|
|
458
|
+
// keystroke from a declared, hook-auto-approvable gate). The offer-level fact is ALWAYS true; the
|
|
459
|
+
// matrix claim and the advice are only true in ONE of three declaration states, and asserting
|
|
460
|
+
// them in the other two would be the same false report this seam exists to remove:
|
|
461
|
+
// unreadable → the empty gates array means UNPARSED, not undeclared: say nothing more;
|
|
462
|
+
// readable, no gate → the claim and the advice both hold;
|
|
463
|
+
// readable, has gate → a green matrix proves plenty and the user already declared their own.
|
|
464
|
+
const declarationState =
|
|
465
|
+
existing.unreadable !== null ? 'unreadable' : existing.gates.some((gate) => !isReviewDependentGate(gate, cwd)) ? 'has-gate' : 'no-gate';
|
|
466
|
+
const noVerificationNote =
|
|
467
|
+
scripts.entries.length > 0
|
|
468
|
+
? null
|
|
469
|
+
: `this offer adds no project-verification gate — nothing here runs your suite, linter or build${
|
|
470
|
+
declarationState === 'no-gate'
|
|
471
|
+
? `, and none is declared either, so a green matrix would prove only that the kit's own checkers ran; declare your own in ${GATES_REL}`
|
|
472
|
+
: ''
|
|
473
|
+
}`;
|
|
474
|
+
const candidates = [rs.candidate, fc.candidate, withholdCoverage ? null : cc.candidate].filter(Boolean);
|
|
415
475
|
return {
|
|
416
476
|
entries: [...scripts.entries, ...candidates],
|
|
417
|
-
notes: [...scripts.notes, rs.note, fc.note,
|
|
477
|
+
notes: [...scripts.notes, noVerificationNote, unreadableNote, rs.note, fc.note, coverageNote].filter(Boolean),
|
|
418
478
|
};
|
|
419
479
|
};
|
|
420
480
|
|
|
@@ -479,6 +539,18 @@ const loadExistingDeclaration = (cwd, deps = {}) => {
|
|
|
479
539
|
return { outcome: 'loaded', readme: typeof parsed._README === 'string' ? parsed._README : undefined, gates };
|
|
480
540
|
};
|
|
481
541
|
|
|
542
|
+
// The declaration as the OFFER sees it: read-only and non-fatal. The preview must survive a
|
|
543
|
+
// declaration --apply would refuse (missing, malformed, symlinked) — it just reports what it could
|
|
544
|
+
// not read, so the withhold is never blamed on the wrong cause.
|
|
545
|
+
const declaredGatesBestEffort = (cwd, deps = {}) => {
|
|
546
|
+
try {
|
|
547
|
+
const existing = loadExistingDeclaration(cwd, deps);
|
|
548
|
+
return { gates: existing.outcome === 'loaded' ? existing.gates : [], unreadable: null };
|
|
549
|
+
} catch (err) {
|
|
550
|
+
return { gates: [], unreadable: err?.message ?? String(err) };
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
482
554
|
const templateReadme = (deps = {}) => {
|
|
483
555
|
const read = deps.readTemplate ?? readFileSync;
|
|
484
556
|
try {
|
|
@@ -531,6 +603,11 @@ export const applyFill = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
|
531
603
|
gates: [...existingGates, ...selected],
|
|
532
604
|
};
|
|
533
605
|
validateDeclaration(merged); // every written declaration passes the runner's validator, always
|
|
606
|
+
// Decision 4 — the coverage invariant is enforced on the declaration that GETS WRITTEN, not on
|
|
607
|
+
// the offer: --only filters, the merge appends, and each of those still reaches a dead pair, a
|
|
608
|
+
// producer landing after an already-last checker, or a second checker under another id.
|
|
609
|
+
const defects = coverageDeclarationDefects(merged.gates, cwd);
|
|
610
|
+
if (defects.length) throw stop(`${defects[0].message} — nothing was written`);
|
|
534
611
|
const body = `${JSON.stringify(merged, null, 2)}\n`;
|
|
535
612
|
const { writtenPath } = writeDocsAiFileAtomic(cwd, GATES_REL, body, deps, { stop, noun: 'a gate declaration' });
|
|
536
613
|
return { outcome: 'written', writtenPath, appended: selected.map((e) => e.id), notes: offer.notes };
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { readFileSync, readdirSync, lstatSync, existsSync } from 'node:fs';
|
|
34
34
|
import { createHash } from 'node:crypto';
|
|
35
35
|
import { homedir } from 'node:os';
|
|
36
|
-
import { dirname, join, resolve
|
|
36
|
+
import { dirname, join, resolve } from 'node:path';
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
import {
|
|
39
39
|
preflightVelocityProfile,
|
|
@@ -55,6 +55,10 @@ import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-re
|
|
|
55
55
|
import { probeSandboxMasks, needsMasksApply } 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, isReviewDependentGate, GATES_REL } from './gates-declaration.mjs';
|
|
59
|
+
// The declared-path resolution + segment containment this item's convergence lane shares with the
|
|
60
|
+
// autonomy render's allowWrite degrade — ONE leaf, so the two answers cannot drift.
|
|
61
|
+
import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
|
|
58
62
|
import { resolveGitHooksPath } from './commit-guard.mjs';
|
|
59
63
|
import { loadConfig } from './orchestration-config.mjs';
|
|
60
64
|
import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
@@ -100,6 +104,8 @@ export const SEVERITIES = Object.freeze({
|
|
|
100
104
|
'sandbox-provision': SEVERITY_OPTIONAL,
|
|
101
105
|
'review-recipe': SEVERITY_ATTENTION,
|
|
102
106
|
'gates-declaration': SEVERITY_OPTIONAL,
|
|
107
|
+
'gates-inert': SEVERITY_ATTENTION,
|
|
108
|
+
'gates-inert.no-verification': SEVERITY_ATTENTION,
|
|
103
109
|
'gate-hook': SEVERITY_OPTIONAL,
|
|
104
110
|
'commit-guard': SEVERITY_OPTIONAL,
|
|
105
111
|
'read-lane': SEVERITY_OPTIONAL,
|
|
@@ -158,6 +164,8 @@ export const WHATS = Object.freeze({
|
|
|
158
164
|
'sandbox-provision.installable': 'the OS sandbox is unavailable: {reason} — installable via the doctor (consent tuple {tuple})',
|
|
159
165
|
'review-recipe': '{degraded}',
|
|
160
166
|
'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',
|
|
167
|
+
'gates-inert': 'the declared coverage checker ({id}) has no producer before it — it certifies nothing this run, or reads a stale lcov',
|
|
168
|
+
'gates-inert.no-verification': "all {n} declared gate(s) are the kit's own checkers — the matrix runs no project-verification command",
|
|
161
169
|
'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
|
|
162
170
|
'commit-guard': 'the gate matrix is final-run-capable but no commit-guard arms the pre-commit hook — a commit needs no green receipt yet',
|
|
163
171
|
'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',
|
|
@@ -214,6 +222,7 @@ export const BENEFITS = Object.freeze({
|
|
|
214
222
|
'sandbox-provision': `velocity — confined ad-hoc commands stop prompting; ${DUAL_SECURITY_BENEFIT}`,
|
|
215
223
|
'review-recipe': 'recipe coverage — the review AND execution recipes you configured actually run instead of silently degrading',
|
|
216
224
|
'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
|
|
225
|
+
'gates-inert': 'honest gates — the declared matrix verifies your project instead of reporting green over a check that ran nothing',
|
|
217
226
|
'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
|
|
218
227
|
'commit-guard': 'integrity — commits require the ONE green --final receipt at the exact staged fingerprint (consented pre-commit arm)',
|
|
219
228
|
'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
|
|
@@ -250,6 +259,10 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
250
259
|
{ id: 'autonomy-policy', mode: 'set-autonomy', advisorKey: 'autonomy-policy' },
|
|
251
260
|
{ id: 'sandbox-provision', mode: 'autonomy-doctor', advisorKey: 'sandbox-provision' },
|
|
252
261
|
{ id: 'gates-declaration', mode: 'gates', advisorKey: 'gates-declaration' },
|
|
262
|
+
// A DECLARED matrix that verifies nothing is its own capability: the gates-declaration offer
|
|
263
|
+
// converges the moment any gate exists, so it can never observe this state. The advisor key names
|
|
264
|
+
// the state it reports (an inert declaration), the capability names what the user gains.
|
|
265
|
+
{ id: 'gates-verification', mode: 'gates', advisorKey: 'gates-inert' },
|
|
253
266
|
{ id: 'gate-hook', mode: 'hook', advisorKey: 'gate-hook' },
|
|
254
267
|
{ id: 'read-lane', mode: 'hook', advisorKey: 'read-lane' },
|
|
255
268
|
{ id: 'commit-guard', mode: 'commit-guard', advisorKey: 'commit-guard' },
|
|
@@ -412,6 +425,52 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
412
425
|
}
|
|
413
426
|
};
|
|
414
427
|
|
|
428
|
+
// The INERT-DECLARATION item: a gate matrix that is declared, runs green, and verifies nothing.
|
|
429
|
+
// Both causes are read off the DECLARATION through the same predicates the runner and the fill
|
|
430
|
+
// decide with — the checker side through canonicalCheckerGates, the producer side through the
|
|
431
|
+
// closed matchesCoverageProducer — so the advisor can never disagree with what --final accepts.
|
|
432
|
+
//
|
|
433
|
+
// Cause A (a canonical coverage checker with no producer anywhere in the declaration) is checked
|
|
434
|
+
// FIRST and reported alone: its remedy — declaring the producer — also resolves cause B, because a
|
|
435
|
+
// producer gate is not a kit checker. Its apply is HAND-APPLY: the producer must precede the
|
|
436
|
+
// checker, and the fill is append-only (it refuses by name rather than reordering), so the edit is
|
|
437
|
+
// the maintainer's.
|
|
438
|
+
//
|
|
439
|
+
// An ABSENT or EMPTY declaration belongs to the gates-declaration item; a malformed one throws out
|
|
440
|
+
// of the validated reader and becomes this probe's stated skip, never a guess.
|
|
441
|
+
export const probeGatesInert = ({ root, deps, add, skip }) => {
|
|
442
|
+
try {
|
|
443
|
+
const declaration = loadDeclaration(root, deps);
|
|
444
|
+
if (declaration.outcome === 'missing') return;
|
|
445
|
+
const gates = declaration.gates;
|
|
446
|
+
if (gates.length === 0) return;
|
|
447
|
+
const checkers = canonicalCheckerGates(gates, root);
|
|
448
|
+
if (checkers.length > 0) {
|
|
449
|
+
// ORDER decides, through the declaration's own shared predicate: a producer declared AFTER the
|
|
450
|
+
// checker leaves it just as inert as no producer at all (it reads nothing, or stale bytes), and
|
|
451
|
+
// only --final refuses that shape — a plain run reports every gate PASS.
|
|
452
|
+
if (coverageProducerPrecedes(gates, gates.indexOf(checkers[0]))) return; // the pair is live
|
|
453
|
+
const id = truncatedTo(oneLineOf(checkers[0].id), templateBudget(WHATS['gates-inert']));
|
|
454
|
+
add(
|
|
455
|
+
'gates-inert',
|
|
456
|
+
fillTemplate(WHATS['gates-inert'], { id }),
|
|
457
|
+
`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} — the fill is append-only and cannot reorder for you`,
|
|
458
|
+
);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (gates.every((gate) => isReviewDependentGate(gate, root))) {
|
|
462
|
+
add(
|
|
463
|
+
'gates-inert',
|
|
464
|
+
fillTemplate(WHATS['gates-inert.no-verification'], { n: gates.length }),
|
|
465
|
+
`node ${q(toolPath('gates-init.mjs'))} --cwd ${q(root)}`,
|
|
466
|
+
'gates-inert.no-verification',
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
skip('gates-inert', err);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
415
474
|
// The D10 consumer surface: once the declaration is FINAL-capable (the canonical core checks
|
|
416
475
|
// present, the checker LAST — the run-gates helper is the one home of that rule), offer the
|
|
417
476
|
// consented commit-guard install — for a MANAGED guardless pre-commit hook AND for an absent one
|
|
@@ -792,7 +851,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
792
851
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
793
852
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
794
853
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
795
|
-
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration']);
|
|
854
|
+
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert']);
|
|
796
855
|
|
|
797
856
|
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
798
857
|
try {
|
|
@@ -849,22 +908,6 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
|
849
908
|
}
|
|
850
909
|
};
|
|
851
910
|
|
|
852
|
-
// A declared `sandbox.filesystem.allowWrite` entry, resolved the way a host that honors the key
|
|
853
|
-
// resolves it: `~` and `~/…` against the resolved home, every other form against the project root.
|
|
854
|
-
const resolveDeclaredDir = (entry, { home, root }) => {
|
|
855
|
-
if (entry === '~') return resolve(home);
|
|
856
|
-
if (entry.startsWith('~/')) return resolve(home, entry.slice(2));
|
|
857
|
-
return resolve(root, entry);
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
// Ancestor-or-equal containment on PATH SEGMENTS, never a raw string prefix — a grant on
|
|
861
|
-
// `<p>/farm` must never read as a grant on the sibling `<p>/farmhouse`. A grant on a DESCENDANT
|
|
862
|
-
// never covers its parent: the parent dir is the one provision writes into.
|
|
863
|
-
const dirCovers = (declaredDir, probeDir) => {
|
|
864
|
-
const base = declaredDir.endsWith(sep) ? declaredDir.slice(0, -sep.length) : declaredDir;
|
|
865
|
-
return probeDir === base || probeDir.startsWith(`${base}${sep}`);
|
|
866
|
-
};
|
|
867
|
-
|
|
868
911
|
// D7 lane 1 — the DECLARATION confirmation. A settings entry is not proof of writable CAPABILITY
|
|
869
912
|
// (runtime truth stays with the provision preflight's real create+delete probe); it is proof the
|
|
870
913
|
// maintainer applied this item's own advice, which is what the item may converge on. Both scopes
|
|
@@ -899,7 +942,7 @@ const declaredWritableDirs = (scope, rel) => {
|
|
|
899
942
|
if (filesystem === null || typeof filesystem !== 'object' || Array.isArray(filesystem)) return [];
|
|
900
943
|
const { allowWrite } = filesystem;
|
|
901
944
|
if (allowWrite === undefined) return [];
|
|
902
|
-
if (!Array.isArray(allowWrite) || !allowWrite.every(
|
|
945
|
+
if (!Array.isArray(allowWrite) || !allowWrite.every(isResolvableDeclaredEntry)) {
|
|
903
946
|
throw new Error(`${rel}: sandbox.filesystem.allowWrite must be an array of non-empty strings`);
|
|
904
947
|
}
|
|
905
948
|
return allowWrite;
|
|
@@ -1010,6 +1053,7 @@ const PROBES = Object.freeze([
|
|
|
1010
1053
|
probeSandboxProvision,
|
|
1011
1054
|
probeReviewRecipe,
|
|
1012
1055
|
probeGates,
|
|
1056
|
+
probeGatesInert,
|
|
1013
1057
|
probeCommitGuard,
|
|
1014
1058
|
probeReadLane,
|
|
1015
1059
|
probeStateBlockHook,
|