@sabaiway/agent-workflow-kit 2.1.0 → 3.1.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 (107) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/README.md +7 -5
  3. package/SKILL.md +16 -8
  4. package/bin/install.mjs +11 -2
  5. package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
  10. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
  11. package/bridges/antigravity-cli-bridge/capability.json +3 -2
  12. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  13. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
  14. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
  15. package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
  16. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
  17. package/bridges/codex-cli-bridge/capability.json +3 -2
  18. package/capability.json +1 -1
  19. package/migrations/3.0.0-hardened-core-loop.md +44 -0
  20. package/migrations/README.md +1 -1
  21. package/package.json +2 -2
  22. package/references/hooks/gate-approve.mjs +1 -1
  23. package/references/modes/bootstrap.md +3 -3
  24. package/references/modes/commit-guard.md +19 -0
  25. package/references/modes/core-evidence.md +15 -0
  26. package/references/modes/coverage-check.md +12 -0
  27. package/references/modes/doc-parity.md +7 -6
  28. package/references/modes/gates.md +7 -6
  29. package/references/modes/grounding.md +1 -2
  30. package/references/modes/hook.md +1 -1
  31. package/references/modes/recommendations.md +1 -0
  32. package/references/modes/review-state.md +3 -3
  33. package/references/modes/upgrade.md +6 -6
  34. package/references/modes/velocity.md +2 -2
  35. package/references/modes/worktrees.md +120 -0
  36. package/references/scripts/archive-decisions.mjs +3 -3
  37. package/references/scripts/archive-decisions.test.mjs +5 -5
  38. package/references/scripts/check-docs-size-cli.test.mjs +41 -0
  39. package/references/scripts/check-docs-size.mjs +46 -29
  40. package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
  41. package/references/scripts/install-git-hooks.mjs +90 -18
  42. package/references/scripts/install-git-hooks.test.mjs +102 -0
  43. package/references/scripts/migrate-gates-branches.test.mjs +157 -0
  44. package/references/scripts/migrate-gates.mjs +395 -0
  45. package/references/scripts/migrate-gates.test.mjs +284 -0
  46. package/references/templates/agent_rules.md +2 -2
  47. package/tools/ack-write.mjs +1 -1
  48. package/tools/atomic-write.mjs +2 -2
  49. package/tools/autonomy-config.mjs +1 -1
  50. package/tools/autonomy-doctor.mjs +2 -2
  51. package/tools/autonomy-write.mjs +1 -1
  52. package/tools/bridge-settings-read.mjs +1 -1
  53. package/tools/bridge-settings.mjs +1 -1
  54. package/tools/changed-surface.mjs +16 -77
  55. package/tools/cheap-agents.mjs +2 -2
  56. package/tools/commands.mjs +26 -10
  57. package/tools/commit-guard.mjs +167 -0
  58. package/tools/core-evidence.mjs +914 -0
  59. package/tools/coverage-check.mjs +260 -0
  60. package/tools/delegation.mjs +1 -1
  61. package/tools/detect-backends.mjs +3 -3
  62. package/tools/doc-parity.mjs +11 -27
  63. package/tools/engine-source.mjs +1 -1
  64. package/tools/family-members.mjs +1 -1
  65. package/tools/family-registry.mjs +1 -1
  66. package/tools/fs-safe.mjs +1 -1
  67. package/tools/gate-hook.mjs +1 -1
  68. package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
  69. package/tools/grounding.mjs +19 -96
  70. package/tools/hide-footprint.mjs +1 -1
  71. package/tools/inject-methodology.mjs +72 -41
  72. package/tools/known-footprint.mjs +1 -1
  73. package/tools/labels.mjs +1 -1
  74. package/tools/lcov.mjs +6 -10
  75. package/tools/lens-region.mjs +1 -1
  76. package/tools/manifest/validate.mjs +19 -0
  77. package/tools/migrate-adr-store.mjs +5 -5
  78. package/tools/orchestration-config.mjs +1 -1
  79. package/tools/orchestration-write.mjs +1 -1
  80. package/tools/presentation.mjs +1 -1
  81. package/tools/procedures.mjs +3 -3
  82. package/tools/recipes.mjs +62 -11
  83. package/tools/recommendations.mjs +97 -14
  84. package/tools/release-scan.mjs +10 -3
  85. package/tools/renderers.mjs +1 -1
  86. package/tools/review-state.mjs +291 -343
  87. package/tools/run-gates.mjs +216 -92
  88. package/tools/sandbox-masks.mjs +14 -14
  89. package/tools/semver-lite.mjs +1 -1
  90. package/tools/set-autonomy.mjs +1 -1
  91. package/tools/set-recipe.mjs +2 -2
  92. package/tools/setup-backends.mjs +1 -1
  93. package/tools/surface.mjs +1 -1
  94. package/tools/uninstall.mjs +1 -1
  95. package/tools/velocity-profile.mjs +9 -9
  96. package/tools/view-model.mjs +1 -1
  97. package/tools/worktrees.mjs +2292 -0
  98. package/references/modes/fold-completeness.md +0 -30
  99. package/references/modes/review-ledger.md +0 -34
  100. package/references/templates/verification-profile.json +0 -10
  101. package/tools/fold-completeness-run.mjs +0 -1120
  102. package/tools/fold-completeness.mjs +0 -672
  103. package/tools/review-ledger-core.mjs +0 -428
  104. package/tools/review-ledger-write.mjs +0 -647
  105. package/tools/review-ledger.mjs +0 -630
  106. package/tools/sarif.mjs +0 -52
  107. package/tools/verification-profile.mjs +0 -219
@@ -98,14 +98,28 @@ Receipt:
98
98
  payload; a continuation receipt is fresh:false (informational-only — it cannot attest the
99
99
  folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY
100
100
  receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe
101
- review never attests) and equally rejects an unmarked one (silence is not a declaration); a write
102
- failure warns, never fails the review
101
+ review never attests) and equally rejects an unmarked one (silence is not a declaration);
102
+ posture = the ACTUAL run posture {model} (agy has no tier), written on EVERY receipt (D5) — the
103
+ gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the
104
+ review), one stderr banner line states the same posture, an ATTESTING review with AGY_MODEL
105
+ explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses
106
+ pre-spend in every mode; a run whose output carries NO recognized '### Verdict' section — empty
107
+ output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session
108
+ error); a write failure warns, never fails the review
103
109
 
104
110
  Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins over built-in default):
105
111
  ${XDG_CONFIG_HOME:-~/.config}/agent-workflow/bridge-settings.conf
106
112
  AGY_HARD_TIMEOUT — hard wall-clock cap, duration string like 5m/30m/90s (built-in default 30m)
107
113
  AGY_REVIEW_ALLOW_ADDDIR — boolean 0/1: 1 arms the oversized --add-dir escape (re-enables the Issue-001 stall risk; default 0)
108
114
 
115
+ Honesty + posture (D4/D5):
116
+ a run whose output carries NO recognized '### Verdict' section — empty output included — exits 4
117
+ with NO receipt: a FAILED review to RE-RUN, never a fatal session error. One stderr banner line
118
+ states the ACTUAL run posture (review posture: model=…) and the receipt records the same
119
+ posture {model} (agy has no tier). An ATTESTING review with AGY_MODEL explicitly emptied
120
+ refuses pre-spend (the actual model would be unknowable; AGY_PROBE=1 is exempt), and a model
121
+ string carrying control bytes refuses pre-spend in every mode.
122
+
109
123
  Closed grammar: unknown flags are rejected; no '--' passthrough (the flag escape is --ungrounded; the env escapes are AGY_PROBE=1 and AGY_REVIEW_ALLOW_ADDDIR=1).
110
124
  Requires at run time: the agy CLI on PATH + a Google AI subscription login (--help needs neither).
111
125
  HELP
@@ -225,9 +239,15 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.1 Pro (High)"
225
239
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
226
240
  # version (drift-guarded by agy-review.test.mjs against capability.json).
227
241
  AW_RECEIPT_BACKEND="agy"
228
- AW_BRIDGE_VERSION="3.0.0"
242
+ AW_BRIDGE_VERSION="4.0.0"
229
243
  # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
230
244
  AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
245
+ # D5 control-byte screen — IMMEDIATELY after resolution, BEFORE the off-frontier advisory (or any
246
+ # other interpolation) can echo raw newline/ESC bytes into stderr/the terminal (round-2 fold).
247
+ if [[ "$AGY_MODEL" == *[$'\x01'-$'\x1f']* ]]; then
248
+ echo "error: AGY_MODEL contains control bytes — fix the setting (env or bridge-settings.conf) and re-run." >&2
249
+ exit 2
250
+ fi
231
251
  # Frontier review models. ANY model is allowed; a sub-frontier one only earns a soft, silenceable warning.
232
252
  FRONTIER_SET=("Gemini 3.1 Pro (High)" "Claude Opus 4.6 (Thinking)" "Claude Sonnet 4.6 (Thinking)")
233
253
 
@@ -444,6 +464,26 @@ receipt_json_scalar() {
444
464
  if [[ -z "${1:-}" ]]; then printf 'null'; else printf '"%s"' "$1"; fi
445
465
  }
446
466
 
467
+ # STRICT JSON string encoding for the ONE free-form receipt field (the posture model display
468
+ # string): backslash then double-quote escaped. Control bytes never reach here — the posture
469
+ # resolution refuses them pre-spend (D5), so these two escapes make the encoding total.
470
+ json_string_escape() {
471
+ local s="${1//\\/\\\\}"
472
+ s="${s//\"/\\\"}"
473
+ printf '%s' "$s"
474
+ }
475
+
476
+ # The D5 posture object this wrapper writes into EVERY receipt (agy has no tier — the label is
477
+ # the model display string). An unknowable model (explicitly emptied on an exempt probe run) is
478
+ # recorded null, never guessed.
479
+ posture_json() {
480
+ if [[ -z "${AGY_MODEL:-}" ]]; then
481
+ printf '{"model":null}'
482
+ else
483
+ printf '{"model":"%s"}' "$(json_string_escape "$AGY_MODEL")"
484
+ fi
485
+ }
486
+
447
487
  # write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|""> [probe: true|false]
448
488
  # Appends ONE receipt line (the AD-038 fixture shape) as a side effect of a SUCCESSFUL review —
449
489
  # to $AW_REVIEW_RECEIPTS when set, else <git dir>/agent-workflow-review-receipts.jsonl (inside the
@@ -467,28 +507,29 @@ write_review_receipt() {
467
507
  fi
468
508
  local line probe_field=',"probe":false'
469
509
  if [[ "$probe" == "true" ]]; then probe_field=',"probe":true'; fi
470
- line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"%s}' \
510
+ line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"%s,"posture":%s}' \
471
511
  "$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
472
512
  "$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
473
- "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field")"
513
+ "$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$probe_field" "$(posture_json)")"
474
514
  if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
475
515
  echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
476
516
  echo " the review-state gate will read the current tree as un-receipted." >&2
477
517
  fi
478
518
  }
479
519
 
480
- # Parse the mandated '### Verdict' section of a captured review: the first non-empty line after the
481
- # heading, matched against the closed verdict vocabulary (SHIP WITH NITS before SHIP substring).
482
- # No heading / no match "unknown" (recorded, never guessed).
520
+ # Parse the mandated '### Verdict' section of a captured review: the first non-empty line after
521
+ # the EXACT heading (`### Verdicts` or any suffix never matches), tested against the closed
522
+ # vocabulary as an ANCHORED PREFIX with a word boundary — the verdict line must START with the
523
+ # token and the next char must be non-word/EOL (SHIP WITH NITS before SHIP; `NOT SHIP` and
524
+ # `SHIPPING` never match). No heading / no match → "unknown" (the D4 failed-run arm owns it).
483
525
  parse_agy_verdict() { # $1 = captured-output file
484
526
  local line
485
- line="$(awk '/^### Verdict/{flag=1; next} flag && NF {print; exit}' "$1" 2>/dev/null)"
486
- case "$line" in
487
- *"SHIP WITH NITS"*) printf 'SHIP WITH NITS' ;;
488
- *REWORK*) printf 'REWORK' ;;
489
- *SHIP*) printf 'SHIP' ;;
490
- *) printf 'unknown' ;;
491
- esac
527
+ line="$(awk '/^### Verdict[[:space:]]*$/{flag=1; next} flag && NF {print; exit}' "$1" 2>/dev/null)"
528
+ if [[ "$line" =~ ^SHIP\ WITH\ NITS([^[:alnum:]_]|$) ]]; then printf 'SHIP WITH NITS'
529
+ elif [[ "$line" =~ ^REWORK([^[:alnum:]_]|$) ]]; then printf 'REWORK'
530
+ elif [[ "$line" =~ ^SHIP([^[:alnum:]_]|$) ]]; then printf 'SHIP'
531
+ else printf 'unknown'
532
+ fi
492
533
  }
493
534
 
494
535
  # Emit the full review surface to stdout: repo map, status (never-committable untracked records
@@ -828,6 +869,21 @@ else
828
869
  fi
829
870
  fi
830
871
 
872
+ # --- D5 pre-spend posture gate + banner (one line, the ACTUAL run posture) ------------------
873
+ # The control-byte screen already ran at AGY_MODEL resolution (before any interpolation). An
874
+ # ATTESTING review (fresh code mode, guards on) whose wrapper cannot know the actual model
875
+ # (AGY_MODEL explicitly emptied → the CLI's own settings default decides) refuses pre-spend;
876
+ # AGY_PROBE=1 runs are exempt (their receipts never attest and record model null).
877
+ # Scoped to the ATTESTING branch only (grounded code, guards on): plan / diff / --ungrounded
878
+ # code mint receipts that never attest, so an emptied model just records posture.model null.
879
+ if [[ -z "$AGY_MODEL" && -z "$resume_mode" && "$REVIEW_PROBE" != "true" && "$REVIEW_ARTIFACT" == "code" && -n "$FACTS_CONTENT" ]]; then
880
+ echo "error: AGY_MODEL is explicitly empty, so the ACTUAL review model is unknowable (the agy CLI's" >&2
881
+ echo " own settings default decides) — an attesting review refuses pre-spend. Fix: unset" >&2
882
+ echo " AGY_MODEL (wrapper default), or set the real model display string; AGY_PROBE=1 is exempt." >&2
883
+ exit 2
884
+ fi
885
+ echo "review posture: model=${AGY_MODEL:-<agy settings default>}" >&2
886
+
831
887
  # --- Execute via agy-run (single home of timeout + subscription + byte ceiling) ---
832
888
  # The output is teed into the private staging dir so the mandated '### Verdict' section can be
833
889
  # parsed into the review receipt — the user-facing stream is unchanged.
@@ -845,8 +901,17 @@ fi
845
901
  set -e
846
902
 
847
903
  # --- Review receipt (AD-038): only a SUCCESSFUL review attests --------------------
904
+ # D4 (wrapper honesty): a run that produced NO recognized '### Verdict' section — empty output
905
+ # included — is a FAILED review: non-zero exit, NO receipt. This is a failed review to RE-RUN,
906
+ # never a fatal session error (documented in --help).
848
907
  if [[ $rc -eq 0 ]]; then
849
908
  verdict="$(parse_agy_verdict "$review_out_file")"
909
+ if [[ "$verdict" == "unknown" ]]; then
910
+ echo "error: the review output carries no recognized '### Verdict' section (closed vocabulary:" >&2
911
+ echo " SHIP / SHIP WITH NITS / REWORK) — a FAILED review; NO receipt was written. Re-run" >&2
912
+ echo " the review; if it recurs, inspect the captured output for what the model produced." >&2
913
+ exit 4
914
+ fi
850
915
  if [[ -n "$resume_mode" ]]; then
851
916
  # A continuation never re-embeds the current artifact (agy holds the ORIGINAL round server-side;
852
917
  # --facts is rejected above), so it cannot attest the folded tree: fresh:false, artifact /
@@ -1,13 +1,13 @@
1
- import { describe, it } from 'node:test';
1
+ import { describe, it, after } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import {
4
4
  mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, readFileSync,
5
- existsSync, readdirSync, symlinkSync,
5
+ existsSync, readdirSync, symlinkSync, cpSync,
6
6
  } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { join, dirname, resolve } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { spawnSync } from 'node:child_process';
10
+ import { spawnSync, execFile } from 'node:child_process';
11
11
 
12
12
  const HERE = dirname(fileURLToPath(import.meta.url));
13
13
  const WRAPPER = join(HERE, 'agy-review.sh');
@@ -38,7 +38,9 @@ const FAKE_AGY = [
38
38
  ' fi; prev="$a"',
39
39
  'done',
40
40
  'if [[ -n "${AGY_FAKE_SLEEP:-}" ]]; then sleep "$AGY_FAKE_SLEEP"; fi',
41
- 'printf "%s\\n" "${AGY_FAKE_OUTPUT:-FAKE_AGY_REVIEW_OUTPUT}"',
41
+ // Unset AGY_FAKE_OUTPUT → a verdict-carrying default (D4: a verdict-less run is a FAILURE, so
42
+ // the success-path tests need one); an EXPLICIT empty value exercises the empty-output failure.
43
+ 'if [[ -z "${AGY_FAKE_OUTPUT+x}" ]]; then printf "FAKE_AGY_REVIEW_OUTPUT\\n### Verdict\\nSHIP\\n"; else printf "%s\\n" "$AGY_FAKE_OUTPUT"; fi',
42
44
  'exit "${AGY_FAKE_EXIT:-0}"',
43
45
  '',
44
46
  ].join('\n');
@@ -63,15 +65,23 @@ const makePathWithout = (root, exclude = []) => {
63
65
  return dir;
64
66
  };
65
67
 
66
- // `clean: true` leaves a pristine committed tree (for the no-diff preflight); the default leaves one
67
- // untracked file so `code` mode has a diff to review.
68
- const makeSandbox = ({ clean = false } = {}) => {
69
- const home = mkdtempSync(join(tmpdir(), 'agy-review-test-'));
68
+ // The PATH farms and the sandbox base are READ-ONLY per invocation, so both are built ONCE and
69
+ // shared: a per-run farm rebuild (thousands of symlinks) plus a per-test `git init`+commit were
70
+ // the suite's dominant wall cost, not the wrapper under test.
71
+ const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'agy-review-shared-'));
72
+ after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
73
+ const farms = new Map();
74
+ const farmFor = (exclude) => {
75
+ const key = exclude.join('|');
76
+ if (!farms.has(key)) farms.set(key, makePathWithout(SHARED_ROOT, exclude));
77
+ return farms.get(key);
78
+ };
79
+
80
+ const TEMPLATE_HOME = (() => {
81
+ const home = join(SHARED_ROOT, 'template-home');
70
82
  const bin = join(home, '.local', 'bin');
71
83
  mkdirSync(bin, { recursive: true });
72
- const stub = join(bin, 'agy');
73
- writeFileSync(stub, FAKE_AGY, { mode: 0o755 });
74
- chmodSync(stub, 0o755);
84
+ writeFileSync(join(bin, 'agy'), FAKE_AGY, { mode: 0o755 });
75
85
  const repo = join(home, 'repo');
76
86
  mkdirSync(repo);
77
87
  const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
@@ -81,13 +91,25 @@ const makeSandbox = ({ clean = false } = {}) => {
81
91
  writeFileSync(join(repo, 'base.txt'), 'committed base\n');
82
92
  g('add', '-A');
83
93
  g('commit', '-qm', 'base');
94
+ return home;
95
+ })();
96
+
97
+ // `clean: true` leaves a pristine committed tree (for the no-diff preflight); the default leaves one
98
+ // untracked file so `code` mode has a diff to review.
99
+ const makeSandbox = ({ clean = false } = {}) => {
100
+ const home = mkdtempSync(join(tmpdir(), 'agy-review-test-'));
101
+ cpSync(TEMPLATE_HOME, home, { recursive: true });
102
+ const bin = join(home, '.local', 'bin');
103
+ chmodSync(join(bin, 'agy'), 0o755);
104
+ const repo = join(home, 'repo');
105
+ const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
84
106
  if (!clean) writeFileSync(join(repo, 'pending.txt'), 'PENDING_UNTRACKED_BODY\n');
85
107
  return { home, bin, repo, g };
86
108
  };
87
109
 
88
110
  const run = (sb, { args, env = {}, cwd } = {}) => {
89
111
  const { home, bin, repo } = sb;
90
- const farm = makePathWithout(home, ['agy', 'agy-run']);
112
+ const farm = farmFor(['agy', 'agy-run']);
91
113
  const cap = {
92
114
  argv: join(home, 'cap-argv'), env: join(home, 'cap-env'), prompt: join(home, 'cap-prompt'),
93
115
  sentinel: join(home, 'cap-sentinel'), adddir: join(home, 'cap-adddir'),
@@ -120,6 +142,44 @@ const run = (sb, { args, env = {}, cwd } = {}) => {
120
142
  };
121
143
  };
122
144
 
145
+ // Async twin of run() for the two sleep-bound timeout tests: spawnSync blocks the event loop
146
+ // for the whole deliberate wait, so a concurrent describe could not overlap them. Same spawn
147
+ // contract and captures.
148
+ const runAsync = (sb, { args, env = {}, cwd } = {}) =>
149
+ new Promise((done) => {
150
+ const { home, bin, repo } = sb;
151
+ const cap = {
152
+ argv: join(home, 'cap-argv'), env: join(home, 'cap-env'), prompt: join(home, 'cap-prompt'),
153
+ sentinel: join(home, 'cap-sentinel'), adddir: join(home, 'cap-adddir'),
154
+ adddirMode: join(home, 'cap-adddir-mode'), artifactMode: join(home, 'cap-artifact-mode'),
155
+ artifactCopy: join(home, 'cap-artifact-copy'),
156
+ };
157
+ const child = execFile('bash', [WRAPPER, ...args], {
158
+ cwd: cwd || repo,
159
+ encoding: 'utf8',
160
+ timeout: 30000,
161
+ env: {
162
+ HOME: home,
163
+ PATH: `${bin}:${farmFor(['agy', 'agy-run'])}`,
164
+ TMPDIR: process.env.TMPDIR ?? '/tmp',
165
+ AGY_FAKE_ARGV: cap.argv, AGY_FAKE_ENV: cap.env, AGY_FAKE_PROMPT: cap.prompt,
166
+ AGY_FAKE_SENTINEL: cap.sentinel, AGY_FAKE_ADDDIR: cap.adddir, AGY_FAKE_ADDDIR_MODE: cap.adddirMode,
167
+ AGY_FAKE_ARTIFACT_MODE: cap.artifactMode, AGY_FAKE_ARTIFACT_COPY: cap.artifactCopy,
168
+ ...env,
169
+ },
170
+ }, (error, stdout, stderr) => {
171
+ const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
172
+ done({
173
+ status: error ? (error.code ?? 1) : 0, stdout, stderr,
174
+ invoked: existsSync(cap.sentinel),
175
+ argv: readIf(cap.argv), capEnv: readIf(cap.env), prompt: readIf(cap.prompt),
176
+ adddir: readIf(cap.adddir).trim(), adddirMode: readIf(cap.adddirMode).trim(),
177
+ artifactMode: readIf(cap.artifactMode).trim(), artifactCopy: readIf(cap.artifactCopy),
178
+ });
179
+ });
180
+ child.stdin.end();
181
+ });
182
+
123
183
  describe('agy-review.sh — model policy advisory (1)', () => {
124
184
  it('warns for a non-frontier model but still runs', () => {
125
185
  const sb = makeSandbox();
@@ -422,11 +482,11 @@ describe('agy-review.sh — resume / round-2 delta (7)', () => {
422
482
  });
423
483
  });
424
484
 
425
- describe('agy-review.sh — delegated guards inherited via agy-run (9, 10)', () => {
426
- it('hard timeout: a sleeping stub is killed at AGY_HARD_TIMEOUT', () => {
485
+ describe('agy-review.sh — delegated guards inherited via agy-run (9, 10)', { concurrency: true }, () => {
486
+ it('hard timeout: a sleeping stub is killed at AGY_HARD_TIMEOUT', async () => {
427
487
  const sb = makeSandbox();
428
488
  const started = Date.now();
429
- const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_SLEEP: '30', AGY_HARD_TIMEOUT: '2s', AGY_TIMEOUT: '2s' } });
489
+ const r = await runAsync(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_SLEEP: '30', AGY_HARD_TIMEOUT: '2s', AGY_TIMEOUT: '2s' } });
430
490
  const elapsed = Date.now() - started;
431
491
  rmSync(sb.home, { recursive: true, force: true });
432
492
  assert.ok(elapsed < 20000, `must return well under the kill-after window, took ${elapsed}ms`);
@@ -550,7 +610,7 @@ const runHelp = (arg) => {
550
610
  const root = mkdtempSync(join(tmpdir(), 'agy-review-help-'));
551
611
  const nongit = join(root, 'nongit');
552
612
  mkdirSync(nongit, { recursive: true });
553
- const path = makePathWithout(root, ['codex', 'agy', 'git']);
613
+ const path = farmFor(['codex', 'agy', 'git']);
554
614
  const r = spawnSync('bash', [WRAPPER, arg], {
555
615
  cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
556
616
  });
@@ -870,7 +930,7 @@ describe('agy-review.sh — declared contract is really accepted (forward guard)
870
930
  // The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (backend/verdict here
871
931
  // carry this bridge's vocabulary; dynamic values are asserted by shape):
872
932
  const RECEIPT_FIXTURE = JSON.parse(
873
- '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z","probe":false}',
933
+ '{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.3.0","timestamp":"2026-07-03T12:00:00Z","probe":false,"posture":{"model":"<display>"}}',
874
934
  );
875
935
  const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
876
936
  const readReceipts = (repo) => {
@@ -962,11 +1022,10 @@ describe('agy-review.sh — review receipts (AD-038)', () => {
962
1022
  assert.equal(receipts.length, 0, 'no run — no receipt');
963
1023
  });
964
1024
 
965
- it('parses REWORK and plain SHIP; records "unknown" when the mandated section is absent', () => {
1025
+ it('parses REWORK and plain SHIP verbatim (an absent section is a FAILED run — the D4 describe owns that arm)', () => {
966
1026
  for (const [output, want] of [
967
1027
  ['### Verdict\nREWORK — the contract is violated.', 'REWORK'],
968
1028
  ['### Verdict\nSHIP — clean.', 'SHIP'],
969
- ['free-form text with no verdict heading', 'unknown'],
970
1029
  ]) {
971
1030
  const sb = makeSandbox();
972
1031
  const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_OUTPUT: output } });
@@ -1077,7 +1136,7 @@ const writeSettings = (sb, text) => {
1077
1136
  };
1078
1137
  const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
1079
1138
 
1080
- describe('agy-review.sh — bridge settings file (bridges 2.3.0)', () => {
1139
+ describe('agy-review.sh — bridge settings file (bridges 2.3.0)', { concurrency: true }, () => {
1081
1140
  it('a file-set AGY_REVIEW_ALLOW_ADDDIR=1 arms the oversized --add-dir escape', () => {
1082
1141
  const sb = makeSandbox();
1083
1142
  writeFileSync(join(sb.repo, 'unique.txt'), `OVERSIZE_UNIQUE_MARKER\n${'x'.repeat(8000)}\n`);
@@ -1122,10 +1181,10 @@ describe('agy-review.sh — bridge settings file (bridges 2.3.0)', () => {
1122
1181
  assert.equal(r.invoked, false);
1123
1182
  });
1124
1183
 
1125
- it('a file-set AGY_HARD_TIMEOUT flows through the agy-run delegation (killed at the file cap)', () => {
1184
+ it('a file-set AGY_HARD_TIMEOUT flows through the agy-run delegation (killed at the file cap)', async () => {
1126
1185
  const sb = makeSandbox();
1127
1186
  writeSettings(sb, 'AGY_HARD_TIMEOUT=2s\n');
1128
- const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_SLEEP: '5' } });
1187
+ const r = await runAsync(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_SLEEP: '5' } });
1129
1188
  rmSync(sb.home, { recursive: true, force: true });
1130
1189
  assert.notEqual(r.status, 0, 'the file cap must apply end-to-end (reader → agy-run → timeout)');
1131
1190
  assert.match(r.stderr, /exceeded the hard cap AGY_HARD_TIMEOUT=2s/);
@@ -1264,3 +1323,93 @@ describe('agy-review.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
1264
1323
  }
1265
1324
  });
1266
1325
  });
1326
+
1327
+ // ── strip-the-kit Phase 4: wrapper honesty (D4) + dispatch-posture labeling (D5) ────────────────
1328
+ describe('agy-review.sh — wrapper honesty: a verdict-less run is a FAILED review (D4)', () => {
1329
+ it('a VERDICT-LESS review output: non-zero exit, NO receipt, the stated re-run recovery', () => {
1330
+ const sb = makeSandbox();
1331
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: 'prose without the mandated section' } });
1332
+ const receipts = readReceipts(sb.repo);
1333
+ rmSync(sb.home, { recursive: true, force: true });
1334
+ assert.notEqual(r.status, 0, 'a verdict-less review never exits 0');
1335
+ assert.equal(receipts.length, 0, 'NO receipt is minted for a failed review');
1336
+ assert.match(r.stderr, /### Verdict/, 'the missing section is named');
1337
+ assert.match(r.stderr, /re-run/i, 'documented as a failed review — re-run, never fatal');
1338
+ });
1339
+
1340
+ it('EMPTY review output is the same failed run (non-zero, no receipt)', () => {
1341
+ const sb = makeSandbox();
1342
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: '' } });
1343
+ const receipts = readReceipts(sb.repo);
1344
+ rmSync(sb.home, { recursive: true, force: true });
1345
+ assert.notEqual(r.status, 0);
1346
+ assert.equal(receipts.length, 0);
1347
+ });
1348
+
1349
+ it('the closed vocabulary still parses (SHIP WITH NITS before SHIP; REWORK) and a recognized run exits 0', () => {
1350
+ for (const [out, want] of [[VERDICT_OUTPUT, 'SHIP WITH NITS'], ['### Verdict\nREWORK — reasons.\n', 'REWORK']]) {
1351
+ const sb = makeSandbox();
1352
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: out } });
1353
+ const receipts = readReceipts(sb.repo);
1354
+ rmSync(sb.home, { recursive: true, force: true });
1355
+ assert.equal(r.status, 0, r.stderr);
1356
+ assert.equal(receipts[0].verdict, want);
1357
+ }
1358
+ });
1359
+ });
1360
+
1361
+ describe('agy-review.sh — dispatch-posture labeling (D5)', () => {
1362
+ it('ONE banner line carries the ACTUAL model and the receipt carries the SAME posture (agy has no tier)', () => {
1363
+ const sb = makeSandbox();
1364
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'] });
1365
+ const receipts = readReceipts(sb.repo);
1366
+ rmSync(sb.home, { recursive: true, force: true });
1367
+ assert.equal(r.status, 0, r.stderr);
1368
+ assert.match(r.stderr, /review posture: model=Gemini 3\.1 Pro \(High\)/, 'the banner states the actual run posture');
1369
+ assert.deepEqual(receipts[0].posture, { model: 'Gemini 3.1 Pro (High)' }, 'banner ↔ receipt parity');
1370
+ assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
1371
+ });
1372
+
1373
+ it('an ATTESTING review with AGY_MODEL explicitly emptied REFUSES pre-spend naming the fix', () => {
1374
+ const sb = makeSandbox();
1375
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: '' } });
1376
+ const receipts = readReceipts(sb.repo);
1377
+ rmSync(sb.home, { recursive: true, force: true });
1378
+ assert.notEqual(r.status, 0);
1379
+ assert.equal(r.invoked, false, 'refused BEFORE any spend');
1380
+ assert.equal(receipts.length, 0);
1381
+ assert.match(r.stderr, /AGY_MODEL/, 'the fix is named');
1382
+ });
1383
+
1384
+ it('AGY_PROBE=1 with AGY_MODEL emptied still runs (probe exempt; posture model null on the probe receipt)', () => {
1385
+ const sb = makeSandbox();
1386
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: '', AGY_PROBE: '1' } });
1387
+ const receipts = readReceipts(sb.repo);
1388
+ rmSync(sb.home, { recursive: true, force: true });
1389
+ assert.equal(r.status, 0, r.stderr);
1390
+ assert.equal(receipts[0].probe, true);
1391
+ assert.deepEqual(receipts[0].posture, { model: null }, 'an unknowable model is recorded null, never guessed');
1392
+ });
1393
+
1394
+ it('a HOSTILE model string (quotes + backslash) rides the receipt strictly JSON-encoded', () => {
1395
+ const hostile = 'we"ird \\ mo"del';
1396
+ const sb = makeSandbox();
1397
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: hostile } });
1398
+ const receipts = readReceipts(sb.repo); // JSON.parse throwing here IS the encoding failure
1399
+ rmSync(sb.home, { recursive: true, force: true });
1400
+ assert.equal(r.status, 0, r.stderr);
1401
+ assert.equal(receipts[0].posture.model, hostile, 'the exact bytes round-trip through strict encoding');
1402
+ assert.match(r.stderr, /review posture: /, 'the banner still renders');
1403
+ });
1404
+
1405
+ it('a model string carrying CONTROL BYTES refuses pre-spend (never a broken banner or receipt)', () => {
1406
+ const sb = makeSandbox();
1407
+ const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_MODEL: `bad${String.fromCharCode(1)}model` } });
1408
+ const receipts = readReceipts(sb.repo);
1409
+ rmSync(sb.home, { recursive: true, force: true });
1410
+ assert.notEqual(r.status, 0);
1411
+ assert.equal(r.invoked, false);
1412
+ assert.equal(receipts.length, 0);
1413
+ assert.match(r.stderr, /control/i);
1414
+ });
1415
+ });