@sabaiway/agent-workflow-kit 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -1
- package/README.md +1 -0
- package/SKILL.md +7 -3
- package/bin/install.mjs +10 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +6 -4
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +20 -7
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +84 -7
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +189 -18
- package/bridges/antigravity-cli-bridge/bin/agy.sh +55 -6
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +77 -41
- package/bridges/antigravity-cli-bridge/capability.json +4 -2
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +5 -0
- package/bridges/codex-cli-bridge/SKILL.md +8 -4
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +129 -11
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +278 -23
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +20 -7
- package/bridges/codex-cli-bridge/bin/codex-review.sh +76 -13
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +112 -17
- package/bridges/codex-cli-bridge/capability.json +19 -4
- package/bridges/codex-cli-bridge/references/driving-codex.md +11 -0
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +3 -2
- package/references/modes/recommendations.md +1 -0
- package/references/modes/upgrade.md +9 -4
- package/references/modes/velocity.md +2 -0
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +2 -2
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/shared/command-shapes.md +22 -0
- package/references/shared/composition-handoff.md +7 -2
- package/references/templates/agent_rules.md +10 -1
- package/tools/autonomy-doctor.mjs +1 -1
- package/tools/bridge-settings-read.mjs +25 -5
- package/tools/changed-surface.mjs +8 -8
- package/tools/commands.mjs +9 -0
- package/tools/delegation.mjs +2 -2
- package/tools/detect-backends.mjs +10 -0
- package/tools/grounding.mjs +13 -13
- package/tools/inject-methodology.mjs +71 -40
- package/tools/lens-region.mjs +113 -36
- package/tools/migrate-adr-store.mjs +3 -3
- package/tools/procedures.mjs +1 -1
- package/tools/recipes.mjs +2 -2
- package/tools/recommendations.mjs +34 -7
- package/tools/release-scan.mjs +12 -2
- package/tools/review-state.mjs +3 -3
- package/tools/sandbox-masks.mjs +13 -13
- package/tools/set-recipe.mjs +1 -1
- package/tools/velocity-profile.mjs +7 -7
- package/tools/worktrees.mjs +2292 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { describe, it, after } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import {
|
|
4
4
|
mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, readFileSync,
|
|
5
|
-
existsSync, readdirSync, symlinkSync,
|
|
5
|
+
existsSync, readdirSync, symlinkSync, cpSync,
|
|
6
6
|
} from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join, dirname, resolve } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { spawnSync, execFile } from 'node:child_process';
|
|
11
11
|
import { createHash } from 'node:crypto';
|
|
12
12
|
|
|
13
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -54,16 +54,23 @@ const FAKE_CODEX = [
|
|
|
54
54
|
'',
|
|
55
55
|
].join('\n');
|
|
56
56
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
|
|
57
|
+
// The PATH farms and the sandbox base are READ-ONLY per invocation, so both are built ONCE and
|
|
58
|
+
// shared (a per-call farm rebuild is thousands of symlinks; a per-test `git init`+commit
|
|
59
|
+
// dominates the sandbox cost).
|
|
60
|
+
const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'codex-review-shared-'));
|
|
61
|
+
after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
|
|
62
|
+
const farms = new Map();
|
|
63
|
+
const farmFor = (exclude) => {
|
|
64
|
+
const key = exclude.join('|');
|
|
65
|
+
if (!farms.has(key)) farms.set(key, makePathWithout(SHARED_ROOT, exclude));
|
|
66
|
+
return farms.get(key);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const TEMPLATE_ROOT = (() => {
|
|
70
|
+
const root = join(SHARED_ROOT, 'template-root');
|
|
62
71
|
const bin = join(root, 'bin');
|
|
63
72
|
mkdirSync(bin, { recursive: true });
|
|
64
|
-
|
|
65
|
-
writeFileSync(stub, FAKE_CODEX, { mode: 0o755 });
|
|
66
|
-
chmodSync(stub, 0o755);
|
|
73
|
+
writeFileSync(join(bin, 'codex'), FAKE_CODEX, { mode: 0o755 });
|
|
67
74
|
const repo = join(root, 'repo');
|
|
68
75
|
mkdirSync(repo);
|
|
69
76
|
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
@@ -74,6 +81,18 @@ const makeSandbox = ({ clean = false } = {}) => {
|
|
|
74
81
|
writeFileSync(join(repo, 'plan.md'), '# Plan\n\nDo a thing in two steps.\n');
|
|
75
82
|
g('add', '-A');
|
|
76
83
|
g('commit', '-qm', 'base');
|
|
84
|
+
return root;
|
|
85
|
+
})();
|
|
86
|
+
|
|
87
|
+
// `clean: true` leaves a committed, pristine tree (for the no-diff preflight);
|
|
88
|
+
// the default leaves one uncommitted untracked file so `code` mode has a diff to
|
|
89
|
+
// review (otherwise the new no-diff preflight short-circuits before codex runs).
|
|
90
|
+
const makeSandbox = ({ clean = false } = {}) => {
|
|
91
|
+
const root = mkdtempSync(join(tmpdir(), 'codex-review-test-'));
|
|
92
|
+
cpSync(TEMPLATE_ROOT, root, { recursive: true });
|
|
93
|
+
const bin = join(root, 'bin');
|
|
94
|
+
chmodSync(join(bin, 'codex'), 0o755);
|
|
95
|
+
const repo = join(root, 'repo');
|
|
77
96
|
if (!clean) writeFileSync(join(repo, 'pending.txt'), 'an uncommitted change to review\n');
|
|
78
97
|
return { root, bin, repo };
|
|
79
98
|
};
|
|
@@ -123,6 +142,35 @@ const run = ({ repo, bin }, { args = ['code'], env = {}, path, cwd } = {}) => {
|
|
|
123
142
|
return { ...r, codexHome, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) };
|
|
124
143
|
};
|
|
125
144
|
|
|
145
|
+
// Async twin of run() for the sleep-bound timeout test: spawnSync blocks the event loop for
|
|
146
|
+
// the whole deliberate wait, so a concurrent describe could not overlap it. Same contract.
|
|
147
|
+
const runAsync = ({ repo, bin }, { args = ['code'], env = {}, path, cwd } = {}) =>
|
|
148
|
+
new Promise((done) => {
|
|
149
|
+
const argvFile = join(repo, '.cap-argv');
|
|
150
|
+
const envFile = join(repo, '.cap-env');
|
|
151
|
+
const stdinFile = join(repo, '.cap-stdin');
|
|
152
|
+
const codexHome = join(repo, '..', 'codex-home');
|
|
153
|
+
const child = execFile('bash', [WRAPPER, ...args], {
|
|
154
|
+
cwd: cwd || repo,
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
timeout: 30000,
|
|
157
|
+
env: {
|
|
158
|
+
PATH: path || `${bin}:${process.env.PATH}`,
|
|
159
|
+
HOME: repo,
|
|
160
|
+
TMPDIR: process.env.TMPDIR ?? '/tmp',
|
|
161
|
+
CODEX_HOME: codexHome,
|
|
162
|
+
CODEX_FAKE_ARGV: argvFile,
|
|
163
|
+
CODEX_FAKE_ENV: envFile,
|
|
164
|
+
CODEX_FAKE_STDIN: stdinFile,
|
|
165
|
+
...env,
|
|
166
|
+
},
|
|
167
|
+
}, (error, stdout, stderr) => {
|
|
168
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
|
|
169
|
+
done({ status: error ? (error.code ?? 1) : 0, stdout, stderr, codexHome, argv: readIf(argvFile), capEnv: readIf(envFile), capStdin: readIf(stdinFile) });
|
|
170
|
+
});
|
|
171
|
+
child.stdin.end();
|
|
172
|
+
});
|
|
173
|
+
|
|
126
174
|
describe('codex-review.sh — quality-first model/effort guard (1.1)', () => {
|
|
127
175
|
it('refuses a non-default CODEX_MODEL', () => {
|
|
128
176
|
const sb = makeSandbox();
|
|
@@ -255,11 +303,11 @@ describe('codex-review.sh — subscription / config isolation (invariant)', () =
|
|
|
255
303
|
});
|
|
256
304
|
});
|
|
257
305
|
|
|
258
|
-
describe('codex-review.sh — hard timeout (1.3)', () => {
|
|
259
|
-
it('kills a hung review at CODEX_HARD_TIMEOUT and reports it', () => {
|
|
306
|
+
describe('codex-review.sh — hard timeout (1.3)', { concurrency: true }, () => {
|
|
307
|
+
it('kills a hung review at CODEX_HARD_TIMEOUT and reports it', async () => {
|
|
260
308
|
const sb = makeSandbox();
|
|
261
309
|
const started = Date.now();
|
|
262
|
-
const r =
|
|
310
|
+
const r = await runAsync(sb, { env: { CODEX_FAKE_SLEEP: '30', CODEX_HARD_TIMEOUT: '2' } });
|
|
263
311
|
const elapsed = Date.now() - started;
|
|
264
312
|
rmSync(sb.root, { recursive: true, force: true });
|
|
265
313
|
assert.ok(elapsed < 18000, `must return well under the kill-after window, took ${elapsed}ms`);
|
|
@@ -269,7 +317,7 @@ describe('codex-review.sh — hard timeout (1.3)', () => {
|
|
|
269
317
|
|
|
270
318
|
it('warns and runs uncapped when neither timeout nor gtimeout is on PATH', () => {
|
|
271
319
|
const sb = makeSandbox();
|
|
272
|
-
const path = `${sb.bin}:${
|
|
320
|
+
const path = `${sb.bin}:${farmFor(['timeout', 'gtimeout'])}`;
|
|
273
321
|
const r = run(sb, { path });
|
|
274
322
|
rmSync(sb.root, { recursive: true, force: true });
|
|
275
323
|
assert.equal(r.status, 0, r.stderr);
|
|
@@ -414,7 +462,7 @@ describe('codex-review.sh — optional structured findings (2.2)', () => {
|
|
|
414
462
|
describe('codex-review.sh — environment preflight (fail fast, before a run)', () => {
|
|
415
463
|
it('STOPs with 127 when codex is not on PATH', () => {
|
|
416
464
|
const sb = makeSandbox();
|
|
417
|
-
const path =
|
|
465
|
+
const path = farmFor(['codex']); // no fake codex, no real codex
|
|
418
466
|
const r = run(sb, { args: ['code'], path });
|
|
419
467
|
rmSync(sb.root, { recursive: true, force: true });
|
|
420
468
|
assert.equal(r.status, 127);
|
|
@@ -570,7 +618,7 @@ const runHelp = (arg) => {
|
|
|
570
618
|
const root = mkdtempSync(join(tmpdir(), 'codex-review-help-'));
|
|
571
619
|
const nongit = join(root, 'nongit');
|
|
572
620
|
mkdirSync(nongit, { recursive: true });
|
|
573
|
-
const path =
|
|
621
|
+
const path = farmFor(['codex', 'agy', 'git']);
|
|
574
622
|
const r = spawnSync('bash', [WRAPPER, arg], {
|
|
575
623
|
cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
|
|
576
624
|
});
|
|
@@ -684,6 +732,12 @@ describe('codex-review.sh — --help contract (manifest-pinned)', () => {
|
|
|
684
732
|
assert.equal(norm(helpSection(help, 'Receipt:').join(' ')), norm(REVIEW_CONTRACT.receipt));
|
|
685
733
|
assert.match(REVIEW_CONTRACT.receipt, /sha256 over the canonical uncommitted-state payload/, 'the fingerprint definition lives in the manifest contract');
|
|
686
734
|
});
|
|
735
|
+
|
|
736
|
+
it('Notes renders the manifest review contract.notes verbatim (AD-061 — a typed contract key that MUST surface)', () => {
|
|
737
|
+
const help = runHelp('--help').stdout;
|
|
738
|
+
assert.ok((REVIEW_CONTRACT.notes ?? []).length >= 2, 'the review contract declares the banner-only-timeout + quote-verbatim notes');
|
|
739
|
+
assert.equal(norm(helpSection(help, 'Notes:').join(' ')), norm(REVIEW_CONTRACT.notes.join(' ')));
|
|
740
|
+
});
|
|
687
741
|
});
|
|
688
742
|
|
|
689
743
|
describe('codex-review.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
|
|
@@ -1277,4 +1331,45 @@ describe('codex-review.sh — dispatch-posture labeling (D5)', () => {
|
|
|
1277
1331
|
assert.equal(receipts.length, 0);
|
|
1278
1332
|
assert.match(r.stderr, /control/i, 'named as the control-byte class, not a policy refusal');
|
|
1279
1333
|
});
|
|
1334
|
+
|
|
1335
|
+
it('the banner appends the RESOLVED hard timeout — banner-only, never in the receipt (AD-061)', () => {
|
|
1336
|
+
const sb = makeSandbox();
|
|
1337
|
+
const r = run(sb, {});
|
|
1338
|
+
const receipts = readReceipts(sb.repo);
|
|
1339
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1340
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1341
|
+
assert.match(r.stderr, /^review posture: model=gpt-5\.6-sol effort=xhigh tier=standard timeout=1800s$/m);
|
|
1342
|
+
assert.deepEqual(Object.keys(receipts[0].posture), ['model', 'effort', 'tier'], 'timeout never enters the receipt posture');
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
it('an INVALID effective CODEX_HARD_TIMEOUT (env — the closed aw_settings_valid bypass) warns and the banner prints the default', () => {
|
|
1346
|
+
const sb = makeSandbox();
|
|
1347
|
+
const r = run(sb, { env: { CODEX_HARD_TIMEOUT: 'nonsense' } });
|
|
1348
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1349
|
+
assert.equal(r.status, 0, r.stderr);
|
|
1350
|
+
assert.match(r.stderr, /invalid value 'nonsense' for CODEX_HARD_TIMEOUT/, 'the fallback is loud');
|
|
1351
|
+
assert.match(r.stderr, /^review posture: .* timeout=1800s$/m, 'the banner prints the built-in default');
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
it('a CODEX_HARD_TIMEOUT carrying CONTROL BYTES refuses pre-spend (the banner-field screen)', () => {
|
|
1355
|
+
const sb = makeSandbox();
|
|
1356
|
+
const r = run(sb, { env: { CODEX_HARD_TIMEOUT: `1800${String.fromCharCode(1)}` } });
|
|
1357
|
+
const receipts = readReceipts(sb.repo);
|
|
1358
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1359
|
+
assert.notEqual(r.status, 0);
|
|
1360
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1361
|
+
assert.equal(receipts.length, 0);
|
|
1362
|
+
assert.match(r.stderr, /control/i);
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it('a DEL (0x7f) byte in a banner field refuses pre-spend like the C0 range', () => {
|
|
1366
|
+
const sb = makeSandbox();
|
|
1367
|
+
const r = run(sb, { env: { CODEX_MODEL: `gpt-5.6-sol${String.fromCharCode(127)}` } });
|
|
1368
|
+
const receipts = readReceipts(sb.repo);
|
|
1369
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
1370
|
+
assert.notEqual(r.status, 0);
|
|
1371
|
+
assert.equal(r.capStdin, '', 'codex is never invoked');
|
|
1372
|
+
assert.equal(receipts.length, 0);
|
|
1373
|
+
assert.match(r.stderr, /control/i);
|
|
1374
|
+
});
|
|
1280
1375
|
});
|
|
@@ -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.1.0",
|
|
7
7
|
"posture": { "model": "gpt-5.6-sol", "effort": "xhigh", "tier": null },
|
|
8
8
|
"provides": ["execute", "review"],
|
|
9
9
|
"roles": {
|
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
"probeRelaxed": ["--add-dir*", "-C*", "--cd*", "--skip-git-repo-check", "--ignore-rules", "--enable*", "--disable*"]
|
|
28
28
|
},
|
|
29
29
|
"notes": [
|
|
30
|
-
"nested-sandbox limit: codex-exec ships its OWN OS sandbox (bwrap workspace-write) and cannot run nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox (excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a preemptive blanket"
|
|
30
|
+
"nested-sandbox limit: codex-exec ships its OWN OS sandbox (bwrap workspace-write) and cannot run nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox (excludedCommands / a per-run consented bypass) on the OBSERVED bwrap/EPERM failure, never a preemptive blanket",
|
|
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
|
+
"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
|
+
"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"
|
|
31
35
|
]
|
|
32
36
|
}
|
|
33
37
|
},
|
|
@@ -43,7 +47,11 @@
|
|
|
43
47
|
],
|
|
44
48
|
"grounding": "automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags",
|
|
45
49
|
"continue": [],
|
|
46
|
-
"receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write failure warns, never fails the review"
|
|
50
|
+
"receipt": "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan mode; verdict parsed from the mandated literal verdict line (schema mode: the verdict field); always fresh:true (one-shot) + grounded:true (native AGENTS.md auto-merge, factsHash null); probe = whether the run relaxed the quality guards (CODEX_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model, effort, tier} (tier null on the standard tier), written on EVERY receipt (D5) — the gate rejects a receipt with an absent/invalid posture (a pre-D5 wrapper minted it; re-run the review), one stderr banner line states the same posture, and a posture value carrying control bytes refuses pre-spend in every mode; a run whose final message carries NO recognized 'Verdict: <ship|revise|rethink>' line — empty or missing output included — exits 4 with NO receipt (D4: a FAILED review to RE-RUN, never a fatal session error); a write failure warns, never fails the review",
|
|
51
|
+
"notes": [
|
|
52
|
+
"the review 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 never enters the receipt posture or the D5 banner↔receipt parity",
|
|
53
|
+
"quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts"
|
|
54
|
+
]
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
},
|
|
@@ -72,7 +80,8 @@
|
|
|
72
80
|
"guardrails": [
|
|
73
81
|
{ "value": "runs under codex's OWN OS sandbox (workspace-write)", "enforcement": "enforced", "condition": "it cannot nest inside another sandbox — the FS turns read-only; route it outside on the OBSERVED failure", "source": "capability.json roles.execute.contract.notes" },
|
|
74
82
|
{ "value": "the guarded passthrough blocks model / sandbox / approval overrides", "enforcement": "enforced", "source": "bin/codex-exec.sh" },
|
|
75
|
-
{ "value": "hard wall-clock cap CODEX_HARD_TIMEOUT (built-in default 3600s)", "enforcement": "enforced", "condition": "only while timeout(1)/gtimeout is on PATH — otherwise the wrapper warns and runs uncapped", "source": "capability.json settings.CODEX_HARD_TIMEOUT" }
|
|
83
|
+
{ "value": "hard wall-clock cap CODEX_HARD_TIMEOUT (built-in default 3600s)", "enforcement": "enforced", "condition": "only while timeout(1)/gtimeout is on PATH — otherwise the wrapper warns and runs uncapped", "source": "capability.json settings.CODEX_HARD_TIMEOUT" },
|
|
84
|
+
{ "value": "ONE exec posture stderr banner line states the ACTUAL run posture before dispatch (session=fresh|resume:<id>; timeout is banner-only)", "enforcement": "enforced", "source": "bin/codex-exec.sh" }
|
|
76
85
|
],
|
|
77
86
|
"customHooks": ["CODEX_PROBE"]
|
|
78
87
|
},
|
|
@@ -87,6 +96,9 @@
|
|
|
87
96
|
"operands": [
|
|
88
97
|
{ "slot": "<plan-file|->", "required": true, "description": "the follow-up instruction file, or - to read it from stdin" }
|
|
89
98
|
],
|
|
99
|
+
"guardrails": [
|
|
100
|
+
{ "value": "ONE exec posture stderr banner line states the ACTUAL run posture before dispatch (session=resume:<id> only AFTER the sidecar id is resolved and validated; timeout is banner-only)", "enforcement": "enforced", "source": "bin/codex-exec.sh" }
|
|
101
|
+
],
|
|
90
102
|
"customHooks": ["CODEX_PROBE"]
|
|
91
103
|
},
|
|
92
104
|
{
|
|
@@ -100,6 +112,9 @@
|
|
|
100
112
|
{ "slot": "<session-id>", "required": true, "description": "the session id the original run printed on stderr" },
|
|
101
113
|
{ "slot": "<plan-file|->", "required": true, "description": "the follow-up instruction file, or - to read it from stdin" }
|
|
102
114
|
],
|
|
115
|
+
"guardrails": [
|
|
116
|
+
{ "value": "ONE exec posture stderr banner line states the ACTUAL run posture before dispatch (session=resume:<id> only AFTER the explicit id is validated against the session-id grammar; timeout is banner-only)", "enforcement": "enforced", "source": "bin/codex-exec.sh" }
|
|
117
|
+
],
|
|
103
118
|
"customHooks": ["CODEX_PROBE"]
|
|
104
119
|
},
|
|
105
120
|
{
|
|
@@ -27,6 +27,17 @@ model or effort for a real run — the wrapper will stop you. Quota is metered i
|
|
|
27
27
|
never from a downgrade. The only opt-out is a throwaway, effort-independent probe: `CODEX_PROBE=1`
|
|
28
28
|
(loud) — never use its output as real work.
|
|
29
29
|
|
|
30
|
+
## Posture banners — quote them verbatim
|
|
31
|
+
|
|
32
|
+
Every dispatch states its ACTUAL posture on ONE stderr line: `codex-exec` emits
|
|
33
|
+
`exec posture: model=… effort=… tier=… sandbox=workspace-write session=fresh|resume:<id> timeout=…`
|
|
34
|
+
(fresh and resume alike, only after the resume id is resolved and validated), `codex-review` emits
|
|
35
|
+
`review posture: model=… effort=… tier=… timeout=…`. When you label a dispatch for a user or a
|
|
36
|
+
record, **quote the posture banner verbatim** — the banner is the machine-stated posture; a prose
|
|
37
|
+
re-type drifts. The `timeout=` field is **banner-only** (exactly the duration handed to
|
|
38
|
+
`timeout(1)`, or `uncapped` when no capping binary is on PATH) — informational, never part of a
|
|
39
|
+
receipt or the banner↔receipt parity.
|
|
40
|
+
|
|
30
41
|
## Exec vs review
|
|
31
42
|
|
|
32
43
|
Use **`codex-exec`** when there is a concrete plan or focused instruction to implement, the project
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
### Mode: bootstrap
|
|
2
2
|
|
|
3
|
-
Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md
|
|
3
|
+
Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md · ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
|
|
4
4
|
|
|
5
5
|
> Bundled sources below (templates, scripts) live in **this skill's own directory** — `${CLAUDE_SKILL_DIR}/` in Claude Code, or the folder containing this `SKILL.md` in Codex / other agents. Use that as the copy/read source; the working directory is the **target project**, not the skill.
|
|
6
6
|
|
|
@@ -9,7 +9,8 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
9
9
|
1. **Recon (read-only).** Before writing anything:
|
|
10
10
|
- `package.json` / `pyproject.toml` / `go.mod` / `Cargo.toml` → stack, package manager, scripts.
|
|
11
11
|
- `ls -la` root → `README`, existing `AGENTS.md`/`CLAUDE.md`, CI configs, linter/formatter configs.
|
|
12
|
-
- `git log --oneline -30`
|
|
12
|
+
- `git log --oneline -30` → recent activity.
|
|
13
|
+
- `git status` → branch, uncommitted changes.
|
|
13
14
|
- `src/` (or equivalent) 2–3 levels deep → modules, routes/pages, components, services, types.
|
|
14
15
|
- Tests (framework, location, E2E?) and linter rules.
|
|
15
16
|
- Record: stack, package manager, daily commands (`dev`/`test`/`lint`/`type-check`), routes/pages, architecture layers.
|
|
@@ -24,6 +24,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
24
24
|
- `sandbox-lane` — surface this note TOGETHER with the sandbox-lanes ladder below (the ladder IS the practical half of the note — inline, never a pointer). Pure DISCOVERABILITY: it surfaces the manifest-declared observed session-sandbox recipe (egress hosts ∪ resolved writable state dirs — `networkHosts` ∪ `writableDirs` of the wired bridges' `capability.json`, the single documentation source) and converges on a NEUTRAL fingerprint acknowledgement recorded by the consent-gated **ack writer** into the family-owned `docs/ai/acks.json` (`sandboxLaneAck`; a changed recipe re-fires the item). The store is family-owned so no host settings validator guards it (AD-055 relocated the ack off the Claude Code settings schema, which rejected the unknown key); the legacy `"agentWorkflow": { "sandboxLaneAck": … }` settings-scope key is still READ for one deprecation window (until the next kit MAJOR). It never claims the settings security keys take effect on any host class, never recommends writing them, and the kit never seeds `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur: a network pre-allow widens egress for EVERY sandboxed command; a write allowance on CLI state dirs would expose credential dirs). Posture history: an IDE-managed session sandbox was live-observed (2026-07-11/12) ignoring hand-applied settings security keys in BOTH scopes, and codex needs a writable HOME (EROFS `~/.codex` in-sandbox); whether a session's sandbox honors the settings keys is runtime-unknowable from the advisor (a denial-only signal) — which is exactly why the item states only detectable facts and no zero-prompt promise on any host class.
|
|
25
25
|
|
|
26
26
|
- `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.
|
|
27
|
+
- `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. Risk profile: a real write-surface widening where honored — scope it deliberately.
|
|
27
28
|
|
|
28
29
|
**Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
|
|
29
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
### Mode: upgrade
|
|
2
2
|
|
|
3
|
-
Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md
|
|
3
|
+
Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKILL_DIR}/references/shared/composition-handoff.md · ${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md · ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
|
|
4
4
|
|
|
5
5
|
1. Read `docs/ai/.workflow-version` (the project's stamped lineage). If missing, treat as a pre-versioned deployment and offer to re-bootstrap conservatively.
|
|
6
6
|
2. **Never-downgrade gate — FIRST, before any write.** Compare the stamp to the **deployment-lineage head** (`3.0.0` — NOT this kit's package version). If the stamp is **greater than the head** or unparseable → **STOP and report**; do not touch a newer / unknown deployment at all (not even the methodology slot). This STOP is one of the few places the number is actionable (*Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`): show the user **the `docs/ai` structure version** their deployment carries versus the one this kit expects, plus the plain one-line two-axes note — naming it the structure version, **never** "lineage head".
|
|
@@ -48,7 +48,12 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
48
48
|
(refresh it with `npx @sabaiway/agent-workflow-engine@latest init`, then re-run) · **over the
|
|
49
49
|
file's line cap — refused** (trim the file, re-run). The section is found by its heading — no
|
|
50
50
|
markers; a renamed heading is preserved + noted. A fully absent/invalid engine is the same hard
|
|
51
|
-
STOP as (c). Exit 0 covers every soft outcome; only the STOP is non-zero.
|
|
51
|
+
STOP as (c). Exit 0 covers every soft outcome; only the STOP is non-zero. The same invocation
|
|
52
|
+
also reconciles the **Communication (user-facing messages)** section from the kit's own bundled
|
|
53
|
+
template canon (no engine involved) — relay its line too: *refreshed* / *already current* /
|
|
54
|
+
*custom edit preserved + note* / *section absent — noted (never an insert)* / *over the line
|
|
55
|
+
cap — refused*; an unreadable bundled template canon is its own loud STOP naming the kit
|
|
56
|
+
reinstall command.
|
|
52
57
|
|
|
53
58
|
**Bridge settings reconcile — stamp-independent, same gate, BEFORE the equal-head short-circuit.**
|
|
54
59
|
Run `node ${CLAUDE_SKILL_DIR}/tools/bridge-settings.mjs --reconcile` and **paste its outcome line
|
|
@@ -56,7 +61,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
56
61
|
**NEVER writes** it (the file lives outside every kit tree — D2), so an unknown/retired key is
|
|
57
62
|
flagged + preserved, never edited. Runs on **every** upgrade; exit 0 covers every outcome.
|
|
58
63
|
4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
|
|
59
|
-
- **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
64
|
+
- **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*) and the **Communication-section** outcome (its own set: refreshed / already current / custom preserved + note / section absent — noted / over the cap — refused); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
60
65
|
- **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
|
|
61
66
|
- **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
|
|
62
67
|
- **Live host/session facts are tool-composed only.** Any claim this report makes about the current host or session state — prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts — must trace to **live tool output** from **this session** (the lines you just composed, or a probe you ran this run); a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection. Full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
|
|
@@ -65,4 +70,4 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
65
70
|
5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
|
|
66
71
|
6. **Collect the migration answers FIRST, then apply.** If `AGENTS.md` is missing BOTH the *Communication language* and *Attribution* blocks — i.e. both blocks are missing (a pre-1.1.0 deployment) — ask the two questions as ONE structured multi-question prompt; record each answer individually, write nothing until ALL are answered, and carry the answers into the migrations below: a migration whose answer was already collected never re-asks (its own "Ask the user" step is the standalone fallback); a single missing block keeps its single ask (step 7). Then apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** — safe to re-run.
|
|
67
72
|
7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`. (An answer already collected by the step-6 batched prompt is carried in — never re-asked here.)
|
|
68
|
-
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 **placed-bridge refresh** lines (pasted verbatim), the step-3 **agent-rules lens**
|
|
73
|
+
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`3.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 **placed-bridge refresh** lines (pasted verbatim), the step-3 **agent-rules lens** + **Communication-section** outcomes (same outcome sets as step 4), the step-3 **bridge-settings reconcile** outcome, and the step-3 **autonomy-declaration ensure** outcome (*seeded* / *already present, preserved*) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). **Every current host/session claim in this report is tool-composed only** — prompts fired, sandbox scope, whether a bypass was needed, network reachability and approval counts must trace to **live tool output** from **this session**, a memory/handover snapshot is **context, never report facts**, and an unbacked claim is **omitted or explicitly marked unverified** (full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
### Mode: velocity
|
|
2
2
|
|
|
3
|
+
Requires: ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
|
|
4
|
+
|
|
3
5
|
The opt-in onboarding **velocity profile** — it seeds a fixed, audited **read-only** Claude Code allowlist into `.claude/settings.json` so an agent stops idling on approval prompts for routine read-only commands while the maintainer is away. It is the family's **first programmatic `.claude/settings.json` writer** (attribution stayed an agent-driven prose merge). **In-agent, opt-in, writes only `.claude/settings.json`**, on one hard rule: **it never allowlists `commit`/`push`/`publish`** — so a direct commit/push/publish still ASKs; the only caveat is the trust-posture residual (below) — its closure is **shipped, opt-in: `${CLAUDE_SKILL_DIR}/references/modes/hook.md`**.
|
|
4
6
|
|
|
5
7
|
**Version-status routing (like the other writer modes):** read `docs/ai/.workflow-version` first — not-deployed → bootstrap; stamp < `3.0.0` → `upgrade`; stamp > head / unparseable → STOP. The tool enforces this in code too (`--apply` STOPs unless the stamp is the lineage head).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
### Mode: worktrees
|
|
2
|
+
|
|
3
|
+
Parallel feature worktrees (v1) — several features implemented simultaneously in DIFFERENT agent
|
|
4
|
+
sessions on one machine/repo, zero interference on working-tree files (the ONE exception: the
|
|
5
|
+
default `node_modules` symlink is a shared MUTABLE dependency cache — see below), unambiguous
|
|
6
|
+
ownership. One thin tool over git; every verification datum is recomputed live from git, never
|
|
7
|
+
read from stored metadata.
|
|
8
|
+
The ONE stored-metadata exception is the PREPARED OID recorded in the handoff: land and cleanup read it back only for recovery.
|
|
9
|
+
|
|
10
|
+
**Run** — `node ${CLAUDE_SKILL_DIR}/tools/worktrees.mjs <subcommand> …`:
|
|
11
|
+
|
|
12
|
+
Git ≥ 2.36 is required for NUL-terminated worktree porcelain; an older Git fails closed with its
|
|
13
|
+
own verbatim error through the existing Git-error surface.
|
|
14
|
+
|
|
15
|
+
- `provision <slug> --plan <path> [--as <name>.md] [--dir <path>] [--branch <name>] [--include <path>]... [--install] [--resume]`
|
|
16
|
+
— create a feature worktree (default: the visible sibling `<repoParent>/<repoName>--<slug>`,
|
|
17
|
+
branch `aw/<slug>`) and populate it: the registry-derived footprint copy-if-missing (a tracked
|
|
18
|
+
file is NEVER overwritten), EXACTLY ONE seeded feature plan, the `handoff-<slug>.md` stub
|
|
19
|
+
(written at provision — the tool's own record; `list` and `cleanup` read it), a
|
|
20
|
+
`node_modules` symlink when main has one and the link stays ignored — a SHARED MUTABLE cache:
|
|
21
|
+
writes through it hit MAIN's node_modules; for isolation RUN the printed isolated-install
|
|
22
|
+
command (`--install` only prints it), and on `--resume` an existing symlink is kept — run the
|
|
23
|
+
printed unlink-first recovery first. Absolute root-pinned gate commands are rebased on UNTRACKED copies only — and only
|
|
24
|
+
while their bytes still equal the MAIN source (or its rebased form); user-modified copies stay
|
|
25
|
+
byte-untouched. `--install` only PRINTS the install command — zero spawn, zero write.
|
|
26
|
+
`--resume <slug>` completes a half-done provision (identity fail-closed; handoff user sections,
|
|
27
|
+
the seeded plan, and edited copies are preserved byte-exact; the provision-record section is
|
|
28
|
+
refreshed atomically; copy-if-missing everywhere).
|
|
29
|
+
Resume identity also binds the EXISTING handoff: at most one `handoff-<slug>.md` may exist and
|
|
30
|
+
its recorded slug AND branch must match the live invocation — a second handoff, a name/record
|
|
31
|
+
mismatch, or a handoff that is not a regular file is a typed STOP before anything is written
|
|
32
|
+
(the writability probe itself runs only after these checks on a resume). The provision record
|
|
33
|
+
is read from exactly one REQUIRED `## Provision record` section (a decoy field elsewhere cannot
|
|
34
|
+
hijack identity); a missing or repeated section, or a duplicated single-valued field, is a typed
|
|
35
|
+
STOP, never last-wins.
|
|
36
|
+
- `list` — read-only: every worktree of this repo with slug (from the handoff file; none →
|
|
37
|
+
"unknown (foreign)"), path, branch, base OID (the worktree HEAD — under the v1 no-commit bar
|
|
38
|
+
that IS the provision base; a manual satellite commit moves it, and land derives its own base
|
|
39
|
+
live), dirty flag, handoff presence, opener suggestion. Honest read errors: only a genuinely
|
|
40
|
+
ABSENT docs/plans under a symlink-free, present worktree dir reads as `handoff: no`; a
|
|
41
|
+
symlinked docs/plans (or ancestor), a handoff-named entry that is not a regular file, a
|
|
42
|
+
vanished worktree dir, or any other read failure renders `handoff: (unreadable)` — never a
|
|
43
|
+
silent "no".
|
|
44
|
+
- `land <slug> --prepare` — stage the satellite's finished diff onto a CLEAN main; the commit is
|
|
45
|
+
NEVER run by the tool — it stays a dialogue ask at the primary session. Land takes the transient
|
|
46
|
+
common-git-dir lock, refuses a dirty main, graph divergence, visible `docs/ai` drift, excluded
|
|
47
|
+
staged paths, unstaged/untracked leftovers, an empty diff, or a red satellite review-state. The
|
|
48
|
+
complete satellite working-tree diff versus its base is inspected (staged and unstaged); every
|
|
49
|
+
unstaged/untracked path is listed and refused, so an accepted transfer cannot silently omit it.
|
|
50
|
+
The binary/no-ext-diff/no-textconv transfer excludes exactly `docs/ai` and `docs/plans`, applies
|
|
51
|
+
into the index, optionally runs the porcelain-visible sync adapter, then runs the main gate
|
|
52
|
+
matrix. It reports main HEAD, TRANSFER, PREPARED, and sync delta OIDs/data; the primary re-attests
|
|
53
|
+
that prepared tree and confirms main HEAD still equals the printed OID before the commit ask.
|
|
54
|
+
- `cleanup <slug> [--branch <name>] [--abandon]` — take the same transient lock and remove a LANDED
|
|
55
|
+
worktree fail-closed after live landed-verification against main HEAD. Verification uses exactly
|
|
56
|
+
the land exclusions (`docs/ai`, `docs/plans`), then checks untracked and ignored content before a
|
|
57
|
+
plain worktree remove, branch `-d`, and prune. Provision-derived ignored containers are ephemeral;
|
|
58
|
+
`node_modules` of any kind is provision-derivable because provision explicitly advises installs.
|
|
59
|
+
Foreign content stops cleanup. `--abandon` is the ONE destructive arm: it DESTROYS unlanded work,
|
|
60
|
+
requires the handoff identity, and is the only path where `--force` may appear.
|
|
61
|
+
|
|
62
|
+
**Honesty:** there is NO preview step on the writers — over-warned by design. The tool never
|
|
63
|
+
commits, never pushes, never runs a subscription CLI. Every content read and regular-file copy
|
|
64
|
+
goes through its one no-follow descriptor door (identity-bound source, exclusive destination,
|
|
65
|
+
descriptor mode update), and tripwire tests keep them the only paths.
|
|
66
|
+
|
|
67
|
+
**Settings:** the parent dir for new worktrees is the `docs/ai/worktrees.json` `{"parentDir": …}`
|
|
68
|
+
project setting (hand-editable strict JSON; absent file → the sibling default; malformed → a
|
|
69
|
+
typed STOP, never a guess). The file must be a REGULAR, non-symlink file reached through a
|
|
70
|
+
symlink-free path — anything else is a typed STOP (the advisor renders the same shape as a
|
|
71
|
+
stated skip), and the ancestor chain is verified even when the leaf is ABSENT (a symlinked
|
|
72
|
+
`docs/` or `docs/ai` never reads as plain absence). `--dir` overrides per invocation.
|
|
73
|
+
|
|
74
|
+
**Host-specific consent (zero-prompt only where the host honors it):** every sibling-dir mutation
|
|
75
|
+
runs a REAL create+delete writability probe first. An unwritable parent prints both the
|
|
76
|
+
settings-native line (`sandbox.filesystem.allowWrite` in `.claude/settings.json`) and the full
|
|
77
|
+
terminal command. On a settings-native host that honors the key, adding the parent can make later
|
|
78
|
+
provision/cleanup promptless. On a harness-managed host that ignores project settings, grant the
|
|
79
|
+
narrow parent through the host/session controls; if that is unavailable, use the printed terminal
|
|
80
|
+
command for each operation. The `recommendations` mode surfaces this lane but treats write access
|
|
81
|
+
as unverified without a trusted host-capability signal.
|
|
82
|
+
|
|
83
|
+
**Landing flow:** provision → work → handoff → land → re-attest → commit → cleanup. Satellite
|
|
84
|
+
commits are outside v1: graph divergence stops land and prints cherry-pick/rebase recovery. A gate
|
|
85
|
+
failure keeps the prepared main tree and names both recovery lanes. A second prepare is reset-only:
|
|
86
|
+
the STOP prints the current staged write-tree, compares it with the PREPARED OID recorded in the
|
|
87
|
+
handoff, distinguishes a converged re-run from foreign staged work, and lists removal commands for
|
|
88
|
+
untracked crash residue before the reset-and-re-run lane. A killed process may leave
|
|
89
|
+
`aw-prepare-lock`; after confirming no land/cleanup process owns it, remove that directory by hand.
|
|
90
|
+
|
|
91
|
+
The optional `scripts/sync-mirrors.mjs` adapter runs as a child. Its contract is porcelain-visible
|
|
92
|
+
output: tracked modifications/deletions or untracked-not-ignored creations. Ignored writes and
|
|
93
|
+
changes inside an already-untracked file are outside observation and therefore out of contract.
|
|
94
|
+
Cleanup reports EBUSY as likely lingering processes/open file descriptors (including a sandbox
|
|
95
|
+
mount); close them and retry, outside the sandbox when needed. Hidden satellite `docs/ai` state is
|
|
96
|
+
ephemeral by design; durable content belongs in the handoff before landing — the handoff carries a
|
|
97
|
+
free-form session-records digest slot (every section outside `## Provision record` is user-owned
|
|
98
|
+
and byte-preserved by the tool).
|
|
99
|
+
|
|
100
|
+
**Ownership:** MAIN owns MAIN-tree files, commits, pushes, releases, the gate matrix, every
|
|
101
|
+
docs/ai record, `docs/plans/queue.md`, and all shared git state — stash, hooks, repo config,
|
|
102
|
+
`.git/info/exclude`, and every ref except the satellite's configured branch. The SATELLITE owns
|
|
103
|
+
that one branch (`aw/<slug>` or the `--branch` override), its feature edits, its seeded plan, and
|
|
104
|
+
the user-owned handoff sections; `## Provision record` remains tool-owned. Satellite forbidden
|
|
105
|
+
verbs (the v1 docs-only bar): no `git commit`/`push`/`tag`/`git stash`/history rewrite — the ONE
|
|
106
|
+
legal rewrite is the tool-printed `git reset --hard` recovery of the satellite's OWN configured
|
|
107
|
+
branch (`aw/<slug>` or the `--branch` override) — no kit lifecycle writers
|
|
108
|
+
(`init`/`upgrade`/`setup`/`hide-footprint`/`install-git-hooks`/`sandbox-masks`/`ack-write`), no
|
|
109
|
+
queue.md writes, no version bumps or publishes, no edits to MAIN's files from the satellite
|
|
110
|
+
session — divergence and the landed verification enforce the observable half; the rest is the
|
|
111
|
+
stated contract. A symlinked `node_modules` under npm workspaces resolves
|
|
112
|
+
workspace self-links to MAIN-tree sources — use the printed isolated install when that matters.
|
|
113
|
+
|
|
114
|
+
**Other harnesses:** PROVEN — the host-installed codex/agy review wrappers run with a provisioned
|
|
115
|
+
worktree as their cwd; the footprint carries the root `AGENTS.md` required by the codex wrappers
|
|
116
|
+
and available to agy as its fallback project context. The host-level `bridge-settings.conf` under
|
|
117
|
+
`${XDG_CONFIG_HOME:-~/.config}/agent-workflow/` is per-host, shared by every worktree, and is not
|
|
118
|
+
copied into the worktree. ASSUMED until probed per harness — each target harness's project-local
|
|
119
|
+
settings and context pickup in a fresh worktree session; treat that as unverified until the target
|
|
120
|
+
harness demonstrates it.
|
|
@@ -649,7 +649,7 @@ const runMigrate = (root, flags, today, deps, log, logError) => {
|
|
|
649
649
|
verifyConservation(oldItems, newItems);
|
|
650
650
|
// Integrity over the FULL final store — existing records ∪ freshly-exploded — not just the new
|
|
651
651
|
// writes: a pre-existing adr/ record whose id also stays RETAINED in HOT would otherwise leave the
|
|
652
|
-
// ADR in two places
|
|
652
|
+
// ADR in two places. Same full-store pattern as runRotate.
|
|
653
653
|
const finalStoreById = new Map(existingStore.map((e) => [e.id, { id: e.id, idNum: e.idNum, fileName: e.fileName }]));
|
|
654
654
|
for (const r of records) finalStoreById.set(r.id, { id: r.id, idNum: r.idNum, fileName: r.fileName });
|
|
655
655
|
assertStoreIntegrity(retained, [...finalStoreById.values()]);
|
|
@@ -783,7 +783,7 @@ const runRotate = (root, flags, today, deps, log, logError) => {
|
|
|
783
783
|
}
|
|
784
784
|
|
|
785
785
|
const records = explode(toExplode, today);
|
|
786
|
-
// Crash-resume (fold-induced
|
|
786
|
+
// Crash-resume (fold-induced): a record from a prior crashed rotate may already be on
|
|
787
787
|
// disk. A byte-identical one is done (deduped, not a duplicate-id error); a divergent-body one is
|
|
788
788
|
// corrupt → FAIL. The FINAL store = existing ∪ freshly-exploded, deduped by id.
|
|
789
789
|
const existingById = new Map(existingStore.map((e) => [e.id, e]));
|
|
@@ -341,7 +341,7 @@ describe('1.3 --migrate --apply — records + snapshot + retire monoliths + HOT
|
|
|
341
341
|
assert.equal(run(['--check', '--today=2026-07-09'], root).code, 0, 'the resumed tree passes --check');
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
-
it('refuses when a pre-existing adr/ record duplicates an ADR that stays in HOT (never two places —
|
|
344
|
+
it('refuses when a pre-existing adr/ record duplicates an ADR that stays in HOT (never two places — review-adr-archive-r04-major-01)', () => {
|
|
345
345
|
const root = makeRoot();
|
|
346
346
|
seedLegacy(root, { hot: ['005', '006'], warm: ['003'], cold: ['001'] });
|
|
347
347
|
mkdirSync(join(root, ADR_DIR_REL), { recursive: true });
|
|
@@ -452,7 +452,7 @@ describe('1.4 --check', () => {
|
|
|
452
452
|
assert.match(errText, /duplicate ADR id AD-002/);
|
|
453
453
|
});
|
|
454
454
|
|
|
455
|
-
it('a decisions.md with NO maxLines cap fails loud (never operates against an unknown budget —
|
|
455
|
+
it('a decisions.md with NO maxLines cap fails loud (never operates against an unknown budget — review-adr-archive-r04-major-02)', () => {
|
|
456
456
|
const root = makeRoot();
|
|
457
457
|
mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
|
|
458
458
|
const noCapFm = '---\ntype: reference\nlastUpdated: 2026-01-01\nscope: permanent\nstaleAfter: never\nowner: none\n---\n';
|
|
@@ -514,7 +514,7 @@ describe('1.4 --check', () => {
|
|
|
514
514
|
assert.match(errText, /two records for AD-001/);
|
|
515
515
|
});
|
|
516
516
|
|
|
517
|
-
it('a NESTED subdirectory in adr/ fails loud (the store is a flat directory —
|
|
517
|
+
it('a NESTED subdirectory in adr/ fails loud (the store is a flat directory — review-adr-archive-r02-major-01)', () => {
|
|
518
518
|
const root = makeRoot();
|
|
519
519
|
seedMigrated(root, { hotIds: ['005'], storeIds: ['001'] });
|
|
520
520
|
mkdirSync(join(root, ADR_DIR_REL, 'nested'), { recursive: true });
|
|
@@ -561,7 +561,7 @@ describe('1.4 default rotate — explode the oldest beyond cap + regenerate the
|
|
|
561
561
|
assert.equal(regenCalls.length, 0, 'a no-op never regenerates the index');
|
|
562
562
|
});
|
|
563
563
|
|
|
564
|
-
it('is crash-resumable: a byte-identical record from a crashed prior rotate is deduped, not a fatal duplicate (
|
|
564
|
+
it('is crash-resumable: a byte-identical record from a crashed prior rotate is deduped, not a fatal duplicate (review-adr-archive-r02-major-02)', () => {
|
|
565
565
|
const root = makeRoot();
|
|
566
566
|
mkdirSync(join(root, ADR_DIR_REL), { recursive: true });
|
|
567
567
|
const blocks = ['005', '006', '007', '008'].map((id) => adrBlock(id));
|
|
@@ -579,7 +579,7 @@ describe('1.4 default rotate — explode the oldest beyond cap + regenerate the
|
|
|
579
579
|
assert.equal(run(['--check', '--today=2026-07-09'], root).code, 0);
|
|
580
580
|
});
|
|
581
581
|
|
|
582
|
-
it('a no-op rotate still REFUSES a corrupt store (partition) — never a silent green no-op (
|
|
582
|
+
it('a no-op rotate still REFUSES a corrupt store (partition) — never a silent green no-op (review-adr-archive-r02-major-03)', () => {
|
|
583
583
|
const root = makeRoot();
|
|
584
584
|
seedMigrated(root, { hotIds: ['005'], storeIds: ['001'] }); // under cap
|
|
585
585
|
const rec = explode(parseDecisionsText(tierText(999, '# T', [adrBlock('010')]), 'x').entries, '2026-07-09')[0];
|