@sabaiway/agent-workflow-kit 1.28.0 → 1.30.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 +93 -0
- package/README.md +4 -1
- package/SKILL.md +67 -6
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +145 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +169 -1
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +120 -3
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +132 -0
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/contracts.md +1 -0
- package/references/hooks/gate-approve.mjs +300 -0
- package/references/templates/agent_rules.md +3 -2
- package/references/templates/handover.md +1 -0
- package/tools/commands.mjs +24 -3
- package/tools/detect-backends.mjs +2 -0
- package/tools/family-registry.mjs +25 -0
- package/tools/gate-hook.mjs +387 -0
- package/tools/grounding.mjs +263 -0
- package/tools/known-footprint.mjs +1 -0
- package/tools/presentation.mjs +1 -0
- package/tools/procedures.mjs +50 -5
- package/tools/recipes.mjs +78 -12
- package/tools/renderers.mjs +6 -0
- package/tools/review-state.mjs +395 -0
- package/tools/set-recipe.mjs +15 -5
- package/tools/uninstall.mjs +175 -26
- package/tools/velocity-profile.mjs +21 -3
- package/tools/view-model.mjs +18 -1
|
@@ -38,7 +38,7 @@ 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
|
+
'printf "%s\\n" "${AGY_FAKE_OUTPUT:-FAKE_AGY_REVIEW_OUTPUT}"',
|
|
42
42
|
'exit "${AGY_FAKE_EXIT:-0}"',
|
|
43
43
|
'',
|
|
44
44
|
].join('\n');
|
|
@@ -553,6 +553,13 @@ describe('agy-review.sh — --help contract (manifest-pinned)', () => {
|
|
|
553
553
|
assert.ok(REVIEW_CONTRACT.continue.length > 0, 'manifest continue must be non-empty');
|
|
554
554
|
setEq(got, REVIEW_CONTRACT.continue.map(norm), 'help continue ⟷ manifest continue');
|
|
555
555
|
});
|
|
556
|
+
|
|
557
|
+
it('Receipt renders the manifest receipt contract verbatim (AD-038 three-way lockstep)', () => {
|
|
558
|
+
const help = runHelp('--help').stdout;
|
|
559
|
+
assert.equal(norm(helpSection(help, 'Receipt:').join(' ')), norm(REVIEW_CONTRACT.receipt));
|
|
560
|
+
assert.match(REVIEW_CONTRACT.receipt, /sha256 over the canonical uncommitted-state payload/, 'the fingerprint definition lives in the manifest contract');
|
|
561
|
+
assert.match(REVIEW_CONTRACT.receipt, /fresh:false/, 'the continuation informational-only clause is contractual');
|
|
562
|
+
});
|
|
556
563
|
});
|
|
557
564
|
|
|
558
565
|
describe('agy-review.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
|
|
@@ -625,3 +632,164 @@ describe('agy-review.sh — declared contract is really accepted (forward guard)
|
|
|
625
632
|
assert.doesNotMatch(r.stdout, /Usage:/);
|
|
626
633
|
});
|
|
627
634
|
});
|
|
635
|
+
|
|
636
|
+
// ── review receipts (AD-038) ─────────────────────────────────────────────────────
|
|
637
|
+
// The normative fixture (docs: the AD-038 plan Decisions — copied verbatim; backend/verdict here
|
|
638
|
+
// carry this bridge's vocabulary; dynamic values are asserted by shape):
|
|
639
|
+
const RECEIPT_FIXTURE = JSON.parse(
|
|
640
|
+
'{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.2.0","timestamp":"2026-07-03T12:00:00Z"}',
|
|
641
|
+
);
|
|
642
|
+
const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
|
|
643
|
+
const readReceipts = (repo) => {
|
|
644
|
+
const p = join(repo, RECEIPTS_REL);
|
|
645
|
+
if (!existsSync(p)) return [];
|
|
646
|
+
return readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
647
|
+
};
|
|
648
|
+
const sha256HexOf = async (buf) => {
|
|
649
|
+
const { createHash } = await import('node:crypto');
|
|
650
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
651
|
+
};
|
|
652
|
+
const VERDICT_OUTPUT = '### Verdict\nSHIP WITH NITS — solid, two nits.\n### Blocking\nnone\n### Non-blocking\n1. nit\n### Questions\nnone';
|
|
653
|
+
|
|
654
|
+
describe('agy-review.sh — review receipts (AD-038)', () => {
|
|
655
|
+
it('a fresh grounded code review appends ONE fixture-shaped receipt (verdict verbatim, factsHash real)', async () => {
|
|
656
|
+
const sb = makeSandbox();
|
|
657
|
+
const r = run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
658
|
+
const receipts = readReceipts(sb.repo);
|
|
659
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
660
|
+
assert.equal(r.status, 0, r.stderr);
|
|
661
|
+
assert.equal(receipts.length, 1, 'exactly one receipt line');
|
|
662
|
+
const receipt = receipts[0];
|
|
663
|
+
assert.deepEqual(Object.keys(receipt), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
|
|
664
|
+
assert.equal(receipt.schema, 1);
|
|
665
|
+
assert.equal(receipt.artifact, 'code');
|
|
666
|
+
assert.equal(receipt.fresh, true);
|
|
667
|
+
assert.match(receipt.fingerprint, /^[0-9a-f]{64}$/, 'a real sha256 hex fingerprint');
|
|
668
|
+
assert.equal(receipt.backend, 'agy');
|
|
669
|
+
assert.equal(receipt.verdict, 'SHIP WITH NITS', 'the mandated ### Verdict section is recorded verbatim');
|
|
670
|
+
assert.equal(receipt.grounded, true, '--facts was supplied');
|
|
671
|
+
assert.equal(receipt.factsHash, await sha256HexOf('a tiny fact'), 'sha256 of the facts payload — an empty/changed facts file is visible');
|
|
672
|
+
assert.equal(receipt.wrapperVersion, MANIFEST.version, 'receipt version ⟷ capability.json version');
|
|
673
|
+
assert.match(receipt.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('an ungrounded fresh run records grounded:false + factsHash null (the vacuous-grounding hole stays visible)', () => {
|
|
677
|
+
const sb = makeSandbox();
|
|
678
|
+
const r = run(sb, { args: ['code'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
679
|
+
const receipts = readReceipts(sb.repo);
|
|
680
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
681
|
+
assert.equal(r.status, 0, r.stderr);
|
|
682
|
+
assert.equal(receipts[0].grounded, false);
|
|
683
|
+
assert.equal(receipts[0].factsHash, null);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('an EMPTY --facts file records grounded:false (fail-closed — vacuous grounding never satisfies the gate) and still warns', () => {
|
|
687
|
+
const sb = makeSandbox();
|
|
688
|
+
writeFileSync(join(sb.home, 'empty-facts.md'), '');
|
|
689
|
+
const r = run(sb, { args: ['code', '--facts', `@${join(sb.home, 'empty-facts.md')}`], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
690
|
+
const receipts = readReceipts(sb.repo);
|
|
691
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
692
|
+
assert.equal(r.status, 0, r.stderr);
|
|
693
|
+
assert.equal(receipts[0].grounded, false, 'an empty payload is recorded as ungrounded, never as grounded-by-flag');
|
|
694
|
+
assert.equal(receipts[0].factsHash, null);
|
|
695
|
+
assert.match(r.stderr, /no --facts supplied|ungrounded review GUESSES/, 'the ungrounded warning fires for an empty payload');
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('parses REWORK and plain SHIP; records "unknown" when the mandated section is absent', () => {
|
|
699
|
+
for (const [output, want] of [
|
|
700
|
+
['### Verdict\nREWORK — the contract is violated.', 'REWORK'],
|
|
701
|
+
['### Verdict\nSHIP — clean.', 'SHIP'],
|
|
702
|
+
['free-form text with no verdict heading', 'unknown'],
|
|
703
|
+
]) {
|
|
704
|
+
const sb = makeSandbox();
|
|
705
|
+
const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_OUTPUT: output } });
|
|
706
|
+
const receipts = readReceipts(sb.repo);
|
|
707
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
708
|
+
assert.equal(r.status, 0, r.stderr);
|
|
709
|
+
assert.equal(receipts[0].verdict, want, `verdict for: ${output.slice(0, 30)}`);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('a continuation receipt is fresh:false with null identity fields, and the wrapper prints the fresh-run notice', () => {
|
|
714
|
+
const sb = makeSandbox();
|
|
715
|
+
const r = run(sb, { args: ['--continue', '--decided', 'already folded'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
716
|
+
const receipts = readReceipts(sb.repo);
|
|
717
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
718
|
+
assert.equal(r.status, 0, r.stderr);
|
|
719
|
+
assert.equal(receipts.length, 1);
|
|
720
|
+
const receipt = receipts[0];
|
|
721
|
+
assert.deepEqual(Object.keys(receipt), Object.keys(RECEIPT_FIXTURE), 'same fixture shape');
|
|
722
|
+
assert.equal(receipt.fresh, false, 'a continuation cannot attest the folded tree');
|
|
723
|
+
assert.equal(receipt.artifact, null);
|
|
724
|
+
assert.equal(receipt.fingerprint, null);
|
|
725
|
+
assert.equal(receipt.grounded, false);
|
|
726
|
+
assert.equal(receipt.factsHash, null);
|
|
727
|
+
assert.equal(receipt.verdict, 'SHIP WITH NITS', 'the round-2 verdict is still recorded (informational)');
|
|
728
|
+
assert.match(r.stderr, /fresh grounded run/, 'the one-line notice names the required fresh run');
|
|
729
|
+
assert.match(r.stderr, /review-state gate/);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('plan mode: artifact "plan", fingerprint = the artifact-file sha256', async () => {
|
|
733
|
+
const sb = makeSandbox();
|
|
734
|
+
writeFileSync(join(sb.repo, 'p.md'), '# plan body\n');
|
|
735
|
+
const r = run(sb, { args: ['plan', 'p.md', '--facts', 'f'], env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
736
|
+
const receipts = readReceipts(sb.repo);
|
|
737
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
738
|
+
assert.equal(r.status, 0, r.stderr);
|
|
739
|
+
assert.equal(receipts[0].artifact, 'plan');
|
|
740
|
+
assert.equal(receipts[0].fingerprint, await sha256HexOf('# plan body\n'), 'plan fingerprint = file sha256');
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('plan/diff outside a git work tree: warn + skip the receipt (exit 0) unless AW_REVIEW_RECEIPTS is set', () => {
|
|
744
|
+
const sb = makeSandbox();
|
|
745
|
+
const outside = join(sb.home, 'no-repo');
|
|
746
|
+
mkdirSync(outside, { recursive: true });
|
|
747
|
+
writeFileSync(join(outside, 'p.md'), '# plan outside git\n');
|
|
748
|
+
|
|
749
|
+
const skipped = run(sb, { args: ['plan', 'p.md', '--facts', 'f'], cwd: outside, env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT } });
|
|
750
|
+
assert.equal(skipped.status, 0, skipped.stderr);
|
|
751
|
+
assert.match(skipped.stderr, /not inside a git work tree and AW_REVIEW_RECEIPTS is unset — skipping/);
|
|
752
|
+
|
|
753
|
+
const override = join(sb.home, 'receipts-override.jsonl');
|
|
754
|
+
const written = run(sb, {
|
|
755
|
+
args: ['plan', 'p.md', '--facts', 'f'],
|
|
756
|
+
cwd: outside,
|
|
757
|
+
env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_RECEIPTS: override },
|
|
758
|
+
});
|
|
759
|
+
const body = existsSync(override) ? readFileSync(override, 'utf8') : '';
|
|
760
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
761
|
+
assert.equal(written.status, 0, written.stderr);
|
|
762
|
+
assert.match(body, /"backend":"agy"/, 'the override path receives the receipt outside a git tree');
|
|
763
|
+
assert.match(body, /"artifact":"plan"/);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('a receipt write failure warns loudly but never fails the review (fail-safe direction)', () => {
|
|
767
|
+
const sb = makeSandbox();
|
|
768
|
+
const r = run(sb, {
|
|
769
|
+
args: ['code', '--facts', 'f'],
|
|
770
|
+
env: { AGY_FAKE_OUTPUT: VERDICT_OUTPUT, AW_REVIEW_RECEIPTS: join(sb.home, 'no-such-dir', 'r.jsonl') },
|
|
771
|
+
});
|
|
772
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
773
|
+
assert.equal(r.status, 0, 'the review run itself succeeds');
|
|
774
|
+
assert.match(r.stderr, /could not append the review receipt/);
|
|
775
|
+
assert.match(r.stdout, /SHIP WITH NITS/, 'the findings still reach stdout');
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('a failed agy run writes NO receipt (only a successful review attests)', () => {
|
|
779
|
+
const sb = makeSandbox();
|
|
780
|
+
const r = run(sb, { args: ['code', '--facts', 'f'], env: { AGY_FAKE_EXIT: '7' } });
|
|
781
|
+
const receipts = readReceipts(sb.repo);
|
|
782
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
783
|
+
assert.notEqual(r.status, 0);
|
|
784
|
+
assert.equal(receipts.length, 0);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('the clean-tree preflight exits before any receipt is written', () => {
|
|
788
|
+
const sb = makeSandbox({ clean: true });
|
|
789
|
+
const r = run(sb, { args: ['code', '--facts', 'f'] });
|
|
790
|
+
const receipts = readReceipts(sb.repo);
|
|
791
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
792
|
+
assert.equal(r.status, 0);
|
|
793
|
+
assert.equal(receipts.length, 0, 'no review ran — no receipt');
|
|
794
|
+
});
|
|
795
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "antigravity-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.2.0",
|
|
7
7
|
"provides": ["review", "probe"],
|
|
8
8
|
"roles": {
|
|
9
9
|
"review": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"continue": [
|
|
28
28
|
"agy-review --continue [--decided @f] [--focus \"…\"]",
|
|
29
29
|
"agy-review --conversation <id> [--decided @f] [--focus \"…\"]"
|
|
30
|
-
]
|
|
30
|
+
],
|
|
31
|
+
"receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); a write failure warns, never fails the review"
|
|
31
32
|
}
|
|
32
33
|
},
|
|
33
34
|
"probe": { "cmd": "agy-run", "source": "bin/agy.sh", "output": "advisory" }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: codex-cli-bridge
|
|
3
3
|
description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
|
|
4
4
|
metadata:
|
|
5
|
-
version: '2.
|
|
5
|
+
version: '2.2.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# codex-cli-bridge
|
|
@@ -52,7 +52,16 @@ Grounding:
|
|
|
52
52
|
Round-2 / resume:
|
|
53
53
|
(none — one-shot; a follow-up review is a fresh run)
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Receipt:
|
|
56
|
+
side effect — a successful review appends one JSON receipt line to
|
|
57
|
+
<git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint =
|
|
58
|
+
sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff +
|
|
59
|
+
untracked-not-ignored contents — the review-payload domain) in code mode, the artifact-file
|
|
60
|
+
sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the
|
|
61
|
+
verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge,
|
|
62
|
+
factsHash null); a write failure warns, never fails the review
|
|
63
|
+
|
|
64
|
+
Environment: CODEX_REVIEW_SCHEMA=1 (structured JSON findings), CODEX_HARD_TIMEOUT (seconds, default 1800), CODEX_PROBE=1 (throwaway probe only), AW_REVIEW_RECEIPTS (receipt file override).
|
|
56
65
|
Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
|
|
57
66
|
HELP
|
|
58
67
|
exit 0
|
|
@@ -61,6 +70,10 @@ esac
|
|
|
61
70
|
|
|
62
71
|
DEFAULT_CODEX_MODEL="gpt-5.5"
|
|
63
72
|
DEFAULT_CODEX_EFFORT="xhigh"
|
|
73
|
+
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
74
|
+
# version (drift-guarded by codex-review.test.mjs against capability.json).
|
|
75
|
+
AW_RECEIPT_BACKEND="codex"
|
|
76
|
+
AW_BRIDGE_VERSION="2.2.0"
|
|
64
77
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|
|
65
78
|
CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
|
|
66
79
|
# Generous hard cap for a slow xhigh review (subscription latency varies).
|
|
@@ -140,8 +153,10 @@ if [[ "${CODEX_REVIEW_SCHEMA:-}" == "1" ]]; then
|
|
|
140
153
|
OUTPUT_FORMAT_CODE="$_json_fmt"
|
|
141
154
|
OUTPUT_FORMAT_PLAN="$_json_fmt"
|
|
142
155
|
else
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
# The verdict line is machine-parsed into the review receipt (AD-038) — mandate ONE exact literal.
|
|
157
|
+
_verdict_fmt="End with EXACTLY ONE final verdict line in this literal, machine-parsed form (that word pair alone on the line, nothing else): 'Verdict: ship' or 'Verdict: revise' or 'Verdict: rethink'."
|
|
158
|
+
OUTPUT_FORMAT_CODE="Output findings ONLY, one per line, as: [blocker|major|minor|nit] — file:line — issue — suggested fix. ${_verdict_fmt}"
|
|
159
|
+
OUTPUT_FORMAT_PLAN="Output findings ONLY, one per line, as: [blocker|major|minor|nit] — location — issue — suggested change. ${_verdict_fmt}"
|
|
145
160
|
fi
|
|
146
161
|
|
|
147
162
|
# True (exit 0) when $1 looks BINARY: a NUL byte in the first 8 KiB (git's own
|
|
@@ -153,6 +168,81 @@ is_binary() {
|
|
|
153
168
|
[[ "${nul:-0}" -gt 0 ]]
|
|
154
169
|
}
|
|
155
170
|
|
|
171
|
+
# --- Review receipts (AD-038) — byte-identical in codex-review.sh and agy-review.sh ---------------
|
|
172
|
+
# sha256 hex of stdin. sha256sum, else shasum -a 256; neither → warn + fail (the caller records a
|
|
173
|
+
# null fingerprint — a null never satisfies the review-state checker, fail-safe direction).
|
|
174
|
+
sha256_stdin() {
|
|
175
|
+
if command -v sha256sum >/dev/null 2>&1; then sha256sum | awk '{print $1}'
|
|
176
|
+
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 | awk '{print $1}'
|
|
177
|
+
else
|
|
178
|
+
echo "warning: no sha256sum/shasum on PATH — cannot compute the review fingerprint." >&2
|
|
179
|
+
return 1
|
|
180
|
+
fi
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# The canonical uncommitted-state fingerprint payload (code mode). Domain == the review payload:
|
|
184
|
+
# tracked staged + unstaged changes + untracked-not-ignored file contents (binary/symlink/non-regular
|
|
185
|
+
# untracked paths ride as name-only notes, mirroring the assembled change set). The prose definition
|
|
186
|
+
# lives in capability.json roles.review.contract.receipt (both bridges, lockstep); the kit checker
|
|
187
|
+
# (tools/review-state.mjs) implements the SAME serialization in node — cross-checked by the kit's
|
|
188
|
+
# review-fingerprint-parity.test.mjs.
|
|
189
|
+
emit_fingerprint_payload() {
|
|
190
|
+
git diff --cached --no-ext-diff
|
|
191
|
+
git diff --no-ext-diff
|
|
192
|
+
local path
|
|
193
|
+
while IFS= read -r -d '' path; do
|
|
194
|
+
if [[ -L "$path" ]]; then
|
|
195
|
+
printf 'untracked-symlink:%s -> %s\n' "$path" "$(readlink -- "$path" 2>/dev/null || echo '?')"
|
|
196
|
+
elif [[ ! -f "$path" ]]; then
|
|
197
|
+
printf 'untracked-nonregular:%s\n' "$path"
|
|
198
|
+
elif is_binary "$path"; then
|
|
199
|
+
printf 'untracked-binary:%s\n' "$path"
|
|
200
|
+
else
|
|
201
|
+
printf 'untracked:%s\n' "$path"
|
|
202
|
+
cat -- "$path"
|
|
203
|
+
fi
|
|
204
|
+
done < <(git ls-files --others --exclude-standard -z)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# sha256 of the canonical payload, emitted from the work-tree ROOT (a subdir invocation hashes the
|
|
208
|
+
# same bytes). Empty output on failure (no git tree / no sha256 tool) — recorded as null.
|
|
209
|
+
compute_tree_fingerprint() {
|
|
210
|
+
( cd "$(git rev-parse --show-toplevel)" && emit_fingerprint_payload ) | sha256_stdin
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# JSON-encode a receipt scalar: empty → null, else a quoted string (every value comes from a closed
|
|
214
|
+
# vocabulary or a hex digest — no escaping needed by construction).
|
|
215
|
+
receipt_json_scalar() {
|
|
216
|
+
if [[ -z "${1:-}" ]]; then printf 'null'; else printf '"%s"' "$1"; fi
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# write_review_receipt <artifact|""> <fresh: true|false> <fingerprint|""> <verdict> <grounded: true|false> <factsHash|"">
|
|
220
|
+
# Appends ONE receipt line (the AD-038 fixture shape) as a side effect of a SUCCESSFUL review —
|
|
221
|
+
# to $AW_REVIEW_RECEIPTS when set, else <git dir>/agent-workflow-review-receipts.jsonl (inside the
|
|
222
|
+
# git dir by construction, so it is never committable). Fail-safe: every failure here warns loudly
|
|
223
|
+
# and returns 0 — a missing receipt fails the kit's review-state CHECKER, never the review run.
|
|
224
|
+
write_review_receipt() {
|
|
225
|
+
local artifact="$1" fresh="$2" fingerprint="$3" verdict="$4" grounded="$5" facts_hash="$6"
|
|
226
|
+
local receipts="${AW_REVIEW_RECEIPTS:-}"
|
|
227
|
+
if [[ -z "$receipts" ]]; then
|
|
228
|
+
local receipt_git_dir
|
|
229
|
+
if ! receipt_git_dir="$(git rev-parse --absolute-git-dir 2>/dev/null)"; then
|
|
230
|
+
echo "warning: not inside a git work tree and AW_REVIEW_RECEIPTS is unset — skipping the review receipt." >&2
|
|
231
|
+
return 0
|
|
232
|
+
fi
|
|
233
|
+
receipts="$receipt_git_dir/agent-workflow-review-receipts.jsonl"
|
|
234
|
+
fi
|
|
235
|
+
local line
|
|
236
|
+
line="$(printf '{"schema":1,"artifact":%s,"fresh":%s,"fingerprint":%s,"backend":"%s","verdict":"%s","grounded":%s,"factsHash":%s,"wrapperVersion":"%s","timestamp":"%s"}' \
|
|
237
|
+
"$(receipt_json_scalar "$artifact")" "$fresh" "$(receipt_json_scalar "$fingerprint")" \
|
|
238
|
+
"$AW_RECEIPT_BACKEND" "$verdict" "$grounded" "$(receipt_json_scalar "$facts_hash")" \
|
|
239
|
+
"$AW_BRIDGE_VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)")"
|
|
240
|
+
if ! printf '%s\n' "$line" >>"$receipts" 2>/dev/null; then
|
|
241
|
+
echo "warning: could not append the review receipt to $receipts — the review itself succeeded;" >&2
|
|
242
|
+
echo " the review-state gate will read the current tree as un-receipted." >&2
|
|
243
|
+
fi
|
|
244
|
+
}
|
|
245
|
+
|
|
156
246
|
# Emit the full review surface to stdout: repo map, status, staged + unstaged
|
|
157
247
|
# diffs, and the CONTENTS of every untracked REGULAR file (NUL-safe iteration).
|
|
158
248
|
# Symlinks are shown as their target (never followed — no out-of-repo leak); other
|
|
@@ -191,6 +281,9 @@ assemble_code_diff() {
|
|
|
191
281
|
mode="${1:-}"
|
|
192
282
|
shift || true
|
|
193
283
|
|
|
284
|
+
REVIEW_ARTIFACT=""
|
|
285
|
+
REVIEW_FINGERPRINT=""
|
|
286
|
+
|
|
194
287
|
case "$mode" in
|
|
195
288
|
plan)
|
|
196
289
|
target="${1:-}"
|
|
@@ -203,6 +296,9 @@ case "$mode" in
|
|
|
203
296
|
echo "error: unexpected arguments after plan file: $*" >&2
|
|
204
297
|
exit 2
|
|
205
298
|
fi
|
|
299
|
+
# Plan-mode receipt identity: the artifact-file sha256 (informational-only for the tree checker).
|
|
300
|
+
REVIEW_ARTIFACT="plan"
|
|
301
|
+
REVIEW_FINGERPRINT="$(sha256_stdin <"$target" || true)"
|
|
206
302
|
fence_plan="Do not read files outside this git working tree; the plan above plus the in-repo code it references are your whole surface."
|
|
207
303
|
directive="You are REVIEWING an implementation plan — ADVISORY ONLY. You are in a read-only sandbox: do NOT edit, create, or delete any file, and do NOT rewrite the plan. Obey the project's Hard Constraints from its root AGENTS.md (already merged into your context). Read the plan below and the relevant repository code it references. ${fence_plan} ${OUTPUT_FORMAT_PLAN} Cover: correctness risks, missing or mis-ordered steps, ambiguities a cold executor would trip on, violated project Hard Constraints, scope creep, and missing verification/gates."
|
|
208
304
|
prompt="${directive}"$'\n\nPLAN:\n'"$(cat -- "$target")"
|
|
@@ -214,6 +310,10 @@ case "$mode" in
|
|
|
214
310
|
echo "codex-review: no uncommitted changes to review — the working tree is clean." >&2
|
|
215
311
|
exit 0
|
|
216
312
|
fi
|
|
313
|
+
# The canonical fingerprint of the tree codex is about to review — computed at assembly time
|
|
314
|
+
# (BEFORE the EXIT trap can fire), so the receipt attests exactly the reviewed state.
|
|
315
|
+
REVIEW_ARTIFACT="code"
|
|
316
|
+
REVIEW_FINGERPRINT="$(compute_tree_fingerprint || true)"
|
|
217
317
|
# Assemble DIRECTLY to a git-dir-local temp file (git-invisible + worktree /
|
|
218
318
|
# submodule safe; 600 perms). Measuring size from the file keeps the whole
|
|
219
319
|
# change set off the bash-variable path (no NUL drop, byte-accurate size).
|
|
@@ -370,3 +470,20 @@ else
|
|
|
370
470
|
echo "warning: codex produced no final-message file — printing the run-trace tail instead." >&2
|
|
371
471
|
tail -n 40 "$trace"
|
|
372
472
|
fi
|
|
473
|
+
|
|
474
|
+
# --- Review receipt (AD-038): parse the verdict, append one receipt line ------
|
|
475
|
+
# Text mode parses the mandated literal 'Verdict: <ship|revise|rethink>' line; CODEX_REVIEW_SCHEMA=1
|
|
476
|
+
# reads the schema's "verdict" field instead; absent either way → "unknown" (recorded, never guessed).
|
|
477
|
+
verdict=""
|
|
478
|
+
if [[ -f "$out" && -s "$out" ]]; then
|
|
479
|
+
if [[ "${CODEX_REVIEW_SCHEMA:-}" == "1" ]]; then
|
|
480
|
+
verdict="$(sed -nE 's/.*"verdict"[[:space:]]*:[[:space:]]*"(ship|revise|rethink)".*/\1/p' "$out" | tail -n1)"
|
|
481
|
+
else
|
|
482
|
+
verdict="$(sed -nE 's/^Verdict: (ship|revise|rethink)[[:space:]]*$/\1/p' "$out" | tail -n1)"
|
|
483
|
+
fi
|
|
484
|
+
fi
|
|
485
|
+
[[ -z "$verdict" ]] && verdict="unknown"
|
|
486
|
+
# codex is grounded by construction (AGENTS.md auto-merge + the precomputed change set): grounded
|
|
487
|
+
# true, factsHash null (native grounding — no separate facts payload exists). Every codex run is a
|
|
488
|
+
# full fresh run (one-shot, no resume) → fresh:true.
|
|
489
|
+
write_review_receipt "$REVIEW_ARTIFACT" true "$REVIEW_FINGERPRINT" "$verdict" true ""
|
|
@@ -8,6 +8,7 @@ import { tmpdir } from 'node:os';
|
|
|
8
8
|
import { join, dirname, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
11
12
|
|
|
12
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const WRAPPER = join(HERE, 'codex-review.sh');
|
|
@@ -641,6 +642,12 @@ describe('codex-review.sh — --help contract (manifest-pinned)', () => {
|
|
|
641
642
|
setEq(got, (REVIEW_CONTRACT.continue ?? []).map(norm), 'help continue ⟷ manifest continue');
|
|
642
643
|
assert.deepEqual(REVIEW_CONTRACT.continue, [], 'codex-review is one-shot — no continue descriptor');
|
|
643
644
|
});
|
|
645
|
+
|
|
646
|
+
it('Receipt renders the manifest receipt contract verbatim (AD-038 three-way lockstep)', () => {
|
|
647
|
+
const help = runHelp('--help').stdout;
|
|
648
|
+
assert.equal(norm(helpSection(help, 'Receipt:').join(' ')), norm(REVIEW_CONTRACT.receipt));
|
|
649
|
+
assert.match(REVIEW_CONTRACT.receipt, /sha256 over the canonical uncommitted-state payload/, 'the fingerprint definition lives in the manifest contract');
|
|
650
|
+
});
|
|
644
651
|
});
|
|
645
652
|
|
|
646
653
|
describe('codex-review.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
|
|
@@ -667,3 +674,128 @@ describe('codex-review.sh — source-level reverse guard (parser arms ⟷ manife
|
|
|
667
674
|
}
|
|
668
675
|
});
|
|
669
676
|
});
|
|
677
|
+
|
|
678
|
+
// ── review receipts (AD-038) ─────────────────────────────────────────────────────
|
|
679
|
+
// The normative fixture (docs: the AD-038 plan Decisions — copied verbatim; field VALUES with
|
|
680
|
+
// dynamic content are asserted by shape):
|
|
681
|
+
const RECEIPT_FIXTURE = JSON.parse(
|
|
682
|
+
'{"schema":1,"artifact":"code","fresh":true,"fingerprint":"<sha256hex>","backend":"codex","verdict":"revise","grounded":true,"factsHash":null,"wrapperVersion":"2.2.0","timestamp":"2026-07-03T12:00:00Z"}',
|
|
683
|
+
);
|
|
684
|
+
const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
|
|
685
|
+
const readReceipts = (repo) => {
|
|
686
|
+
const p = join(repo, RECEIPTS_REL);
|
|
687
|
+
if (!existsSync(p)) return [];
|
|
688
|
+
return readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
689
|
+
};
|
|
690
|
+
const sha256Hex = (buf) => createHash('sha256').update(buf).digest('hex');
|
|
691
|
+
|
|
692
|
+
describe('codex-review.sh — review receipts (AD-038)', () => {
|
|
693
|
+
it('a successful code review appends ONE fixture-shaped receipt (text-mode verdict parse)', () => {
|
|
694
|
+
const sb = makeSandbox();
|
|
695
|
+
const r = run(sb, { env: { CODEX_FAKE_FINAL: '[major] — a.txt:1 — x — y\nVerdict: revise' } });
|
|
696
|
+
const receipts = readReceipts(sb.repo);
|
|
697
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
698
|
+
assert.equal(r.status, 0, r.stderr);
|
|
699
|
+
assert.equal(receipts.length, 1, 'exactly one receipt line');
|
|
700
|
+
const receipt = receipts[0];
|
|
701
|
+
assert.deepEqual(Object.keys(receipt), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
|
|
702
|
+
assert.equal(receipt.schema, 1);
|
|
703
|
+
assert.equal(receipt.artifact, 'code');
|
|
704
|
+
assert.equal(receipt.fresh, true, 'every codex run is a fresh one-shot');
|
|
705
|
+
assert.match(receipt.fingerprint, /^[0-9a-f]{64}$/, 'a real sha256 hex fingerprint');
|
|
706
|
+
assert.equal(receipt.backend, 'codex');
|
|
707
|
+
assert.equal(receipt.verdict, 'revise', 'the mandated literal verdict line is parsed');
|
|
708
|
+
assert.equal(receipt.grounded, true, 'codex is grounded by construction');
|
|
709
|
+
assert.equal(receipt.factsHash, null, 'native grounding — no separate facts payload');
|
|
710
|
+
assert.equal(receipt.wrapperVersion, MANIFEST.version, 'receipt version ⟷ capability.json version');
|
|
711
|
+
assert.match(receipt.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('the code-mode fingerprint tracks the uncommitted state (same tree → same hash; edit → different)', () => {
|
|
715
|
+
const sb = makeSandbox();
|
|
716
|
+
// Route the fake-codex capture files to /dev/null so the runs themselves leave the repo
|
|
717
|
+
// byte-identical (the default capture files land inside the repo and would change the tree).
|
|
718
|
+
const quiet = { CODEX_FAKE_ARGV: '/dev/null', CODEX_FAKE_ENV: '/dev/null', CODEX_FAKE_STDIN: '/dev/null', CODEX_FAKE_FINAL: 'Verdict: ship' };
|
|
719
|
+
run(sb, { env: quiet });
|
|
720
|
+
run(sb, { env: quiet });
|
|
721
|
+
writeFileSync(join(sb.repo, 'pending.txt'), 'edited after the first two reviews\n');
|
|
722
|
+
run(sb, { env: quiet });
|
|
723
|
+
const receipts = readReceipts(sb.repo);
|
|
724
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
725
|
+
assert.equal(receipts.length, 3);
|
|
726
|
+
assert.equal(receipts[0].fingerprint, receipts[1].fingerprint, 'an unchanged tree re-fingerprints identically');
|
|
727
|
+
assert.notEqual(receipts[1].fingerprint, receipts[2].fingerprint, 'an edited tree changes the fingerprint');
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('CODEX_REVIEW_SCHEMA=1 reads the schema verdict field', () => {
|
|
731
|
+
const sb = makeSandbox();
|
|
732
|
+
const r = run(sb, {
|
|
733
|
+
env: { CODEX_REVIEW_SCHEMA: '1', CODEX_FAKE_FINAL: '{"findings":[],"verdict":"ship","notes":"ok"}' },
|
|
734
|
+
});
|
|
735
|
+
const receipts = readReceipts(sb.repo);
|
|
736
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
737
|
+
assert.equal(r.status, 0, r.stderr);
|
|
738
|
+
assert.equal(receipts[0].verdict, 'ship');
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('no parseable verdict → recorded as "unknown", never guessed', () => {
|
|
742
|
+
const sb = makeSandbox();
|
|
743
|
+
const r = run(sb, { env: { CODEX_FAKE_FINAL: 'looks fine to me overall' } });
|
|
744
|
+
const receipts = readReceipts(sb.repo);
|
|
745
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
746
|
+
assert.equal(r.status, 0, r.stderr);
|
|
747
|
+
assert.equal(receipts[0].verdict, 'unknown');
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('plan mode: artifact "plan", fingerprint = the artifact-file sha256', () => {
|
|
751
|
+
const sb = makeSandbox();
|
|
752
|
+
const planBytes = readFileSync(join(sb.repo, 'plan.md'));
|
|
753
|
+
const r = run(sb, { args: ['plan', 'plan.md'], env: { CODEX_FAKE_FINAL: 'Verdict: ship' } });
|
|
754
|
+
const receipts = readReceipts(sb.repo);
|
|
755
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
756
|
+
assert.equal(r.status, 0, r.stderr);
|
|
757
|
+
assert.equal(receipts[0].artifact, 'plan');
|
|
758
|
+
assert.equal(receipts[0].fingerprint, sha256Hex(planBytes), 'plan fingerprint = file sha256');
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('AW_REVIEW_RECEIPTS overrides the receipt destination', () => {
|
|
762
|
+
const sb = makeSandbox();
|
|
763
|
+
const override = join(sb.root, 'my-receipts.jsonl');
|
|
764
|
+
const r = run(sb, { env: { AW_REVIEW_RECEIPTS: override, CODEX_FAKE_FINAL: 'Verdict: ship' } });
|
|
765
|
+
const inGitDir = readReceipts(sb.repo);
|
|
766
|
+
const atOverride = existsSync(override) ? readFileSync(override, 'utf8') : '';
|
|
767
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
768
|
+
assert.equal(r.status, 0, r.stderr);
|
|
769
|
+
assert.equal(inGitDir.length, 0, 'nothing written to the default path');
|
|
770
|
+
assert.match(atOverride, /"backend":"codex"/);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('a receipt write failure warns loudly but never fails the review (fail-safe direction)', () => {
|
|
774
|
+
const sb = makeSandbox();
|
|
775
|
+
const r = run(sb, {
|
|
776
|
+
env: { AW_REVIEW_RECEIPTS: join(sb.repo, 'no-such-dir', 'r.jsonl'), CODEX_FAKE_FINAL: 'Verdict: ship' },
|
|
777
|
+
});
|
|
778
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
779
|
+
assert.equal(r.status, 0, 'the review run itself succeeds');
|
|
780
|
+
assert.match(r.stderr, /could not append the review receipt/);
|
|
781
|
+
assert.match(r.stdout, /Verdict: ship/, 'the findings still reach stdout');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it('a failed codex run writes NO receipt (only a successful review attests)', () => {
|
|
785
|
+
const sb = makeSandbox();
|
|
786
|
+
const r = run(sb, { env: { CODEX_FAKE_EXIT: '5' } });
|
|
787
|
+
const receipts = readReceipts(sb.repo);
|
|
788
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
789
|
+
assert.notEqual(r.status, 0);
|
|
790
|
+
assert.equal(receipts.length, 0);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('the clean-tree preflight exits before any receipt is written', () => {
|
|
794
|
+
const sb = makeSandbox({ clean: true });
|
|
795
|
+
const r = run(sb);
|
|
796
|
+
const receipts = readReceipts(sb.repo);
|
|
797
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
798
|
+
assert.equal(r.status, 0);
|
|
799
|
+
assert.equal(receipts.length, 0, 'no review ran — no receipt');
|
|
800
|
+
});
|
|
801
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "codex-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.2.0",
|
|
7
7
|
"provides": ["execute", "review"],
|
|
8
8
|
"roles": {
|
|
9
9
|
"execute": {
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"codex-review code [extra focus...]"
|
|
39
39
|
],
|
|
40
40
|
"grounding": "automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags",
|
|
41
|
-
"continue": []
|
|
41
|
+
"continue": [],
|
|
42
|
+
"receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); a write failure warns, never fails the review"
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
},
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
package/references/contracts.md
CHANGED
|
@@ -27,6 +27,7 @@ diverge:
|
|
|
27
27
|
|---|---|---|---|---|
|
|
28
28
|
| `/.claude/skills/` | Claude Code | dir | no | local-dev skills; absorbs the AD-013 one-off |
|
|
29
29
|
| `/.claude/agents/` | Claude Code | dir | no | project subagent definitions (incl. the kit-placed cheap-lane vehicles) |
|
|
30
|
+
| `/.claude/hooks/` | Claude Code | dir | no | project hooks (incl. the kit-placed gate-approval hook) |
|
|
30
31
|
| `/.cursor/rules/` | Cursor | dir | no | project rule files |
|
|
31
32
|
| `/.cursorrules` | Cursor (legacy) | file | **yes** | legacy single-file rules |
|
|
32
33
|
| `/.codeium/` | Codeium/Windsurf | dir | no | home-scoped launchers live under `~/`, out of scope |
|