@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/SKILL.md +9 -1
  3. package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
  5. package/bridges/antigravity-cli-bridge/capability.json +1 -1
  6. package/bridges/codex-cli-bridge/SKILL.md +1 -1
  7. package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
  8. package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
  9. package/bridges/codex-cli-bridge/capability.json +1 -1
  10. package/capability.json +1 -1
  11. package/package.json +1 -1
  12. package/references/modes/commit-guard.md +2 -2
  13. package/references/modes/control-bytes.md +91 -0
  14. package/references/modes/flow-writer.md +16 -4
  15. package/references/modes/procedures.md +2 -0
  16. package/references/modes/review-state.md +1 -1
  17. package/references/modes/robustness-brief.md +61 -0
  18. package/references/modes/velocity.md +2 -0
  19. package/references/robustness-literals.json +107 -0
  20. package/tools/changed-surface.mjs +1 -1
  21. package/tools/commands.mjs +14 -0
  22. package/tools/commit-guard.mjs +20 -9
  23. package/tools/control-bytes.mjs +264 -0
  24. package/tools/core-evidence.mjs +33 -20
  25. package/tools/coverage-check.mjs +1 -1
  26. package/tools/dispatch-store-read.mjs +2 -2
  27. package/tools/flow-check-git-lane.mjs +2 -1
  28. package/tools/flow-delta-proof.mjs +4 -4
  29. package/tools/flow-record-shape.mjs +44 -2
  30. package/tools/flow-round-gates.mjs +151 -0
  31. package/tools/flow-store-read.mjs +1 -1
  32. package/tools/flow-writer.mjs +235 -82
  33. package/tools/fold-scope.mjs +7 -3
  34. package/tools/fs-read-nofollow.mjs +56 -4
  35. package/tools/git-env.mjs +120 -0
  36. package/tools/grounding.mjs +4 -3
  37. package/tools/plan-shape-facts.mjs +23 -10
  38. package/tools/plan-shape.mjs +8 -0
  39. package/tools/procedures.mjs +18 -4
  40. package/tools/repo-lex.mjs +29 -0
  41. package/tools/review-state.mjs +43 -34
  42. package/tools/robustness-brief.mjs +209 -0
  43. package/tools/robustness-literals.mjs +101 -0
  44. package/tools/sandbox-masks.mjs +2 -1
  45. package/tools/set-flow.mjs +2 -1
  46. package/tools/source-size-scope.mjs +19 -25
  47. package/tools/tracked-tree-census.mjs +3 -3
  48. package/tools/velocity-profile.mjs +5 -2
  49. package/tools/worktree-handoff-return.mjs +2 -2
  50. package/tools/worktrees.mjs +2 -2
@@ -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
- // the cwd is not a git work tree
25
- // (the gate can never arm there, so nothing is announced); and when
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
- const root = gitLine(['rev-parse', '--show-toplevel'], cwd) ?? cwd;
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/diagnostics only; the OBLIGATIONS come from the configured
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.fingerprint == null) return { code: 0, reason: `not a git work tree — nothing to fingerprint${earlyNotes}` };
579
- // A clean tree PASSES, but never silently: the no-plan arm above already returned, so reaching
580
- // here means >=1 plan IS in flight and this gate arms the moment the tree turns dirty. Naming
581
- // each file forward-looking is what makes the arm discoverable BEFORE it blocks a pending commit.
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(' tree: not a git work tree');
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 ?? '(unresolvable — no git dir)'} (${state.receiptCount} line(s)${state.malformed ? `, ${state.malformed} malformed — inspect the file` : ''})`);
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-git-tree / obligations satisfied (reviewed: >=1 ship-class attestation;
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');
@@ -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: 256 * 1024 * 1024, windowsHide: true });
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
  };
@@ -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: 64 * 1024 * 1024, windowsHide: true });
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
- // Raw bytes in, raw bytes out: `ls-files -s -z` emits mode, object and stage, then a TAB, then the
32
- // path, then a NUL — and the path half is never decoded before it has been matched, so a name
33
- // carrying a tab or a newline survives intact (splitting on lines would mangle it).
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 result = spawn('git', ['ls-files', '-s', '-z'], { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
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);