@sabaiway/agent-workflow-kit 5.1.0 → 5.2.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 +55 -0
- package/SKILL.md +13 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +14 -3
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +220 -30
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +264 -8
- package/bridges/antigravity-cli-bridge/bin/agy.sh +12 -2
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +18 -0
- package/bridges/antigravity-cli-bridge/capability.json +19 -13
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -5
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +3 -2
- package/bridges/codex-cli-bridge/bin/codex-review.sh +205 -34
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +276 -5
- package/bridges/codex-cli-bridge/capability.json +8 -6
- package/bridges/codex-cli-bridge/references/driving-codex.md +2 -2
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +2 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/flow-writer.md +37 -0
- package/references/modes/gates.md +4 -4
- package/references/modes/procedures.md +4 -2
- package/references/modes/receipt-deadline.md +16 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/set-flow.md +22 -0
- package/tools/cheap-agents.mjs +8 -2
- package/tools/commands.mjs +24 -2
- package/tools/commit-guard.mjs +44 -9
- package/tools/core-evidence.mjs +25 -22
- package/tools/detect-backends.mjs +32 -11
- package/tools/doc-parity.mjs +21 -6
- package/tools/flow-check.mjs +806 -0
- package/tools/flow-record.mjs +795 -0
- package/tools/flow-store-read.mjs +114 -0
- package/tools/flow-store.mjs +1178 -0
- package/tools/flow-writer.mjs +1265 -0
- package/tools/fs-read-nofollow.mjs +128 -0
- package/tools/gates-declaration.mjs +184 -0
- package/tools/gates-init.mjs +59 -17
- package/tools/orchestration-config.mjs +87 -10
- package/tools/orchestration-write.mjs +3 -3
- package/tools/plan-files.mjs +35 -0
- package/tools/procedures.mjs +75 -11
- package/tools/receipt-deadline.mjs +242 -0
- package/tools/recipes.mjs +21 -0
- package/tools/repo-lex.mjs +22 -0
- package/tools/review-state.mjs +240 -80
- package/tools/run-gates.mjs +361 -139
- package/tools/set-flow.mjs +465 -0
- package/tools/velocity-profile.mjs +8 -2
|
@@ -315,14 +315,16 @@ describe('codex-review.sh — hard timeout (1.3)', { concurrency: true }, () =>
|
|
|
315
315
|
assert.match(r.stderr, /exceeded the hard cap/);
|
|
316
316
|
});
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
// Flow-orchestration Phase 4.2 (#26): the uncapped lane is CLOSED — without a capping binary the
|
|
319
|
+
// preflight refuses by name BEFORE any CLI run (the pre-fix wrapper warned and ran uncapped).
|
|
320
|
+
it('fails CLOSED when neither timeout nor gtimeout is on PATH — refuses by name, codex never runs', () => {
|
|
319
321
|
const sb = makeSandbox();
|
|
320
322
|
const path = `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`;
|
|
321
323
|
const r = run(sb, { path });
|
|
322
324
|
rmSync(sb.root, { recursive: true, force: true });
|
|
323
|
-
assert.equal(r.status,
|
|
324
|
-
assert.match(r.stderr, /
|
|
325
|
-
assert.
|
|
325
|
+
assert.equal(r.status, 127, 'the hard-timeout preflight is a refusal, never a warned uncapped run');
|
|
326
|
+
assert.match(r.stderr, /hard-timeout preflight fails CLOSED/);
|
|
327
|
+
assert.equal(r.capStdin, '', 'codex must NOT be invoked when the preflight refuses');
|
|
326
328
|
});
|
|
327
329
|
});
|
|
328
330
|
|
|
@@ -524,7 +526,7 @@ describe('codex-review.sh — mode dispatch & plan validation', () => {
|
|
|
524
526
|
const r = run(sb, { args: ['bogus'] });
|
|
525
527
|
rmSync(sb.root, { recursive: true, force: true });
|
|
526
528
|
assert.equal(r.status, 2);
|
|
527
|
-
assert.match(r.stderr, /usage: .* plan <plan-file> \| code/);
|
|
529
|
+
assert.match(r.stderr, /usage: .* plan <plan-file> \[--nonce <n>\] \| code \[--nonce <n>\]/);
|
|
528
530
|
});
|
|
529
531
|
|
|
530
532
|
it('no mode prints usage and STOPs (exit 2)', () => {
|
|
@@ -1035,6 +1037,275 @@ describe('codex-review.sh — review receipts (AD-038)', () => {
|
|
|
1035
1037
|
assert.match(atOverride, /"backend":"codex"/);
|
|
1036
1038
|
});
|
|
1037
1039
|
|
|
1040
|
+
// The wrapper-minted finding manifest (flow-orchestration Phase 4.2, Decision 2/P5/P24-25):
|
|
1041
|
+
// nonce-supplied dispatches mint {schema, backend, nonce, fingerprint, findings} beside the
|
|
1042
|
+
// receipt, atomic + no-clobber + ORDERED — a failed mint EXCLUDES the receipt append.
|
|
1043
|
+
describe('finding manifest (AW_REVIEW_NONCE)', () => {
|
|
1044
|
+
const manifestPath = (repo, nonce) => join(repo, '.git', `agent-workflow-finding-manifest-codex-${nonce}.json`);
|
|
1045
|
+
// Capture files ride /dev/null so repeated runs see an UNCHANGED tree (the manifest binds the
|
|
1046
|
+
// fingerprint — a moved tree would legitimately change its bytes).
|
|
1047
|
+
const quiet = { CODEX_FAKE_ARGV: '/dev/null', CODEX_FAKE_ENV: '/dev/null', CODEX_FAKE_STDIN: '/dev/null', CODEX_FAKE_FINAL: 'Verdict: ship' };
|
|
1048
|
+
|
|
1049
|
+
it('a nonce-supplied code dispatch mints the {backend, nonce}-named manifest carrying the captured findings + the receipt fingerprint', () => {
|
|
1050
|
+
const sb = makeSandbox();
|
|
1051
|
+
const r = run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'r1-d1' } });
|
|
1052
|
+
const receipts = readReceipts(sb.repo);
|
|
1053
|
+
const manifest = JSON.parse(readFileSync(manifestPath(sb.repo, 'r1-d1'), 'utf8'));
|
|
1054
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1055
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1056
|
+
assert.equal(receipts.length, 1, 'the receipt landed beside the manifest');
|
|
1057
|
+
assert.deepEqual(Object.keys(manifest), ['schema', 'backend', 'nonce', 'fingerprint', 'findings'], 'the closed manifest key set, in order');
|
|
1058
|
+
assert.equal(manifest.schema, 1);
|
|
1059
|
+
assert.equal(manifest.backend, 'codex');
|
|
1060
|
+
assert.equal(manifest.nonce, 'r1-d1');
|
|
1061
|
+
assert.equal(manifest.fingerprint, receipts[0].fingerprint, 'the manifest binds the SAME reviewed-tree fingerprint as the receipt');
|
|
1062
|
+
assert.equal(manifest.findings, 'Verdict: ship\n', 'findings = the captured final message VERBATIM');
|
|
1063
|
+
assert.equal(receipts[0].nonce, 'r1-d1', 'a nonce-supplied receipt carries the dispatch nonce — the flow round-land matcher requires exact equality (dispatch identity end-to-end)');
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('a nonce-less invocation mints NO manifest and adds NO nonce field — the receipt field set is unchanged (Decision 2 both branches)', () => {
|
|
1067
|
+
const sb = makeSandbox();
|
|
1068
|
+
const r = run(sb, { env: { ...quiet } });
|
|
1069
|
+
const receipts = readReceipts(sb.repo);
|
|
1070
|
+
const gitEntries = readdirSync(join(sb.repo, '.git')).filter((n) => n.startsWith('agent-workflow-finding-manifest-'));
|
|
1071
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1072
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1073
|
+
assert.equal(gitEntries.length, 0, 'no nonce, no manifest');
|
|
1074
|
+
assert.deepEqual(Object.keys(receipts[0]), Object.keys(RECEIPT_FIXTURE), 'the receipt line field set is unchanged');
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it('a byte-identical re-mint is an idempotent no-op — the second receipt still lands', () => {
|
|
1078
|
+
const sb = makeSandbox();
|
|
1079
|
+
assert.equal(run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'r1-d1' } }).status, 0);
|
|
1080
|
+
const before = readFileSync(manifestPath(sb.repo, 'r1-d1'), 'utf8');
|
|
1081
|
+
const r2 = run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'r1-d1' } });
|
|
1082
|
+
const receipts = readReceipts(sb.repo);
|
|
1083
|
+
const after = readFileSync(manifestPath(sb.repo, 'r1-d1'), 'utf8');
|
|
1084
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1085
|
+
assert.equal(r2.status, 0, r2.stderr);
|
|
1086
|
+
assert.equal(receipts.length, 2, 'both receipts landed — the idempotent manifest no-op never excludes');
|
|
1087
|
+
assert.equal(after, before, 'the manifest bytes are untouched');
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it('DIFFERENT bytes at the derived name refuse loudly AND EXCLUDE the receipt append (ordering, behaviorally)', () => {
|
|
1091
|
+
const sb = makeSandbox();
|
|
1092
|
+
writeFileSync(manifestPath(sb.repo, 'r1-d1'), '{"schema":1,"backend":"codex","nonce":"r1-d1","fingerprint":null,"findings":"other bytes"}\n');
|
|
1093
|
+
const r = run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'r1-d1' } });
|
|
1094
|
+
const receipts = readReceipts(sb.repo);
|
|
1095
|
+
const manifest = readFileSync(manifestPath(sb.repo, 'r1-d1'), 'utf8');
|
|
1096
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1097
|
+
assert.equal(r.status, 0, 'the review itself still succeeds (the artifact lane failed loudly)');
|
|
1098
|
+
assert.match(r.stderr, /DIFFERENT bytes or is not a regular file — no-clobber refuses loudly/);
|
|
1099
|
+
assert.match(r.stderr, /receipt append is EXCLUDED/);
|
|
1100
|
+
assert.equal(receipts.length, 0, 'a nonce-supplied dispatch never lands a receipt without its manifest');
|
|
1101
|
+
assert.match(manifest, /other bytes/, 'the pre-existing manifest is never clobbered');
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('a tmp-unlink failure after a successful mint warns with the ORPHAN PATH and still appends the receipt (preload-forced; parity carries the agy twin)', () => {
|
|
1105
|
+
const sb = makeSandbox();
|
|
1106
|
+
const preload = join(sb.root, 'unlink-fail-preload.cjs');
|
|
1107
|
+
writeFileSync(preload, "const fs = require('node:fs');\nconst real = fs.unlinkSync;\nfs.unlinkSync = (p) => { if (String(p).includes('.tmp')) { const e = new Error('EPERM'); e.code = 'EPERM'; throw e; } return real(p); };\n");
|
|
1108
|
+
const r = run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'orph1', NODE_OPTIONS: `--require ${preload}` } });
|
|
1109
|
+
const receipts = readReceipts(sb.repo);
|
|
1110
|
+
const manifestExists = existsSync(manifestPath(sb.repo, 'orph1'));
|
|
1111
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1112
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1113
|
+
assert.match(r.stderr, /orphan left at: \S*\.tmp/, 'the warning names the exact orphan path — never a silent leftover');
|
|
1114
|
+
assert.equal(manifestExists, true, 'the manifest itself was published');
|
|
1115
|
+
assert.equal(receipts.length, 1, 'the pair guarantee holds — minted + receipted, the orphan stated');
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// MANIFEST-TMP-ORPHAN-ON-FAILURE: on a FAILURE exit whose temp unlink ALSO fails, the error
|
|
1119
|
+
// names the orphan path — parameterized over BOTH failure codes (rc 3 no-clobber, rc 1 fs
|
|
1120
|
+
// failure); a failure whose temp IS removable stays orphan-silent.
|
|
1121
|
+
const UNLINK_FAIL = "const fs = require('node:fs');\nconst real = fs.unlinkSync;\nfs.unlinkSync = (p) => { if (String(p).includes('.tmp')) { const e = new Error('EPERM'); e.code = 'EPERM'; throw e; } return real(p); };\n";
|
|
1122
|
+
const LINK_FAIL = "const fsLink = require('node:fs');\nfsLink.linkSync = () => { const e = new Error('EPERM'); e.code = 'EPERM'; throw e; };\n";
|
|
1123
|
+
for (const failure of [
|
|
1124
|
+
{ rc: 3, name: 'no-clobber (rc 3)', preload: UNLINK_FAIL, plant: true, errRe: /DIFFERENT bytes or is not a regular file/ },
|
|
1125
|
+
{ rc: 1, name: 'fs failure (rc 1)', preload: UNLINK_FAIL + LINK_FAIL, plant: false, errRe: /could not compose or write the finding manifest/ },
|
|
1126
|
+
]) {
|
|
1127
|
+
it(`a FAILURE exit (${failure.name}) whose temp unlink also fails names the ORPHAN PATH in the error`, () => {
|
|
1128
|
+
const sb = makeSandbox();
|
|
1129
|
+
if (failure.plant) writeFileSync(manifestPath(sb.repo, 'orphf1'), 'planted different bytes\n');
|
|
1130
|
+
const preload = join(sb.root, 'orphan-fail-preload.cjs');
|
|
1131
|
+
writeFileSync(preload, failure.preload);
|
|
1132
|
+
const r = run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'orphf1', NODE_OPTIONS: `--require ${preload}` } });
|
|
1133
|
+
const receipts = readReceipts(sb.repo);
|
|
1134
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1135
|
+
assert.equal(r.status, 0, 'the review itself still succeeds (the artifact lane failed loudly)');
|
|
1136
|
+
assert.match(r.stderr, failure.errRe);
|
|
1137
|
+
assert.match(r.stderr, /orphan left at: \S*\.tmp/, 'the failure branch names the exact orphan path — never a silent leftover');
|
|
1138
|
+
assert.equal(receipts.length, 0, 'a failed mint still EXCLUDES the receipt');
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
it('a FAILURE exit whose temp IS removable stays orphan-silent (no leftover, no orphan line)', () => {
|
|
1143
|
+
const sb = makeSandbox();
|
|
1144
|
+
writeFileSync(manifestPath(sb.repo, 'orphf2'), 'planted different bytes\n');
|
|
1145
|
+
const r = run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'orphf2' } });
|
|
1146
|
+
const leftovers = readdirSync(join(sb.repo, '.git')).filter((n) => n.endsWith('.tmp'));
|
|
1147
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1148
|
+
assert.equal(r.status, 0, 'the review itself still succeeds (the artifact lane failed loudly)');
|
|
1149
|
+
assert.match(r.stderr, /DIFFERENT bytes or is not a regular file/);
|
|
1150
|
+
assert.doesNotMatch(r.stderr, /orphan left at:/, 'a removed temp is not an orphan — the line would train readers to ignore it');
|
|
1151
|
+
assert.deepEqual(leftovers, [], 'the temp really was removed');
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
it('a SYMLINK at the derived manifest path refuses and EXCLUDES the receipt — even when its target is byte-identical', () => {
|
|
1155
|
+
const sb = makeSandbox();
|
|
1156
|
+
assert.equal(run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'sym1' } }).status, 0);
|
|
1157
|
+
const mPath = manifestPath(sb.repo, 'sym1');
|
|
1158
|
+
const target = join(sb.repo, '.git', 'manifest-target-copy.json');
|
|
1159
|
+
writeFileSync(target, readFileSync(mPath));
|
|
1160
|
+
rmSync(mPath);
|
|
1161
|
+
symlinkSync(target, mPath);
|
|
1162
|
+
const r = run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'sym1' } });
|
|
1163
|
+
const receipts = readReceipts(sb.repo);
|
|
1164
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1165
|
+
assert.equal(r.status, 0, 'the review itself still succeeds (the artifact lane failed loudly)');
|
|
1166
|
+
assert.match(r.stderr, /receipt append is EXCLUDED/);
|
|
1167
|
+
assert.equal(receipts.length, 1, 'the second receipt is EXCLUDED — a symlinked manifest is never read through as the idempotent no-op');
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it('a FIFO at the derived manifest path refuses fast and EXCLUDES the receipt (O_NONBLOCK — no hang; fix characterization)', () => {
|
|
1171
|
+
const sb = makeSandbox();
|
|
1172
|
+
const mPath = manifestPath(sb.repo, 'fifo1');
|
|
1173
|
+
assert.equal(spawnSync('mkfifo', [mPath], { encoding: 'utf8' }).status, 0, 'mkfifo fixture');
|
|
1174
|
+
const r = run(sb, { env: { ...quiet, AW_REVIEW_NONCE: 'fifo1' } });
|
|
1175
|
+
const receipts = readReceipts(sb.repo);
|
|
1176
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1177
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1178
|
+
assert.match(r.stderr, /receipt append is EXCLUDED/);
|
|
1179
|
+
assert.equal(receipts.length, 0, 'a FIFO manifest is never read (fstat-first) and the receipt is excluded');
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it('a BOM-prefixed findings payload round-trips VERBATIM into the manifest (U+FEFF preserved)', () => {
|
|
1183
|
+
const sb = makeSandbox();
|
|
1184
|
+
const r = run(sb, { env: { ...quiet, CODEX_FAKE_FINAL: '\uFEFFFinding A\nVerdict: ship', AW_REVIEW_NONCE: 'b1' } });
|
|
1185
|
+
const manifest = JSON.parse(readFileSync(manifestPath(sb.repo, 'b1'), 'utf8'));
|
|
1186
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1187
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1188
|
+
assert.equal(manifest.findings, '\uFEFFFinding A\nVerdict: ship\n', 'the captured findings are VERBATIM — a stripped BOM would move the findingDigest');
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
it('an unsafe nonce refuses PRE-SPEND (exit 2, codex never runs) — a non-ASCII letter refuses under a UTF-8 locale too', () => {
|
|
1192
|
+
const sb = makeSandbox();
|
|
1193
|
+
const r = run(sb, { env: { AW_REVIEW_NONCE: '../escape' } });
|
|
1194
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1195
|
+
assert.equal(r.status, 2);
|
|
1196
|
+
assert.match(r.stderr, /safe nonce grammar/);
|
|
1197
|
+
assert.equal(r.capStdin, '', 'the containment refusal fires before any CLI spend');
|
|
1198
|
+
// The grammar ENUMERATES the ASCII set (no ranges): a locale-collated [A-Za-z] could admit a
|
|
1199
|
+
// non-ASCII letter the kit's JS reader then refuses, breaking correlation after a paid run.
|
|
1200
|
+
const utf8 = makeSandbox();
|
|
1201
|
+
const r2 = run(utf8, { env: { AW_REVIEW_NONCE: 'ré1', LC_ALL: 'en_US.UTF-8', LANG: 'en_US.UTF-8' } });
|
|
1202
|
+
rmSync(utf8.root, { recursive: true, force: true });
|
|
1203
|
+
assert.equal(r2.status, 2, 'a non-ASCII nonce letter refuses whatever the locale collation says');
|
|
1204
|
+
assert.match(r2.stderr, /safe nonce grammar/);
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
it('plan mode with a nonce mints the manifest too (fingerprint = the artifact sha256)', () => {
|
|
1208
|
+
const sb = makeSandbox();
|
|
1209
|
+
const planBytes = readFileSync(join(sb.repo, 'plan.md'));
|
|
1210
|
+
const r = run(sb, { args: ['plan', 'plan.md'], env: { CODEX_FAKE_FINAL: 'Verdict: ship', AW_REVIEW_NONCE: 'p1' } });
|
|
1211
|
+
const manifest = JSON.parse(readFileSync(manifestPath(sb.repo, 'p1'), 'utf8'));
|
|
1212
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1213
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1214
|
+
assert.equal(manifest.fingerprint, sha256Hex(planBytes));
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it('a FAILED review (no verdict) mints neither receipt nor manifest — the pair rides success only', () => {
|
|
1218
|
+
const sb = makeSandbox();
|
|
1219
|
+
const r = run(sb, { env: { CODEX_FAKE_FINAL: 'no verdict here', AW_REVIEW_NONCE: 'r9' } });
|
|
1220
|
+
const receipts = readReceipts(sb.repo);
|
|
1221
|
+
const exists = existsSync(manifestPath(sb.repo, 'r9'));
|
|
1222
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1223
|
+
assert.notEqual(r.status, 0);
|
|
1224
|
+
assert.equal(receipts.length, 0);
|
|
1225
|
+
assert.equal(exists, false, 'no success, no manifest');
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// The --nonce flag (FLOW-NONCE-DISPATCH-LANE): the plain-argument lane onto the SAME seam —
|
|
1229
|
+
// for hosts whose dispatch policy has no env-prefix form.
|
|
1230
|
+
it('--nonce in code mode rides the AW_REVIEW_NONCE seam: manifest minted, receipt nonce-stamped, focus words intact', () => {
|
|
1231
|
+
const sb = makeSandbox();
|
|
1232
|
+
const r = run(sb, { args: ['code', '--nonce', 'f1-d1', 'look', 'harder'], env: { CODEX_FAKE_FINAL: 'Verdict: ship' } });
|
|
1233
|
+
const receipts = readReceipts(sb.repo);
|
|
1234
|
+
const manifest = JSON.parse(readFileSync(manifestPath(sb.repo, 'f1-d1'), 'utf8'));
|
|
1235
|
+
const stdin = r.capStdin;
|
|
1236
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1237
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1238
|
+
assert.equal(manifest.nonce, 'f1-d1');
|
|
1239
|
+
assert.equal(receipts[0].nonce, 'f1-d1', 'the flag stamps the receipt exactly like the env form');
|
|
1240
|
+
assert.match(stdin, /Extra focus: look harder/, 'the flag pair is stripped — trailing focus words still ride');
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
it('--nonce in plan mode mints the manifest (the consult-dispatch lane) — the flag never trips the no-extra-args refusal', () => {
|
|
1244
|
+
const sb = makeSandbox();
|
|
1245
|
+
const r = run(sb, { args: ['plan', 'plan.md', '--nonce', 'p2'], env: { CODEX_FAKE_FINAL: 'Verdict: ship' } });
|
|
1246
|
+
const exists = existsSync(manifestPath(sb.repo, 'p2'));
|
|
1247
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1248
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1249
|
+
assert.equal(exists, true);
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it('an unsafe --nonce value refuses PRE-SPEND (exit 2, codex never runs) — same grammar as the env screen', () => {
|
|
1253
|
+
const sb = makeSandbox();
|
|
1254
|
+
const r = run(sb, { args: ['code', '--nonce', 'a/b'] });
|
|
1255
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1256
|
+
assert.equal(r.status, 2);
|
|
1257
|
+
assert.match(r.stderr, /safe nonce grammar/);
|
|
1258
|
+
assert.equal(r.capStdin, '', 'the refusal fires before any CLI spend');
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it('a missing value, a duplicate flag, and a disagreeing env+flag pair each refuse (exit 2); an agreeing pair proceeds', () => {
|
|
1262
|
+
const sb = makeSandbox();
|
|
1263
|
+
const missing = run(sb, { args: ['code', '--nonce'] });
|
|
1264
|
+
assert.equal(missing.status, 2);
|
|
1265
|
+
assert.match(missing.stderr, /--nonce needs a value/);
|
|
1266
|
+
const dup = run(sb, { args: ['code', '--nonce', 'n1', '--nonce', 'n2'] });
|
|
1267
|
+
assert.equal(dup.status, 2);
|
|
1268
|
+
assert.match(dup.stderr, /duplicate --nonce/);
|
|
1269
|
+
const clash = run(sb, { args: ['code', '--nonce', 'n1'], env: { AW_REVIEW_NONCE: 'n2' } });
|
|
1270
|
+
assert.equal(clash.status, 2);
|
|
1271
|
+
assert.match(clash.stderr, /disagrees with the AW_REVIEW_NONCE environment value/);
|
|
1272
|
+
const agree = run(sb, { args: ['code', '--nonce', 'n3'], env: { AW_REVIEW_NONCE: 'n3', CODEX_FAKE_FINAL: 'Verdict: ship' } });
|
|
1273
|
+
const exists = existsSync(manifestPath(sb.repo, 'n3'));
|
|
1274
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1275
|
+
assert.equal(agree.status, 0, agree.stderr);
|
|
1276
|
+
assert.equal(exists, true, 'an agreeing pair is ONE seam value — the dispatch proceeds');
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
it('a grammar-valid leading-dash --nonce value is ACCEPTED — the flag lane spans the whole declared grammar', () => {
|
|
1280
|
+
const sb = makeSandbox();
|
|
1281
|
+
const r = run(sb, { args: ['code', '--nonce', '--n1'], env: { CODEX_FAKE_FINAL: 'Verdict: ship' } });
|
|
1282
|
+
const receipts = readReceipts(sb.repo);
|
|
1283
|
+
const exists = existsSync(manifestPath(sb.repo, '--n1'));
|
|
1284
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1285
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1286
|
+
assert.equal(exists, true, 'the {backend, nonce}-named manifest lands under the leading-dash nonce');
|
|
1287
|
+
assert.equal(receipts[0].nonce, '--n1', 'the receipt carries the exact grammar-valid value — flag lane ≡ env lane');
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
it('an EMPTY --nonce value refuses pre-spend under the grammar screen (presence is tracked separately from the value)', () => {
|
|
1291
|
+
const sb = makeSandbox();
|
|
1292
|
+
const r = run(sb, { args: ['code', '--nonce', ''] });
|
|
1293
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1294
|
+
assert.equal(r.status, 2);
|
|
1295
|
+
assert.match(r.stderr, /safe nonce grammar/);
|
|
1296
|
+
assert.equal(r.capStdin, '', 'the refusal fires before any CLI spend');
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
it('a duplicate --nonce after an EMPTY first value still refuses as a duplicate — an empty value never erases presence', () => {
|
|
1300
|
+
const sb = makeSandbox();
|
|
1301
|
+
const r = run(sb, { args: ['code', '--nonce', '', '--nonce', 'n2'] });
|
|
1302
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1303
|
+
assert.equal(r.status, 2);
|
|
1304
|
+
assert.match(r.stderr, /duplicate --nonce/);
|
|
1305
|
+
assert.equal(r.capStdin, '', 'the refusal fires before any CLI spend');
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1038
1309
|
it('a receipt write failure warns loudly but never fails the review (fail-safe direction)', () => {
|
|
1039
1310
|
const sb = makeSandbox();
|
|
1040
1311
|
const r = run(sb, {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "codex-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.3.0",
|
|
7
7
|
"posture": { "model": "gpt-5.6-sol", "effort": "xhigh", "tier": null },
|
|
8
8
|
"provides": ["execute", "review"],
|
|
9
9
|
"roles": {
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"output": "advisory",
|
|
43
43
|
"contract": {
|
|
44
44
|
"invocations": [
|
|
45
|
-
"codex-review plan <plan-file>",
|
|
46
|
-
"codex-review code [extra focus...]"
|
|
45
|
+
"codex-review plan <plan-file> [--nonce <n>]",
|
|
46
|
+
"codex-review code [--nonce <n>] [extra focus...]"
|
|
47
47
|
],
|
|
48
48
|
"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",
|
|
49
49
|
"continue": [],
|
|
50
|
-
"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); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write failure warns, never fails the review",
|
|
50
|
+
"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); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); when the dispatch nonce seam is supplied — the AW_REVIEW_NONCE environment value or its plain-argument equivalent --nonce <n> (one seam: the flag assigns the same value; supplying both with different values refuses pre-spend) — under the safe grammar [A-Za-z0-9._-]{1,64} (anything else refuses pre-spend), the wrapper first mints the finding MANIFEST {schema, backend, nonce, fingerprint, findings} beside the receipts file (agent-workflow-finding-manifest-<backend>-<nonce>.json; atomic, no-clobber — a byte-identical rewrite is an idempotent no-op, different bytes refuse loudly) ORDERED before the receipt append — a failed manifest write EXCLUDES the receipt append, so a nonce-supplied dispatch can never land a receipt without its readable manifest; a nonce-less invocation adds NO nonce field and mints NO finding manifest (the existing wrapperVersion field still changes with each bridge release); a write failure warns, never fails the review",
|
|
51
51
|
"notes": [
|
|
52
|
-
"the review posture banner appends a banner-only timeout=<duration
|
|
52
|
+
"the review posture banner appends a banner-only timeout=<duration> field — exactly the duration handed to timeout(1); the hard-timeout preflight fails CLOSED when no timeout/gtimeout binary exists (the wrapper refuses by name before any CLI run, so an uncapped review run can no longer happen), and the field never enters the receipt posture or the D5 banner↔receipt parity",
|
|
53
53
|
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
|
|
54
54
|
]
|
|
55
55
|
}
|
|
@@ -130,7 +130,8 @@
|
|
|
130
130
|
"whenNotTo": ["a working-tree change set — that is review.code"],
|
|
131
131
|
"invocationRefs": [{ "contractField": "invocations", "index": 0 }],
|
|
132
132
|
"operands": [
|
|
133
|
-
{ "slot": "<plan-file>", "required": true, "description": "the plan file under review" }
|
|
133
|
+
{ "slot": "<plan-file>", "required": true, "description": "the plan file under review" },
|
|
134
|
+
{ "slot": "[--nonce <n>]", "required": false, "description": "the flow dispatch nonce — the plain-argument lane onto the AW_REVIEW_NONCE seam (round-open prints the value; flag and a non-empty env must agree)" }
|
|
134
135
|
],
|
|
135
136
|
"guardrails": [
|
|
136
137
|
{ "value": "read-only sandbox — codex cannot edit, create or delete a file", "enforcement": "enforced", "source": "bin/codex-review.sh" },
|
|
@@ -152,6 +153,7 @@
|
|
|
152
153
|
"whenNotTo": ["a clean tree — the wrapper exits before spending a run"],
|
|
153
154
|
"invocationRefs": [{ "contractField": "invocations", "index": 1 }],
|
|
154
155
|
"operands": [
|
|
156
|
+
{ "slot": "[--nonce <n>]", "required": false, "description": "the flow dispatch nonce — the plain-argument lane onto the AW_REVIEW_NONCE seam (round-open prints the value; flag and a non-empty env must agree)" },
|
|
155
157
|
{ "slot": "[extra focus...]", "required": false, "description": "extra focus words appended to the review directive" }
|
|
156
158
|
],
|
|
157
159
|
"guardrails": [
|
|
@@ -35,8 +35,8 @@ Every dispatch states its ACTUAL posture on ONE stderr line: `codex-exec` emits
|
|
|
35
35
|
`review posture: model=… effort=… tier=… timeout=…`. When you label a dispatch for a user or a
|
|
36
36
|
record, **quote the posture banner verbatim** — the banner is the machine-stated posture; a prose
|
|
37
37
|
re-type drifts. The `timeout=` field is **banner-only** (exactly the duration handed to
|
|
38
|
-
`timeout(1)
|
|
39
|
-
receipt or the banner↔receipt parity.
|
|
38
|
+
`timeout(1)`; on exec `uncapped` when no capping binary is on PATH, while `codex-review` fails
|
|
39
|
+
CLOSED pre-spend there) — informational, never part of a receipt or the banner↔receipt parity.
|
|
40
40
|
|
|
41
41
|
## Exec vs review
|
|
42
42
|
|
|
@@ -146,8 +146,8 @@ from stdout (no `-o` needed). Only a *raw* `codex exec resume` outside the wrapp
|
|
|
146
146
|
A backgrounded/hung run survives otherwise, so both wrappers wrap codex in `timeout`/`gtimeout`
|
|
147
147
|
(`--kill-after=15s`): `CODEX_HARD_TIMEOUT` defaults to **3600s (exec)** / **1800s (review)**, sized for a
|
|
148
148
|
slow `xhigh` run. Exit 124/137 ⇒ "exceeded the hard cap" (raise the cap or narrow the task). If neither
|
|
149
|
-
`timeout` nor `gtimeout` is on `PATH`,
|
|
150
|
-
no-op.
|
|
149
|
+
`timeout` nor `gtimeout` is on `PATH`, `codex-exec` **warns loudly and runs uncapped** — never a silent
|
|
150
|
+
no-op — while `codex-review` **refuses pre-spend** (the fail-closed hard-timeout preflight).
|
|
151
151
|
|
|
152
152
|
## Subscription / config invariant
|
|
153
153
|
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.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",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
### Mode: flow-writer
|
|
2
|
+
|
|
3
|
+
<!-- opt-in-capability: none — the writer serves an already-armed flow (arming is set-flow's, adoption is explicit); the advisor-offer decision rides the Plan-4 dogfood/release wave -->
|
|
4
|
+
|
|
5
|
+
The **explicit flow-store writer** — the answer to every `flow-check` refusal that names a mintable record class. The checker's refusals print the **exact pasteable command** this tool runs; the store's own semantic preflight (lock-serialized, validated, atomic) is the **single legality door** — the writer adds NO second validator, and an illegal transition surfaces the store's own refusal **verbatim**.
|
|
6
|
+
|
|
7
|
+
Run **`node ${CLAUDE_SKILL_DIR}/tools/flow-writer.mjs <arm> …`** — the arm set (Decision 8; every record class a flow refusal names as recovery):
|
|
8
|
+
|
|
9
|
+
| arm | what it records |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `adoption <plan-file> [--label <l>] [--cycle <n>]` | a chain's FIRST record — binds the plan's frontmatter `planId` + content digest (#58); the plan file is only read |
|
|
12
|
+
| `park <planId>` / `resume <planId>` / `complete <planId>` | the explicit plan-lane transitions (#59) — park is a resumable suspension, complete is the plan terminal |
|
|
13
|
+
| `refresh <planId> --cause <text> --refreshed-record <digest>` | a within-step re-attestation binding an existing record (bookkeeping-delta re-attestations ride this) |
|
|
14
|
+
| `re-baseline <planId>` | the disjoint-base-motion recovery — records the pre-motion base (#62) |
|
|
15
|
+
| `rerun-cause --attempt <id> --cause <text>` | legalizes exactly one confirmed final-gates retry (#65) — mint it on the RETRY tree |
|
|
16
|
+
| `down-mark --backend <b> --reason <r> --expires-at <ISO>` | a sticky reviewer down-mark with an explicit TTL instant |
|
|
17
|
+
| `down-mark-up --backend <b> [--target <digest>]` | closes the backend's ACTIVE mark upward (auto-resolved when `--target` is omitted) |
|
|
18
|
+
| `down-mark-clear --backend <b> [--target <digest>]` | clears the backend's ACTIVE mark (auto-resolved when `--target` is omitted) |
|
|
19
|
+
| `degrade-justification --backend <b> [--down-mark <d>] [--degrade-digest <d>]` | binds a core degrade to a then-active down-mark (#25) — both digests resolve automatically; an explicit digest only VERIFIES the resolved authority (a foreign digest never mints), and the store's locked preflight re-refuses a mark closed in the race window |
|
|
20
|
+
| `maintainer-override <planId> --backend <b> --checkpoint-approved` | the checkpoint-approved ship-over-veto record — it **prints the FULL bound set** it is about to record and **requires the explicit flag** (#38); without the flag the bound set still prints and nothing is written |
|
|
21
|
+
| `consult-attestation <planId> --backend <b> --nonce <n> --proposed-fix-digest <d>` | binds a consult to a real dispatch's findings (#11/#33): `findingDigest` is **computed from the `{backend, nonce}`-named finding manifest** beside the receipts file (never hand-supplied); `proposedFixDigest` is the explicit consult-time input; refuses without an open step, or without a readable identity-matching manifest |
|
|
22
|
+
| `round-open <planId> --backend <b> [--backend <b> …] [--step <stepId>] [--new-cycle] [--justification <t>]` | the round's **pre-dispatch half** (#41): mints the round record BEFORE any backend runs — per `--backend` a fresh nonce + the receipts-file byte-length watermark; stdout prints one `dispatch backend=<b> nonce=<n> watermark=<w>` line per dispatch (pass the nonce as `--nonce <n>` on the wrapper invocation — the plain-argument lane onto the `AW_REVIEW_NONCE` seam; hand the pair to `receipt-deadline`). Boundary invocations need `--step`; `--new-cycle` reopens a converged step in the NEXT cycle (the redesign valve); in-step invocations open the NEXT round — **a fingerprint move never rides a revision**. Refuses while the current round holds a pending unjustified dispatch or an undispositioned non-ship receipt (a new round would strand it permanently) |
|
|
23
|
+
| `round-land <planId> [--dispose folded\|queued\|rejected --finding <quote> …]` | the round's **post-arrival half** (#42/#13/#33): revises the ONE round record in place — exactly one fresh code-artifact non-probe line of the dispatched backend **carrying the dispatch's exact nonce** (the wrapper stamps `AW_REVIEW_NONCE` into the receipt — a delayed or cross-chain answer can never cross-bind) past the watermark is the candidate, and only the canonical **ATTESTING** class binds (`receiptDigest` = its canonical digest; `findingManifestDigest` = sha256 of the manifest bytes; both **computed from the files**); a defective answer (unmarked/malformed/ungrounded/unrecognized-verdict) refuses naming its class and the justified-degrade recovery; an ambiguous newer set, a foreign-tree receipt/manifest, or a missing/malformed/symlinked/foreign-identity manifest refuses. `--dispose` appends one disposition whose `findingDigest` = sha256 of the **quoted finding**, verified a substring of a landed manifest whose **byte digest still equals the ledger's** (folded proofs must resolve: a consult-attestation in the flow store or a red-proof in the core store) |
|
|
24
|
+
| `freeze <planId>` / `converged <planId>` | the step terminals, gated on **completeness + the sanctioned-move rule** (no premature terminal): every dispatch landed or **justified-degraded** at the dispatched tree (a core degrade + a mint-time-valid `degrade-justification` at the round's `{base, fingerprint}` — a bare degrade is not base-bound); every landed non-ship receipt rides a round with a non-empty disposition ledger (a form floor — semantic per-finding completeness is an honest limit); the tree sits at the last round's fingerprint or reached it through an **anchored** declared bookkeeping-delta chain + refresh (a pre-round chain never re-certifies a later move). Both walks **re-run on the locked store snapshot** at append time |
|
|
25
|
+
| `unfreeze <planId> [--justification <t>]` | reopens the frozen step (in-step) or the just-converged terminal (boundary); **cap 1 per cycle** — the design's post-freeze checkpoint |
|
|
26
|
+
| `internal-attestation <planId> --lens <l> … [--degraded <b> …] --model <m> [--effort <e>] [--tier <t>] --authority <a>` | the #28 internal-review attestation at the open step's round — **gated on the #68 arming predicate**: EVERY in-flight plan must be covered by an adopted chain (planId + content digest + owner); an uncovered plan refuses **naming the file**, never a relaxation |
|
|
27
|
+
| `write-plan-id <plan-file> --plan-id <id>` | adds the frontmatter `planId` line — bounded to an EXISTING regular file under `docs/plans/` (never a symlink), contained-atomic, same-id idempotent, **different-id refuses** (#58) |
|
|
28
|
+
|
|
29
|
+
Operand shapes: a positional operand may follow a literal `--` and a value flag accepts `--flag=<value>` — the lanes a leading-dash id or value rides; the checker's printed recoveries compose exactly these shapes. Every arm computes its **tree context** (owner, base, fingerprint; cycle/round from the chain walk) — you never hand-supply tree identity. Chain arms **refuse a foreign worktree's chain** (#57): move a chain only from the worktree that adopted it.
|
|
30
|
+
|
|
31
|
+
**Round-machinery caps (enforced at the arms; the store owns transition legality):** HARD_MAX **3 rounds per {cycle, step}** and **1 post-freeze unfreeze per cycle**. Every cap refusal is **self-servable** (Decision 8 — never a human wait-state): the over-cap mint requires an explicit non-empty `--justification <text>`, echoed in the mint report — the over-cap record itself is the durable trail, and the justification surfaces at the phase's commit ask; an in-cap `--justification` refuses (an input that binds nothing is never silently dropped). `round-open` also refuses while the current round holds a pending undegraded dispatch — a new round would strand it unlandable.
|
|
32
|
+
|
|
33
|
+
**Exit codes:** `0` success (incl. the `write-plan-id` idempotent no-op); `2` usage; `1` refusal (a store STOP verbatim, a derivation failure, or the missing checkpoint flag).
|
|
34
|
+
|
|
35
|
+
Output is **English/structured** — **localize it to the user's conversational language** when you narrate.
|
|
36
|
+
|
|
37
|
+
**Invariants:** writer (appends to the git-common-dir flow store; `write-plan-id` writes one plan file) · never commits · never runs a subscription CLI · the store preflight is the single legality door · refusal recoveries paste back into this tool.
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- opt-in-capability: gates-declaration -->
|
|
4
4
|
|
|
5
|
-
The **generic project gate runner** — it batches the project's OWN declared verification commands into one run. The runner itself **writes nothing on a plain run, never commits, and never runs a subscription CLI**; what it EXECUTES is the project's own declaration, with the caller's privileges (trust posture: a batching convenience over commands the project already runs by hand — **not a sandbox**).
|
|
5
|
+
The **generic project gate runner** — it batches the project's OWN declared verification commands into one run. The runner itself **writes nothing on a plain run, never commits, and never runs a subscription CLI**; what it EXECUTES is the project's own declaration, with the caller's privileges (trust posture: a batching convenience over commands the project already runs by hand — **not a sandbox**). Two modes write state: **`--final`** runs the FULL declared matrix as the D3(a) final verification run and mints the receipt the commit guard consumes (step 4), and an ARMED **`--pre-review`** records its subset attempt in the flow store (step 3; unarmed repos byte-unchanged).
|
|
6
6
|
|
|
7
7
|
Run `node ${CLAUDE_SKILL_DIR}/tools/run-gates.mjs [--cwd <project>] [--only <id>]… [--final]`:
|
|
8
8
|
|
|
9
9
|
1. **Reads `docs/ai/gates.json`** (strict JSON, hand-editable; seeded from `references/templates/gates.json`). Each gate is `{ id, title, cmd }` — `id` a unique kebab handle, `cmd` **ONE bash command line** (brace/glob expansion works; a host without bash gets a loud preflight error, exit 6 — never a silent reinterpretation under another shell). The declaration names **WHAT to check, never who executes it** — the schema has no lane/model/routing fields and rejects unknown keys loudly.
|
|
10
10
|
2. **Runs each gate from the project root** and prints a per-gate **PASS/FAIL table** plus **one machine-readable summary line** as the last line (`[run-gates] status=… gates=… passed=… failed=… failed_ids=…`). A failing gate's own output is preserved **verbatim** (triage without re-running); a green gate's output is not echoed; gates after a failure still run. **Exit 0 iff all selected gates are green.**
|
|
11
|
-
3. **Honest outcomes, each distinct — never a silent green:** a **missing** declaration (exit 3 — the report names the recovery: create `docs/ai/gates.json` from the template; `upgrade` re-seeds a missing one), an **empty** `gates` list (exit 4), a **malformed/invalid** declaration (exit 5, loud `path: reason`). Repeatable **`--only <id>`** re-runs a subset; an unknown id is a loud usage error (exit 2).
|
|
12
|
-
4. **`--final`** — the D3(a) final verification run: it REFUSES `--only` (a subset never attests) and a declaration lacking the canonical core checks (ONE plain invocation each of the kit's OWN `review-state.mjs --check` and `coverage-check.mjs --check`, the checker declared LAST — a masked form, a compound, or a lookalike path never counts); deletes the stale git-dir lcov before the suite; exports `AW_GIT_DIR` + `AW_LCOV_FILE` to every gate cmd; records EVERY attempt (start + completed green/red) in the core-evidence store via its sole writer; and binds the receipt to { fingerprint before/after · the full declaration · per-gate results · the canonical red-proof + degrade evidence hashes · the sha of the lcov the checker actually read (exactly ONE `lcov-sha256` machine line, end-re-hashed) }. An artifact moving UNDER the run is a named `integrityFailure
|
|
11
|
+
3. **Honest outcomes, each distinct — never a silent green:** a **missing** declaration (exit 3 — the report names the recovery: create `docs/ai/gates.json` from the template; `upgrade` re-seeds a missing one), an **empty** `gates` list (exit 4), a **malformed/invalid** declaration (exit 5, loud `path: reason`). Repeatable **`--only <id>`** re-runs a subset; an unknown id is a loud usage error (exit 2). **`--pre-review`** runs the DERIVED mechanical subset (#66): the full matrix minus every gate whose cmd is a canonical kit checker invocation — derivation **matches canonical checker paths in the cmd strings** (realpath-resolved `--check` forms of review-state / commit-guard / coverage-check / flow-check, never a project-authored id), so a project abstracting a checker behind its own script declares it in `flow.pregateExclude` (an unknown id refuses loudly, exit 5). A failing subset gate gets the review-dependent diagnosis, naming the mechanical reset (a declared exclude changes the `subsetDigest`). **Under an ARMED flow (exactly one open adopted chain owned by this worktree) every subset run is RECORDED** as a `subset-attempt` via the flow store's locked append factory — the context keys `{planId, cycle, stepId, foldBatch, subsetDigest}`; index + hard-stop state are computed under the lock against the pre-run identity. **Hard stop (Decision 7/8):** the SECOND red records and exits red; past two reds every attempt needs `--diagnosis "<non-empty, byte-distinct from the prior>"` (recorded, self-servable); the THIRD red EXHAUSTS the context — further solo runs refuse, and only a recorded fresh-eyes consult verdict (a grounded bridge consult-attestation at this round context) reopens ONE further attempt. Armed-but-unrecordable (zero/several open chains, broken store) refuses loudly; a spawn failure records NO attempt; unarmed repos stay byte-unchanged. Mutually exclusive with `--only`/`--final` (exit 2); plain and `--final` runs never load the config.
|
|
12
|
+
4. **`--final`** — the D3(a) final verification run: it REFUSES `--only` (a subset never attests) and a declaration lacking the canonical core checks (ONE plain invocation each of the kit's OWN `review-state.mjs --check` and `coverage-check.mjs --check`, the checker declared LAST — a masked form, a compound, or a lookalike path never counts); deletes the stale git-dir lcov before the suite; exports `AW_GIT_DIR` + `AW_LCOV_FILE` to every gate cmd; records EVERY attempt (start + completed green/red) in the core-evidence store via its sole writer; and binds the receipt to { fingerprint before/after · the full declaration · per-gate results · the canonical red-proof + degrade evidence hashes · the sha of the lcov the checker actually read (exactly ONE `lcov-sha256` machine line, end-re-hashed) · **`evidenceHashes.flow`** when a flow store exists (D10: the sha of the OWNER-SCOPED flow projection — foreign worktrees never move it, except same-fingerprint planId-less globals, which share this tree's decision context; absent store → absent field; a broken store refuses up front) }. An artifact moving UNDER the run — the flow projection included — is a named `integrityFailure`; the receipt lands red. Stated residual: the movement arm is best-effort — an append racing the receipt write is refused at commit by the guard. A receipt that cannot be written is its own distinct outcome (exit 8): green gates never read as success without it. `${CLAUDE_SKILL_DIR}/references/modes/commit-guard.md` consumes the receipt at commit time (the guard re-hashes the live projection against it — a post-final append, or the store vanishing, refuses the commit).
|
|
13
13
|
|
|
14
14
|
The declaration is **seeded at bootstrap** (the template loop, `${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md` step 6) and **ensured-if-missing on upgrade** from THIS kit's own template twin (`${CLAUDE_SKILL_DIR}/references/modes/upgrade.md` step 3) — independent of the installed memory substrate's age; an existing file is always **preserved byte-for-byte**. It is deliberately **not** a delegation-required memory asset: gates are optional, and absence is an honest runner outcome, not a deployment failure.
|
|
15
15
|
|
|
@@ -21,4 +21,4 @@ Declared gates can also be **auto-approved** (no permission prompt on a byte-exa
|
|
|
21
21
|
|
|
22
22
|
**Consent-gated filling — the init preview, not part of the runner (D9).** The template `gates.json` is seeded EMPTY; FILLING it is a consented preview at init (`node ${CLAUDE_SKILL_DIR}/tools/gates-init.mjs --cwd <project>`, dry-run by default — prints the derived entries and **writes NOTHING**; `--apply [--only <id>]…` appends exactly the consented entries on your explicit yes; append-only, id collisions refused). The offer derivation is **closed-world** (AD-052): only a terminating-class script NAME (test / lint / type-check / build — never dev/watch/serve, never a write-mode or release/publish/deploy variant) whose BODY is a member of the literal runner allowlist is offered — membership, never blocklist screening: the worst case is a legit command not offered, never a dangerous one offered. The offered cmd is the uniform hook-free **`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <allowlisted-body>`** — `exec` runs a command, not a named script, so no pre/post hook can fire (npm/pnpm/yarn alike; never `<pm> run <name>`, which re-exposes hooks), and the Corepack env prefix blocks a hostile `packageManager` pin from fetching the PM binary before exec. npm is pinned `--offline --script-shell /bin/sh`; pnpm/yarn refuse an absent runner without network (a user-installed cache/global/PATH runner executing is user machine state — part of the disclosed residual); a family without a verified fail-closed exec contract is WITHHELD loudly. **Disclose before the yes** (the preview prints it): gates.json is a PRIVILEGED file — the wired hook auto-approves byte-exact declared commands — and a script gate runs project-controlled tooling the preview does not sandbox (safe-by-construction = the OFFER DERIVATION). At upgrade the only gates.json writer is the consented legacy migration (`${CLAUDE_SKILL_DIR}/references/modes/upgrade.md`).
|
|
23
23
|
|
|
24
|
-
**Invariants:** the runner writes nothing on a plain run; `--final`'s ONE evidence write rides the core-evidence sole writer (the runner never opens the store itself) · never commits · never runs a subscription CLI · executes only the project's OWN declared commands (never a kit-invented one) · the bash contract fails loud, never reinterprets · gates-init is a separate consent-per-run preview — append-only, never pre-approved by any velocity tier.
|
|
24
|
+
**Invariants:** the runner writes nothing on a plain run; `--final`'s ONE evidence write rides the core-evidence sole writer (the runner never opens the store itself) · an ARMED `--pre-review`'s ONE flow write rides the flow store's locked append factory (unarmed: byte-unchanged) · never commits · never runs a subscription CLI · executes only the project's OWN declared commands (never a kit-invented one) · the bash contract fails loud, never reinterprets · gates-init is a separate consent-per-run preview — append-only, never pre-approved by any velocity tier.
|
|
@@ -11,9 +11,11 @@ The two v1 activities (canon in the **installed engine**, `references/procedures
|
|
|
11
11
|
|
|
12
12
|
Run **`node ${CLAUDE_SKILL_DIR}/tools/procedures.mjs <activity> [--override <slot>=<recipe>]… [--json]`**. It reads the activity's steps live from the engine and prints them **verbatim**, then the **resolved effective recipe per slot** from the per-project config + the read-only backend detector:
|
|
13
13
|
|
|
14
|
-
1. **Config = `docs/ai/orchestration.json`** — strict JSON, **agent-writable via `/agent-workflow-kit set-recipe` (`${CLAUDE_SKILL_DIR}/references/modes/set-recipe.md`) OR hand-edited** (the kit reads + validates it; `procedures`/`recipes` stay read-only — the writer is `set-recipe`). Shape: `{ "<activity>": { "<slot>": "<recipe>" } }`; all slots optional (an absent slot → its computed default, stated); an optional `"_README"` string is allowed + ignored; a
|
|
14
|
+
1. **Config = `docs/ai/orchestration.json`** — strict JSON, **agent-writable via `/agent-workflow-kit set-recipe` (`${CLAUDE_SKILL_DIR}/references/modes/set-recipe.md`) OR hand-edited** (the kit reads + validates it; `procedures`/`recipes` stay read-only — the writer is `set-recipe`). Shape: `{ "<activity>": { "<slot>": "<recipe>" } }`; all slots optional (an absent slot → its computed default, stated); an optional `"_README"` string is allowed + ignored; a `"flow"` object must carry the NUMERIC `"schema": 1` (the kit's accepted flow schema version) and validates against the CLOSED structural schema-1 key set (unknown flow keys and malformed per-key shapes refuse loudly; deep environment floors stay on the `set-flow` arming path). `review` accepts `solo|reviewed|council`; `execute` accepts `solo|delegated`. Seeded by `init` (a user-editable template) — see `${CLAUDE_SKILL_DIR}/references/modes/bootstrap.md`.
|
|
15
15
|
2. **Default resolution (config silent):** `review` → Reviewed if any review-capable backend is `ready`, else Solo (never Council by default); `execute` → Solo (Delegated is opt-in). **Degradation:** a config/computed default degrades **gracefully with a stated reason** (Council → Reviewed → Solo; Delegated → Solo); a per-run **`--override <slot>=<recipe>`** that can't be satisfied degrades **loudly** (a flagged warning, so you tell the user) — but is **still exit 0** (a valid request that gracefully degraded).
|
|
16
|
-
3. **Exit codes:** `0` success; `2` usage (unknown `<activity>` / bad `--override` — a bare `--override <recipe>`, an unknown slot, an invalid recipe-for-slot, or a duplicate slot); `1` config error (malformed / schema-invalid / unreadable `orchestration.json`) **or** engine error (the installed engine is absent / invalid / **too old** to ship `references/procedures.md` — upgrade it with `npx @sabaiway/agent-workflow-engine@latest init`). A `1`/`2` failure is loud (`path: reason`), never a silent fallback. **Lagging-kit honesty:** a kit predating the `"flow"` key that reads a config carrying one fails this config load loudly (exit `1`, reddening its full gate matrix);
|
|
16
|
+
3. **Exit codes:** `0` success; `2` usage (unknown `<activity>` / bad `--override` — a bare `--override <recipe>`, an unknown slot, an invalid recipe-for-slot, or a duplicate slot); `1` config error (malformed / schema-invalid / unreadable `orchestration.json`) **or** engine error (the installed engine is absent / invalid / **too old** to ship `references/procedures.md` — upgrade it with `npx @sabaiway/agent-workflow-engine@latest init`). A `1`/`2` failure is loud (`path: reason`), never a silent fallback. **Lagging-kit honesty:** a kit predating the `"flow"` key that reads a config carrying one fails this config load loudly (exit `1`, reddening its full gate matrix); the `set-flow` arming path now enforces the declared `kitMinVersion` floor with a null-guarded comparison (an unparseable version never passes), while tolerate-first ordering remains the only protection for readers older than the `"flow"` key itself — no in-config floor can reach a kit that dies on the unknown key.
|
|
17
|
+
|
|
18
|
+
**Flow armed-halves block (session-start read side).** When the config carries a `flow` block, the advisor also renders `Flow (schema 1) — armed halves (config · chain · bookkeeping):` — the **config half** (preset · councilRounds · kitMinVersion), the **chain half** (a light read-only probe of the flow store on the checker's fixed path: ARMED at an adoption record, UNARMED for an absent or unadopted store, fail-closed BROKEN wording for a malformed one), and the **bookkeeping half** (each declared path: declared non-excluded — the tracked-file floor verifies on the `set-flow` arming path — vs loudly DECLARED-EXCLUDED). A config with no `flow` block renders byte-identically to before and pays no store probe.
|
|
17
19
|
|
|
18
20
|
**Cap-soft-skip degradation (the feature's only AUTO route).** The activity procedures are auto-discoverable only through the one-line **`workflow:methodology`** pointer (this kit + the engine carry `disable-model-invocation:true`, so NL like "write a plan" does **not** auto-load this skill). On a deployment whose methodology pointer was cap-soft-skipped — or whose pre-existing customized pointer lacks the procedures clause — the procedures are still reachable by **explicitly** invoking `/agent-workflow-kit procedures`; surface that plainly rather than treating it as a gap.
|
|
19
21
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
### Mode: receipt-deadline
|
|
2
|
+
|
|
3
|
+
<!-- opt-in-capability: none — a read-only per-dispatch waiter; it guards no repeatable surface (the review obligations gate is review-state, already covered) -->
|
|
4
|
+
|
|
5
|
+
The **per-dispatch receipt-ARRIVAL deadline runner** (flow-orchestration #41/#50): it waits for ONE dispatched review to **answer**, never for the review obligations to be satisfied — satisfaction is receipt ARRIVAL past the watermark — a strictly-newer parseable receipt line from the dispatched backend (or its nonce-matched finding manifest, preferred when present) — never obligation satisfaction. For "block until `--check` would PASS" use `review-state --await` instead; this runner answers the narrower per-dispatch question "did THIS dispatch come back at all?", which is what the round dispatch ledger's deadline discipline needs.
|
|
6
|
+
|
|
7
|
+
Run **`node ${CLAUDE_SKILL_DIR}/tools/receipt-deadline.mjs --backend <name> --watermark <bytes> [--nonce <nonce>] [--timeout <s>]`**:
|
|
8
|
+
|
|
9
|
+
1. **`--watermark`** is the receipts-file **byte length minted BEFORE the dispatch** (the round dispatch-ledger `receiptWatermark`). The runner additionally binds the file **prefix below that offset IN-PROCESS at start**: a shrunken file or a rewritten prefix refuses **loudly for the lifetime of the run** — a truncate-and-rewrite can never masquerade as arrival — and the watermark must sit **on a line boundary** (a positive offset whose preceding byte is not a newline refuses loudly at start: the pre-dispatch tail was unterminated, so an appended receipt would physically continue that malformed line). Honest limit: the prefix binding is a **runtime guard, never a persisted proof** (the persisted ledger watermark stays the plain integer).
|
|
10
|
+
2. **Arrival** = a newline-terminated, parseable receipt line **from that backend** starting at/after the watermark offset. A malformed line never satisfies (and never masks a later valid one); a foreign backend's line never satisfies; a partial (unterminated) append is not a receipt yet.
|
|
11
|
+
3. **`--nonce`** (the dispatch nonce under the safe grammar `[A-Za-z0-9._-]{1,64}`): when the `{backend, nonce}`-named finding manifest exists beside the receipts file, the runner **prefers that correlation** — the manifest is minted atomically BEFORE the receipt append, and it carries the dispatch identity, so it can never be another dispatch's receipt. A malformed or foreign-identity manifest refuses loudly.
|
|
12
|
+
4. **Timeout** (default 900s) fires ONLY when no receipt landed, and its wording **names the watermark**. An authoritative NEGATIVE verdict is not this tool's business — arrival is arrival, whatever the verdict says; the obligations verdict lives in `review-state`.
|
|
13
|
+
|
|
14
|
+
**Exit codes:** `0` arrived; `1` timeout or a loud refusal (shrunken/rewritten store, malformed manifest, no git tree); `2` usage (including an unsafe nonce).
|
|
15
|
+
|
|
16
|
+
**Invariants:** read-only · never writes, never commits, never runs a subscription CLI · the clock is injectable for tests · the receipts path is `<git dir>/agent-workflow-review-receipts.jsonl` (`AW_REVIEW_RECEIPTS` overrides).
|
|
@@ -9,7 +9,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/review-state.mjs [--check] [--json]`:
|
|
|
9
9
|
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.
|
|
10
10
|
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 CONFIGURED solo recipe (a computed readiness-degrade NEVER silently becomes solo — it needs the explicit degrade record below), 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 SATISFIED for the current tree. **A clean-tree PASS under a NON-SOLO review obligation is never silent about a latent arm:** when a `reviewed` or `council` recipe is configured, it NAMES every plan in flight and states that this gate arms as soon as the tree is dirty — the condition is discoverable BEFORE it blocks, instead of surfacing at the worst moment (a pending commit, or the landing of a feature worktree when main first turns dirty). Nothing is announced under a configured `solo` recipe or a non-git cwd, where the gate can never arm. **Satisfaction is ship-class-only on the LATEST NORMAL receipt (D3(b)):** per backend, the latest probe-free current-fingerprint receipt is selected FIRST and THEN verdict-checked — only the recognized ship-class vocabulary (`ship` / `ship with nits`) satisfies; a recognized NEGATIVE (`revise` / `rethink` / `rework`) is an authoritative VETO (an earlier ship never survives a later revise); an UNRECOGNIZED verdict (e.g. `unknown` from a dead run) fails CLOSED unconditionally — a later `unknown` never lets an earlier SHIP stand, and a fresh normal re-run supersedes it. **The ONLY escape is an explicit degrade RECORD** (`core-evidence.mjs degrade --backend <name> --reason "…"`, fingerprint-bound to the current tree) — and never all backends: ≥1 non-degraded ship-class receipt is required whenever ≥1 backend is configured; a malformed evidence store denies the escape fail-closed but never fails an independently-satisfied tree. Exit 1 otherwise — missing, **stale** (ANY edit after a review moves the fingerprint), ungrounded, vetoed, or unrecognized. 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 — per receipt, so a real review at the same fingerprint still satisfies. **Silence is not a declaration:** a malformed *or* absent probe marker is rejected fail-closed and stated in the check line. 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.
|
|
11
11
|
3. **Wire it as a gate by hand OR via the explicit-consent init preview — never without consent (AD-021/D9).** 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 gates-init preview (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`, consent-fill 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 — and `run-gates --final` requires this check among its canonical core gates.
|
|
12
|
-
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 inherits the `--check` decision whole — a backend with a current-tree degrade RECORD stops being waited on (the shared decideCheck), so you never hand-`--await` around a known degrade. Still read-only (it re-reads the receipts + the evidence store); solo / no-plan / clean-tree resolve instantly.
|
|
12
|
+
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 inherits the `--check` decision whole — a backend with a current-tree degrade RECORD stops being waited on (the shared decideCheck), so you never hand-`--await` around a known degrade. **An AUTHORITATIVE veto terminates the wait loudly BEFORE the deadline** (flow-orchestration Decision 4/#50): a landed recognized-negative verdict for the current tree is the dispatched review's *answer*, so `--await` exits 1 with `VETO — …` immediately instead of misclassifying it as a timeout — only a fresh review can move a landed negative, never waiting. Still read-only (it re-reads the receipts + the evidence store); solo / no-plan / clean-tree resolve instantly. For waiting on **one dispatch's arrival** (never obligation satisfaction) use the receipt-arrival deadline runner, `${CLAUDE_SKILL_DIR}/references/modes/receipt-deadline.md`.
|
|
13
13
|
|
|
14
14
|
**Human residual (stated, accepted):** `git commit --no-verify` and receipt-file deletion/forgery remain possible — this is a self-discipline mechanism against silent process drift, not a security boundary.
|
|
15
15
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
### Mode: set-flow
|
|
2
|
+
|
|
3
|
+
<!-- opt-in-capability: none — the flow arms only by explicit maintainer action at a plan boundary (#52); the advisor-offer decision rides the Plan-4 dogfood/release wave -->
|
|
4
|
+
|
|
5
|
+
The **arming writer** for the `flow` block of `docs/ai/orchestration.json` — the answer to *"turn the review-flow machinery on for this project."* **Division of labor (AD-025 discipline):** YOU turn the user's plain language into explicit `--preset` / `--set <key>=<value>` / `--unset <key>` ops; the KIT does the deterministic parse → merge → floor-check → preview → write. It **previews by default** (writes nothing); `--write` applies. It **never runs a backend and never commits**. Hand-editing the config stays fully supported.
|
|
6
|
+
|
|
7
|
+
Run **`node ${CLAUDE_SKILL_DIR}/tools/set-flow.mjs [--preset <council|reviewed|internal-only>] [--set <key>=<value>]… [--unset <key>]… [--write] [--json]`**:
|
|
8
|
+
|
|
9
|
+
1. **Merge (#30):** the preset is a **seed** — its values come verbatim from the kit's schema-1 canon (the same literal fixture the config validator pins); explicit `--set` keys win over the seed, the seed wins over the existing block, and `schema` is pinned by the kit (never an op). `candidates` are never seeded — they name the project's REAL backends (`--set candidates=codex:review,agy:review`). The **merged flow block previews** before any write.
|
|
10
|
+
2. **Arming floors (#31 — the config validator stays shape-only; every DEEP floor lives on this path):**
|
|
11
|
+
- **`kitMinVersion` (Decision 6, #54):** the null-guarded semver comparison — an unparseable version on either side **never** passes (the bare `>= 0` shape fails open on null and is banned).
|
|
12
|
+
- **`debtQueue` / `convergenceSummary` (#37/#69):** each declared path must be a **single regular TRACKED file** or carry its explicit `…Excluded: true` declaration (**loud**); never a symlink or directory; never under `docs/ai/`; never a literal substring of any declared gate `cmd` (`docs/ai/gates.json`).
|
|
13
|
+
- Floors hold on the preview **and** the write lane — exit `1`, nothing written, until every floor passes.
|
|
14
|
+
3. **The disclosed residual (printed on every floor evaluation):** the bookkeeping floors decide only what is decidable at arming time: a gate command reading the declared path INDIRECTLY (through its own script), content-level abuse inside the file, and a path re-pointed after arming stay undecided — bookkeeping WRITES are bound by digest and custody proof at the checker instead (#37/#69); this line is the honest boundary, not a pretended rule
|
|
15
|
+
4. **After a successful `--write`:** only the **config half** is armed — the **chain half** arms at plan adoption (`flow-writer adoption <plan-file>`), and `gates-init` offers the full checker TRIO (review-state + coverage-check + flow-check) for a flow-carrying config.
|
|
16
|
+
5. **Exit codes:** `0` success/preview; `2` usage (bad key/value/flag, a bare `--write`); `1` floor refusal, config error (the file is left untouched), or a write STOP (no deployment / symlinked config).
|
|
17
|
+
|
|
18
|
+
**Lagging-kit honesty (verbatim contract):** a kit predating the `"flow"` key that reads a config carrying one fails this config load loudly (exit `1`, reddening its full gate matrix); the `set-flow` arming path now enforces the declared `kitMinVersion` floor with a null-guarded comparison (an unparseable version never passes), while tolerate-first ordering remains the only protection for readers older than the `"flow"` key itself — no in-config floor can reach a kit that dies on the unknown key
|
|
19
|
+
|
|
20
|
+
Output is **English/structured** — **localize it to the user's conversational language** when you narrate.
|
|
21
|
+
|
|
22
|
+
**Invariants:** writer (writes only `docs/ai/orchestration.json`) · never commits · never runs a subscription CLI · previews by default · every deep floor on this path only · hand-edit stays first-class.
|