@sabaiway/agent-workflow-kit 1.43.0 → 1.45.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 (53) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +8 -4
  3. package/SKILL.md +20 -8
  4. package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +87 -16
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +3 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +4 -2
  8. package/bridges/antigravity-cli-bridge/capability.json +3 -2
  9. package/bridges/codex-cli-bridge/SKILL.md +1 -1
  10. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +3 -0
  11. package/bridges/codex-cli-bridge/bin/codex-review.sh +90 -19
  12. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +4 -0
  13. package/bridges/codex-cli-bridge/capability.json +3 -2
  14. package/bridges/codex-cli-bridge/references/driving-codex.md +3 -2
  15. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +3 -2
  16. package/capability.json +1 -1
  17. package/package.json +1 -1
  18. package/references/modes/autonomy-doctor.md +21 -0
  19. package/references/modes/bootstrap.md +1 -1
  20. package/references/modes/doc-parity.md +2 -1
  21. package/references/modes/grounding.md +8 -7
  22. package/references/modes/recommendations.md +14 -0
  23. package/references/modes/review-state.md +1 -1
  24. package/references/modes/sandbox-masks.md +15 -0
  25. package/references/modes/upgrade.md +10 -7
  26. package/references/modes/velocity.md +13 -2
  27. package/references/shared/composition-handoff.md +21 -16
  28. package/references/shared/report-footer.md +5 -1
  29. package/references/templates/AGENTS.md +2 -1
  30. package/references/templates/autonomy.json +3 -0
  31. package/tools/autonomy-config.mjs +13 -3
  32. package/tools/autonomy-doctor.mjs +488 -0
  33. package/tools/commands.mjs +24 -3
  34. package/tools/delegation.mjs +9 -5
  35. package/tools/detect-backends.mjs +2 -2
  36. package/tools/doc-parity.mjs +31 -2
  37. package/tools/engine-source.mjs +6 -0
  38. package/tools/family-registry.mjs +21 -7
  39. package/tools/fold-completeness-run.mjs +5 -0
  40. package/tools/grounding.mjs +139 -22
  41. package/tools/inject-methodology.mjs +117 -28
  42. package/tools/manifest/schema.md +20 -0
  43. package/tools/manifest/validate.mjs +26 -0
  44. package/tools/procedures.mjs +61 -8
  45. package/tools/recipes.mjs +94 -15
  46. package/tools/recommendations.mjs +469 -0
  47. package/tools/review-ledger-write.mjs +14 -0
  48. package/tools/review-ledger.mjs +3 -2
  49. package/tools/review-state.mjs +101 -24
  50. package/tools/run-gates.mjs +3 -0
  51. package/tools/sandbox-masks.mjs +370 -0
  52. package/tools/set-recipe.mjs +13 -1
  53. package/tools/velocity-profile.mjs +231 -24
@@ -414,6 +414,8 @@ Usage:
414
414
  node review-ledger-write.mjs record --json '<round-payload>' [--from-receipts] [--cwd <dir>]
415
415
  node review-ledger-write.mjs classify --json '<triage-payload>' [--cwd <dir>]
416
416
  node review-ledger-write.mjs override --json '<override-payload>' [--cwd <dir>]
417
+ (every verb also accepts --json @<file> — the payload read from a file, keeping the command
418
+ line PLAIN: an inline JSON argv falls outside plain-invocation allow heuristics and prompts)
417
419
 
418
420
  Every verb operates on the current SEGMENT — (loop, base = git rev-parse HEAD): round numbers,
419
421
  caps, and teeth reset only when a gated commit moves base (schema 4; records carry base).
@@ -461,6 +463,18 @@ const parseArgs = (argv) => {
461
463
  } else if (a === '--json') {
462
464
  opts.json = argv[i + 1];
463
465
  if (opts.json === undefined) throw usageFail('--json needs a JSON payload');
466
+ // `--json @<file>` reads the payload from a file: a large inline JSON argv (quotes, braces)
467
+ // falls outside every plain-invocation allow heuristic and prompts, and hand-composing it on
468
+ // a command line is exactly the error-prone class --from-receipts exists to shrink — the
469
+ // file form keeps the COMMAND plain while the payload stays explicit (AD-044 Plan 4).
470
+ if (opts.json.startsWith('@')) {
471
+ const payloadPath = opts.json.slice(1);
472
+ try {
473
+ opts.json = readFileSync(payloadPath, 'utf8');
474
+ } catch (err) {
475
+ throw usageFail(`--json @${payloadPath}: unreadable payload file (${err.code ?? err.message})`);
476
+ }
477
+ }
464
478
  i += 1;
465
479
  } else if (a === '--from-receipts') {
466
480
  opts.fromReceipts = true;
@@ -569,8 +569,9 @@ gated commit, so the round-counter reset is earned, never declared.
569
569
  observed-red receipts, quarantined probes. Counts only — interpretation stays with you.
570
570
 
571
571
  The writer is a SEPARATE tool (review-ledger-write.mjs record/classify/override) — this read-only
572
- checker never imports it. Human residual: git commit --no-verify, ledger-file editing, and forged
573
- counts remain possible a self-discipline mechanism, not a security boundary.
572
+ checker never imports it. Sandbox-safe: runs fully inside an OS sandbox (fs + git reads, no
573
+ network) the D4 sandbox lane. Human residual: git commit --no-verify, ledger-file editing, and
574
+ forged counts remain possible — a self-discipline mechanism, not a security boundary.
574
575
 
575
576
  Exit codes: 0 pass (or plain report); 1 check failed or config error (loud); 2 usage.`;
576
577
 
@@ -32,10 +32,16 @@
32
32
  // a folded tree; only a fresh grounded re-run mints a gate-satisfying receipt).
33
33
  //
34
34
  // The fingerprint is the ONE canonical uncommitted-state identity — sha256 over: staged diff +
35
- // unstaged diff + untracked-not-ignored file contents (binary/symlink/non-regular untracked paths
36
- // ride as name-only notes). Domain == the review-payload domain the wrappers assemble; the prose
37
- // definition lives in each bridge's capability.json roles.review.contract.receipt, and the bash
38
- // twin lives in both wrappers cross-checked by test/review-fingerprint-parity.test.mjs.
35
+ // unstaged diff + untracked-not-ignored file contents (binary untracked files, symlinks, and
36
+ // directories/gitlinks ride as name-only notes). NEVER-COMMITTABLE untracked stat classes
37
+ // character/block devices, FIFOs, sockets are EXCLUDED from the domain entirely (no note): a
38
+ // sandbox that injects device masks into the work tree can no longer move the fingerprint or dirty
39
+ // the clean check (AD-044 Plan 4; the class is lstat-keyed because a lying dirent is exactly how
40
+ // the masks surface). Untracked SYMLINKS and DIRECTORIES stay in the domain — both are committable
41
+ // (a directory listed by `git ls-files --others` as `dir/` is an embedded repo, i.e. a gitlink).
42
+ // Domain == the review-payload domain the wrappers assemble; the prose definition lives in each
43
+ // bridge's capability.json roles.review.contract.receipt, and the bash twin lives in both
44
+ // wrappers — cross-checked by test/review-fingerprint-parity.test.mjs.
39
45
  //
40
46
  // HUMAN residual (accepted, documented): `git commit --no-verify` skips any pre-commit gate, and
41
47
  // deleting/editing the receipt file forges state — receipts live in the git dir (never committable)
@@ -46,8 +52,8 @@
46
52
  // Node >= 18. No side effects on import (the isDirectRun idiom).
47
53
 
48
54
  import { readFileSync, readdirSync, lstatSync, readlinkSync, openSync, readSync, closeSync } from 'node:fs';
49
- import { join } from 'node:path';
50
- import { pathToFileURL } from 'node:url';
55
+ import { join, dirname } from 'node:path';
56
+ import { pathToFileURL, fileURLToPath } from 'node:url';
51
57
  import { spawnSync } from 'node:child_process';
52
58
  import { createHash } from 'node:crypto';
53
59
  import { detectBackends } from './detect-backends.mjs';
@@ -102,10 +108,23 @@ const isBinaryFile = (path) => {
102
108
  }
103
109
  };
104
110
 
111
+ // The never-committable untracked stat classes (Decision 1, AD-044 Plan 4): character/block
112
+ // devices, FIFOs, sockets — git content can never carry them, so they are excluded from the ENTIRE
113
+ // review domain (fingerprint payload, clean check; the wrappers' bash twin filters the assembled
114
+ // payload identically). lstat-keyed by design: the sandbox mask class surfaces exactly where the
115
+ // dirent LIES (readdir says file, lstat says char device). A null stat (vanished path) is NOT in
116
+ // the class — it keeps its name-only note, like directories (gitlinks) and symlinks.
117
+ export const isNeverCommittableStat = (stat) =>
118
+ stat != null &&
119
+ (stat.isCharacterDevice() || stat.isBlockDevice() || stat.isFIFO() || stat.isSocket());
120
+
105
121
  // The canonical payload bytes: staged diff + unstaged diff + the untracked-not-ignored section —
106
122
  // byte-identical to the wrappers' emit_fingerprint_payload (same git invocations, same headers,
107
123
  // same ls-files ordering), emitted from the work-tree ROOT. Returns null outside a git work tree.
108
- export const computeFingerprintPayload = (cwd) => {
124
+ // The lstat is injectable ONLY so tests can prove the never-committable filter non-vacuously (a
125
+ // lying lstat over a git-visible fixture path — the sandbox mechanism itself); production callers
126
+ // never pass it.
127
+ export const computeFingerprintPayload = (cwd, { lstat = lstatSync } = {}) => {
109
128
  const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
110
129
  if (top == null) return null;
111
130
  const staged = gitBuf(['diff', '--cached', '--no-ext-diff'], top);
@@ -117,10 +136,11 @@ export const computeFingerprintPayload = (cwd) => {
117
136
  const full = join(top, rel);
118
137
  let stat = null;
119
138
  try {
120
- stat = lstatSync(full);
139
+ stat = lstat(full);
121
140
  } catch {
122
141
  stat = null;
123
142
  }
143
+ if (isNeverCommittableStat(stat)) continue;
124
144
  if (stat?.isSymbolicLink()) {
125
145
  let target = '?';
126
146
  try {
@@ -142,26 +162,75 @@ export const computeFingerprintPayload = (cwd) => {
142
162
  };
143
163
 
144
164
  // sha256 hex of the canonical payload, or null outside a git work tree.
145
- export const computeTreeFingerprint = (cwd) => {
146
- const payload = computeFingerprintPayload(cwd);
165
+ export const computeTreeFingerprint = (cwd, fsx) => {
166
+ const payload = computeFingerprintPayload(cwd, fsx);
147
167
  return payload == null ? null : createHash('sha256').update(payload).digest('hex');
148
168
  };
149
169
 
150
- // Clean = nothing staged, nothing unstaged, no untracked-not-ignored paths (the wrappers' no-diff
151
- // preflight). null when not decidable (not a git work tree). Anchored at the work-tree ROOT like
152
- // the fingerprint: `git ls-files --others` is cwd-SCOPED, so a subdirectory invocation would
153
- // otherwise miss root/sibling untracked paths and report a dirty tree as clean (codex R1 finding).
154
- export const isTreeClean = (cwd) => {
170
+ // Clean = nothing staged, nothing unstaged, no REVIEWABLE untracked-not-ignored paths (the
171
+ // wrappers' no-diff preflight). Never-committable untracked paths (device/FIFO/socket) do not
172
+ // count as dirty same filter as the fingerprint, so the two can never disagree about a
173
+ // masks-only tree. An lstat failure keeps the path in the domain (dirty), mirroring the
174
+ // fingerprint's null-stat note. null when not decidable (not a git work tree). Anchored at the
175
+ // work-tree ROOT like the fingerprint: `git ls-files --others` is cwd-SCOPED, so a subdirectory
176
+ // invocation would otherwise miss root/sibling untracked paths and report a dirty tree as clean
177
+ // (codex R1 finding).
178
+ export const isTreeClean = (cwd, { lstat = lstatSync } = {}) => {
155
179
  const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
156
180
  if (top == null) return null;
157
181
  const staged = gitRaw(['diff', '--cached', '--quiet'], top);
158
182
  const unstaged = gitRaw(['diff', '--quiet'], top);
159
183
  if (staged.error || unstaged.error || staged.status > 1 || unstaged.status > 1) return null;
160
- const untracked = gitBuf(['ls-files', '--others', '--exclude-standard'], top);
161
- if (untracked == null) return null;
162
- return staged.status === 0 && unstaged.status === 0 && untracked.toString('utf8').trim() === '';
184
+ const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top);
185
+ if (untrackedZ == null) return null;
186
+ const reviewable = untrackedZ
187
+ .toString('utf8')
188
+ .split('\0')
189
+ .filter(Boolean)
190
+ .filter((rel) => {
191
+ try {
192
+ return !isNeverCommittableStat(lstat(join(top, rel)));
193
+ } catch {
194
+ return true;
195
+ }
196
+ });
197
+ return staged.status === 0 && unstaged.status === 0 && reviewable.length === 0;
163
198
  };
164
199
 
200
+ // ── the sandbox-masks advisory (D lane, AD-044 Plan 4 Phase 1.5) ────────────────────
201
+
202
+ // Count the never-committable untracked paths the STANDARD walk still shows. The review domain
203
+ // ignores them by construction; this count only feeds ONE non-failing advisory line naming the
204
+ // cosmetic sandbox-masks apply — an applied managed block hides the paths from --exclude-standard,
205
+ // so the advisory disappears exactly when the status noise does (no standing detector).
206
+ export const countNeverCommittableUntracked = (cwd, { lstat = lstatSync } = {}) => {
207
+ const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
208
+ if (top == null) return 0;
209
+ const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top);
210
+ if (untrackedZ == null) return 0;
211
+ return untrackedZ
212
+ .toString('utf8')
213
+ .split('\0')
214
+ .filter(Boolean)
215
+ .filter((rel) => {
216
+ try {
217
+ return isNeverCommittableStat(lstat(join(top, rel)));
218
+ } catch {
219
+ return false;
220
+ }
221
+ }).length;
222
+ };
223
+
224
+ // Shell-quote one argument for a COPY-PASTE advisory command: plain safe tokens stay bare; anything
225
+ // else rides single quotes (a space/metacharacter path must never render a dead or unsafe paste —
226
+ // codex R1). Exported for the sandbox-masks probe, which renders the same apply one-liner.
227
+ export const shellQuoteArg = (s) => (/^[A-Za-z0-9_/.\-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
228
+
229
+ const maskAdvisoryLine = (state) =>
230
+ state.maskedUntracked > 0
231
+ ? `notice: ${state.maskedUntracked} never-committable untracked path(s) (device/FIFO/socket) are ignored by the review domain — hide them from git status: node ${shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'sandbox-masks.mjs'))} --cwd ${shellQuoteArg(state.root)} --apply`
232
+ : '';
233
+
165
234
  // ── plan-in-flight detector (the AD-038 naming convention; documented in queue.md) ─────
166
235
 
167
236
  // Scratch by the naming convention: EXECUTE-/FEEDBACK- prefixes, or a name carrying PROMPT/prompt/
@@ -292,7 +361,7 @@ export const degradedExemptSet = ({ records, readError, malformed, base, plans,
292
361
  // work-tree ROOT when one exists — the fingerprint is root-anchored, so a subdirectory invocation
293
362
  // must read the same config/plans or a dirty unreceipted tree could false-PASS as "no plan in
294
363
  // flight" (codex R1 finding). Outside a git tree the cwd is the only anchor (and --check exits 0).
295
- export const buildState = ({ cwd, env = process.env, detect = detectBackends } = {}) => {
364
+ export const buildState = ({ cwd, env = process.env, detect = detectBackends, lstat = lstatSync } = {}) => {
296
365
  const root = gitLine(['rev-parse', '--show-toplevel'], cwd) ?? cwd;
297
366
  const { config, source: configSource } = loadConfig(root);
298
367
  let detection = [];
@@ -306,8 +375,10 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends } =
306
375
  const { dispatch } = planRecipe(resolved.recipe, detection);
307
376
  const requiredBackends = dispatch.map((d) => DISPLAY_ALIASES[d.backend] ?? d.backend);
308
377
  const plans = plansInFlight(root);
309
- const fingerprint = computeTreeFingerprint(cwd);
310
- const clean = fingerprint == null ? null : isTreeClean(cwd);
378
+ // The injected lstat threads through EVERY stat-dependent computation (fingerprint, clean, the
379
+ // mask count) a partial injection would let a test observe an inconsistent state (codex R3).
380
+ const fingerprint = computeTreeFingerprint(cwd, { lstat });
381
+ const clean = fingerprint == null ? null : isTreeClean(cwd, { lstat });
311
382
  const receiptsPath = resolveReceiptsPath(cwd, env);
312
383
  const { receipts, malformed } = receiptsPath ? readReceipts(receiptsPath) : { receipts: [], malformed: 0 };
313
384
  const backends = requiredBackends.map((b) => ({ backend: b, ...backendReceiptStatus(receipts, b, fingerprint) }));
@@ -324,6 +395,7 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends } =
324
395
  requiredBackends,
325
396
  backends,
326
397
  plans,
398
+ root,
327
399
  fingerprint,
328
400
  clean,
329
401
  receiptsPath,
@@ -334,6 +406,7 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends } =
334
406
  ledgerMalformed: ledgerMalformed ?? 0,
335
407
  ledgerReadError: ledgerReadError ?? null,
336
408
  degradedExempt,
409
+ maskedUntracked: countNeverCommittableUntracked(cwd, { lstat }),
337
410
  detectionWarning,
338
411
  };
339
412
  };
@@ -444,6 +517,7 @@ AND is not degraded-exempt. Declare it as a project gate by hand (docs/ai/gates.
444
517
  explicit-consent seeder (tools/seed-gates.mjs) — never without consent.
445
518
 
446
519
  Read-only: never writes, never commits, never runs a subscription CLI; spawns read-only git queries.
520
+ Sandbox-safe: runs fully inside an OS sandbox (fs + git reads, no network) — the D4 sandbox lane.
447
521
  Human residual: git commit --no-verify and receipt-file deletion remain possible — this is a
448
522
  self-discipline mechanism, not a security boundary.
449
523
 
@@ -459,15 +533,18 @@ export const main = (argv, ctx = {}) => {
459
533
  if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
460
534
  const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
461
535
  if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
462
- const state = buildState({ cwd, env, detect });
536
+ const state = buildState({ cwd, env, detect, lstat: ctx.lstat });
463
537
  const check = decideCheck(state);
538
+ // The mask advisory is NON-FAILING by contract: one notice line, never an exit-code arm.
539
+ const advisory = maskAdvisoryLine(state);
464
540
  if (argv.includes('--json')) {
465
541
  return { code: argv.includes('--check') ? check.code : 0, stdout: JSON.stringify({ ...state, check }, null, 2), stderr: '' };
466
542
  }
467
543
  if (argv.includes('--check')) {
468
- return { code: check.code, stdout: `review-state check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`, stderr: '' };
544
+ const line = `review-state check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`;
545
+ return { code: check.code, stdout: advisory ? `${line}\n${advisory}` : line, stderr: '' };
469
546
  }
470
- return { code: 0, stdout: formatHuman(state, check), stderr: '' };
547
+ return { code: 0, stdout: advisory ? `${formatHuman(state, check)}\n${advisory}` : formatHuman(state, check), stderr: '' };
471
548
  } catch (err) {
472
549
  return { code: err.exitCode ?? 1, stdout: '', stderr: `review-state: ${err.message}` };
473
550
  }
@@ -84,6 +84,9 @@ const USAGE = [
84
84
  'In --record mode the unit-tests gate is CREDITED (not re-spawned) when it is the FIRST declared gate',
85
85
  'AND the fold-completeness runner already ran that EXACT command green at the current tree',
86
86
  '(fingerprint-bound + exit-0 + cmd-identity); positioned after another gate, it always re-spawns.',
87
+ 'Sandbox-safe: the runner itself needs no network and writes only repo-local state — the D4 sandbox',
88
+ 'lane; each DECLARED gate command is the project\'s own, so ITS sandbox-safety is command-shape',
89
+ 'dependent (first try the sandbox-safe shape — cache under $TMPDIR, offline/notifier off).',
87
90
  `Exit codes: 0 ok · 1 gate failure · 2 usage · 3 missing declaration · 4 empty gates list ·`,
88
91
  '5 malformed/invalid declaration · 6 bash unavailable · 7 --record asked but the record failed.',
89
92
  ].join('\n');