@sabaiway/agent-workflow-kit 5.2.0 → 5.4.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 +110 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +11 -3
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +154 -35
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +257 -4
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/references/driving-codex.md +5 -3
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +33 -15
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/coverage-check.md +1 -1
- package/references/modes/gates.md +7 -2
- package/references/modes/recommendations.md +3 -1
- package/references/modes/upgrade.md +1 -1
- package/references/modes/velocity.md +5 -1
- package/references/scripts/archive-decisions.mjs +340 -15
- package/references/scripts/archive-decisions.test.mjs +522 -2
- package/references/scripts/migrate-gates.mjs +102 -10
- package/references/scripts/migrate-gates.test.mjs +37 -0
- package/tools/core-evidence.mjs +42 -2
- package/tools/coverage-check.mjs +23 -7
- package/tools/coverage-producer.mjs +68 -0
- package/tools/coverage-state.mjs +24 -0
- package/tools/declared-paths.mjs +32 -0
- package/tools/detect-backends.mjs +1 -0
- package/tools/dispatch-record.mjs +926 -0
- package/tools/doc-parity.mjs +19 -4
- package/tools/flow-check.mjs +48 -12
- package/tools/gates-declaration.mjs +49 -0
- package/tools/gates-init.mjs +83 -6
- package/tools/recommendations.mjs +63 -19
- package/tools/run-gates.mjs +111 -32
- package/tools/velocity-profile.mjs +102 -23
|
@@ -46,6 +46,13 @@ const FAKE_CODEX = [
|
|
|
46
46
|
' cat <<EOF',
|
|
47
47
|
'{"type":"turn.started"}',
|
|
48
48
|
'{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"FAKE_FINAL_MESSAGE"}}',
|
|
49
|
+
'EOF',
|
|
50
|
+
// The event seam: verbatim extra stream lines (JSONL items, plain noise, or both) between the
|
|
51
|
+
// opening events and turn.completed — a multi-line value emits multiple lines.
|
|
52
|
+
' if [[ -n "${CODEX_FAKE_EVENT:-}" ]]; then echo "$CODEX_FAKE_EVENT"; fi',
|
|
53
|
+
// A file-borne twin: a payload too large for the environment (E2BIG) still has to be emittable.
|
|
54
|
+
' if [[ -n "${CODEX_FAKE_EVENT_FILE:-}" ]]; then cat "$CODEX_FAKE_EVENT_FILE"; fi',
|
|
55
|
+
' cat <<EOF',
|
|
49
56
|
'{"type":"turn.completed","usage":{}}',
|
|
50
57
|
'EOF',
|
|
51
58
|
'else',
|
|
@@ -365,6 +372,203 @@ describe('codex-exec.sh — clean output + session capture (1.2)', () => {
|
|
|
365
372
|
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a permission failure from unrelated code is not nested-sandbox proof');
|
|
366
373
|
});
|
|
367
374
|
|
|
375
|
+
// ── the rc == 0 arm: the SURVIVED nested-sandbox failure ──
|
|
376
|
+
// The class the failed-run arm cannot see: the backend hits the nested sandbox, degrades to "I
|
|
377
|
+
// cannot check", and exits 0 — a paid run spent on an ungrounded answer with nothing saying so.
|
|
378
|
+
// Both serialized shapes below were observed on the INSTALLED codex-cli 0.147.0: a finished item
|
|
379
|
+
// carries {"exit_code":2,"status":"failed"}, an in-flight one {"exit_code":null,"status":"in_progress"}.
|
|
380
|
+
const cmdItem = ({ command = '/bin/bash -lc probe', output = '', exitCode = null, status = 'completed', id = 'item_1' }) =>
|
|
381
|
+
JSON.stringify({ type: 'item.completed', item: { id, type: 'command_execution', command, aggregated_output: output, exit_code: exitCode, status } });
|
|
382
|
+
const MECHANISM = 'bwrap: setting up sandbox';
|
|
383
|
+
const FAILURE = 'mkdir /newroot: Read-only file system';
|
|
384
|
+
const SIGNATURE = `${MECHANISM}: ${FAILURE}\n`;
|
|
385
|
+
|
|
386
|
+
it('an rc == 0 run whose trace carries a command_execution with a NONZERO exit_code and the signature warns loudly and still prints the answer', () => {
|
|
387
|
+
const sb = makeSandbox();
|
|
388
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: cmdItem({ output: SIGNATURE, exitCode: 1, status: 'completed' }) } });
|
|
389
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
390
|
+
assert.equal(r.status, 0, 'the warning lane never changes the exit status');
|
|
391
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'the answer is printed FIRST, on stdout, unchanged');
|
|
392
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'names the class');
|
|
393
|
+
assert.match(r.stderr, /UNGROUNDED/, 'names the consequence for the answer above');
|
|
394
|
+
assert.match(r.stderr, /excludedCommands|per-run consented bypass/, 'names the reroute');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('an rc == 0 run whose trace carries a command_execution with a null exit_code and an explicitly FAILED status warns', () => {
|
|
398
|
+
const sb = makeSandbox();
|
|
399
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: cmdItem({ output: SIGNATURE, exitCode: null, status: 'failed' }) } });
|
|
400
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
401
|
+
assert.equal(r.status, 0);
|
|
402
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'the serialized failed status is the second failure proof');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('plain non-JSON stderr lines before and after a matching failed command_execution do not suppress the warning', () => {
|
|
406
|
+
const sb = makeSandbox();
|
|
407
|
+
const r = run(sb, {
|
|
408
|
+
env: {
|
|
409
|
+
CODEX_FAKE_STDERR: 'ERROR codex_core::session: failed to load skill /x/SKILL.md: missing field description',
|
|
410
|
+
CODEX_FAKE_EVENT: `not json at all\n${cmdItem({ output: SIGNATURE, exitCode: 2, status: 'failed' })}\nstill not json`,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
414
|
+
assert.equal(r.status, 0);
|
|
415
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'the merged stream is judged line by line — noise is not evidence and never a stop');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('the resume lane warns on an rc == 0 nested-sandbox signature — the lane the incident fired on', () => {
|
|
419
|
+
const sb = makeSandbox();
|
|
420
|
+
const r = run(sb, {
|
|
421
|
+
args: ['--resume', 'sess-nested', '-'], input: 'continue',
|
|
422
|
+
env: { CODEX_FAKE_EVENT: cmdItem({ output: SIGNATURE, exitCode: 1, status: 'failed' }) },
|
|
423
|
+
});
|
|
424
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
425
|
+
assert.equal(r.status, 0);
|
|
426
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/);
|
|
427
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'the whole point of unifying the capture');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('an rc == 0 run with a clean trace warns nothing', () => {
|
|
431
|
+
const sb = makeSandbox();
|
|
432
|
+
const r = run(sb);
|
|
433
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
434
|
+
assert.equal(r.status, 0, r.stderr);
|
|
435
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a clean run must stay silent');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('a lone mechanism token and a lone failure token each warn nothing on the rc == 0 lane', () => {
|
|
439
|
+
for (const output of [`${MECHANISM} version 0.11.0\n`, `curl: (7) ${FAILURE}\n`]) {
|
|
440
|
+
const sb = makeSandbox();
|
|
441
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: cmdItem({ output, exitCode: 1, status: 'failed' }) } });
|
|
442
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
443
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, `a lone token class is not proof: ${output}`);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('nested-sandbox text appearing ONLY inside an agent_message item never warns', () => {
|
|
448
|
+
const sb = makeSandbox();
|
|
449
|
+
const event = JSON.stringify({ type: 'item.completed', item: { id: 'item_9', type: 'agent_message', text: `I hit ${SIGNATURE}` } });
|
|
450
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: event } });
|
|
451
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
452
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'the model TALKING about a sandbox is not a failed tool call');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('a SUCCESSFUL command_execution whose output merely QUOTES both tokens never warns', () => {
|
|
456
|
+
const sb = makeSandbox();
|
|
457
|
+
// The concrete false positive: codex-exec.sh itself carries both token classes, so any
|
|
458
|
+
// successful grep over it would trip a loose whole-trace rule.
|
|
459
|
+
const r = run(sb, {
|
|
460
|
+
env: { CODEX_FAKE_EVENT: cmdItem({ command: '/bin/bash -lc grep -n bwrap codex-exec.sh', output: SIGNATURE, exitCode: 0, status: 'completed' }) },
|
|
461
|
+
});
|
|
462
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
463
|
+
assert.equal(r.status, 0, r.stderr);
|
|
464
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a command that SUCCEEDED proves nothing failed');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('a command_execution with a null exit_code and no proven failed status never warns', () => {
|
|
468
|
+
const sb = makeSandbox();
|
|
469
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: cmdItem({ output: SIGNATURE, exitCode: null, status: 'in_progress' }) } });
|
|
470
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
471
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a null exit_code is never failure by itself');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('tokens split across two different items never warn', () => {
|
|
475
|
+
const sb = makeSandbox();
|
|
476
|
+
const split = [
|
|
477
|
+
cmdItem({ id: 'item_1', output: `${MECHANISM} version 0.11.0\n`, exitCode: 1, status: 'failed' }),
|
|
478
|
+
cmdItem({ id: 'item_2', output: `curl: (7) ${FAILURE}\n`, exitCode: 1, status: 'failed' }),
|
|
479
|
+
].join('\n');
|
|
480
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: split } });
|
|
481
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
482
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'the combination must sit in ONE item — two failures are not one nested sandbox');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ── object membership, not substring co-occurrence ──
|
|
486
|
+
// Testing the four fields independently is not enough: position in the line is not membership in
|
|
487
|
+
// the item. The scan walks ONE contiguous chain of raw delimiters instead, and every gap in that
|
|
488
|
+
// chain is inside a JSON string, where a quote is escaped and cannot forge the next delimiter.
|
|
489
|
+
it('a decoy object carrying the type, with the failure fields on a DIFFERENT item, never warns', () => {
|
|
490
|
+
const sb = makeSandbox();
|
|
491
|
+
const decoy = '{"type":"item.completed","decoy":{"type":"command_execution"},"item":{"type":"agent_message","aggregated_output":"bwrap: operation not permitted","exit_code":0,"status":"failed"}}';
|
|
492
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: decoy } });
|
|
493
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
494
|
+
assert.equal(r.status, 0, r.stderr);
|
|
495
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'the type belongs to the decoy; the failure fields belong to an agent_message');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('a decoy carrying BOTH the type and a command, with the failure fields on a DIFFERENT item, never warns', () => {
|
|
499
|
+
const sb = makeSandbox();
|
|
500
|
+
// Anchoring on a longer literal is not enough: the skip between fields must itself be PROVEN to
|
|
501
|
+
// be one JSON string's content, or the walk leaves the decoy's command and lands in the
|
|
502
|
+
// agent_message's fields.
|
|
503
|
+
const decoy = '{"type":"item.completed","decoy":{"type":"command_execution","command":"x"},"item":{"type":"agent_message","aggregated_output":"bwrap: setting up sandbox: operation not permitted","exit_code":1,"status":"failed"}}';
|
|
504
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: decoy } });
|
|
505
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
506
|
+
assert.equal(r.status, 0, r.stderr);
|
|
507
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'an unvalidated gap lets the walk cross an object boundary');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('a genuinely failed item with a ~200KB aggregated_output still warns — and the scan does not hang', () => {
|
|
511
|
+
const sb = makeSandbox();
|
|
512
|
+
// Two edges at once: the signature sits FIRST, so any early-exit consumer must not lose it, and
|
|
513
|
+
// the field is far larger than a pipe buffer. It also pins the cost: the quadratic bash string
|
|
514
|
+
// spellings of this walk hang the wrapper outright at this size.
|
|
515
|
+
const big = `${SIGNATURE}${'x'.repeat(200000)}`;
|
|
516
|
+
// The payload rides a FILE: 200KB in the environment is E2BIG on a normal host.
|
|
517
|
+
const payload = join(sb.repo, 'big-event.jsonl');
|
|
518
|
+
writeFileSync(payload, `${cmdItem({ output: big, exitCode: 1, status: 'failed' })}\n`);
|
|
519
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT_FILE: payload } });
|
|
520
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
521
|
+
assert.equal(r.status, 0, r.stderr);
|
|
522
|
+
assert.match(r.stderr, /NESTED-SANDBOX/, 'a large output must not silently drop a real signature');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('a plain non-JSON log line carrying the same substrings never warns', () => {
|
|
526
|
+
const sb = makeSandbox();
|
|
527
|
+
const lookalike = `ERROR codex_core: replaying "type":"command_execution","command":"x","aggregated_output":"${SIGNATURE.trim()}","exit_code":1,"status":"failed"`;
|
|
528
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: lookalike } });
|
|
529
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
530
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'prose ABOUT an event is not an event — an event line starts with {');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('a FOREIGN "status":"failed" elsewhere on the line never proves a SUCCESSFUL item failed', () => {
|
|
534
|
+
const sb = makeSandbox();
|
|
535
|
+
const event = `${cmdItem({ output: SIGNATURE, exitCode: 0, status: 'completed' })}{"type":"turn.failed","status":"failed"}`;
|
|
536
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: event } });
|
|
537
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
538
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'the failed status must sit immediately after THIS item exit_code');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('an escaped delimiter inside aggregated_output never fools the slice', () => {
|
|
542
|
+
const sb = makeSandbox();
|
|
543
|
+
const r = run(sb, {
|
|
544
|
+
env: { CODEX_FAKE_EVENT: cmdItem({ output: `${SIGNATURE}","exit_code":1,"status":"failed"`, exitCode: 0, status: 'completed' }) },
|
|
545
|
+
});
|
|
546
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
547
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'a quote inside a JSON string is escaped, so the raw delimiter cannot occur there');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('only the FIRST command_execution item of a line is judged — a second item on the same line is missed (a STATED false negative)', () => {
|
|
551
|
+
const sb = makeSandbox();
|
|
552
|
+
const glued = `${cmdItem({ id: 'item_1', output: 'all good\n', exitCode: 0, status: 'completed' })}${cmdItem({ id: 'item_2', output: SIGNATURE, exitCode: 1, status: 'failed' })}`;
|
|
553
|
+
const r = run(sb, { env: { CODEX_FAKE_EVENT: glued } });
|
|
554
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
555
|
+
assert.doesNotMatch(r.stderr, /NESTED-SANDBOX/, 'under-firing is the deliberate direction on a warning lane; this pins it so it cannot change silently');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('a trace of plain non-JSON lines alone carrying both tokens never warns on the rc == 0 arm — while the FAILED arm warns on exactly those bytes', () => {
|
|
559
|
+
const bytes = `${MECHANISM}: ${FAILURE}`;
|
|
560
|
+
const clean = makeSandbox();
|
|
561
|
+
const ok = run(clean, { env: { CODEX_FAKE_STDERR: bytes } });
|
|
562
|
+
rmSync(clean.root, { recursive: true, force: true });
|
|
563
|
+
assert.equal(ok.status, 0, ok.stderr);
|
|
564
|
+
assert.doesNotMatch(ok.stderr, /NESTED-SANDBOX/, 'on a COMPLETED run only per-item evidence speaks');
|
|
565
|
+
const failed = makeSandbox();
|
|
566
|
+
const bad = run(failed, { env: { CODEX_FAKE_STDERR: bytes, CODEX_FAKE_EXIT: '1' } });
|
|
567
|
+
rmSync(failed.root, { recursive: true, force: true });
|
|
568
|
+
assert.equal(bad.status, 1);
|
|
569
|
+
assert.match(bad.stderr, /NESTED-SANDBOX/, 'the failed-run arm keeps its loose whole-trace rule — that is what makes the dual policy visible');
|
|
570
|
+
});
|
|
571
|
+
|
|
368
572
|
it('warns (never silently) when the session sidecar cannot be written', () => {
|
|
369
573
|
const sb = makeSandbox();
|
|
370
574
|
const blocker = join(sb.repo, 'blocker');
|
|
@@ -440,6 +644,33 @@ describe('codex-exec.sh — resume entrypoint restates every invariant (3.1)', (
|
|
|
440
644
|
/approval_policy=never/, /sandbox_workspace_write\.network_access=false/,
|
|
441
645
|
];
|
|
442
646
|
|
|
647
|
+
// `codex exec resume` accepts a NARROWER flag set than `codex exec`, and the difference is not
|
|
648
|
+
// guessable: the hermetic fake accepts any argv, so a flag the real CLI rejects passes every unit
|
|
649
|
+
// test and then fails pre-spend on the first live resume. That happened — `--color never` was
|
|
650
|
+
// added to this lane unprobed and broke it outright. This list is transcribed from
|
|
651
|
+
// `codex exec resume --help` on codex-cli 0.147.0 (probed 2026-08-08); anything the wrapper sends
|
|
652
|
+
// that is not on it fails HERE instead of in front of a user.
|
|
653
|
+
const RESUME_ACCEPTED_FLAGS = new Set([
|
|
654
|
+
'--last', '--all', '-c', '--config', '--enable', '-i', '--image', '--strict-config', '--disable',
|
|
655
|
+
'-m', '--model', '--dangerously-bypass-approvals-and-sandbox', '--dangerously-bypass-hook-trust',
|
|
656
|
+
'--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules',
|
|
657
|
+
'--output-schema', '--json', '-o', '--output-last-message', '-h', '--help',
|
|
658
|
+
]);
|
|
659
|
+
|
|
660
|
+
it('every flag the resume lane sends is one the REAL `codex exec resume` accepts', () => {
|
|
661
|
+
const sb = makeSandbox();
|
|
662
|
+
const r = run(sb, { args: ['--resume', 'sess-flags', '-'], input: 'go' });
|
|
663
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
664
|
+
assert.equal(r.status, 0, r.stderr);
|
|
665
|
+
const sent = r.argv.split('\n').filter((a) => a.startsWith('-') && a !== '-');
|
|
666
|
+
assert.ok(sent.length > 0, 'the resume argv must carry flags at all');
|
|
667
|
+
for (const flag of sent) {
|
|
668
|
+
assert.ok(RESUME_ACCEPTED_FLAGS.has(flag),
|
|
669
|
+
`resume sends ${flag}, which \`codex exec resume --help\` does not list — it would exit 2 pre-spend`);
|
|
670
|
+
}
|
|
671
|
+
assert.equal(sent.includes('--color'), false, 'the regression this list exists to prevent');
|
|
672
|
+
});
|
|
673
|
+
|
|
443
674
|
it('--resume <id>: composes `exec resume <id>` with the full restated policy', () => {
|
|
444
675
|
const sb = makeSandbox();
|
|
445
676
|
const r = run(sb, { args: ['--resume', 'sess-xyz', '-'], input: 'continue please' });
|
|
@@ -447,10 +678,32 @@ describe('codex-exec.sh — resume entrypoint restates every invariant (3.1)', (
|
|
|
447
678
|
assert.equal(r.status, 0, r.stderr);
|
|
448
679
|
assert.match(r.argv, /(^|\n)sess-xyz(\n|$)/, 'the session id is passed positionally');
|
|
449
680
|
for (const inv of RESUME_INVARIANTS) assert.match(r.argv, inv, `resume argv must include ${inv}`);
|
|
450
|
-
assert.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
681
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'resume prints the final message');
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// The capture unification: resume used to be the odd mode out — no -o, no --json, its event
|
|
685
|
+
// stream nowhere — which is precisely why the lane the nested-sandbox incident fired on had no
|
|
686
|
+
// evidence surface. `codex exec resume` accepts both (live-probed, codex-cli 0.147.0).
|
|
687
|
+
it('resume composes the unified capture — -o and --json, and NOT --color (which it rejects)', () => {
|
|
688
|
+
const sb = makeSandbox();
|
|
689
|
+
const r = run(sb, { args: ['--resume', 'sess-unified', '-'], input: 'continue please' });
|
|
690
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
691
|
+
assert.equal(r.status, 0, r.stderr);
|
|
692
|
+
assert.match(r.argv, /(^|\n)-o(\n|$)/, 'resume writes the final message through -o');
|
|
693
|
+
assert.match(r.argv, /(^|\n)--json(\n|$)/, 'resume streams the structured events');
|
|
694
|
+
// The evidence surface is shared with a fresh run; the COLOUR flag is not, because
|
|
695
|
+
// `codex exec resume` does not accept it. Sending it exits 2 before the run starts.
|
|
696
|
+
assert.equal(/(^|\n)--color(\n|$)/.test(r.argv), false, 'resume rejects --color — probed on codex-cli 0.147.0');
|
|
697
|
+
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'resume stdout is still the final message');
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('resume falls back to the trace tail when the final-message file is missing', () => {
|
|
701
|
+
const sb = makeSandbox();
|
|
702
|
+
const r = run(sb, { args: ['--resume', 'sess-noout', '-'], input: 'go', env: { CODEX_FAKE_NO_OUT: '1' } });
|
|
703
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
704
|
+
assert.equal(r.status, 0, r.stderr);
|
|
705
|
+
assert.match(r.stderr, /no final-message file/, 'the fallback is loud, never silent');
|
|
706
|
+
assert.match(r.stdout, /turn\.completed/, 'the trace tail carries the event stream resume now captures');
|
|
454
707
|
});
|
|
455
708
|
|
|
456
709
|
it('--resume-last reads the session id from the sidecar', () => {
|
|
@@ -278,7 +278,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
|
|
|
278
278
|
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
279
279
|
# version (drift-guarded by codex-review.test.mjs against capability.json).
|
|
280
280
|
AW_RECEIPT_BACKEND="codex"
|
|
281
|
-
AW_BRIDGE_VERSION="3.
|
|
281
|
+
AW_BRIDGE_VERSION="3.4.1"
|
|
282
282
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|
|
283
283
|
CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
|
|
284
284
|
# Generous hard cap for a slow xhigh review (subscription latency varies).
|
|
@@ -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.1",
|
|
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
|
},
|
|
@@ -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 — though NOT
|
|
47
|
+
`--color`, which stays on the fresh lane), so the resume lane is no longer the odd one out: it used
|
|
48
|
+
to print its final message straight to stdout with the event stream nowhere, which left the very
|
|
49
|
+
mode a nested-sandbox incident fired on without a structured evidence surface. 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
|
|
|
@@ -132,14 +145,19 @@ the prompt fence ("do not read outside the working tree, except the precomputed-
|
|
|
132
145
|
|
|
133
146
|
## `resume` — resets posture, restated via `-c`
|
|
134
147
|
|
|
135
|
-
`codex exec resume` re-dispatches an existing session without re-sending context.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
`codex exec resume` re-dispatches an existing session without re-sending context. Its accepted flag
|
|
149
|
+
set is **NARROWER than `codex exec`'s** and must be read off `codex exec resume --help`, never
|
|
150
|
+
assumed from the parent command: it **rejects the posture flags** `-s`/`--add-dir`/`-C`, **resets**
|
|
151
|
+
the sandbox/approval/network posture, and — probed on codex-cli 0.147.0 — accepts
|
|
152
|
+
`-c`/`-m`/`--last`/`-o`/`--json` but **NOT `--color`**. Sending an unaccepted flag exits 2 before the
|
|
153
|
+
run starts, so the failure is loud and costs no quota; the wrapper's own test pins the accepted set
|
|
154
|
+
(`RESUME_ACCEPTED_FLAGS`) precisely because the hermetic fake CLI accepts any argv and cannot answer
|
|
155
|
+
this question. The `codex-exec --resume`/`--resume-last` entrypoint handles the reset: it restates
|
|
156
|
+
the entire policy via `-c` (`sandbox_mode=workspace-write`, `approval_policy=never`,
|
|
157
|
+
`sandbox_workspace_write.network_access=false`) plus the pinned `-m`/effort and
|
|
158
|
+
`--ignore-user-config`, reads the session id from the sidecar (or an argument), and applies the same
|
|
159
|
+
EVIDENCE posture as a fresh run — `-o` for the final message, `--json` into the trace. Only a *raw*
|
|
160
|
+
`codex exec resume` outside the wrapper loses the posture.
|
|
143
161
|
|
|
144
162
|
## Hard timeout
|
|
145
163
|
|
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.4.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",
|
|
@@ -8,7 +8,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/core-evidence.mjs <verb> …`:
|
|
|
8
8
|
|
|
9
9
|
1. **`red-proof "<test-file>#<test-name-pattern>"`** — the D3(c) observed-red DECLARATION, minted **BEFORE a bugfix is applied**: it runs the named test N times (default 3, `AW_CORE_EVIDENCE_RERUNS`; per-run timeout `AW_CORE_EVIDENCE_TIMEOUT_S`) on the CURRENT (pre-fix) tree, requires red N/N (observed green / unresolvable / mixed / timed-out are DISTINGUISHED refusals — nothing is written; mixed/timeout is QUARANTINE, no override lane), and records { testId · repo-relative file · content sha256 (custody) · N/N red · base = HEAD sha · the pre-fix tree fingerprint }. The final run (`coverage-check --check`) then requires every current-base record's test green N/N with the hash unchanged; a record whose pre-fix fingerprint EQUALS the current tree never satisfies (nothing changed — reuse/forgery); a commit expires records (base moves); editing a bound test file means re-observing red (the new record supersedes the old — same {base, testId} key).
|
|
10
10
|
2. **`degrade --backend <name> --reason "<why>"`** — the ONLY escape from a required review backend (D3(b)): an explicit per-backend, per-tree record at the CURRENT fingerprint. Never all backends: the review gate still requires ≥1 non-degraded ship-class receipt whenever ≥1 backend is configured. Any tree edit re-stales the record (fingerprint-bound).
|
|
11
|
-
3. **`summary`** — the ONE stateless end-of-loop render (D6): gate result from the latest final attempt, per-backend verdicts from the review receipts, red-proof outcomes, degrade records. Computed from the stores at read time — no ledger, no rounds, nothing remembered. A malformed store exits non-zero and WITHHOLDS the affected section, never renders a partial as complete.
|
|
11
|
+
3. **`summary`** — the ONE stateless end-of-loop render (D6): gate result from the latest final attempt, per-backend verdicts from the review receipts, red-proof outcomes, degrade records. Computed from the stores at read time — no ledger, no rounds, nothing remembered. A malformed store exits non-zero and WITHHOLDS the affected section, never renders a partial as complete. The final-run line NAMES an absent coverage verdict from the `coverage` token the run RECORDED (an ADDITIVE optional field on the `final` record — the closed `certified | not-run | unknown` set, never `none`, and `certified` requires a bound `lcovSha256`): GREEN never renders unqualified over a run that issued no verdict. A LEGACY receipt carrying no token and no lcov digest is named as exactly that, never as a claim about what it read — the withheld verdict travels here too, as DETAIL beside the unchanged status word.
|
|
12
12
|
|
|
13
13
|
**Promptless writer lane (D7):** both verbs ride plain `node ${CLAUDE_SKILL_DIR}/tools/core-evidence.mjs …` invocations — surface the paste-ready allow entries once (`.claude/settings.json`), and a full loop transcript runs at zero writer approval prompts.
|
|
14
14
|
|
|
@@ -6,7 +6,7 @@ The **final-run checker** (strip-the-kit D3(c)+(d)) — two deterministic arms o
|
|
|
6
6
|
|
|
7
7
|
Run `node ${CLAUDE_SKILL_DIR}/tools/coverage-check.mjs --check [--cwd <dir>]`:
|
|
8
8
|
|
|
9
|
-
0. **Attestation precondition (the provenance arm).** An lcov on disk carries no evidence of the tree it came from, so reading one and issuing a verdict certifies whatever happens to be there — the false GREEN direction is the dangerous one, because a line appended AFTER the suite ran has no `DA` entry and therefore reads non-executable ("nothing to cover"). Provenance is a CONSEQUENCE in exactly one context: a `run-gates --final` run deletes the artifact before any gate spawns. That runner mints a random nonce and writes `final-start.attempt` as a ONE-WAY COMMITMENT over `{nonce, tree fingerprint, base}`; the raw nonce rides the environment to this child, which recomputes the commitment and requires the record to carry it. Neither half suffices alone — a bare nonce is unverifiable, a persisted attempt id is reconstructible from public repo state — and the commitment is also the only place the BASE is bound, since no record stores it. The raw nonce is stripped from the red-proof probe environment so no descendant retains a live capability. Outcomes: **attested** → the coverage verdict is issued; **no handshake** → `attested=no` + `NO VERDICT` (exit 0, findings still printed, uncovered lines still exit 1 — the findings contract is unchanged); **a handshake describing another tree or matching no recorded attempt** → `REFUSED` (exit 1), never a verdict in either direction. One fully anchored `coverage-check: attested=<yes|no>` machine line rides every run, on the same exactly-once contract as the sha line. Stated residuals, both named rather than implied: (a) an operator who runs both processes can forge the store or the code — the kit's standing self-discipline posture, not a security boundary; (b) **"the run owns the artifact" is exclusive by CONVENTION over the fixed path, not enforced** — a writer outside the run (a second `run-gates`, a hand-run `--only unit-tests`, an orphaned test process) can place foreign evidence between the deletion and the checker's read, and every check then agrees. Closing (b) needs an attempt-unique artifact path, which the runner can name but the declared producer cmd must reference — queued as LCOV-EXCLUSIVE-OWNERSHIP. What this arm removes is the false green that needs no second process and nobody trying: evidence that predates the edit.
|
|
9
|
+
0. **Attestation precondition (the provenance arm).** An lcov on disk carries no evidence of the tree it came from, so reading one and issuing a verdict certifies whatever happens to be there — the false GREEN direction is the dangerous one, because a line appended AFTER the suite ran has no `DA` entry and therefore reads non-executable ("nothing to cover"). Provenance is a CONSEQUENCE in exactly one context: a `run-gates --final` run deletes the artifact before any gate spawns. That runner mints a random nonce and writes `final-start.attempt` as a ONE-WAY COMMITMENT over `{nonce, tree fingerprint, base}`; the raw nonce rides the environment to this child, which recomputes the commitment and requires the record to carry it. Neither half suffices alone — a bare nonce is unverifiable, a persisted attempt id is reconstructible from public repo state — and the commitment is also the only place the BASE is bound, since no record stores it. The raw nonce is stripped from the red-proof probe environment so no descendant retains a live capability. `attested=` states whether a coverage VERDICT was ISSUED — pass **or** fail — never whether coverage passed: a valid handshake over an lcov that lists uncovered lines still reads `attested=yes` and still exits 1. Outcomes: **attested** → the coverage verdict is issued; **no handshake** → `attested=no` + `NO VERDICT` (exit 0, findings still printed, uncovered lines still exit 1 — the findings contract is unchanged); **a valid handshake over a run that read NO lcov BYTES** (the file is absent, or the path was refused as a non-regular file) → `attested=no` + `NO VERDICT`: the run owned the artifact's lifetime and read nothing, so it certifies NOTHING — reading `yes` there would be the false green this arm exists to close, one layer up. The predicate is the consumed bytes (`lcov-sha256` is a digest, not `none`), never the skip flag, so a refused path can never attest either. The two halves are independent: WITHHOLDING the verdict never changes an exit code — an absent file stays exit 0 (the loud `skipped-no-lcov`), and the refused non-regular path keeps its OWN fail-closed exit 1; **a handshake describing another tree or matching no recorded attempt** → `REFUSED` (exit 1), never a verdict in either direction. One fully anchored `coverage-check: attested=<yes|no>` machine line rides every run, on the same exactly-once contract as the sha line — and a withheld verdict TRAVELS: `run-gates` carries it as `coverage=not-run` on the summary line and names it on the checker's table row, and `core-evidence summary` never renders an unqualified GREEN over a final record that consumed no lcov. Stated residuals, both named rather than implied: (a) an operator who runs both processes can forge the store or the code — the kit's standing self-discipline posture, not a security boundary; (b) **"the run owns the artifact" is exclusive by CONVENTION over the fixed path, not enforced** — a writer outside the run (a second `run-gates`, a hand-run `--only unit-tests`, an orphaned test process) can place foreign evidence between the deletion and the checker's read, and every check then agrees. Closing (b) needs an attempt-unique artifact path, which the runner can name but the declared producer cmd must reference — queued as LCOV-EXCLUSIVE-OWNERSHIP. What this arm removes is the false green that needs no second process and nobody trying: evidence that predates the edit.
|
|
10
10
|
1. **Coverage arm (D3(d)):** every CHANGED executable Node line (`.mjs`/`.cjs`/`.js`, tracked working-vs-HEAD changes + untracked-not-ignored files) must be covered — uncovered lines are LISTED `file:line` and fail; a changed file ABSENT from the lcov map is a file-level red (never "non-executable" by silence); changed out-of-domain files (e.g. `.sh`) and unsupported-source files (e.g. `.ts`) are LISTED — the claim is narrowed honestly, not widened. NO lcov file at the path = a LOUD `skipped-no-lcov` (exit 0, stated — produce the file via the unit-tests gate's lcov reporters); a symlink at the path is a refusal (lstat, no-follow).
|
|
11
11
|
2. **Red-proof arm (D3(c)):** every authoritative current-base `red-proof` declaration must verify — the bound test file exists (deleted fails), its content sha256 matches the declaration (custody), the test resolves (zero-match fails) and runs green N/N NOW, and the declaration's pre-fix fingerprint differs from the current tree (equal = reuse/forgery, refused). A malformed evidence store fails CLOSED.
|
|
12
12
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
### Mode: gates
|
|
2
2
|
|
|
3
3
|
<!-- opt-in-capability: gates-declaration -->
|
|
4
|
+
<!-- opt-in-capability: gates-verification -->
|
|
4
5
|
|
|
5
6
|
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
7
|
|
|
7
8
|
Run `node ${CLAUDE_SKILL_DIR}/tools/run-gates.mjs [--cwd <project>] [--only <id>]… [--final]`:
|
|
8
9
|
|
|
9
10
|
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
|
-
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
|
+
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=… coverage=…`). 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
12
|
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
13
|
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
14
|
|
|
@@ -19,6 +20,10 @@ Declared gates can also be **auto-approved** (no permission prompt on a byte-exa
|
|
|
19
20
|
|
|
20
21
|
**Candidate line — the review-receipt gate (opt-in, never auto-seeded; AD-021).** Projects that configure a reviewed/council `plan-execution.review` recipe can declare the review-state check as one more gate — the exact candidate `{ id, title, cmd }` line and its contract live under `${CLAUDE_SKILL_DIR}/references/modes/review-state.md` (step 3).
|
|
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
|
+
**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. A screened-out body is always named — and when nothing but kit checkers remains, the preview says so in plain words: the offer carries **no project-verification gate at all**. **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`).
|
|
24
|
+
|
|
25
|
+
**The coverage PRODUCER and the canonical checker are declared together or not at all.** `coverage-check` READS an lcov; something must WRITE it, and a checker with no producer PASSES (`skipped-no-lcov`) certifying nothing. A **producer** is a CLOSED set of full command forms, never a substring probe: the suite body `node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination="$AW_GIT_DIR/agent-workflow-lcov.info" --test-reporter=spec --test-reporter-destination=stdout` (optionally + the project's own test paths), bare or behind ONE verified per-PM `exec` wrapper. A cmd that merely MENTIONS the destination (in an `echo`, as a bare substring) or carries a PARTIAL flag set is **not** one. The tail must be path-shaped — path/glob/quoting bytes only, nothing that could sequence, redirect or substitute a command, no plain leading `-`. **Residual:** the screen judges SOURCE bytes — brace *sequence* expansion can produce others (`{Y..a}`), though bash never re-scans an expansion result as syntax, so such a byte is literal argument data, not an operator; the leading-`-` rule is first-order only (`'--flag'`, `{path,--flag}` pass); neither proves the lcov SURVIVES — "producer" means *configured with the reporters*, and a run producing none is caught at runtime as `skipped-no-lcov`. The destination rides `AW_GIT_DIR`, exported to every gate child on plain and `--final` runs alike (`AW_LCOV_FILE` is `--final`-only), so one cmd survives the unmet-producer preflight in both modes. On BOTH declaration paths: the fill preview WIRES those reporters onto a `node --test` body — the one allowlist member producing lcov unaided, every other body is emitted unchanged — WITHHOLDS the `coverage-check` candidate, with a named note, when neither the offer nor the declaration carries a producer; and `--apply` REFUSES a checker with no producer, a checker that is not LAST (an ORDERING refusal — reorder by hand; the fill is append-only), or a SECOND canonical checker. The legacy migration likewise never ADDS the checker into a producer-less declaration, and reports an already-declared one as INERT. A producer declared AFTER the checker leaves it just as inert (it reads nothing, or stale bytes) — ORDER is the rule. An already-declared inert pair, and a matrix of nothing but kit checkers, are surfaced by the advisor's `gates-inert` item (`${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`); its cause-A remedy is HAND-APPLY because the fill cannot reorder.
|
|
26
|
+
|
|
27
|
+
**`coverage=` states what the run can honestly say about coverage.** A CLOSED four-value set, always present, DETAIL only — exit code, `status=`, the receipt status, `--final` acceptance and the commit-guard disposition are untouched. `coverage=certified` — the checker consumed an lcov and ISSUED a verdict, pass **or** fail (one listing uncovered lines still reads certified and still exits 1). `coverage=not-run` — the checker ran and issued NO verdict (no lcov bytes were read, or the run holds no attestation context). `coverage=none` — no canonical checker ran here (an `--only` subset, the `--pre-review` subset). `coverage=unknown` — the run ended before the gates produced a signal (missing / empty / malformed declaration, no bash, a pre-spend refusal), or that signal is unreadable: the checker could not spawn, or its two anchored lines are missing, duplicated, or CONTRADICTORY (`attested=yes` over `lcov-sha256=none` certifies nothing) — fail closed. The value is DERIVED by cross-reading both anchored machine lines, the bytes the `--final` receipt binds, so it can never disagree with what the checker printed; the checker's table row names a withheld verdict in the same words, and a `--final` receipt RECORDS the token (`none` never rides one — a final run always selects the checker).
|
|
23
28
|
|
|
24
29
|
**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.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md
|
|
6
6
|
|
|
7
|
-
The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · an optional `recipe:` line (the `sandbox-lane` live recipe — egress hosts + resolved writable dirs — the `worktrees-dir` hand-apply-first grant advice, or the `agents` hidden-mode reconcile follow-up) · the exact consent-gated apply one-liner}**. The tool computes deterministic English DATA; **you PRESENT the section in the user's conversational language** — every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings stay **byte-exact**; show the raw tool block on request (the AD-032 report-contract lane — the tool cannot know the dialogue language, so the language rendering is your presentation layer).
|
|
7
|
+
The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, a declared gate matrix that verifies nothing, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · an optional `recipe:` line (the `sandbox-lane` live recipe — egress hosts + resolved writable dirs — the `worktrees-dir` hand-apply-first grant advice, or the `agents` hidden-mode reconcile follow-up) · the exact consent-gated apply one-liner}**. The tool computes deterministic English DATA; **you PRESENT the section in the user's conversational language** — every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings stay **byte-exact**; show the raw tool block on request (the AD-032 report-contract lane — the tool cannot know the dialogue language, so the language rendering is your presentation layer).
|
|
8
8
|
|
|
9
9
|
**Live host/session facts are tool-composed only.** Every fact this section states about the current
|
|
10
10
|
host or session — prompts fired, sandbox scope, whether a bypass was needed, network reachability,
|
|
@@ -27,6 +27,8 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
27
27
|
- `read-lane` — enabling the opt-in read-only compound lane auto-approves *compounds* (and singles) of the seeded read-only core that carry ZERO shell metaprogramming: an UNATTENDED trust extension, bounded by the audited read-only core (never a command outside it; prompt-bypass only, never a sandbox bypass) and applied regardless of which of those core commands you seeded as individual settings rules. It is a PROJECT-PERSISTENT declaration in `docs/ai/lanes.json` — every future session, subagents' Bash too where the host fires hooks on subagent Bash, and (committed) every checkout. The apply depends on state: when the lane is OFF, it is the `gate-hook --read-lane` preview (whose own currency check refuses a stale hook — a pre-1.48 hook never reads `lanes.json`); when the placed hook is STALE (an enabled lane over an old hook) or MISSING, the item instead surfaces a **delete-to-reseed** / re-place recovery (a destructive `rm` + `--apply`, an attention item — never the safe preview). Risk profile: a bounded read-only trust-posture extension — no write/exec exposure beyond the audited core.
|
|
28
28
|
- `worktrees-dir` — on a settings-native host that honors the key, the HAND-APPLY line widens the OS-sandbox WRITE surface to the whole worktrees parent dir: every sibling path under it (other repositories included) becomes agent-writable, and the widening persists for every later session. A harness-managed host may ignore that project setting; grant the narrow parent through host/session controls or use the provision terminal fallback instead. When that scope is wider than you want, narrow it FIRST: create a dedicated dir yourself (outside the agent's write surface), point `docs/ai/worktrees.json` `parentDir` at it, then re-run recommendations — the item re-renders with the narrowed dir. The kit never writes sandbox filesystem allowances itself; the line is always yours to paste. **Convergence** is two-path: a declared `sandbox.filesystem.allowWrite` entry covering the probed dir (either settings scope — `~` and `~/…` resolve against home, and coverage is path-segment-aware, so a grant on a sibling or on a child never counts), or — for a host that ignores that key — the neutral dir-bound acknowledgement recorded by this item's consent-gated apply one-liner (`ack-write --lane worktrees-dir` — a dry-run preview that prints the exact `--apply`; recorded as `worktreesDirAck` in the family-owned `docs/ai/acks.json`, never a security key), while the grant advice itself rides the `recipe:` line as the labeled FIRST step; against a trusted host NO the apply stays the HAND-APPLY grant advice and no ack is offered. Neither is proof of write CAPABILITY: the provision preflight's real create+delete probe stays the runtime truth, and the fingerprint is bound to the **resolved probe dir**, so the item re-fires only when that resolved dir changes (two absent `parentDir` values sharing an existing ancestor resolve to the same dir and keep the same ack). Risk profile: a real write-surface widening where honored — scope it deliberately.
|
|
29
29
|
|
|
30
|
+
- `gates-inert` — the gate matrix is DECLARED but verifies nothing, and the two causes have different remedies, so the item renders a different apply for each. **Cause A** — a canonical `coverage-check` gate with no producer gate declared BEFORE it (none at all, or one declared after it, which writes the lcov too late): the run certifies no coverage of its own — `coverage=not-run` when nothing wrote an lcov, or a verdict over STALE bytes an earlier run left in the git dir, which is worse because it reads as `coverage=certified`. This item is what surfaces that state at upgrade instead of leaving it to be noticed. The remedy needs the producer to run BEFORE the checker — declared or MOVED there — and the `gates-init` fill is append-only, so it cannot reorder an existing declaration; this arm is therefore **HAND-APPLY**: the maintainer edits `docs/ai/gates.json` (the exact producer form lives in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), never you, never the kit. **Cause B** — every declared gate is one of the kit's own canonical checkers, so the matrix runs no project-verification command at all: the apply is the `gates-init` dry-run preview, the same consent-gated seeder the `gates-declaration` item renders, and after the SAME confirmation you run the `--apply` line it prints. Neither cause changes any gate result, exit code or receipt — the item adds no enforcement, only the offer. Risk profile: `docs/ai/gates.json` is a PRIVILEGED file (a declared gate is auto-approvable by the wired hook), so cause A stays maintainer-only editing and cause B appends a project-controlled command on your explicit yes.
|
|
31
|
+
|
|
30
32
|
- `adr-store-migration` — other items write project files too; what is unique here is that the crossing **overwrites and deletes files the project already has**: it replaces the deployed enforcement scripts in `scripts/` (the directional subset — only basenames the project already has; a locally-edited copy is snapshotted first, never silently clobbered) and, where a retired archive file exists, DELETES it once conservation has been proven. That is why it is **HAND-APPLY** and why the command shown in the apply slot is a **`--dry-run`** — it writes nothing and prints the whole plan. `--apply` is a SEPARATE step, run only after that plan has been shown and **fresh consent** obtained for it; the consent flow executes only the apply slot, so an item that needs consent AFTER its preview cannot use that lane at all. Every write is idempotent and the run is re-runnable to completion after any interruption, so a re-run repairs rather than double-applies. It never commits. Risk profile: overwrite + delete of existing project files, gated on a preview you have actually read.
|
|
31
33
|
|
|
32
34
|
**Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
|
|
@@ -24,7 +24,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
24
24
|
|
|
25
25
|
**Gate-declaration ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Ensure `docs/ai/gates.json` exists: **create it from `${CLAUDE_SKILL_DIR}/references/templates/gates.json` if missing** — the kit's OWN template twin, so this works even when the installed memory substrate predates the gates feature (a stale memory never silently loses it); **an existing file is preserved byte-for-byte** (a project's declared gate matrix is authored content — never clobbered, never refreshed in place; unlike the orchestration `_README` there is no note-refresh here). Report it in the step 4 / step 8 success report (*seeded* / *already present*). Like the config ensure, this reaches an equal-head deployment without a lineage-head bump or a migration file (a `.json`, inherently outside the docs cap-validator).
|
|
26
26
|
|
|
27
|
-
**Legacy gates.json migration (consented preview — D8).** An EXISTING declaration may still carry the retired review-ledger / fold-completeness checks. Run the preview `node ${CLAUDE_SKILL_DIR}/references/scripts/migrate-gates.mjs --kit-tools ${CLAUDE_SKILL_DIR}/tools --cwd <project>` (dry-run — writes NOTHING), show the user the exact plan, and only on an explicit yes re-run it with `--apply`: canonical legacy entries (matched by their documented single-invocation cmd forms) are REMOVED, the canonical `unit-tests` cmd gains the built-in lcov reporters, and the coverage-check gate is ADDED last — atomic and COMPLETE, so the migrated declaration satisfies `run-gates --final`. CUSTOMIZED entries are NEVER auto-touched: the preview names each with a paste-ready recovery, and the commit guard must NOT be installed until they are resolved. This is the ONLY gates.json writer at upgrade (the consented FILL preview runs at init).
|
|
27
|
+
**Legacy gates.json migration (consented preview — D8).** An EXISTING declaration may still carry the retired review-ledger / fold-completeness checks. Run the preview `node ${CLAUDE_SKILL_DIR}/references/scripts/migrate-gates.mjs --kit-tools ${CLAUDE_SKILL_DIR}/tools --cwd <project>` (dry-run — writes NOTHING), show the user the exact plan, and only on an explicit yes re-run it with `--apply`: canonical legacy entries (matched by their documented single-invocation cmd forms) are REMOVED, the canonical `unit-tests` cmd gains the built-in lcov reporters, and the coverage-check gate is ADDED last — atomic and COMPLETE, so the migrated declaration satisfies `run-gates --final`. **The checker rides a PRODUCER or is not declared at all** (`${CLAUDE_SKILL_DIR}/references/modes/gates.md`): with no gate producing the lcov it reads, the migration does NOT add it, an already-declared one is reported INERT, the result is not called final-run-capable, and the preview prints the paste-ready suite cmd to declare by hand — nothing is ever removed for you. CUSTOMIZED entries are NEVER auto-touched: the preview names each with a paste-ready recovery, and the commit guard must NOT be installed until they are resolved. This is the ONLY gates.json writer at upgrade (the consented FILL preview runs at init).
|
|
28
28
|
|
|
29
29
|
**Autonomy-declaration ensure (seed-if-missing) — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Ensure `docs/ai/autonomy.json` exists: **create it from `${CLAUDE_SKILL_DIR}/references/templates/autonomy.json` if missing** (the kit's OWN template twin, mirrored from memory — so a stale memory never silently loses the seed); **an existing file is preserved byte-for-byte** (a declared policy is authored content — never clobbered, never refreshed in place). The seed is SPARSE (the onboarding note only) and **defaults-equivalent** — deploying it never changes behavior (the computed defaults stay the policy until the user declares levels with `/agent-workflow-kit set-autonomy` or by hand). Report it in the step 4 / step 8 success report (*seeded* / *already present, preserved*). Like the other config ensures, no lineage-head bump or migration file (a `.json`, outside the docs cap-validator).
|
|
30
30
|
|