@sabaiway/agent-workflow-kit 5.1.0 → 5.3.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 +95 -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 +17 -7
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +156 -36
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +228 -4
- 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 +10 -7
- package/bridges/codex-cli-bridge/references/driving-codex.md +7 -5
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +26 -12
- 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/references/scripts/archive-decisions.mjs +340 -15
- package/references/scripts/archive-decisions.test.mjs +522 -2
- 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 +33 -11
- package/tools/dispatch-record.mjs +926 -0
- package/tools/doc-parity.mjs +21 -6
- package/tools/flow-check.mjs +842 -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.4.0",
|
|
7
7
|
"posture": { "model": "gpt-5.6-sol", "effort": "xhigh", "tier": null },
|
|
8
8
|
"provides": ["execute", "review"],
|
|
9
9
|
"roles": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"exec posture banner: ONE stderr line before dispatch states the ACTUAL run posture — exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> timeout=… — from RESOLVED post-validation values; the resume id is validated pre-spend, and control bytes in any banner field refuse pre-spend",
|
|
32
32
|
"threat model: the sidecar byte and grammar screens detect corrupted input under a trusted parent environment. A hostile parent environment — including exported shell functions or PATH substitution of core/backend commands — is outside the threat model and can substitute the backend itself. Targeted shadow-proof resolution protects banner/dispatch honesty from accidental shadowing; it is not an environment security boundary",
|
|
33
33
|
"the exec posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it is never persisted in a receipt or session sidecar",
|
|
34
|
-
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
|
|
34
|
+
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts",
|
|
35
|
+
"every-run nested-sandbox scan (DUAL policy, deliberately two different rules): the scan runs on EVERY completed run, not only a failed one, because a run that SURVIVES the nested-sandbox failure exits 0 with an ungrounded answer and nothing said so. Failed run (rc != 0): the existing loose whole-trace combination rule prints the recovery hint. Successful run (rc == 0): a warning fires ONLY on precise per-item evidence — both a sandbox-mechanism token AND a permission/read-only failure token inside the aggregated_output of ONE command_execution item whose failure is PROVEN (a nonzero exit_code, or the serialized status \"failed\"); a null exit_code is never failure by itself, tokens split across two items never fire, and a successful command's output never fires. The answer is printed FIRST on stdout, then the warning on stderr. HONEST RESIDUAL: the exit status does NOT change on that lane (a distinct nonzero exit would give a heuristic scan DENY polarity, refusing real work whenever the scan over-warns), so an orchestrator keying on exit status alone can still bank an ungrounded answer — the stderr warning is the signal"
|
|
35
36
|
]
|
|
36
37
|
}
|
|
37
38
|
},
|
|
@@ -42,14 +43,14 @@
|
|
|
42
43
|
"output": "advisory",
|
|
43
44
|
"contract": {
|
|
44
45
|
"invocations": [
|
|
45
|
-
"codex-review plan <plan-file>",
|
|
46
|
-
"codex-review code [extra focus...]"
|
|
46
|
+
"codex-review plan <plan-file> [--nonce <n>]",
|
|
47
|
+
"codex-review code [--nonce <n>] [extra focus...]"
|
|
47
48
|
],
|
|
48
49
|
"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
50
|
"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",
|
|
51
|
+
"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
52
|
"notes": [
|
|
52
|
-
"the review posture banner appends a banner-only timeout=<duration
|
|
53
|
+
"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
54
|
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
|
|
54
55
|
]
|
|
55
56
|
}
|
|
@@ -130,7 +131,8 @@
|
|
|
130
131
|
"whenNotTo": ["a working-tree change set — that is review.code"],
|
|
131
132
|
"invocationRefs": [{ "contractField": "invocations", "index": 0 }],
|
|
132
133
|
"operands": [
|
|
133
|
-
{ "slot": "<plan-file>", "required": true, "description": "the plan file under review" }
|
|
134
|
+
{ "slot": "<plan-file>", "required": true, "description": "the plan file under review" },
|
|
135
|
+
{ "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
136
|
],
|
|
135
137
|
"guardrails": [
|
|
136
138
|
{ "value": "read-only sandbox — codex cannot edit, create or delete a file", "enforcement": "enforced", "source": "bin/codex-review.sh" },
|
|
@@ -152,6 +154,7 @@
|
|
|
152
154
|
"whenNotTo": ["a clean tree — the wrapper exits before spending a run"],
|
|
153
155
|
"invocationRefs": [{ "contractField": "invocations", "index": 1 }],
|
|
154
156
|
"operands": [
|
|
157
|
+
{ "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
158
|
{ "slot": "[extra focus...]", "required": false, "description": "extra focus words appended to the review directive" }
|
|
156
159
|
],
|
|
157
160
|
"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
|
|
|
@@ -75,9 +75,11 @@ codex-review code "focus on the reducer and its tests"
|
|
|
75
75
|
`codex-exec` prepends an **orchestrator execution contract**: work in the current tree, never
|
|
76
76
|
git-write, *obey* the already-merged `AGENTS.md` (Hard Constraints + declared gates), self-review the
|
|
77
77
|
diff (incl. untracked files), run the project's declared gates (STOP if none are declared), don't
|
|
78
|
-
commit, report blockers. It captures only codex's **final message** (`-o
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
commit, report blockers. It captures only codex's **final message** (`-o`) and, on a **non-resume**
|
|
79
|
+
run, records the session id to `${CODEX_SESSION_FILE:-./.codex-last-session}`. The JSON event stream
|
|
80
|
+
+ reasoning go to a run trace that is **read before it is discarded** — fresh runs and resumes share
|
|
81
|
+
ONE capture posture, and the wrapper scans that trace on every completed run for a nested-sandbox
|
|
82
|
+
failure the run SURVIVED (see [`sandbox-and-flags.md`](sandbox-and-flags.md#clean-output-capture)).
|
|
81
83
|
|
|
82
84
|
## Resume — iterate without re-sending context
|
|
83
85
|
|
|
@@ -39,14 +39,27 @@ and passes no separate network flag — the `sandbox_workspace_write.*` config (
|
|
|
39
39
|
|
|
40
40
|
### Clean output capture
|
|
41
41
|
|
|
42
|
-
`-o`/`--output-last-message` writes ONLY
|
|
43
|
-
stream (incl. `thread.started`, which
|
|
42
|
+
**ONE capture posture, fresh runs and resumes alike.** `-o`/`--output-last-message` writes ONLY
|
|
43
|
+
codex's final message; `--json` streams the structured event stream (incl. `thread.started`, which
|
|
44
|
+
carries the session id) into the run trace, with stderr merged into it; `--color never` +
|
|
44
45
|
`-c hide_agent_reasoning=true` + `-c model_reasoning_summary=none` strip colour and chain-of-thought.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
`codex exec resume` accepts `-o` and `--json` too (live-probed on codex-cli 0.147.0), so the resume
|
|
47
|
+
lane is no longer the odd one out — it used to print its final message straight to stdout with the
|
|
48
|
+
event stream nowhere, which left the very mode a nested-sandbox incident fired on with no evidence
|
|
49
|
+
surface at all. Net effect: the wrapper prints just the final answer. **Reasoning still runs at
|
|
50
|
+
`xhigh`** — quality is unchanged; only the *noise* is dropped. On success `codex-exec` extracts the
|
|
51
|
+
session id from the trace and records it to `${CODEX_SESSION_FILE:-./.codex-last-session}` (so
|
|
52
|
+
`--resume-last` can find it) and echoes `session: <id>` to stderr. On a missing/empty final-message
|
|
53
|
+
file it falls back to the trace tail (loud, never silent).
|
|
54
|
+
|
|
55
|
+
The trace is **read before it is discarded**, on every completed run. A `command_execution` item in
|
|
56
|
+
the `--json` stream carries `{command, aggregated_output, exit_code, status}`, and `aggregated_output`
|
|
57
|
+
is that tool call's combined stdout+stderr — exactly where an absorbed `bwrap` failure lands. The
|
|
58
|
+
scan is line-oriented and tolerant of the mixed stream the merge produces: a line that is not a
|
|
59
|
+
well-formed item is simply not evidence, never a parse error. See the dual policy in
|
|
60
|
+
[`../capability.json`](../capability.json) `roles.execute.contract.notes` — the failed-run arm keeps
|
|
61
|
+
its loose whole-trace rule, and the successful-run arm demands both tokens inside ONE item whose
|
|
62
|
+
failure is proven.
|
|
50
63
|
|
|
51
64
|
## Quality-first guard (pinned model & effort)
|
|
52
65
|
|
|
@@ -134,20 +147,21 @@ the prompt fence ("do not read outside the working tree, except the precomputed-
|
|
|
134
147
|
|
|
135
148
|
`codex exec resume` re-dispatches an existing session without re-sending context. It **rejects the
|
|
136
149
|
posture flags** `-s`/`--add-dir`/`-C` and **resets** the sandbox/approval/network posture (it DOES
|
|
137
|
-
accept `-c`/`-m`/`--last`/`-o`/`--json
|
|
150
|
+
accept `-c`/`-m`/`--last`/`-o`/`--json`/`--color`, live-probed on codex-cli 0.147.0).
|
|
138
151
|
The `codex-exec --resume`/`--resume-last` entrypoint handles the reset: it restates the entire policy
|
|
139
152
|
via `-c` (`sandbox_mode=workspace-write`, `approval_policy=never`,
|
|
140
153
|
`sandbox_workspace_write.network_access=false`) plus the pinned `-m`/effort and `--ignore-user-config`,
|
|
141
|
-
reads the session id from the sidecar (or an argument), and
|
|
142
|
-
|
|
154
|
+
reads the session id from the sidecar (or an argument), and applies the **same** capture posture as a
|
|
155
|
+
fresh run — `-o` for the final message, `--json` + `--color never` into the trace. Only a *raw*
|
|
156
|
+
`codex exec resume` outside the wrapper loses the posture.
|
|
143
157
|
|
|
144
158
|
## Hard timeout
|
|
145
159
|
|
|
146
160
|
A backgrounded/hung run survives otherwise, so both wrappers wrap codex in `timeout`/`gtimeout`
|
|
147
161
|
(`--kill-after=15s`): `CODEX_HARD_TIMEOUT` defaults to **3600s (exec)** / **1800s (review)**, sized for a
|
|
148
162
|
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.
|
|
163
|
+
`timeout` nor `gtimeout` is on `PATH`, `codex-exec` **warns loudly and runs uncapped** — never a silent
|
|
164
|
+
no-op — while `codex-review` **refuses pre-spend** (the fail-closed hard-timeout preflight).
|
|
151
165
|
|
|
152
166
|
## Subscription / config invariant
|
|
153
167
|
|
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.3.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
|
|