@sabaiway/agent-workflow-kit 10.6.0 → 11.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 +85 -0
- package/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/commit-guard.md +2 -2
- package/references/modes/control-bytes.md +91 -0
- package/references/modes/flow-writer.md +16 -4
- package/references/modes/procedures.md +2 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/robustness-brief.md +61 -0
- package/references/modes/velocity.md +2 -0
- package/references/robustness-literals.json +107 -0
- package/tools/changed-surface.mjs +1 -1
- package/tools/commands.mjs +14 -0
- package/tools/commit-guard.mjs +20 -9
- package/tools/control-bytes.mjs +264 -0
- package/tools/core-evidence.mjs +33 -20
- package/tools/coverage-check.mjs +1 -1
- package/tools/dispatch-store-read.mjs +2 -2
- package/tools/flow-check-git-lane.mjs +2 -1
- package/tools/flow-delta-proof.mjs +4 -4
- package/tools/flow-record-shape.mjs +44 -2
- package/tools/flow-round-gates.mjs +151 -0
- package/tools/flow-store-read.mjs +1 -1
- package/tools/flow-writer.mjs +235 -82
- package/tools/fold-scope.mjs +7 -3
- package/tools/fs-read-nofollow.mjs +56 -4
- package/tools/git-env.mjs +120 -0
- package/tools/grounding.mjs +4 -3
- package/tools/plan-shape-facts.mjs +23 -10
- package/tools/plan-shape.mjs +8 -0
- package/tools/procedures.mjs +18 -4
- package/tools/repo-lex.mjs +29 -0
- package/tools/review-state.mjs +43 -34
- package/tools/robustness-brief.mjs +209 -0
- package/tools/robustness-literals.mjs +101 -0
- package/tools/sandbox-masks.mjs +2 -1
- package/tools/set-flow.mjs +2 -1
- package/tools/source-size-scope.mjs +19 -25
- package/tools/tracked-tree-census.mjs +3 -3
- package/tools/velocity-profile.mjs +5 -2
- package/tools/worktree-handoff-return.mjs +2 -2
- package/tools/worktrees.mjs +2 -2
package/tools/review-state.mjs
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
// the tree is clean (nothing to review — under a non-solo review obligation the PASS
|
|
22
22
|
// still NAMES every plan in flight and states that the gate arms as soon as the tree
|
|
23
23
|
// turns dirty, so a latent arm is discoverable before it blocks a pending commit); when
|
|
24
|
-
//
|
|
25
|
-
//
|
|
24
|
+
// git ITSELF answers not-a-repository under both discoveries (the ONLY non-work-tree
|
|
25
|
+
// location that passes; the five other states refuse by name); and when
|
|
26
26
|
// the obligations are SATISFIED: under `reviewed`, >=1 backend's latest normal receipt
|
|
27
27
|
// attests SHIP-CLASS (ship / ship with nits) for the current tree; under `council`,
|
|
28
28
|
// EVERY review-capable backend attests ship-class OR carries an explicit current-tree
|
|
@@ -109,7 +109,8 @@ import { selectReliedOnReceipt, evaluateVetoOverride, evaluateInternalAttestatio
|
|
|
109
109
|
// in leaf modules so the procedures read surface reaches them without this module's write-API
|
|
110
110
|
// graph; re-exported here so every historical consumer keeps its import site.
|
|
111
111
|
import { PLANS_REL, isScratchPlanName, plansInFlight } from './plan-files.mjs';
|
|
112
|
-
import { shellQuoteArg } from './repo-lex.mjs';
|
|
112
|
+
import { escapeForDisplay, shellQuoteArg } from './repo-lex.mjs';
|
|
113
|
+
import { GIT_MAX_BUFFER, resolveGitLocation, stripGitLocationEnv, withGitPath } from './git-env.mjs';
|
|
113
114
|
import { readDelegationLedger } from './dispatch-store-read.mjs';
|
|
114
115
|
import {
|
|
115
116
|
decideHeldSession, HELD_EXECUTE_WRAPPER, HELD_RECEIPT_BACKEND, judgeLedger,
|
|
@@ -148,7 +149,6 @@ export {
|
|
|
148
149
|
const ACTIVITY = 'plan-execution';
|
|
149
150
|
const SLOT = 'review';
|
|
150
151
|
const CORE_EVIDENCE_TOOL = shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'core-evidence.mjs'));
|
|
151
|
-
const GIT_MAX_BUFFER = 256 * 1024 * 1024; // a full-tree diff can be large; never truncate silently
|
|
152
152
|
// --await (BUGFREE-3 / AD-049, item (d)) bounds + poll cadence. The default timeout is generous —
|
|
153
153
|
// a real grounded bridge review can take minutes — and every value is overridable (--timeout / the
|
|
154
154
|
// injectable clock) so hermetic tests never spend wall-clock.
|
|
@@ -157,18 +157,18 @@ export const AWAIT_POLL_MS = 5000;
|
|
|
157
157
|
|
|
158
158
|
// ── git plumbing (read-only queries; injectable for tests) ─────────────────────────
|
|
159
159
|
|
|
160
|
-
const gitRaw = (args, cwd) =>
|
|
161
|
-
spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
160
|
+
const gitRaw = (args, cwd, env) =>
|
|
161
|
+
spawnSync('git', args, { cwd, env: withGitPath(env ?? process.env), maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
162
162
|
|
|
163
163
|
// stdout Buffer of a git query, or null when git fails (not a repo / git absent).
|
|
164
|
-
const gitBuf = (args, cwd) => {
|
|
165
|
-
const r = gitRaw(args, cwd);
|
|
164
|
+
const gitBuf = (args, cwd, env) => {
|
|
165
|
+
const r = gitRaw(args, cwd, env);
|
|
166
166
|
if (r.error || r.status !== 0) return null;
|
|
167
167
|
return r.stdout;
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
-
const gitLine = (args, cwd) => {
|
|
171
|
-
const buf = gitBuf(args, cwd);
|
|
170
|
+
const gitLine = (args, cwd, env) => {
|
|
171
|
+
const buf = gitBuf(args, cwd, env);
|
|
172
172
|
return buf == null ? null : buf.toString('utf8').replace(/\r?\n$/, '');
|
|
173
173
|
};
|
|
174
174
|
|
|
@@ -181,10 +181,10 @@ const gitLine = (args, cwd) => {
|
|
|
181
181
|
// ignores them by construction; this count only feeds ONE non-failing advisory line naming the
|
|
182
182
|
// cosmetic sandbox-masks apply — an applied managed block hides the paths from --exclude-standard,
|
|
183
183
|
// so the advisory disappears exactly when the status noise does (no standing detector).
|
|
184
|
-
export const countNeverCommittableUntracked = (cwd, { lstat = lstatSync } = {}) => {
|
|
185
|
-
const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
184
|
+
export const countNeverCommittableUntracked = (cwd, { lstat = lstatSync, env } = {}) => {
|
|
185
|
+
const top = gitLine(['rev-parse', '--show-toplevel'], cwd, env);
|
|
186
186
|
if (top == null) return 0;
|
|
187
|
-
const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top);
|
|
187
|
+
const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top, env);
|
|
188
188
|
if (untrackedZ == null) return 0;
|
|
189
189
|
return untrackedZ
|
|
190
190
|
.toString('utf8')
|
|
@@ -345,7 +345,10 @@ export const buildHeldSessionState = ({
|
|
|
345
345
|
// must read the same config/plans or a dirty unreceipted tree could false-PASS as "no plan in
|
|
346
346
|
// flight". Outside a git tree the cwd is the only anchor (and --check exits 0).
|
|
347
347
|
export const buildState = ({ cwd, env = process.env, detect = detectBackends, surveyVehicle, lstat = lstatSync, readFile = readFileSync } = {}) => {
|
|
348
|
-
|
|
348
|
+
// WHICH repository, first; the config/plan anchor is the stripped discovery's top (from a subdirectory the bare cwd would miss docs/plans — decideCheck refuses on an unanchored root).
|
|
349
|
+
const location = resolveGitLocation(cwd, { env });
|
|
350
|
+
const anchoredTop = location.top ?? gitLine(['rev-parse', '--show-toplevel'], cwd, stripGitLocationEnv(env));
|
|
351
|
+
const root = anchoredTop ?? cwd;
|
|
349
352
|
const { config, source: configSource } = loadConfig(root);
|
|
350
353
|
// A bridge-detector throw reaches the hook, never a catch that would also lose the surveyed
|
|
351
354
|
// executor vehicle: bridge readiness goes unknown (fail closed below), the carrier stays known.
|
|
@@ -354,16 +357,15 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, su
|
|
|
354
357
|
detectionWarning = `backend detection failed (${(err && err.message) || err}) — bridge readiness unknown.`;
|
|
355
358
|
};
|
|
356
359
|
const detection = composeReadiness(root, { detect, surveyVehicle, onDetectError });
|
|
357
|
-
// The resolver stays for DISPLAY
|
|
358
|
-
// recipe (never the readiness-degraded effective one — no silent solo).
|
|
360
|
+
// The resolver stays for DISPLAY only; the OBLIGATIONS come from the configured recipe (never the readiness-degraded effective one — no silent solo).
|
|
359
361
|
const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity: ACTIVITY, slot: SLOT });
|
|
360
362
|
const obligations = requiredBackendsForConfiguredRecipe({ config: config ?? {}, readiness: detection, detectionFailed: detectionWarning != null });
|
|
361
363
|
const requiredBackends = obligations.backends;
|
|
362
364
|
const plans = plansInFlight(root);
|
|
363
365
|
// The injected lstat threads through EVERY stat-dependent computation (fingerprint, clean, the
|
|
364
366
|
// mask count) — a partial injection would let a test observe an inconsistent state.
|
|
365
|
-
const fingerprint = computeTreeFingerprint(cwd, { lstat });
|
|
366
|
-
const clean = fingerprint == null ? null : isTreeClean(cwd, { lstat });
|
|
367
|
+
const fingerprint = location.state === 'work-tree' ? computeTreeFingerprint(cwd, { lstat, env }) : null;
|
|
368
|
+
const clean = fingerprint == null ? null : isTreeClean(cwd, { lstat, env });
|
|
367
369
|
const receiptsPath = resolveReceiptsPath(cwd, env);
|
|
368
370
|
const receiptsRead = receiptsPath ? readReceipts(receiptsPath) : { receipts: [], malformed: 0 };
|
|
369
371
|
const { receipts, malformed } = receiptsRead;
|
|
@@ -469,6 +471,8 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, su
|
|
|
469
471
|
backends: armedBackends,
|
|
470
472
|
plans,
|
|
471
473
|
root,
|
|
474
|
+
rootAnchored: anchoredTop !== null,
|
|
475
|
+
location,
|
|
472
476
|
fingerprint,
|
|
473
477
|
clean,
|
|
474
478
|
receiptsPath,
|
|
@@ -482,7 +486,7 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, su
|
|
|
482
486
|
evidenceUnavailable: degrade.unavailable,
|
|
483
487
|
degradedExempt,
|
|
484
488
|
heldSession,
|
|
485
|
-
maskedUntracked: countNeverCommittableUntracked(cwd, { lstat }),
|
|
489
|
+
maskedUntracked: countNeverCommittableUntracked(cwd, { lstat, env }),
|
|
486
490
|
detectionWarning,
|
|
487
491
|
flowPresent,
|
|
488
492
|
flowArmed,
|
|
@@ -519,7 +523,7 @@ const backendFailurePart = (b, state) => {
|
|
|
519
523
|
if (b.state === 'unrecognized-verdict') return `${b.backend}: the latest normal receipt carries an unrecognized verdict (${JSON.stringify(b.verdict)}) — an unknown verdict never attests (fail-closed); re-run the review`;
|
|
520
524
|
if (b.state === 'ungrounded') return `${b.backend}: the latest normal receipt is ungrounded — re-run grounded (--facts)`;
|
|
521
525
|
if (b.state === 'probe') return `${b.backend}: only probe receipts for the current tree (CODEX_PROBE=1 / AGY_PROBE=1 relaxes the quality guards) — a probe review never attests; re-run a real one`;
|
|
522
|
-
if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${state.receiptsPath}`;
|
|
526
|
+
if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${escapeForDisplay(state.receiptsPath)}`;
|
|
523
527
|
if (b.state === 'stale') return `${b.backend}: receipts exist but none matches the current tree (edited after review) — run a fresh review`;
|
|
524
528
|
return `${b.backend}: no receipt — run its review wrapper, or record an explicit degrade (node ${CORE_EVIDENCE_TOOL} degrade --backend ${b.backend} --reason "...")`;
|
|
525
529
|
};
|
|
@@ -530,7 +534,7 @@ const describesHeldRecovery = (state) => {
|
|
|
530
534
|
const canRetry = substitution.folded === false;
|
|
531
535
|
if (state.evidenceUnavailable === true) {
|
|
532
536
|
const retry = canRetry ? '; or fold the retry of that thread' : '';
|
|
533
|
-
return ` — the evidence store is unavailable, so the degrade lane cannot be judged: repair it (inspect ${state.evidenceStorePath}) and re-check${retry}`;
|
|
537
|
+
return ` — the evidence store is unavailable, so the degrade lane cannot be judged: repair it (inspect ${escapeForDisplay(state.evidenceStorePath)}) and re-check${retry}`;
|
|
534
538
|
}
|
|
535
539
|
const canDegrade = typeof substitution.postTreeDigest === 'string'
|
|
536
540
|
&& state.fingerprint === substitution.postTreeDigest;
|
|
@@ -553,11 +557,13 @@ export const decideCheck = (state) => {
|
|
|
553
557
|
// Store diagnostics ride EVERY check line, early exits (and the unknowable arm) included — a
|
|
554
558
|
// malformed receipt line, an unavailable evidence store, or an unreadable receipts store is
|
|
555
559
|
// never hidden behind any exit.
|
|
556
|
-
const malformedNote = state.malformed > 0 ? ` — ${state.malformed} malformed receipt line(s) ignored; inspect ${state.receiptsPath}` : '';
|
|
560
|
+
const malformedNote = state.malformed > 0 ? ` — ${state.malformed} malformed receipt line(s) ignored; inspect ${escapeForDisplay(state.receiptsPath)}` : '';
|
|
557
561
|
const evidenceNote = state.evidenceUnavailable
|
|
558
|
-
? ` — evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}); the degrade escape is denied (fail-closed) — inspect ${state.evidenceStorePath}`
|
|
562
|
+
? ` — evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}); the degrade escape is denied (fail-closed) — inspect ${escapeForDisplay(state.evidenceStorePath)}`
|
|
559
563
|
: '';
|
|
560
|
-
const earlyNotes = `${malformedNote}${evidenceNote}${state.receiptsReadError ? ` — receipts store unreadable (${state.receiptsReadError}); inspect ${state.receiptsPath}` : ''}`;
|
|
564
|
+
const earlyNotes = `${malformedNote}${evidenceNote}${state.receiptsReadError ? ` — receipts store unreadable (${state.receiptsReadError}); inspect ${escapeForDisplay(state.receiptsPath)}` : ''}`;
|
|
565
|
+
// The git LOCATION (hand-built states carry none; the fingerprint decides there) rules below the solo arm: not-a-repository passes, the four other non-work-tree states refuse by name.
|
|
566
|
+
const location = state.location ?? { state: state.fingerprint == null ? 'not-a-repository' : 'work-tree', cause: 'no location was resolved for a hand-built state' };
|
|
561
567
|
if (state.heldSession !== null && state.heldSession !== undefined) {
|
|
562
568
|
const held = decideHeldSession(state.heldSession);
|
|
563
569
|
const heldRecovery = describesHeldRecovery(state);
|
|
@@ -574,11 +580,14 @@ export const decideCheck = (state) => {
|
|
|
574
580
|
: `no reviewer backend is ready — the computed ${ACTIVITY}.${SLOT} default is solo`;
|
|
575
581
|
return { code: 0, reason: `${why} — no receipt required${earlyNotes}` };
|
|
576
582
|
}
|
|
583
|
+
// An UNANCHORED root (git dead everywhere): the bare-cwd plan inventory cannot say "no plan".
|
|
584
|
+
if (location.state !== 'work-tree' && location.state !== 'not-a-repository' && state.rootAnchored === false) return { code: 1, reason: `the git location is ${location.state} — ${escapeForDisplay(location.cause)} — and the project root cannot be anchored, so the plan arm cannot decide (fail closed)${earlyNotes}` };
|
|
577
585
|
if (state.plans.length === 0) return { code: 0, reason: `no plan in flight (docs/plans/ holds no active plan) — no receipt required${earlyNotes}` };
|
|
578
|
-
if (state.
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
//
|
|
586
|
+
if (location.state !== 'work-tree' && location.state !== 'not-a-repository') return { code: 1, reason: `the git location is ${location.state} — ${escapeForDisplay(location.cause)} — refusing to judge receipts for a tree git cannot name (fail closed)${earlyNotes}` };
|
|
587
|
+
if (location.state === 'not-a-repository') return { code: 0, reason: `not a git repository (git's own answer: ${escapeForDisplay(location.cause)}) — nothing to fingerprint${earlyNotes}` };
|
|
588
|
+
if (state.fingerprint == null || state.clean === null) return { code: 1, reason: `the tree ${state.fingerprint == null ? 'fingerprint' : 'clean check'} is undecidable in a work tree (a git query failed or was killed) — refusing to judge receipts (fail closed)${earlyNotes}` };
|
|
589
|
+
// A clean tree PASSES, but never silently: >=1 plan IS in flight here, and this gate arms the
|
|
590
|
+
// moment the tree turns dirty — naming each file makes the arm discoverable BEFORE it blocks.
|
|
582
591
|
if (state.clean === true) {
|
|
583
592
|
// Each name is byte-preserving and quoted because this reason is one gate-report line.
|
|
584
593
|
const named = state.plans.map((p) => quoteReportName(p)).join(', ');
|
|
@@ -595,7 +604,7 @@ export const decideCheck = (state) => {
|
|
|
595
604
|
// whatever the recipe consults, so a recipe flip to solo can never bury one.
|
|
596
605
|
if (state.obligations.recipe === 'solo') {
|
|
597
606
|
if (state.receiptsReadError != null) {
|
|
598
|
-
return { code: 1, reason: `the receipts store is unreadable (${state.receiptsReadError}) — "no standing veto" cannot be established, so the internal-only floor fails closed; inspect ${state.receiptsPath}${earlyNotes}` };
|
|
607
|
+
return { code: 1, reason: `the receipts store is unreadable (${state.receiptsReadError}) — "no standing veto" cannot be established, so the internal-only floor fails closed; inspect ${escapeForDisplay(state.receiptsPath)}${earlyNotes}` };
|
|
599
608
|
}
|
|
600
609
|
const standing = state.soloVetoRows.filter((b) => (b.state === 'current' && !b.shipClass && !b.overrideLabel) || b.state === 'unrecognized-verdict');
|
|
601
610
|
if (standing.length > 0) {
|
|
@@ -641,7 +650,7 @@ export const decideCheck = (state) => {
|
|
|
641
650
|
.filter((b) => !(printed.has(b.backend) && b.state === 'rejected'))
|
|
642
651
|
.reduce((n, b) => n + (b.markerRejected ?? 0) + (b.unmarkedRejected ?? 0), 0);
|
|
643
652
|
const markerNote = total > 0
|
|
644
|
-
? ` — ${total} receipt(s) rejected: an untrustworthy probe marker (malformed, or absent — silence is not a declaration) — fail-closed; inspect ${state.receiptsPath}`
|
|
653
|
+
? ` — ${total} receipt(s) rejected: an untrustworthy probe marker (malformed, or absent — silence is not a declaration) — fail-closed; inspect ${escapeForDisplay(state.receiptsPath)}`
|
|
645
654
|
: '';
|
|
646
655
|
return `${markerNote}${earlyNotes}`;
|
|
647
656
|
};
|
|
@@ -716,12 +725,12 @@ const formatHuman = (state, check) => {
|
|
|
716
725
|
];
|
|
717
726
|
if (state.detectionWarning) lines.push(` ⚠ ${state.detectionWarning}`);
|
|
718
727
|
lines.push(` plan in flight: ${state.plans.length ? state.plans.map((name) => quoteReportName(name)).join(', ') : '(none)'}`);
|
|
719
|
-
if (state.fingerprint == null) lines.push(
|
|
728
|
+
if (state.fingerprint == null) lines.push(` tree: ${state.location != null && state.location.state !== 'work-tree' ? `git location ${state.location.state} — ${escapeForDisplay(state.location.cause)}` : 'the fingerprint is undecidable (a git query failed or was killed)'}`);
|
|
720
729
|
else if (state.clean === true) lines.push(' tree: clean (nothing to review)');
|
|
721
730
|
else lines.push(` tree fingerprint: ${state.fingerprint}`);
|
|
722
731
|
if (state.heldSession != null) lines.push(` ${decideHeldSession(state.heldSession).line}`);
|
|
723
|
-
lines.push(` receipts: ${state.receiptsPath
|
|
724
|
-
if (state.receiptsReadError) lines.push(` ⚠ receipts store unreadable (${state.receiptsReadError}) — inspect ${state.receiptsPath}`);
|
|
732
|
+
lines.push(` receipts: ${state.receiptsPath == null ? '(unresolvable — no git dir)' : escapeForDisplay(state.receiptsPath)} (${state.receiptCount} line(s)${state.malformed ? `, ${state.malformed} malformed — inspect the file` : ''})`);
|
|
733
|
+
if (state.receiptsReadError) lines.push(` ⚠ receipts store unreadable (${state.receiptsReadError}) — inspect ${escapeForDisplay(state.receiptsPath)}`);
|
|
725
734
|
if (state.evidenceUnavailable) lines.push(` ⚠ evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}) — the degrade escape is denied (fail-closed)`);
|
|
726
735
|
const exempt = new Set(state.degradedExempt);
|
|
727
736
|
for (const b of state.backends) {
|
|
@@ -778,7 +787,7 @@ malformed, foreign or audit-refused ledger, a failed HEAD read, or a SUBSTITUTED
|
|
|
778
787
|
exits 1 (a substitution is lifted only by a codex-exec degrade at the substituted return's own
|
|
779
788
|
tree, or the fold of that thread's retry). THEN: 0 for solo / no plan in flight /
|
|
780
789
|
a clean tree (under a non-solo review obligation the PASS names every plan in flight and its arm) /
|
|
781
|
-
not-a-
|
|
790
|
+
a cwd where git ITSELF answers not-a-repository (the only passing non-work-tree state; the four others exit 1 by name) / obligations satisfied (reviewed: >=1 ship-class attestation;
|
|
782
791
|
council: every backend ship-class or degrade-recorded, >=1 real ship); 1 on a veto, an
|
|
783
792
|
unrecognized verdict, a missing/stale/ungrounded/probe-only backend without a degrade record,
|
|
784
793
|
an all-degraded tree, or a down detector with no configured recipe.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, resolve, sep } from 'node:path';
|
|
5
|
+
import { readShippedRobustnessLiterals, parseRobustTag } from './robustness-literals.mjs';
|
|
6
|
+
import { checkPlanStructure, formatFindings, isSweep } from './plan-shape.mjs';
|
|
7
|
+
import { expandSweepPaths } from './plan-shape-facts.mjs';
|
|
8
|
+
import { readFileBytesNoFollowCapped } from './fs-read-nofollow.mjs';
|
|
9
|
+
import { escapeForDisplay, lexicalRepoRelative } from './repo-lex.mjs';
|
|
10
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
11
|
+
|
|
12
|
+
const PLAN_READ_CAP = 1_048_576;
|
|
13
|
+
const ROW_ID = /^[A-Za-z0-9._-]+$/u;
|
|
14
|
+
const SEARCHABLE_KINDS = new Set(['env', 'flag', 'ref', 'syscall', 'errno', 'argv']);
|
|
15
|
+
const SHIPPED_LIST_PATH = 'agent-workflow-kit/references/robustness-literals.json';
|
|
16
|
+
const USAGE = 'Usage: node robustness-brief.mjs --plan <plan> [--row <id>] [--coverage]';
|
|
17
|
+
const usage = (message) => Object.assign(new Error(message), { usage: true });
|
|
18
|
+
const isInside = (root, path) => path === root || path.startsWith(`${root}${sep}`);
|
|
19
|
+
const decodePlan = (bytes) => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
20
|
+
|
|
21
|
+
const parseArgs = (argv, index = 0, state = { plan: null, row: null, coverage: false }) => {
|
|
22
|
+
if (index === argv.length) {
|
|
23
|
+
if (state.plan === null) throw usage('absent --plan');
|
|
24
|
+
if (state.row !== null && state.coverage) throw usage('--row cannot be combined with --coverage');
|
|
25
|
+
return state;
|
|
26
|
+
}
|
|
27
|
+
const flag = argv[index];
|
|
28
|
+
if (flag === '--coverage') {
|
|
29
|
+
if (state.coverage) throw usage('duplicate --coverage');
|
|
30
|
+
return parseArgs(argv, index + 1, { ...state, coverage: true });
|
|
31
|
+
}
|
|
32
|
+
const key = flag === '--plan' ? 'plan' : flag === '--row' ? 'row' : null;
|
|
33
|
+
const value = argv[index + 1];
|
|
34
|
+
if (key === null) throw usage(`unknown argument ${flag}`);
|
|
35
|
+
if (state[key] !== null) throw usage(`duplicate ${flag}`);
|
|
36
|
+
if (value === undefined || value.startsWith('--') || value.length === 0) throw usage(`absent operand for ${flag}`);
|
|
37
|
+
if (key === 'row' && !ROW_ID.test(value)) throw usage(`invalid --row ${value}`);
|
|
38
|
+
return parseArgs(argv, index + 2, { ...state, [key]: value });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const renderClass = (entry) => [
|
|
42
|
+
`- **${entry.id}** — ${entry.prove}`,
|
|
43
|
+
...entry.members.map((member) => ` - \`${member.literal}\` — ${member.note} (${member.source})`),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const isIdentifierByte = (byte) => (byte >= 0x30 && byte <= 0x39) ||
|
|
47
|
+
(byte >= 0x41 && byte <= 0x5a) || byte === 0x5f || (byte >= 0x61 && byte <= 0x7a);
|
|
48
|
+
const hasBoundedLiteral = (bytes, literal) => {
|
|
49
|
+
const token = Buffer.from(literal, 'utf8');
|
|
50
|
+
const cursor = { from: 0 };
|
|
51
|
+
while (cursor.from <= bytes.length - token.length) {
|
|
52
|
+
const hit = bytes.indexOf(token, cursor.from);
|
|
53
|
+
if (hit === -1) return false;
|
|
54
|
+
const leftBounded = hit === 0 || !isIdentifierByte(bytes[hit - 1]);
|
|
55
|
+
const rightBounded = literal.endsWith('_') || hit + token.length === bytes.length || !isIdentifierByte(bytes[hit + token.length]);
|
|
56
|
+
if (leftBounded && rightBounded) return true;
|
|
57
|
+
cursor.from = hit + 1;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
export const isCoverageDocument = (path) => /\.(?:md|txt)$/u.test(path) || path === SHIPPED_LIST_PATH;
|
|
62
|
+
const describeReadOutcome = (read) => read.outcome === 'foreign'
|
|
63
|
+
? `non-regular ${read.className ?? 'file'}`
|
|
64
|
+
: read.outcome === 'over-cap' ? `over-cap${read.cap ? ` (cap ${read.cap})` : ''}`
|
|
65
|
+
: read.outcome === 'error' ? `error${read.code ? ` (${read.code})` : ''}` : String(read.outcome ?? 'unknown');
|
|
66
|
+
const readCoverageFile = (readRowFile, path) => {
|
|
67
|
+
try {
|
|
68
|
+
const read = readRowFile(path);
|
|
69
|
+
if (read == null) return { outcome: 'error', code: 'empty read outcome' };
|
|
70
|
+
if (read.outcome === 'ok' && !Buffer.isBuffer(read.bytes)) return { outcome: 'error', code: 'ok outcome carried no Buffer' };
|
|
71
|
+
return read;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { outcome: 'error', code: error?.code ?? error?.message ?? 'read threw' };
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
export const computeRowCoverage = (planText, list, { expandRowPath, readRowFile }) => {
|
|
77
|
+
const shape = checkPlanStructure(planText, {});
|
|
78
|
+
const classes = new Set(list.classes.map(({ id }) => id));
|
|
79
|
+
const entries = shape.parsed.rows.filter((entry) => entry.valid && !isCoverageDocument(entry.path));
|
|
80
|
+
return entries.reduce((answer, entry) => {
|
|
81
|
+
if (answer.refusal !== null) return answer;
|
|
82
|
+
const robust = parseRobustTag(entry.responsibility);
|
|
83
|
+
if (robust.refusal) return { ...answer, refusal: { id: entry.id, reason: robust.refusal } };
|
|
84
|
+
const unknown = robust.classes.find((classId) => !classes.has(classId));
|
|
85
|
+
if (unknown) return { ...answer, refusal: { id: entry.id, reason: `unknown class ${unknown}` } };
|
|
86
|
+
const tagged = robust.classes;
|
|
87
|
+
const emptyRow = { id: entry.id, path: entry.path, tagged, present: [], uncovered: [], absent: false, deleted: false };
|
|
88
|
+
if (entry.verb === 'delete') {
|
|
89
|
+
return { ...answer, rows: [...answer.rows, { ...emptyRow, deleted: true }] };
|
|
90
|
+
}
|
|
91
|
+
const paths = isSweep(entry.path) ? expandRowPath(entry.path) : [entry.path];
|
|
92
|
+
const reads = paths.map((path) => ({ path, read: readCoverageFile(readRowFile, path) }));
|
|
93
|
+
const refused = reads.find(({ read }) => !['ok', 'absent'].includes(read.outcome));
|
|
94
|
+
if (refused) {
|
|
95
|
+
return { ...answer, refusal: { id: entry.id, path: refused.path, outcome: describeReadOutcome(refused.read) } };
|
|
96
|
+
}
|
|
97
|
+
if (paths.length === 0 || reads.some(({ read }) => read.outcome === 'absent')) {
|
|
98
|
+
return { ...answer, rows: [...answer.rows, { ...emptyRow, absent: true }] };
|
|
99
|
+
}
|
|
100
|
+
const matches = list.classes.flatMap((classEntry) => {
|
|
101
|
+
const member = classEntry.members.find((candidate) => SEARCHABLE_KINDS.has(candidate.kind) &&
|
|
102
|
+
reads.some(({ read }) => hasBoundedLiteral(read.bytes, candidate.literal)));
|
|
103
|
+
return member ? [{ id: entry.id, classId: classEntry.id, literal: member.literal }] : [];
|
|
104
|
+
});
|
|
105
|
+
const present = matches.map(({ classId }) => classId);
|
|
106
|
+
const uncovered = present.filter((classId) => !tagged.includes(classId));
|
|
107
|
+
return {
|
|
108
|
+
rows: [...answer.rows, { ...emptyRow, present, uncovered }],
|
|
109
|
+
evidence: [...answer.evidence, ...matches.filter(({ classId }) => uncovered.includes(classId))],
|
|
110
|
+
refusal: null,
|
|
111
|
+
};
|
|
112
|
+
}, { rows: [], evidence: [], refusal: null });
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const renderCell = (value) => escapeForDisplay(value).replaceAll('|', '\\|');
|
|
116
|
+
export const renderCoverage = (rows, displayPath) => [
|
|
117
|
+
`## Robustness coverage — ${escapeForDisplay(displayPath)}`,
|
|
118
|
+
'| row | path | tagged | present | uncovered |',
|
|
119
|
+
'| --- | --- | --- | --- | --- |',
|
|
120
|
+
...rows.map((row) => row.deleted
|
|
121
|
+
? `| ${row.id} | ${renderCell(row.path)} | delete | — | — |`
|
|
122
|
+
: row.absent ? `| ${row.id} | ${renderCell(row.path)} | absent | — | — |`
|
|
123
|
+
: `| ${row.id} | ${renderCell(row.path)} | ${row.tagged.join(',') || '—'} | ${row.present.join(',') || '—'} | ${row.uncovered.join(',') || '—'} |`),
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
export const renderBrief = (planText, list, { row, displayPath }) => {
|
|
127
|
+
const shape = checkPlanStructure(planText, {});
|
|
128
|
+
if (shape.findings.length > 0) return { code: 1, lines: formatFindings(shape, escapeForDisplay(displayPath)).split('\n').map(escapeForDisplay) };
|
|
129
|
+
const classes = new Map(list.classes.map((entry) => [entry.id, entry]));
|
|
130
|
+
const rows = shape.parsed.rows.filter((entry) => entry.valid).map((entry) => ({
|
|
131
|
+
...entry,
|
|
132
|
+
robust: parseRobustTag(entry.responsibility),
|
|
133
|
+
}));
|
|
134
|
+
const refused = rows.find((entry) => entry.robust.refusal || entry.robust.classes.some((classId) => !classes.has(classId)));
|
|
135
|
+
if (refused) {
|
|
136
|
+
const reason = refused.robust.refusal ?? `unknown class ${refused.robust.classes.find((classId) => !classes.has(classId))}`;
|
|
137
|
+
return { code: 1, lines: [`robustness-brief: ${refused.id}: robust-class — ${reason}`] };
|
|
138
|
+
}
|
|
139
|
+
const selected = row === null ? rows.filter((entry) => entry.robust.classes.length > 0) : rows.filter((entry) => entry.id === row);
|
|
140
|
+
if (row !== null && selected.length === 0) return { code: 1, lines: [`robustness-brief: ${row}: row is absent`] };
|
|
141
|
+
if (row !== null && selected[0].robust.classes.length === 0) return { code: 1, lines: [`robustness-brief: ${row}: row is untagged`] };
|
|
142
|
+
if (selected.length === 0) return { code: 0, lines: [`Robustness literals: ${escapeForDisplay(displayPath)} has no robust tags.`] };
|
|
143
|
+
return {
|
|
144
|
+
code: 0,
|
|
145
|
+
lines: [
|
|
146
|
+
`## Robustness literals — list version ${list.version} · ${escapeForDisplay(displayPath)}`,
|
|
147
|
+
...selected.flatMap((entry) => [
|
|
148
|
+
'',
|
|
149
|
+
`### ${entry.id} — ${escapeForDisplay(entry.path)}`,
|
|
150
|
+
...entry.robust.classes.flatMap((classId) => renderClass(classes.get(classId))),
|
|
151
|
+
]),
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = parseArgs(argv);
|
|
159
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
160
|
+
const realpath = deps.realpath ?? realpathSync;
|
|
161
|
+
const cwdReal = realpath(cwd);
|
|
162
|
+
if (!isAbsolute(parsed.plan) && !lexicalRepoRelative(parsed.plan).ok) throw usage(`uncontained --plan ${parsed.plan}`);
|
|
163
|
+
const planPath = resolve(cwd, parsed.plan);
|
|
164
|
+
let planReal;
|
|
165
|
+
try {
|
|
166
|
+
planReal = realpath(planPath);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error?.code === 'ENOENT') throw usage(`absent --plan ${parsed.plan}`);
|
|
169
|
+
throw usage(`unreadable --plan ${parsed.plan} (${error?.code ?? error.message})`);
|
|
170
|
+
}
|
|
171
|
+
if (!isInside(cwdReal, planReal)) throw usage(`uncontained --plan ${parsed.plan}`);
|
|
172
|
+
const read = (deps.readPlan ?? readFileBytesNoFollowCapped)(planPath, PLAN_READ_CAP);
|
|
173
|
+
if (read.outcome !== 'ok') {
|
|
174
|
+
const outcome = read.outcome === 'foreign' ? `non-regular ${read.className ?? 'file'}` : read.outcome;
|
|
175
|
+
throw usage(`${outcome} --plan ${parsed.plan}${read.cap ? ` (cap ${read.cap})` : ''}`);
|
|
176
|
+
}
|
|
177
|
+
let planText;
|
|
178
|
+
try {
|
|
179
|
+
planText = decodePlan(read.bytes);
|
|
180
|
+
} catch {
|
|
181
|
+
throw usage(`malformed-utf8 --plan ${parsed.plan}`);
|
|
182
|
+
}
|
|
183
|
+
const list = (deps.readList ?? readShippedRobustnessLiterals)();
|
|
184
|
+
const brief = renderBrief(planText, list, { row: parsed.row, displayPath: parsed.plan });
|
|
185
|
+
if (!parsed.coverage || checkPlanStructure(planText, {}).findings.length > 0) return brief;
|
|
186
|
+
const sweepPaths = checkPlanStructure(planText, {}).parsed.rows
|
|
187
|
+
.filter((entry) => entry.valid && isSweep(entry.path) && !isCoverageDocument(entry.path))
|
|
188
|
+
.map((entry) => entry.path);
|
|
189
|
+
const expansions = deps.expandRowPath === undefined && sweepPaths.length > 0 ? expandSweepPaths(cwd, sweepPaths) : null;
|
|
190
|
+
const coverage = computeRowCoverage(planText, list, {
|
|
191
|
+
expandRowPath: deps.expandRowPath ?? ((path) => expansions[path] ?? []),
|
|
192
|
+
readRowFile: deps.readRowFile ?? ((path) => readFileBytesNoFollowCapped(resolve(cwd, path), PLAN_READ_CAP)),
|
|
193
|
+
});
|
|
194
|
+
if (coverage.refusal) {
|
|
195
|
+
const { id, path, outcome, reason } = coverage.refusal;
|
|
196
|
+
throw usage(reason === undefined ? `${id}: ${escapeForDisplay(path)} — ${outcome}` : `${id}: robust-class — ${reason}`);
|
|
197
|
+
}
|
|
198
|
+
const lines = [...renderCoverage(coverage.rows, parsed.plan), ...coverage.evidence.map(({ id, classId, literal }) => `${id}:${classId}:${literal}`)];
|
|
199
|
+
return { code: coverage.evidence.length > 0 ? 1 : 0, lines };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return { code: 2, lines: [`robustness-brief: usage — ${escapeForDisplay(error.message)}`, USAGE] };
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (isDirectRun(import.meta.url)) {
|
|
206
|
+
const result = main();
|
|
207
|
+
(result.code === 2 ? console.error : console.log)(result.lines.join('\n'));
|
|
208
|
+
process.exitCode = result.code;
|
|
209
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
4
|
+
|
|
5
|
+
export const KINDS = Object.freeze(['env', 'flag', 'ref', 'syscall', 'errno', 'state', 'argv']);
|
|
6
|
+
const KIND_SET = new Set(KINDS);
|
|
7
|
+
const TOP_KEYS = Object.freeze(['schema', 'version', 'classes']);
|
|
8
|
+
const CLASS_KEYS = Object.freeze(['id', 'prove', 'members']);
|
|
9
|
+
const MEMBER_KEYS = Object.freeze(['literal', 'kind', 'note', 'source']);
|
|
10
|
+
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
11
|
+
const ADR = /^AD-\d+$/u;
|
|
12
|
+
const CONTROL = /[\p{Cc}\u2028\u2029\uFFFD]/u;
|
|
13
|
+
const SHIPPED_LIST = fileURLToPath(new URL('../references/robustness-literals.json', import.meta.url));
|
|
14
|
+
|
|
15
|
+
const refuse = (code, detail) => {
|
|
16
|
+
throw Object.assign(new Error(`robustness-literals: ${code}${detail ? ` — ${detail}` : ''}`), { code });
|
|
17
|
+
};
|
|
18
|
+
const requireObject = (value, label) => {
|
|
19
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) refuse('type', `${label} must be an object`);
|
|
20
|
+
};
|
|
21
|
+
const requireKeys = (value, keys, label) => {
|
|
22
|
+
requireObject(value, label);
|
|
23
|
+
const unknown = Object.keys(value).find((key) => !keys.includes(key));
|
|
24
|
+
if (unknown !== undefined) refuse('unknown-key', `${label}.${unknown}`);
|
|
25
|
+
const missing = keys.find((key) => !Object.hasOwn(value, key));
|
|
26
|
+
if (missing !== undefined) refuse('missing-key', `${label}.${missing}`);
|
|
27
|
+
};
|
|
28
|
+
const requireString = (value, label) => {
|
|
29
|
+
if (typeof value !== 'string') refuse('type', `${label} must be a string`);
|
|
30
|
+
if (value.length === 0) refuse('empty-string', label);
|
|
31
|
+
if (CONTROL.test(value)) refuse('control', label);
|
|
32
|
+
};
|
|
33
|
+
const isRepoSource = (source) => !source.startsWith('/') && !source.includes('\\') &&
|
|
34
|
+
!source.split('/').includes('..') && (source.includes('/') || source.includes('.'));
|
|
35
|
+
const freezeList = (list) => Object.freeze({
|
|
36
|
+
...list,
|
|
37
|
+
classes: Object.freeze(list.classes.map((entry) => Object.freeze({
|
|
38
|
+
...entry,
|
|
39
|
+
members: Object.freeze(entry.members.map((member) => Object.freeze(member))),
|
|
40
|
+
}))),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const validateList = (value) => {
|
|
44
|
+
requireKeys(value, TOP_KEYS, 'list');
|
|
45
|
+
if (value.schema !== 1) refuse('schema', 'schema must be 1');
|
|
46
|
+
if (!Number.isSafeInteger(value.version) || value.version < 1) refuse('version', 'version must be an integer >= 1');
|
|
47
|
+
if (!Array.isArray(value.classes)) refuse('type', 'classes must be an array');
|
|
48
|
+
if (value.classes.length === 0) refuse('empty-class', 'classes must not be empty');
|
|
49
|
+
const ids = new Set();
|
|
50
|
+
for (const [classIndex, entry] of value.classes.entries()) {
|
|
51
|
+
const label = `classes[${classIndex}]`;
|
|
52
|
+
requireKeys(entry, CLASS_KEYS, label);
|
|
53
|
+
requireString(entry.id, `${label}.id`);
|
|
54
|
+
if (!SLUG.test(entry.id)) refuse('class-id', entry.id);
|
|
55
|
+
if (ids.has(entry.id)) refuse('duplicate-class', entry.id);
|
|
56
|
+
ids.add(entry.id);
|
|
57
|
+
requireString(entry.prove, `${label}.prove`);
|
|
58
|
+
if (!Array.isArray(entry.members)) refuse('type', `${label}.members must be an array`);
|
|
59
|
+
if (entry.members.length === 0) refuse('empty-class', entry.id);
|
|
60
|
+
const literals = new Set();
|
|
61
|
+
for (const [memberIndex, member] of entry.members.entries()) {
|
|
62
|
+
const memberLabel = `${label}.members[${memberIndex}]`;
|
|
63
|
+
requireKeys(member, MEMBER_KEYS, memberLabel);
|
|
64
|
+
for (const key of MEMBER_KEYS) requireString(member[key], `${memberLabel}.${key}`);
|
|
65
|
+
if (literals.has(member.literal)) refuse('duplicate-literal', `${entry.id}:${member.literal}`);
|
|
66
|
+
literals.add(member.literal);
|
|
67
|
+
if (!KIND_SET.has(member.kind)) refuse('kind', member.kind);
|
|
68
|
+
if (!ADR.test(member.source) && !SLUG.test(member.source) && !isRepoSource(member.source)) refuse('source', member.source);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return freezeList(value);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const readRobustnessLiterals = (path, io = {}) => {
|
|
75
|
+
const read = readRegularFileNoFollow(path, io);
|
|
76
|
+
if (read.outcome !== 'ok') refuse('type', `${path} must be a readable regular file (${read.className ?? read.code ?? read.outcome})`);
|
|
77
|
+
try {
|
|
78
|
+
return validateList(JSON.parse(read.content));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error?.code) throw error;
|
|
81
|
+
refuse('type', `${path} is not valid JSON (${error.message})`);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const readShippedRobustnessLiterals = (deps = {}) =>
|
|
86
|
+
readRobustnessLiterals((deps.resolvePath ?? (() => SHIPPED_LIST))(), deps.io);
|
|
87
|
+
|
|
88
|
+
export const parseRobustTag = (text) => {
|
|
89
|
+
const matches = [...String(text ?? '').matchAll(/robust:(\S*)/gu)];
|
|
90
|
+
if (matches.length === 0) return { classes: [] };
|
|
91
|
+
if (matches.length > 1) return { refusal: 'multiple-tags' };
|
|
92
|
+
if (matches[0][1] === '') return { refusal: 'no-class' };
|
|
93
|
+
const classes = matches[0][1].split(',');
|
|
94
|
+
if (classes.some((entry) => entry === '')) return { refusal: 'empty-class' };
|
|
95
|
+
if (new Set(classes).size !== classes.length) return { refusal: 'duplicate-class' };
|
|
96
|
+
if (classes.some((entry) => !SLUG.test(entry))) return { refusal: 'invalid-class' };
|
|
97
|
+
return { classes };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const classIds = (list) => Object.freeze(list.classes.map((entry) => entry.id));
|
|
101
|
+
export const digestOf = (list) => createHash('sha256').update(JSON.stringify(list.classes)).digest('hex');
|
package/tools/sandbox-masks.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { isDirectRun } from './direct-run.mjs';
|
|
|
42
42
|
import { fail } from './orchestration-config.mjs';
|
|
43
43
|
import { isNeverCommittableStat, shellQuoteArg } from './review-state.mjs';
|
|
44
44
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
45
|
+
import { GIT_MAX_BUFFER } from './git-env.mjs';
|
|
45
46
|
|
|
46
47
|
export const MASKS_FENCE_START = '# >>> agent-workflow sandbox-masks — managed block, fully REPLACED by the kit sandbox-masks lane; do not hand-edit inside >>>';
|
|
47
48
|
export const MASKS_FENCE_END = '# <<< agent-workflow sandbox-masks <<<';
|
|
@@ -59,7 +60,7 @@ const gitLine = (args, cwd) => {
|
|
|
59
60
|
// The UNFILTERED untracked walk: --others WITHOUT --exclude-standard, so a mask already hidden by
|
|
60
61
|
// an earlier apply stays visible to a rerun (the full-block replace re-derives it, codex).
|
|
61
62
|
const listUntrackedUnfilteredZ = (root) => {
|
|
62
|
-
const r = spawnSync('git', ['ls-files', '--others', '-z'], { cwd: root, encoding: 'utf8', maxBuffer:
|
|
63
|
+
const r = spawnSync('git', ['ls-files', '--others', '-z'], { cwd: root, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
63
64
|
if (r.error || r.status !== 0) return null;
|
|
64
65
|
return r.stdout.split('\0').filter(Boolean);
|
|
65
66
|
};
|
package/tools/set-flow.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import { loadDeclaration } from './run-gates.mjs';
|
|
|
50
50
|
import { compareSemver } from './semver-lite.mjs';
|
|
51
51
|
import { lexicalRepoRelative } from './core-evidence.mjs';
|
|
52
52
|
import { readAuthoritativeVersion } from './manifest/validate.mjs';
|
|
53
|
+
import { GIT_MAX_BUFFER } from './git-env.mjs';
|
|
53
54
|
|
|
54
55
|
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
55
56
|
const GATES_REL = 'docs/ai/gates.json';
|
|
@@ -197,7 +198,7 @@ const meetsVersionFloor = (version, floor) => {
|
|
|
197
198
|
return cmp !== null && cmp >= 0;
|
|
198
199
|
};
|
|
199
200
|
|
|
200
|
-
const defaultRunGit = (args, cwd) => spawnSync('git', args, { cwd, maxBuffer:
|
|
201
|
+
const defaultRunGit = (args, cwd) => spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
201
202
|
|
|
202
203
|
const gitToplevel = (cwd, runGit) => {
|
|
203
204
|
const r = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
@@ -18,37 +18,18 @@ import { readFileSync } from 'node:fs';
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
20
|
import { configFail, configPathFor, scopeFail } from './source-size-refusal.mjs';
|
|
21
|
+
import { GIT_MAX_BUFFER, parseIndexEntries, resolveGitLocation, withGitPath } from './git-env.mjs';
|
|
21
22
|
|
|
22
23
|
const LF = 0x0a;
|
|
23
24
|
const CR = 0x0d;
|
|
24
|
-
const TAB = 0x09;
|
|
25
25
|
const SLASH = 0x2f;
|
|
26
26
|
|
|
27
|
-
const GIT_MAX_BUFFER = 256 * 1024 * 1024;
|
|
28
27
|
const SYMLINK_MODE = '120000';
|
|
29
28
|
const GITLINK_MODE = '160000';
|
|
30
29
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// The object id is carried through rather than dropped: the record already holds it, and a consumer
|
|
35
|
-
// that must read what the index HOLDS (not what the worktree happens to show) needs exactly that.
|
|
36
|
-
const parseIndexEntries = (buf) => {
|
|
37
|
-
const entries = [];
|
|
38
|
-
let start = 0;
|
|
39
|
-
while (start < buf.length) {
|
|
40
|
-
let end = buf.indexOf(0, start);
|
|
41
|
-
if (end === -1) end = buf.length;
|
|
42
|
-
const record = buf.subarray(start, end);
|
|
43
|
-
const tab = record.indexOf(TAB);
|
|
44
|
-
if (tab !== -1) {
|
|
45
|
-
const head = record.subarray(0, tab).toString('utf8').split(' ');
|
|
46
|
-
entries.push({ mode: head[0], sha: head[1], stage: Number(head[2]), path: record.subarray(tab + 1) });
|
|
47
|
-
}
|
|
48
|
-
start = end + 1;
|
|
49
|
-
}
|
|
50
|
-
return entries;
|
|
51
|
-
};
|
|
30
|
+
// The `ls-files -s -z` record parser lives in the git leaf (raw bytes in, raw path bytes out, the
|
|
31
|
+
// object id carried through so a consumer reading what the index HOLDS has it) — one home shared
|
|
32
|
+
// with the control-byte gate.
|
|
52
33
|
|
|
53
34
|
// A path whose bytes are not valid UTF-8 round-trips to something DIFFERENT (the replacement char is
|
|
54
35
|
// lossy) — that is the whole test, and it is exact.
|
|
@@ -65,11 +46,24 @@ const bufSegmentPrefix = (path, prefix) =>
|
|
|
65
46
|
const bufEndsWith = (path, suffix) =>
|
|
66
47
|
path.length >= suffix.length && path.subarray(path.length - suffix.length).equals(suffix);
|
|
67
48
|
|
|
49
|
+
// The enumeration-error class (exit 2) also refuses every git LOCATION but a work tree, by its own
|
|
50
|
+
// state name — a redirecting GIT_DIR would otherwise enumerate ANOTHER repository's index in
|
|
51
|
+
// silence — and a runner that throws, which is an answer here, never a raw stack.
|
|
68
52
|
export const enumerateIndex = (cwd, deps = {}) => {
|
|
69
53
|
const spawn = deps.spawn ?? spawnSync;
|
|
70
|
-
const
|
|
54
|
+
const env = deps.env ?? process.env;
|
|
55
|
+
const location = resolveGitLocation(cwd, { spawn, env });
|
|
56
|
+
if (location.state !== 'work-tree') {
|
|
57
|
+
throw configFail(`the git index could not be enumerated in ${cwd} (the git location is ${location.state}: ${location.cause}) — the declared scope is unknown, so nothing is judged`);
|
|
58
|
+
}
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = spawn('git', ['ls-files', '-s', '-z'], { cwd, env: withGitPath(env), maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
result = { error: err };
|
|
64
|
+
}
|
|
71
65
|
if (result.error || result.status !== 0) {
|
|
72
|
-
const why = result.error ? result.error.message : `git exited ${result.status}`;
|
|
66
|
+
const why = result.error ? result.error.message : result.signal ? `git was killed by ${result.signal}` : `git exited ${result.status}`;
|
|
73
67
|
throw configFail(`the git index could not be enumerated in ${cwd} (${why}) — the declared scope is unknown, so nothing is judged`);
|
|
74
68
|
}
|
|
75
69
|
return parseIndexEntries(result.stdout);
|