@sabaiway/agent-workflow-kit 1.48.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -0
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +42 -11
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +176 -4
- package/bridges/antigravity-cli-bridge/bin/agy.sh +20 -2
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +79 -2
- package/bridges/antigravity-cli-bridge/capability.json +160 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +20 -2
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +136 -2
- package/bridges/codex-cli-bridge/bin/codex-review.sh +42 -11
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +168 -4
- package/bridges/codex-cli-bridge/capability.json +118 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/review-ledger.md +1 -1
- package/references/modes/review-state.md +2 -2
- package/references/modes/setup.md +7 -2
- package/references/modes/upgrade.md +9 -9
- package/tools/detect-backends.mjs +2 -2
- package/tools/doc-parity.mjs +7 -0
- package/tools/fs-safe.mjs +28 -5
- package/tools/manifest/schema.md +62 -0
- package/tools/manifest/validate.mjs +327 -0
- package/tools/review-ledger-core.mjs +74 -0
- package/tools/review-ledger-write.mjs +19 -10
- package/tools/review-ledger.mjs +21 -12
- package/tools/review-state.mjs +89 -28
- package/tools/setup-backends.mjs +48 -3
|
@@ -618,6 +618,32 @@ const extractArgCaseArms = (source) => {
|
|
|
618
618
|
};
|
|
619
619
|
const splitArms = (labels) => (labels ?? []).flatMap((l) => l.split('|'));
|
|
620
620
|
|
|
621
|
+
// The source lines that really EXECUTE: a heredoc body (the --help text) and a comment both carry
|
|
622
|
+
// names without carrying logic, so a bare name-grep over the whole source stays green after the
|
|
623
|
+
// logic is deleted. Reuses the same heredoc discipline as extractArgCaseArms.
|
|
624
|
+
const executableLines = (source) => {
|
|
625
|
+
const out = [];
|
|
626
|
+
let heredoc = null;
|
|
627
|
+
for (const raw of source.split('\n')) {
|
|
628
|
+
if (heredoc) {
|
|
629
|
+
if (raw.trim() === heredoc) heredoc = null;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
|
|
633
|
+
if (hd) { heredoc = hd[1]; continue; }
|
|
634
|
+
if (raw.trimStart().startsWith('#')) continue;
|
|
635
|
+
out.push(raw);
|
|
636
|
+
}
|
|
637
|
+
return out;
|
|
638
|
+
};
|
|
639
|
+
// An env var is really CONSULTED when an executable test compares it: [[ "${NAME:-}" == … ]].
|
|
640
|
+
const consultsEnv = (source, name) =>
|
|
641
|
+
executableLines(source).some((l) => new RegExp(`\\[\\[[^\\]]*\\$\\{?${name}\\b[^\\]]*(==|!=)`).test(l));
|
|
642
|
+
// The operand slots a rendered invocation form really carries: <angle> and [bracket] placeholders.
|
|
643
|
+
// The optional `@` prefix rides WITH the slot (`@<facts-file>` is one operand, not a bare
|
|
644
|
+
// `<facts-file>` behind a stray character) — the catalog declares the whole token a user types.
|
|
645
|
+
const SLOT_RE = /@?<[^<>]+>|\[[^[\]]*\]/g;
|
|
646
|
+
|
|
621
647
|
describe('codex-review.sh — --help contract (manifest-pinned)', () => {
|
|
622
648
|
it('--help and -h exit 0 pre-preflight (no codex, no git, no AGENTS.md)', () => {
|
|
623
649
|
for (const arg of ['--help', '-h']) {
|
|
@@ -679,11 +705,114 @@ describe('codex-review.sh — source-level reverse guard (parser arms ⟷ manife
|
|
|
679
705
|
});
|
|
680
706
|
});
|
|
681
707
|
|
|
708
|
+
// ── mode catalog ⟷ wrapper reality (BRIDGE-MODES-CATALOG) ─────────────────────────
|
|
709
|
+
// The kit validator owns the catalog's INTERNAL shape; these arms pin what only the wrapper source
|
|
710
|
+
// can settle — the catalog documents THIS wrapper's real modes and real escape hatches, and every
|
|
711
|
+
// contract invocation the wrapper honours is cataloged (adding a mode without one fails here).
|
|
712
|
+
describe('codex-review.sh — mode catalog ⟷ wrapper reality (manifest-pinned)', () => {
|
|
713
|
+
const source = readFileSync(WRAPPER, 'utf8');
|
|
714
|
+
const arms = extractArgCaseArms(source);
|
|
715
|
+
const catalog = MANIFEST.modeCatalog ?? [];
|
|
716
|
+
const reviewEntries = catalog.filter((e) => e.role === 'review');
|
|
717
|
+
const reviewPrimaries = reviewEntries.filter((e) => e.kind === 'primary');
|
|
718
|
+
|
|
719
|
+
it('the catalog submodes ARE the wrapper\'s real parser mode arms (both directions)', () => {
|
|
720
|
+
const modes = splitArms(arms.get('"$mode"')).filter((a) => a !== '*');
|
|
721
|
+
assert.ok(reviewPrimaries.length > 0, 'the manifest must catalog its review modes');
|
|
722
|
+
setEq(reviewPrimaries.map((e) => e.submode), modes, 'catalog submodes ⟷ real parser mode arms');
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('every review entry composes BY REFERENCE and every reference resolves', () => {
|
|
726
|
+
for (const entry of reviewEntries) {
|
|
727
|
+
assert.ok(
|
|
728
|
+
Array.isArray(entry.invocationRefs) && entry.invocationRefs.length > 0,
|
|
729
|
+
`${entry.key}: a contract-backed entry references at least one contract descriptor`,
|
|
730
|
+
);
|
|
731
|
+
assert.ok(!Object.hasOwn(entry, 'descriptor'), `${entry.key}: a contract-backed entry never restates a literal descriptor`);
|
|
732
|
+
for (const ref of entry.invocationRefs) {
|
|
733
|
+
assert.equal(
|
|
734
|
+
typeof REVIEW_CONTRACT[ref.contractField]?.[ref.index], 'string',
|
|
735
|
+
`${entry.key}: ref ${ref.contractField}[${ref.index}] does not resolve into the manifest contract`,
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('every review contract invocation is claimed by exactly ONE catalog entry (no uncataloged mode)', () => {
|
|
742
|
+
const claims = reviewEntries.flatMap((e) => e.invocationRefs.map((r) => `${r.contractField}[${r.index}]`));
|
|
743
|
+
assert.equal(new Set(claims).size, claims.length, 'a contract invocation is claimed at most once');
|
|
744
|
+
const declared = [
|
|
745
|
+
...REVIEW_CONTRACT.invocations.map((_, i) => `invocations[${i}]`),
|
|
746
|
+
...(REVIEW_CONTRACT.continue ?? []).map((_, i) => `continue[${i}]`),
|
|
747
|
+
];
|
|
748
|
+
setEq(new Set(claims), declared, 'catalog claims ⟷ declared contract invocations');
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('every env-hook the catalog aims at a review mode is a real EXECUTABLE guard, not a mention', () => {
|
|
752
|
+
const hooks = catalog.filter((e) => e.kind === 'env-hook' && e.parents.some((p) => reviewPrimaries.some((r) => r.key === p)));
|
|
753
|
+
assert.ok(hooks.length > 0, 'CODEX_PROBE must be cataloged as an env-hook over the review modes');
|
|
754
|
+
for (const hook of hooks) {
|
|
755
|
+
assert.ok(
|
|
756
|
+
consultsEnv(source, hook.key),
|
|
757
|
+
`env-hook ${hook.key} is named in the source but never TESTED in an executable condition — a help/comment mention would keep a name-grep green after the logic is deleted`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('the catalog operand slots set-EQUAL the slots its rendered forms really carry (both directions)', () => {
|
|
763
|
+
for (const entry of reviewEntries) {
|
|
764
|
+
const forms = entry.invocationRefs.map((r) => REVIEW_CONTRACT[r.contractField][r.index]);
|
|
765
|
+
// The DEDUPLICATED UNION over every resolved form: a plural-ref entry legitimately spreads its
|
|
766
|
+
// slots across forms, so per-form equality would false-fail a correct catalog.
|
|
767
|
+
const realSlots = new Set(forms.flatMap((f) => f.match(SLOT_RE) ?? []));
|
|
768
|
+
setEq(new Set((entry.operands ?? []).map((o) => o.slot)), realSlots, `${entry.key}: catalog operands ⟷ the slots its forms really carry`);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it('an entry rendering a LITERAL descriptor is slot-checked too (env-hooks have no role to filter on)', () => {
|
|
773
|
+
// The contract-backed arm above filters by role — and an env-hook HAS no role, so its descriptor
|
|
774
|
+
// would never be slot-checked. That is exactly how a hardcoded dead path can reach the discovery
|
|
775
|
+
// surface looking ready-to-run. Every literal-descriptor kind is covered here: env-hooks and
|
|
776
|
+
// contract-free primaries.
|
|
777
|
+
const literalEntries = catalog.filter((e) => typeof e.descriptor === 'string');
|
|
778
|
+
assert.ok(literalEntries.length > 0, 'CODEX_PROBE must be cataloged with a literal descriptor');
|
|
779
|
+
for (const entry of literalEntries) {
|
|
780
|
+
const realSlots = new Set(entry.descriptor.match(SLOT_RE) ?? []);
|
|
781
|
+
setEq(new Set((entry.operands ?? []).map((o) => o.slot)), realSlots, `${entry.key}: catalog operands ⟷ the slots its descriptor really carries`);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('CODEX_PROBE really relaxes the guard on EVERY review parent the catalog claims (behavioural)', () => {
|
|
786
|
+
// The catalog CLAIMS these modes are modified by the hook; prove it per parent rather than
|
|
787
|
+
// trusting a source scan: an off-pin model is exit 2 normally, exit 0 under probe.
|
|
788
|
+
const hook = catalog.find((e) => e.key === 'CODEX_PROBE');
|
|
789
|
+
const drive = { 'review.plan': ['plan', 'plan.md'], 'review.code': ['code'] };
|
|
790
|
+
const reviewParents = hook.parents.filter((p) => reviewPrimaries.some((r) => r.key === p));
|
|
791
|
+
assert.ok(reviewParents.length > 0, 'CODEX_PROBE must claim at least one review parent');
|
|
792
|
+
for (const parent of reviewParents) {
|
|
793
|
+
assert.ok(drive[parent], `no behavioural drive for claimed parent "${parent}" — add one`);
|
|
794
|
+
// The exit code alone is weak evidence: pin the real DISPATCH too (capStdin is non-empty only
|
|
795
|
+
// when codex was actually invoked), so a run that dies early can never pass the probe-on branch.
|
|
796
|
+
const guarded = makeSandbox();
|
|
797
|
+
const off = run(guarded, { args: drive[parent], env: { CODEX_MODEL: 'not-the-pinned-model' } });
|
|
798
|
+
rmSync(guarded.root, { recursive: true, force: true });
|
|
799
|
+
assert.equal(off.status, 2, `${parent}: the quality guard must refuse an off-pin model without the hook`);
|
|
800
|
+
assert.equal(off.capStdin, '', `${parent}: the guard must refuse BEFORE spending a run`);
|
|
801
|
+
|
|
802
|
+
const probed = makeSandbox();
|
|
803
|
+
const on = run(probed, { args: drive[parent], env: { CODEX_MODEL: 'not-the-pinned-model', CODEX_PROBE: '1' } });
|
|
804
|
+
rmSync(probed.root, { recursive: true, force: true });
|
|
805
|
+
assert.equal(on.status, 0, `${parent}: CODEX_PROBE=1 must really relax the guard — the catalog claims it does`);
|
|
806
|
+
assert.notEqual(on.capStdin, '', `${parent}: CODEX_PROBE=1 must really reach codex, not merely exit 0`);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
682
811
|
// ── review receipts (AD-038) ─────────────────────────────────────────────────────
|
|
683
|
-
// The normative fixture
|
|
812
|
+
// The normative fixture: the AD-038 shape + the D3 self-declaring probe marker (field VALUES with
|
|
684
813
|
// dynamic content are asserted by shape):
|
|
685
814
|
const RECEIPT_FIXTURE = JSON.parse(
|
|
686
|
-
'{"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"}',
|
|
815
|
+
'{"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}',
|
|
687
816
|
);
|
|
688
817
|
const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
|
|
689
818
|
const readReceipts = (repo) => {
|
|
@@ -715,6 +844,40 @@ describe('codex-review.sh — review receipts (AD-038)', () => {
|
|
|
715
844
|
assert.match(receipt.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
|
|
716
845
|
});
|
|
717
846
|
|
|
847
|
+
// The probe marker (BRIDGE-MODES-CATALOG, D3): a CODEX_PROBE=1 review runs with the
|
|
848
|
+
// frontier-model/max-effort guard OFF, so its receipt must be distinguishable — the kit's
|
|
849
|
+
// review-state gate rejects a probe-marked receipt. EVERY receipt carries the marker (true or
|
|
850
|
+
// false): it self-declares, so the gate reads the fact rather than inferring it from a version
|
|
851
|
+
// string that bumps in a different release phase. Silence is not a declaration.
|
|
852
|
+
it('CODEX_PROBE=1 stamps probe:true — a throwaway probe can never attest a tree (D3)', () => {
|
|
853
|
+
const sb = makeSandbox();
|
|
854
|
+
const r = run(sb, { env: { CODEX_PROBE: '1', CODEX_FAKE_FINAL: 'Verdict: ship' } });
|
|
855
|
+
const receipts = readReceipts(sb.repo);
|
|
856
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
857
|
+
assert.equal(r.status, 0, r.stderr);
|
|
858
|
+
assert.equal(receipts[0].probe, true, 'a probe-relaxed run marks its own receipt');
|
|
859
|
+
assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'fixture key set + order');
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// Every receipt SELF-DECLARES: the kit's gate reads the marker, never the wrapper version — so
|
|
863
|
+
// the marker must not depend on a version bump landing in the same release phase.
|
|
864
|
+
it('a normal review self-declares probe:false — the receipt states the fact, not a version', () => {
|
|
865
|
+
const sb = makeSandbox();
|
|
866
|
+
run(sb, { env: { CODEX_FAKE_FINAL: 'Verdict: ship' } });
|
|
867
|
+
const receipts = readReceipts(sb.repo);
|
|
868
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
869
|
+
assert.equal(receipts[0].probe, false, 'silence is not a declaration — the gate rejects an unmarked receipt');
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('the marker tracks the RELAXED GUARD, not the model — a probe on an off-pinned model still marks', () => {
|
|
873
|
+
const sb = makeSandbox();
|
|
874
|
+
const r = run(sb, { env: { CODEX_PROBE: '1', CODEX_MODEL: 'gpt-5-mini', CODEX_EFFORT: 'low', CODEX_FAKE_FINAL: 'Verdict: ship' } });
|
|
875
|
+
const receipts = readReceipts(sb.repo);
|
|
876
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
877
|
+
assert.equal(r.status, 0, r.stderr);
|
|
878
|
+
assert.equal(receipts[0].probe, true, 'exactly the runs the guard let through unpinned are marked');
|
|
879
|
+
});
|
|
880
|
+
|
|
718
881
|
it('the code-mode fingerprint tracks the uncommitted state (same tree → same hash; edit → different)', () => {
|
|
719
882
|
const sb = makeSandbox();
|
|
720
883
|
// Route the fake-codex capture files to /dev/null so the runs themselves leave the repo
|
|
@@ -1020,8 +1183,9 @@ describe('codex-review.sh — settings surface ⟷ manifest (D6, manifest-pinned
|
|
|
1020
1183
|
assert.ok(arm, `no validation arm for ${s.key}`);
|
|
1021
1184
|
if (s.kind === 'enum') for (const v of s.values) assert.ok(arm[1].includes(`"${v}"`), `${s.key}: enum value '${v}' not pinned`);
|
|
1022
1185
|
if (s.kind === 'integer') {
|
|
1023
|
-
|
|
1024
|
-
|
|
1186
|
+
// Issue-012 refactor: min/max are pinned as the aw_int_in_range helper's positional bounds
|
|
1187
|
+
// (`aw_int_in_range "$v" <min> <max>`) — the overflow-safe range check replaced raw arithmetic.
|
|
1188
|
+
assert.match(arm[1], new RegExp(`aw_int_in_range "\\$v" ${s.min} ${s.max}\\b`), `${s.key}: min/max ${s.min}/${s.max} not pinned as the aw_int_in_range bounds`);
|
|
1025
1189
|
}
|
|
1026
1190
|
if (s.kind === 'boolean') assert.ok(arm[1].includes('"0"') && arm[1].includes('"1"'), `${s.key}: boolean 0/1 not pinned`);
|
|
1027
1191
|
if (s.kind === 'duration') {
|
|
@@ -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.8.0",
|
|
7
7
|
"provides": ["execute", "review"],
|
|
8
8
|
"roles": {
|
|
9
9
|
"execute": {
|
|
@@ -42,10 +42,126 @@
|
|
|
42
42
|
],
|
|
43
43
|
"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",
|
|
44
44
|
"continue": [],
|
|
45
|
-
"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; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) 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"
|
|
45
|
+
"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; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) 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); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); a write failure warns, never fails the review"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
|
+
"modeCatalog": [
|
|
50
|
+
{
|
|
51
|
+
"key": "exec",
|
|
52
|
+
"kind": "primary",
|
|
53
|
+
"role": "execute",
|
|
54
|
+
"purpose": "Execute a bounded plan in a sandboxed workspace and hand back a diff to review.",
|
|
55
|
+
"whenToUse": [
|
|
56
|
+
"a bounded sub-task whose acceptance criteria are already pinned",
|
|
57
|
+
"work you will review and commit yourself — the backend never commits"
|
|
58
|
+
],
|
|
59
|
+
"whenNotTo": [
|
|
60
|
+
"work whose design is still open — decide it first, then delegate",
|
|
61
|
+
"anything you cannot check: the diff is advisory until you review it"
|
|
62
|
+
],
|
|
63
|
+
"invocationRefs": [
|
|
64
|
+
{ "contractField": "invocations", "index": 0 },
|
|
65
|
+
{ "contractField": "invocations", "index": 1 }
|
|
66
|
+
],
|
|
67
|
+
"operands": [
|
|
68
|
+
{ "slot": "<plan-file|->", "required": true, "description": "the plan or instruction file, or - to read it from stdin" },
|
|
69
|
+
{ "slot": "<extra codex flags...>", "required": false, "description": "extra codex flags, filtered by the guarded passthrough tiers" }
|
|
70
|
+
],
|
|
71
|
+
"guardrails": [
|
|
72
|
+
{ "value": "runs under codex's OWN OS sandbox (workspace-write)", "enforcement": "enforced", "condition": "it cannot nest inside another sandbox — the FS turns read-only; route it outside on the OBSERVED failure", "source": "capability.json roles.execute.contract.notes" },
|
|
73
|
+
{ "value": "the guarded passthrough blocks model / sandbox / approval overrides", "enforcement": "enforced", "source": "bin/codex-exec.sh" },
|
|
74
|
+
{ "value": "hard wall-clock cap CODEX_HARD_TIMEOUT (built-in default 3600s)", "enforcement": "enforced", "condition": "only while timeout(1)/gtimeout is on PATH — otherwise the wrapper warns and runs uncapped", "source": "capability.json settings.CODEX_HARD_TIMEOUT" }
|
|
75
|
+
],
|
|
76
|
+
"customHooks": ["CODEX_PROBE"]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"key": "exec.resume-last",
|
|
80
|
+
"kind": "continuation",
|
|
81
|
+
"role": "execute",
|
|
82
|
+
"purpose": "Continue the most recent codex-exec session with a follow-up instruction.",
|
|
83
|
+
"whenToUse": ["a small delta on work the same session already holds in context"],
|
|
84
|
+
"whenNotTo": ["a fresh task — a stale session carries stale assumptions"],
|
|
85
|
+
"invocationRefs": [{ "contractField": "continue", "index": 0 }],
|
|
86
|
+
"operands": [
|
|
87
|
+
{ "slot": "<plan-file|->", "required": true, "description": "the follow-up instruction file, or - to read it from stdin" }
|
|
88
|
+
],
|
|
89
|
+
"customHooks": ["CODEX_PROBE"]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"key": "exec.resume",
|
|
93
|
+
"kind": "continuation",
|
|
94
|
+
"role": "execute",
|
|
95
|
+
"purpose": "Continue a NAMED codex-exec session by the session id it printed.",
|
|
96
|
+
"whenToUse": ["resuming a specific earlier session after other runs happened in between"],
|
|
97
|
+
"invocationRefs": [{ "contractField": "continue", "index": 1 }],
|
|
98
|
+
"operands": [
|
|
99
|
+
{ "slot": "<session-id>", "required": true, "description": "the session id the original run printed on stderr" },
|
|
100
|
+
{ "slot": "<plan-file|->", "required": true, "description": "the follow-up instruction file, or - to read it from stdin" }
|
|
101
|
+
],
|
|
102
|
+
"customHooks": ["CODEX_PROBE"]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"key": "review.plan",
|
|
106
|
+
"kind": "primary",
|
|
107
|
+
"role": "review",
|
|
108
|
+
"submode": "plan",
|
|
109
|
+
"purpose": "Critique an implementation plan before any of its code exists.",
|
|
110
|
+
"whenToUse": [
|
|
111
|
+
"a plan draft heading into its review council",
|
|
112
|
+
"checking whether a cold executor could really run the plan"
|
|
113
|
+
],
|
|
114
|
+
"whenNotTo": ["a working-tree change set — that is review.code"],
|
|
115
|
+
"invocationRefs": [{ "contractField": "invocations", "index": 0 }],
|
|
116
|
+
"operands": [
|
|
117
|
+
{ "slot": "<plan-file>", "required": true, "description": "the plan file under review" }
|
|
118
|
+
],
|
|
119
|
+
"guardrails": [
|
|
120
|
+
{ "value": "read-only sandbox — codex cannot edit, create or delete a file", "enforcement": "enforced", "source": "bin/codex-review.sh" },
|
|
121
|
+
{ "value": "runs on the pinned frontier model at max effort", "enforcement": "enforced", "condition": "unless CODEX_PROBE=1 relaxes the guard for a throwaway probe", "source": "bin/codex-review.sh" },
|
|
122
|
+
{ "value": "a successful review appends one receipt line the review-state gate reads", "enforcement": "enforced", "condition": "a receipt write failure warns and the review still succeeds — the tree then reads un-receipted", "source": "capability.json roles.review.contract.receipt" }
|
|
123
|
+
],
|
|
124
|
+
"customHooks": ["CODEX_PROBE"]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"key": "review.code",
|
|
128
|
+
"kind": "primary",
|
|
129
|
+
"role": "review",
|
|
130
|
+
"submode": "code",
|
|
131
|
+
"purpose": "Review the uncommitted working-tree change set, precomputed in full for codex.",
|
|
132
|
+
"whenToUse": [
|
|
133
|
+
"a finished segment heading into its review round",
|
|
134
|
+
"a second opinion on real code before the commit ask"
|
|
135
|
+
],
|
|
136
|
+
"whenNotTo": ["a clean tree — the wrapper exits before spending a run"],
|
|
137
|
+
"invocationRefs": [{ "contractField": "invocations", "index": 1 }],
|
|
138
|
+
"operands": [
|
|
139
|
+
{ "slot": "[extra focus...]", "required": false, "description": "extra focus words appended to the review directive" }
|
|
140
|
+
],
|
|
141
|
+
"guardrails": [
|
|
142
|
+
{ "value": "read-only sandbox — codex cannot edit, create or delete a file", "enforcement": "enforced", "source": "bin/codex-review.sh" },
|
|
143
|
+
{ "value": "an oversized change set rides via a git-dir temp file, never truncated", "enforcement": "enforced", "source": "bin/codex-review.sh" },
|
|
144
|
+
{ "value": "a successful review appends one receipt line the review-state gate reads", "enforcement": "enforced", "condition": "a receipt write failure warns and the review still succeeds — the tree then reads un-receipted", "source": "capability.json roles.review.contract.receipt" }
|
|
145
|
+
],
|
|
146
|
+
"customHooks": ["CODEX_PROBE"]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"key": "CODEX_PROBE",
|
|
150
|
+
"kind": "env-hook",
|
|
151
|
+
"parents": ["exec", "exec.resume-last", "exec.resume", "review.plan", "review.code"],
|
|
152
|
+
"purpose": "Relax the quality guards for a THROWAWAY probe run.",
|
|
153
|
+
"whenToUse": [
|
|
154
|
+
"a probe whose answer cannot depend on model or effort",
|
|
155
|
+
"checking that the wrapper plumbing works at all"
|
|
156
|
+
],
|
|
157
|
+
"whenNotTo": ["any run whose output informs what ships — a probe never attests"],
|
|
158
|
+
"descriptor": "CODEX_PROBE=1 codex-review code",
|
|
159
|
+
"guardrails": [
|
|
160
|
+
{ "value": "a probe review mints a probe-marked receipt the review-state gate rejects", "enforcement": "enforced", "source": "agent-workflow-kit tools/review-state.mjs" },
|
|
161
|
+
{ "value": "on codex-exec it also relaxes the passthrough to the probeRelaxed tier", "enforcement": "enforced", "source": "bin/codex-exec.sh" }
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
],
|
|
49
165
|
"settings": [
|
|
50
166
|
{
|
|
51
167
|
"key": "CODEX_SERVICE_TIER",
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
|
@@ -13,7 +13,7 @@ The review-round **LEDGER** (DEBT-REVIEW-CAP / AD-045) — it turns the prose re
|
|
|
13
13
|
- **`--telemetry`** → read-only COUNTS across ALL loops and BOTH ledgers (never combined with the other flags — a mixed-mode gate cmd would silently pass): rounds/segments per loop, finding-origin totals, classification distribution (incl. `refuted`), per-backend verdict + divergence-round counts, override usage by scope, gate-run counts (quality-green / red results by gate id), fold runs, observed-red receipts, quarantined probes. Counts only — which gates earn their keep stays the maintainer's judgment.
|
|
14
14
|
|
|
15
15
|
2. **Writer** — `node ${CLAUDE_SKILL_DIR}/tools/review-ledger-write.mjs record|classify|override|batch --json '<payload>'` (the SOLE writer, over the shared `atomic-write` core):
|
|
16
|
-
- `record` appends one round — `{ loop, round, origins, backends, findings }` (activity defaults to `plan-execution`). **The teeth (Decision 5 + AD-048, all per SEGMENT):** it REFUSES (typed STOP) to append a round WHILE `decideStop` on the segment's records is `triage-required` (an UNCLASSIFIED surviving blocking finding at/after the cap); refuses ANY round beyond the **hard-max ceiling of 3 within one segment** unconditionally (the counter reopens only at the next gated commit); refuses while a blocking finding of the segment's previous round **VANISHED unclassified** (present means present-as-blocking — a downgrade to minor does not survive; a pending `escalate` never clears; `refuted` is the honest phantom lane); refuses while the changed source surface exceeds the **diff-size cap** (`AW_REVIEW_DIFF_CAP`, default 400 new-side lines of assessable + unsupported SOURCE — tests and out-of-domain never count, pure deletions are free) without a recorded segment `size-cap` override; and refuses without a **quality-green gate-run** at the current fingerprint (`run-gates --record` — gates-before-review is computed, not remembered; a `--only` subset or a tree-changed run never satisfies; red PROCESS gates — the kit's own `--check` loop gates — never block). **Integrity binding (Decision 7):** each NON-degraded backend needs
|
|
16
|
+
- `record` appends one round — `{ loop, round, origins, backends, findings }` (activity defaults to `plan-execution`). **The teeth (Decision 5 + AD-048, all per SEGMENT):** it REFUSES (typed STOP) to append a round WHILE `decideStop` on the segment's records is `triage-required` (an UNCLASSIFIED surviving blocking finding at/after the cap); refuses ANY round beyond the **hard-max ceiling of 3 within one segment** unconditionally (the counter reopens only at the next gated commit); refuses while a blocking finding of the segment's previous round **VANISHED unclassified** (present means present-as-blocking — a downgrade to minor does not survive; a pending `escalate` never clears; `refuted` is the honest phantom lane); refuses while the changed source surface exceeds the **diff-size cap** (`AW_REVIEW_DIFF_CAP`, default 400 new-side lines of assessable + unsupported SOURCE — tests and out-of-domain never count, pure deletions are free) without a recorded segment `size-cap` override; and refuses without a **quality-green gate-run** at the current fingerprint (`run-gates --record` — gates-before-review is computed, not remembered; a `--only` subset or a tree-changed run never satisfies; red PROCESS gates — the kit's own `--check` loop gates — never block). **Integrity binding (Decision 7):** each NON-degraded backend needs an **attesting** code receipt for the current tree — a round cannot be recorded for a tree no bridge really reviewed. *Attesting* is the SHARED predicate (`review-ledger-core`) that `review-state` and the round cross-check read too, so the gates can never disagree about what counts: a fresh `code` receipt at the current fingerprint that is **grounded** AND carries an explicit **`probe:false`**. A `probe:true` receipt never attests (a probe review runs with the quality guards off), and a **malformed or ABSENT** marker is rejected fail-closed — silence is not a declaration, so a receipt from a pre-marker bridge no longer counts (refresh it: `npx @sabaiway/agent-workflow-kit@latest init`). Each exclusion states its own reason, because the recoveries differ. **`--from-receipts`** (BUGFREE-3 / AD-049) DRAFTS the `backends[]` from those receipts instead of hand-composing them: each recipe-named backend's verdict comes from its fresh grounded code receipt and its counts are computed from the supplied `findings` (`origins` / `findings` stay explicit input); a recipe-named backend with no receipt is a LOUD stop — the draft never invents one (supply it explicitly as a degraded backend if the bridge really is down).
|
|
17
17
|
- `classify` appends one triage — each surviving blocking finding of a SEGMENT round classified `fixable-bug` / `inherent-layer-residual` / `escalate` / **`refuted`** (v4 — the honest lane for a phantom finding, refuted against code: a MANDATORY non-empty `note` cites the grounds; never silently dropped, never folded). **A `fixable-bug` REQUIRES a `testId`** (M2/AD-046, schema v2) — the red→green test that pins the fold, formatted `<test-file>#<test-name-pattern>` (a `#` separator, both halves non-empty; no file-suffix rule); `inherent-layer-residual` / `escalate` may omit it. The writer validates the FORMAT only. **This is what BREAKS the deadlock:** once every surviving blocking finding is classified, `record` permits the next round (a `fixable-bug` classification lets the fix round run — no deadlock).
|
|
18
18
|
- `override` appends one override record (schema v3, BUGFREE-1/AD-047) — the LOUD, durable waiver the **fold-completeness** gate consumes, `{ loop, round, scope, reason }` plus the per-scope payload (EXACT — a stray key is refused): scope **`oracle-change`** → `files[]`, the repo-relative pre-existing test files whose tamper flag it lifts (a fix diff that deliberately rewrote test expectations); scope **`red-proof`** → `testId`, the ONE bound test whose observed-red receipt + custody it waives (a red genuinely unestablishable pre-fold, or a custody file legitimately edited post-fix); scope **`size-cap`** (v4) → `sanctionedLines`, the EXACT changed-surface magnitude the waiver sanctions — **segment-scoped** (it dies at the next commit; a grown surface needs a fresh recorded sanction). Teeth: schema validation, fail-closed ledger read, round-sequence integrity, and the loop must be the SINGLE in-flight plan. QUARANTINE (a flaky/timed-out probe) has NO override lane. Overrides never enter `decideStop` — the two v3 scopes are fold-completeness inputs; `size-cap` is a writer-tooth input.
|
|
19
19
|
- **`gate-run` records (v4, kind `gate-run`)** are minted by `run-gates --record` through the writer's `recordGateRun` API (the runner never opens the ledger itself) — the D5 green-baseline receipt: the FULL declaration + exactly what ran + the tree fingerprint BEFORE and AFTER the run, segment-framed with NO round number, minted only inside the single in-flight loop. A red run records honestly (telemetry fuel); **quality-green** (every declared NON-process gate green, tree unchanged under the run) is judged at read time. Revert-first beyond this is not mechanically observable at this layer — it ships as protocol + telemetry visibility (consecutive red gate-runs), stated plainly.
|
|
@@ -4,8 +4,8 @@ Read-only **review-receipt checker** (AD-038) — it makes *"reviewed ≠ shippe
|
|
|
4
4
|
|
|
5
5
|
Run `node ${CLAUDE_SKILL_DIR}/tools/review-state.mjs [--check] [--json]`:
|
|
6
6
|
|
|
7
|
-
1. Plain run → the human report: resolved recipe + source, plan-in-flight, tree fingerprint, per-backend receipt state (current / stale / ungrounded / missing) with verdict + grounding + timestamp.
|
|
8
|
-
2. **`--check`** → the gate exit code. The **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a solo-resolved recipe (configured, or degraded there — no ready reviewer), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a non-git cwd, or every recipe-named backend receipted **current + grounded** — **or degraded-exempt**: the current segment's latest **review-ledger** round records that backend `degraded:true` at the current tree fingerprint, with ≥1 non-degraded recipe-named backend present + grounded and the ledger reading clean (AD-050; a corrupt ledger DENIES the exemption fail-closed but never fails an otherwise-satisfied tree — the tool header is the single home). Exit 1 when a backend is missing, **stale** (ANY edit after its review moves the fingerprint), or grounded:false under reviewed/council, and is not degraded-exempt. **Presence, not unanimity:** verdict adjudication (ship/revise, the divergence crossover) stays orchestrator judgment — the gate only proves the configured backends really reviewed THIS tree (a recorded degrade counts as "reviewed", never as "converged" — that stays `review-ledger`'s job). Plan/diff receipts and continuations (`agy-review --continue`) are **informational-only**: after a fold, only a **fresh grounded re-run** (`codex-review code`; `agy-review code --facts @f`) restores green.
|
|
7
|
+
1. Plain run → the human report: resolved recipe + source, plan-in-flight, tree fingerprint, per-backend receipt state (current / stale / ungrounded / probe / rejected / missing) with verdict + grounding + timestamp.
|
|
8
|
+
2. **`--check`** → the gate exit code. The **normative exit contract lives in the tool header** (the single home — do not re-enumerate it elsewhere): exit 0 for a solo-resolved recipe (configured, or degraded there — no ready reviewer), no plan in flight (the `docs/plans` naming convention: `queue.md` and `EXECUTE-`/`FEEDBACK-`-prefixed or `PROMPT`/`prompt`/`handoff`-carrying names are scratch), a clean tree, a non-git cwd, or every recipe-named backend receipted **current + grounded** — **or degraded-exempt**: the current segment's latest **review-ledger** round records that backend `degraded:true` at the current tree fingerprint, with ≥1 non-degraded recipe-named backend present + grounded and the ledger reading clean (AD-050; a corrupt ledger DENIES the exemption fail-closed but never fails an otherwise-satisfied tree — the tool header is the single home). Exit 1 when a backend is missing, **stale** (ANY edit after its review moves the fingerprint), or grounded:false under reviewed/council, and is not degraded-exempt. **Presence, not unanimity:** verdict adjudication (ship/revise, the divergence crossover) stays orchestrator judgment — the gate only proves the configured backends really reviewed THIS tree (a recorded degrade counts as "reviewed", never as "converged" — that stays `review-ledger`'s job). Plan/diff receipts and continuations (`agy-review --continue`) are **informational-only**: after a fold, only a **fresh grounded re-run** (`codex-review code`; `agy-review code --facts @f`) restores green. **Probe receipts never attest either:** a `CODEX_PROBE=1` / `AGY_PROBE=1` review runs with the frontier-model/max-effort guard OFF, so the wrapper stamps `probe:true` and this checker excludes it — a backend whose only current receipts are probes fails with its own stated reason, distinct from stale. The filter is **per receipt**, so a real review at the same fingerprint still satisfies. **Silence is not a declaration:** a marker-aware wrapper writes `probe` on **every** successful review — `true` or `false` — so the receipt states the fact itself, and a **malformed** *or* **absent** marker is rejected **fail-closed** and stated in the check line, never silently dropped. This is deliberately **not** keyed on the wrapper version: the version bumps in a different release phase than the marker lands, so a version floor would reject the very receipts the current wrappers write — and it could only ever proxy a fact the receipt already carries. Accepted cost: a pre-marker receipt stops satisfying — re-run the review with a marker-aware bridge. Honest bound: receipts are **not authenticated** (a forger could write `probe:false` as easily as any other field) — like the rest of the receipt this is a self-discipline mechanism, not a security boundary.
|
|
9
9
|
3. **Wire it as a gate by hand OR via the explicit-consent seeder — never without consent (AD-021/AD-042).** The candidate line for your own `docs/ai/gates.json`: `{ "id": "review-state", "title": "Review receipts current for the uncommitted tree", "cmd": "node <path-to-this-skill>/tools/review-state.mjs --check" }` — with the path your project actually reaches the kit by, QUOTED so a path with spaces survives, executable from the project root. The consent-gated seeder (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`, consent-seed section) offers exactly this entry — path resolved and quoted — ONLY when your `docs/ai/orchestration.json` declares `reviewed`/`council` on `plan-execution.review` (the slot this checker enforces); it writes nothing without your explicit yes. Once declared, the opt-in `${CLAUDE_SKILL_DIR}/references/modes/hook.md` auto-approves it like any other declared gate.
|
|
10
10
|
4. **`--await [--timeout <s>]`** (BUGFREE-3 / AD-049) → BLOCK until every recipe-named backend is SATISFIED for the current tree (i.e. until `--check` would PASS), or the bounded timeout elapses (a loud exit 1; default 900s). Run it after dispatching the review bridges to WAIT for their receipts to land instead of hand-polling a pid: the durable completion signal is the **receipt**, never a process event (a harness "completed" notification fires early; a bridge's output late-flushes). It waits until each backend has a fresh grounded receipt **OR is degraded-exempt**: once a current-tree degrade is recorded in the review-ledger, `--await` stops waiting for that backend and returns READY (AD-050 — it inherits the `--check` exemption via the shared decision), so you no longer hand-`--await` around a known degrade. Still read-only (it only re-reads state, now the ledger too); solo / no-plan / clean-tree resolve instantly.
|
|
11
11
|
|
|
@@ -11,8 +11,13 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs [<backend>] [--bindir <pa
|
|
|
11
11
|
runs as its fourth stamp-independent reconcile): refresh every bridge `setup` **already placed**
|
|
12
12
|
from the kit's bundled copies + re-link its wrappers; an absent bridge is a stated skip (**never**
|
|
13
13
|
placed), a placed bridge newer than the bundle is a stated skip naming the kit update (**never**
|
|
14
|
-
downgraded), and every outcome line is composed by the tool — paste verbatim.
|
|
15
|
-
|
|
14
|
+
downgraded), and every outcome line is composed by the tool — paste verbatim. When the skills dir
|
|
15
|
+
is **read-only this session** and the placed bridge is already at the bundled version, the
|
|
16
|
+
equal-version re-sync it would run cannot write: that outcome is `skipped-readonly` — a **stated
|
|
17
|
+
skip** (exit 0, not a failure) naming the current version, the skipped/incomplete re-sync, and the
|
|
18
|
+
read-only cause; it never claims a re-sync ran, and any local drift persists until a writable rerun.
|
|
19
|
+
(A version-**behind** refresh blocked by the same read-only dir stays a loud `could not refresh`,
|
|
20
|
+
its recovery pointing at a writable rerun.) Does not combine with `--dry-run`.
|
|
16
21
|
- `--help`, `-h` — usage.
|
|
17
22
|
|
|
18
23
|
For each backend it:
|
|
@@ -31,14 +31,14 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
31
31
|
**Placed-bridge refresh — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Run
|
|
32
32
|
`node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed` and **paste its per-bridge
|
|
33
33
|
output lines verbatim** — every outcome line is composed by the tool (*refreshed* / *already
|
|
34
|
-
current* / *skipped — not placed* / *could not refresh* + its recovery)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
non-fatal
|
|
34
|
+
current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + its recovery). It
|
|
35
|
+
is **refresh-only**: it refreshes a bridge **`setup` already placed** from this kit's bundled copies
|
|
36
|
+
and re-links its wrappers; an **absent** bridge is a stated skip, **never a first placement**
|
|
37
|
+
(placement stays the opt-in `${CLAUDE_SKILL_DIR}/references/modes/setup.md` — AD-009/AD-011 honesty intact), a placed bridge
|
|
38
|
+
**newer** than the bundle is a stated skip naming the kit update (**never a downgrade**), and
|
|
39
|
+
`skipped-readonly` is an equal-version re-sync a **read-only** skills dir blocked this session (a
|
|
40
|
+
stated skip, exit 0 — not a failure). Runs on **every** upgrade (equal-head too), no lineage-head
|
|
41
|
+
bump; a *could not refresh* line is non-fatal — relay it plainly with its recovery.
|
|
42
42
|
|
|
43
43
|
**Agent-rules lens refresh — stamp-independent, same gate, BEFORE the equal-head short-circuit.**
|
|
44
44
|
Run `node ${CLAUDE_SKILL_DIR}/tools/lens-region.mjs reconcile <project>/docs/ai/agent_rules.md`
|
|
@@ -56,7 +56,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
56
56
|
**NEVER writes** it (the file lives outside every kit tree — D2), so an unknown/retired key is
|
|
57
57
|
flagged + preserved, never edited. Runs on **every** upgrade; exit 0 covers every outcome.
|
|
58
58
|
4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
|
|
59
|
-
- **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/verification-profile.json` profile was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
59
|
+
- **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/verification-profile.json` profile was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
60
60
|
- **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
|
|
61
61
|
- **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
|
|
62
62
|
- **Live host/session facts are tool-composed only.** Any claim this report makes about the current host or session state — prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts — must trace to **live tool output** from **this session** (the lines you just composed, or a probe you ran this run); a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection. Full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
|
|
@@ -91,7 +91,7 @@ const RAW_BACKENDS = [
|
|
|
91
91
|
],
|
|
92
92
|
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',
|
|
93
93
|
continue: [],
|
|
94
|
-
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; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) 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',
|
|
94
|
+
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; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) 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); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit\'s review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); a write failure warns, never fails the review',
|
|
95
95
|
},
|
|
96
96
|
},
|
|
97
97
|
bin: 'codex',
|
|
@@ -124,7 +124,7 @@ const RAW_BACKENDS = [
|
|
|
124
124
|
'agy-review --continue [--decided @f] [--focus "…"]',
|
|
125
125
|
'agy-review --conversation <id> [--decided @f] [--focus "…"]',
|
|
126
126
|
],
|
|
127
|
-
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; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) 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",
|
|
127
|
+
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; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) 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); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); a write failure warns, never fails the review",
|
|
128
128
|
notes: [
|
|
129
129
|
'pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt',
|
|
130
130
|
],
|
package/tools/doc-parity.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
VERDICT_SKIPS_TEMPLATE,
|
|
37
37
|
ACKS_FILE,
|
|
38
38
|
} from './recommendations.mjs';
|
|
39
|
+
import { SKIPPED_READONLY } from './setup-backends.mjs';
|
|
39
40
|
|
|
40
41
|
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
41
42
|
|
|
@@ -45,6 +46,7 @@ const AUTONOMY_DOCTOR_DOC = 'references/modes/autonomy-doctor.md';
|
|
|
45
46
|
const RECOMMENDATIONS_DOC = 'references/modes/recommendations.md';
|
|
46
47
|
const UPGRADE_DOC = 'references/modes/upgrade.md';
|
|
47
48
|
const VELOCITY_DOC = 'references/modes/velocity.md';
|
|
49
|
+
const SETUP_DOC = 'references/modes/setup.md';
|
|
48
50
|
|
|
49
51
|
// A typed usage failure (exit 2) for the CLI parser — the codebase's typed-error idiom (no classes).
|
|
50
52
|
const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
|
|
@@ -99,6 +101,11 @@ export const BINDINGS = Object.freeze([
|
|
|
99
101
|
// (the incident's "mode-doc apply text stays in lockstep" acceptance as a mechanism, not prose).
|
|
100
102
|
// Bound in BOTH docs that name the path (recommendations.md + velocity.md).
|
|
101
103
|
valueBinding('acks-file', ACKS_FILE, ACKS_FILE, [RECOMMENDATIONS_DOC, VELOCITY_DOC]),
|
|
104
|
+
// The refresh read-only degrade outcome (REFRESH-EROFS-HONESTY / AD-056): the new skipped-readonly
|
|
105
|
+
// token must render in BOTH mode contracts that enumerate the placed-bridge refresh outcomes
|
|
106
|
+
// (setup.md owns --refresh-placed; upgrade.md pastes its lines) — a reworded doc dropping the
|
|
107
|
+
// outcome fails this pin plus the gate. The token tracks the exported SETUP constant.
|
|
108
|
+
valueBinding('refresh-skipped-readonly', SKIPPED_READONLY, SKIPPED_READONLY, [SETUP_DOC, UPGRADE_DOC]),
|
|
102
109
|
].map((b) => Object.freeze(b)));
|
|
103
110
|
|
|
104
111
|
// ── the pure checker (readText is injectable for hermetic tests) ────────────────────────
|
package/tools/fs-safe.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
lstatSync, existsSync, mkdirSync, readdirSync, copyFileSync, readlinkSync, symlinkSync,
|
|
20
|
-
rmSync, unlinkSync,
|
|
20
|
+
rmSync, unlinkSync, readFileSync,
|
|
21
21
|
} from 'node:fs';
|
|
22
22
|
import { dirname, join, resolve, relative, sep, isAbsolute } from 'node:path';
|
|
23
23
|
|
|
@@ -68,6 +68,27 @@ export const assertContainedRealPath = (root, dest, deps = {}) => {
|
|
|
68
68
|
rel.split(sep).filter(Boolean).reduce(walk, root);
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
// Read-only write-boundary tagging (REFRESH-EROFS-HONESTY / AD-056). A DESTINATION-side write failure
|
|
72
|
+
// of the read-only class is TAGGED (err.readonlyWriteBoundary) so the refresh-only driver can classify
|
|
73
|
+
// an equal-version repair-on-rerun that cannot write as a STATED skip — never a false red. The tag is
|
|
74
|
+
// applied ONLY around the three write primitives, so a READ-side failure (bundle read / dir listing)
|
|
75
|
+
// is never absorbed: the degrade classifies at the write boundary, not by a broad err.code sniff over
|
|
76
|
+
// the whole copy. EROFS is destination-side by nature; mkdir/symlink write only the dest; an
|
|
77
|
+
// EACCES/EPERM at copyFile (which also READS the source) is destination-provable ONLY when the source
|
|
78
|
+
// is readable — else it is a source-side read failure that must stay loud.
|
|
79
|
+
const READONLY_WRITE_ERRNOS = new Set(['EROFS', 'EACCES', 'EPERM']);
|
|
80
|
+
export const isReadonlyWriteBoundary = (err) => Boolean(err && err.readonlyWriteBoundary);
|
|
81
|
+
const throwTaggedReadonly = (err, primitive, src, readFile) => {
|
|
82
|
+
if (err && READONLY_WRITE_ERRNOS.has(err.code)) {
|
|
83
|
+
let destinationSide = err.code === 'EROFS' || primitive !== 'copyFile';
|
|
84
|
+
if (!destinationSide) {
|
|
85
|
+
try { readFile(src); destinationSide = true; } catch { destinationSide = false; }
|
|
86
|
+
}
|
|
87
|
+
if (destinationSide) err.readonlyWriteBoundary = true;
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
90
|
+
};
|
|
91
|
+
|
|
71
92
|
// Recursive refresh copy. Guards every dest via assertContainedRealPath first, then:
|
|
72
93
|
// symlink src → additive: skip if dest exists, else mirror the link target.
|
|
73
94
|
// directory src → mkdir -p dest, recurse.
|
|
@@ -80,20 +101,22 @@ export const copyTreeRefresh = (src, dest, root, deps = {}) => {
|
|
|
80
101
|
const copyFile = deps.copyFile ?? copyFileSync;
|
|
81
102
|
const readlink = deps.readlink ?? readlinkSync;
|
|
82
103
|
const symlink = deps.symlink ?? symlinkSync;
|
|
104
|
+
const readFile = deps.readFile ?? readFileSync;
|
|
83
105
|
|
|
84
106
|
assertContainedRealPath(root, dest, deps);
|
|
85
107
|
const stat = lstat(src);
|
|
86
108
|
if (stat.isSymbolicLink()) {
|
|
87
109
|
if (exists(dest)) return;
|
|
88
|
-
|
|
110
|
+
const target = readlink(src); // read-side (a readlink failure is never tagged as a write boundary)
|
|
111
|
+
try { symlink(target, dest); } catch (err) { throwTaggedReadonly(err, 'symlink', src, readFile); }
|
|
89
112
|
} else if (stat.isDirectory()) {
|
|
90
|
-
mkdir(dest);
|
|
113
|
+
try { mkdir(dest); } catch (err) { throwTaggedReadonly(err, 'mkdir', src, readFile); }
|
|
91
114
|
for (const entry of readdir(src)) {
|
|
92
115
|
copyTreeRefresh(join(src, entry), join(dest, entry), root, deps);
|
|
93
116
|
}
|
|
94
117
|
} else {
|
|
95
|
-
mkdir(dirname(dest));
|
|
96
|
-
copyFile(src, dest);
|
|
118
|
+
try { mkdir(dirname(dest)); } catch (err) { throwTaggedReadonly(err, 'mkdir', src, readFile); }
|
|
119
|
+
try { copyFile(src, dest); } catch (err) { throwTaggedReadonly(err, 'copyFile', src, readFile); }
|
|
97
120
|
}
|
|
98
121
|
};
|
|
99
122
|
|