@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.
- package/CHANGELOG.md +94 -1
- package/README.md +7 -5
- package/SKILL.md +16 -8
- package/bin/install.mjs +11 -2
- package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/migrations/3.0.0-hardened-core-loop.md +44 -0
- package/migrations/README.md +1 -1
- package/package.json +2 -2
- package/references/hooks/gate-approve.mjs +1 -1
- package/references/modes/bootstrap.md +3 -3
- package/references/modes/commit-guard.md +19 -0
- package/references/modes/core-evidence.md +15 -0
- package/references/modes/coverage-check.md +12 -0
- package/references/modes/doc-parity.md +7 -6
- package/references/modes/gates.md +7 -6
- package/references/modes/grounding.md +1 -2
- package/references/modes/hook.md +1 -1
- package/references/modes/recommendations.md +1 -0
- package/references/modes/review-state.md +3 -3
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +2 -2
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +3 -3
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
- package/references/scripts/install-git-hooks.mjs +90 -18
- package/references/scripts/install-git-hooks.test.mjs +102 -0
- package/references/scripts/migrate-gates-branches.test.mjs +157 -0
- package/references/scripts/migrate-gates.mjs +395 -0
- package/references/scripts/migrate-gates.test.mjs +284 -0
- package/references/templates/agent_rules.md +2 -2
- package/tools/ack-write.mjs +1 -1
- package/tools/atomic-write.mjs +2 -2
- package/tools/autonomy-config.mjs +1 -1
- package/tools/autonomy-doctor.mjs +2 -2
- package/tools/autonomy-write.mjs +1 -1
- package/tools/bridge-settings-read.mjs +1 -1
- package/tools/bridge-settings.mjs +1 -1
- package/tools/changed-surface.mjs +16 -77
- package/tools/cheap-agents.mjs +2 -2
- package/tools/commands.mjs +26 -10
- package/tools/commit-guard.mjs +167 -0
- package/tools/core-evidence.mjs +914 -0
- package/tools/coverage-check.mjs +260 -0
- package/tools/delegation.mjs +1 -1
- package/tools/detect-backends.mjs +3 -3
- package/tools/doc-parity.mjs +11 -27
- package/tools/engine-source.mjs +1 -1
- package/tools/family-members.mjs +1 -1
- package/tools/family-registry.mjs +1 -1
- package/tools/fs-safe.mjs +1 -1
- package/tools/gate-hook.mjs +1 -1
- package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
- package/tools/grounding.mjs +19 -96
- package/tools/hide-footprint.mjs +1 -1
- package/tools/inject-methodology.mjs +72 -41
- package/tools/known-footprint.mjs +1 -1
- package/tools/labels.mjs +1 -1
- package/tools/lcov.mjs +6 -10
- package/tools/lens-region.mjs +1 -1
- package/tools/manifest/validate.mjs +19 -0
- package/tools/migrate-adr-store.mjs +5 -5
- package/tools/orchestration-config.mjs +1 -1
- package/tools/orchestration-write.mjs +1 -1
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +62 -11
- package/tools/recommendations.mjs +97 -14
- package/tools/release-scan.mjs +10 -3
- package/tools/renderers.mjs +1 -1
- package/tools/review-state.mjs +291 -343
- package/tools/run-gates.mjs +216 -92
- package/tools/sandbox-masks.mjs +14 -14
- package/tools/semver-lite.mjs +1 -1
- package/tools/set-autonomy.mjs +1 -1
- package/tools/set-recipe.mjs +2 -2
- package/tools/setup-backends.mjs +1 -1
- package/tools/surface.mjs +1 -1
- package/tools/uninstall.mjs +1 -1
- package/tools/velocity-profile.mjs +9 -9
- package/tools/view-model.mjs +1 -1
- package/tools/worktrees.mjs +2292 -0
- package/references/modes/fold-completeness.md +0 -30
- package/references/modes/review-ledger.md +0 -34
- package/references/templates/verification-profile.json +0 -10
- package/tools/fold-completeness-run.mjs +0 -1120
- package/tools/fold-completeness.mjs +0 -672
- package/tools/review-ledger-core.mjs +0 -428
- package/tools/review-ledger-write.mjs +0 -647
- package/tools/review-ledger.mjs +0 -630
- package/tools/sarif.mjs +0 -52
- 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);
|
|
102
|
-
|
|
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="
|
|
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
|
|
481
|
-
#
|
|
482
|
-
#
|
|
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
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
+
});
|