@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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// check-docs-size-cli.test.mjs — runCli branch pins the subprocess smokes cannot reach
|
|
2
|
+
// in-process (Phase-5 coverage fill; the main spec file is parity-frozen, so these ride a
|
|
3
|
+
// colocated file): the unknown-argument refusal and the written-empty-index guard.
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { runCli } from './check-docs-size.mjs';
|
|
10
|
+
|
|
11
|
+
const cli = async (argv) => {
|
|
12
|
+
const { code, stdout, stderr } = await runCli(argv);
|
|
13
|
+
return { code, stdout, stderr };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('check-docs-size runCli — refusal branches', () => {
|
|
17
|
+
it('an unknown argument exits 2 naming it', async () => {
|
|
18
|
+
const { code, stderr } = await cli(['--bogus']);
|
|
19
|
+
assert.equal(code, 2);
|
|
20
|
+
assert.match(stderr, /Unknown argument: --bogus/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('--write-index landing on a sink path (index stat size 0) is the loud written-empty refusal', async () => {
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'cds-cli-'));
|
|
25
|
+
try {
|
|
26
|
+
mkdirSync(join(root, 'docs', 'ai'), { recursive: true });
|
|
27
|
+
writeFileSync(
|
|
28
|
+
join(root, 'docs', 'ai', 'a.md'),
|
|
29
|
+
'---\ntype: state\nlastUpdated: 2026-07-18\nscope: session\nstaleAfter: never\nowner: none\nmaxLines: 10\n---\n\n# a\n',
|
|
30
|
+
);
|
|
31
|
+
// The index path is a symlink into /dev/null: the write lands, the stat reads size 0 —
|
|
32
|
+
// the guard must refuse loudly instead of reporting a written index.
|
|
33
|
+
symlinkSync('/dev/null', join(root, 'docs', 'ai', 'index.md'));
|
|
34
|
+
const { code, stderr } = await cli(['--write-index', `--root=${root}`]);
|
|
35
|
+
assert.equal(code, 2);
|
|
36
|
+
assert.match(stderr, /index\.md was written empty/);
|
|
37
|
+
} finally {
|
|
38
|
+
rmSync(root, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -103,25 +103,19 @@ export const discoverMeta = async (root = ROOT) => {
|
|
|
103
103
|
return { projectName, hierarchicalLinks, onDemandLinks };
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
// Pure argv parser (no I/O, no exit): `help` / `error` ride out as data for runCli to render.
|
|
106
107
|
const parseArgs = (argv) => {
|
|
107
108
|
const flags = { report: false, writeIndex: false, checkIndex: false, quiet: false };
|
|
108
109
|
const opts = { today: null, root: null };
|
|
109
|
-
for (const arg of argv
|
|
110
|
+
for (const arg of argv) {
|
|
110
111
|
if (arg === '--report') flags.report = true;
|
|
111
112
|
else if (arg === '--write-index') flags.writeIndex = true;
|
|
112
113
|
else if (arg === '--check-index') flags.checkIndex = true;
|
|
113
114
|
else if (arg === '--quiet') flags.quiet = true;
|
|
114
115
|
else if (arg.startsWith('--today=')) opts.today = arg.slice('--today='.length);
|
|
115
116
|
else if (arg.startsWith('--root=')) opts.root = arg.slice('--root='.length);
|
|
116
|
-
else if (arg === '--help' || arg === '-h') {
|
|
117
|
-
|
|
118
|
-
'Usage: check-docs-size.mjs [--report|--write-index|--check-index] [--today=YYYY-MM-DD] [--root=<dir>] [--quiet]',
|
|
119
|
-
);
|
|
120
|
-
process.exit(0);
|
|
121
|
-
} else {
|
|
122
|
-
console.error(`Unknown argument: ${arg}`);
|
|
123
|
-
process.exit(2);
|
|
124
|
-
}
|
|
117
|
+
else if (arg === '--help' || arg === '-h') return { flags, opts, help: true };
|
|
118
|
+
else return { flags, opts, error: `Unknown argument: ${arg}` };
|
|
125
119
|
}
|
|
126
120
|
return { flags, opts };
|
|
127
121
|
};
|
|
@@ -218,7 +212,7 @@ const formatRow = (row) => {
|
|
|
218
212
|
return { status, sizeCell, ...row };
|
|
219
213
|
};
|
|
220
214
|
|
|
221
|
-
const printReport = (rows, quiet) => {
|
|
215
|
+
const printReport = (rows, quiet, log = console.log) => {
|
|
222
216
|
const widths = {
|
|
223
217
|
status: 2,
|
|
224
218
|
path: Math.max(4, ...rows.map((r) => r.path.length)),
|
|
@@ -228,15 +222,15 @@ const printReport = (rows, quiet) => {
|
|
|
228
222
|
};
|
|
229
223
|
const printable = quiet ? rows.filter((r) => r.errors.length || r.warnings.length) : rows;
|
|
230
224
|
if (printable.length > 0) {
|
|
231
|
-
|
|
225
|
+
log(
|
|
232
226
|
`${'S'.padEnd(widths.status)} ${'PATH'.padEnd(widths.path)} ${'SIZE/MAX'.padEnd(widths.size)} ${'TYPE'.padEnd(widths.type)} ${'UPDATED'.padEnd(widths.updated)}`,
|
|
233
227
|
);
|
|
234
228
|
for (const row of printable) {
|
|
235
|
-
|
|
229
|
+
log(
|
|
236
230
|
`${row.status.padEnd(widths.status)} ${row.path.padEnd(widths.path)} ${row.sizeCell.padEnd(widths.size)} ${(row.frontmatter?.type ?? '').padEnd(widths.type)} ${(row.frontmatter?.lastUpdated ?? '').padEnd(widths.updated)}`,
|
|
237
231
|
);
|
|
238
|
-
for (const err of row.errors)
|
|
239
|
-
for (const warn of row.warnings)
|
|
232
|
+
for (const err of row.errors) log(` - ERROR ${err}`);
|
|
233
|
+
for (const warn of row.warnings) log(` - WARN ${warn}`);
|
|
240
234
|
}
|
|
241
235
|
}
|
|
242
236
|
};
|
|
@@ -361,9 +355,29 @@ export const regenerateIndex = async (root, todayStr = null) => {
|
|
|
361
355
|
return { indexPath, files: rows.length };
|
|
362
356
|
};
|
|
363
357
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
358
|
+
// The return-code entry point (no process.argv / process.exit / console inside): argv[] →
|
|
359
|
+
// { code, stdout, stderr }. The thin shell at the bottom is the only process-coupled code.
|
|
360
|
+
export const runCli = async (argv, deps = {}) => {
|
|
361
|
+
const stdoutLines = [];
|
|
362
|
+
const stderrLines = [];
|
|
363
|
+
const log = (line) => stdoutLines.push(line);
|
|
364
|
+
const logError = (line) => stderrLines.push(line);
|
|
365
|
+
const result = (code) => ({
|
|
366
|
+
code,
|
|
367
|
+
stdout: stdoutLines.length > 0 ? `${stdoutLines.join('\n')}\n` : '',
|
|
368
|
+
stderr: stderrLines.length > 0 ? `${stderrLines.join('\n')}\n` : '',
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const { flags, opts, help, error } = parseArgs(argv);
|
|
372
|
+
if (help) {
|
|
373
|
+
log('Usage: check-docs-size.mjs [--report|--write-index|--check-index] [--today=YYYY-MM-DD] [--root=<dir>] [--quiet]');
|
|
374
|
+
return result(0);
|
|
375
|
+
}
|
|
376
|
+
if (error) {
|
|
377
|
+
logError(error);
|
|
378
|
+
return result(2);
|
|
379
|
+
}
|
|
380
|
+
const { root, docsDir, indexPath } = pathsFor(opts.root ? resolve(opts.root) : (deps.root ?? ROOT));
|
|
367
381
|
const today = computeToday(opts.today);
|
|
368
382
|
const files = (await walkMarkdownFiles(docsDir)).sort();
|
|
369
383
|
const inspected = await Promise.all(files.map((f) => inspectFile(f, today, root)));
|
|
@@ -373,11 +387,11 @@ const main = async () => {
|
|
|
373
387
|
|
|
374
388
|
if (flags.writeIndex) {
|
|
375
389
|
await writeIndex(rows, today, meta, indexPath);
|
|
376
|
-
|
|
390
|
+
log(`Wrote ${relative(root, indexPath)}`);
|
|
377
391
|
const after = await stat(indexPath);
|
|
378
392
|
if (after.size === 0) {
|
|
379
|
-
|
|
380
|
-
|
|
393
|
+
logError('index.md was written empty');
|
|
394
|
+
return result(2);
|
|
381
395
|
}
|
|
382
396
|
}
|
|
383
397
|
|
|
@@ -385,28 +399,31 @@ const main = async () => {
|
|
|
385
399
|
const onDisk = existsSync(indexPath) ? await readFile(indexPath, 'utf8') : null;
|
|
386
400
|
const { fresh } = checkIndexFreshness(rows, onDisk, meta);
|
|
387
401
|
if (!fresh) {
|
|
388
|
-
|
|
402
|
+
logError(
|
|
389
403
|
`[check-docs-size] FAIL: ${relative(root, indexPath)} is stale (out of sync with source frontmatter). Regenerate the index (--write-index) and commit the regenerated file.`,
|
|
390
404
|
);
|
|
391
|
-
|
|
405
|
+
return result(1);
|
|
392
406
|
}
|
|
393
|
-
|
|
407
|
+
log(
|
|
394
408
|
`[check-docs-size] OK — ${relative(root, indexPath)} is in sync with source frontmatter.`,
|
|
395
409
|
);
|
|
396
|
-
return;
|
|
410
|
+
return result(0);
|
|
397
411
|
}
|
|
398
412
|
|
|
399
|
-
printReport(rows, flags.quiet);
|
|
413
|
+
printReport(rows, flags.quiet, log);
|
|
400
414
|
const errorCount = rows.reduce((n, r) => n + r.errors.length, 0);
|
|
401
415
|
const warnCount = rows.reduce((n, r) => n + r.warnings.length, 0);
|
|
402
|
-
|
|
416
|
+
log(
|
|
403
417
|
`\n${rows.length} files inspected — ${errorCount} error(s), ${warnCount} warning(s)`,
|
|
404
418
|
);
|
|
405
419
|
|
|
406
|
-
|
|
420
|
+
return result(errorCount > 0 && !flags.report ? 1 : 0);
|
|
407
421
|
};
|
|
408
422
|
|
|
409
423
|
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
410
424
|
if (isDirectRun) {
|
|
411
|
-
await
|
|
425
|
+
const { code, stdout, stderr } = await runCli(process.argv.slice(2));
|
|
426
|
+
if (stdout) process.stdout.write(stdout);
|
|
427
|
+
if (stderr) process.stderr.write(stderr);
|
|
428
|
+
process.exitCode = code;
|
|
412
429
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
### Command shapes — the promptless bar for instructed reads & probes
|
|
2
|
+
|
|
3
|
+
When a mode doc tells you to read a file or probe project state WITHOUT prescribing the exact
|
|
4
|
+
command (a recon read, the version-stamp read, any "check X" instruction), the shape is yours —
|
|
5
|
+
and improvised shapes are where approval prompts come from. The bar:
|
|
6
|
+
|
|
7
|
+
- **Reads ride the host's file-read tool** (Read/Grep/Glob in Claude Code, or your agent's
|
|
8
|
+
equivalent) whenever one exists — a file-read tool never fires a shell approval prompt.
|
|
9
|
+
- **No file-read tool → ONE plain undecorated command per probe:** no `;`/`&&` compounds, no
|
|
10
|
+
redirects, no pipes, no command substitution — one probe per invocation. This applies the
|
|
11
|
+
deployed agent-rules lens’s «plain pipeline per call» discipline here as a stricter
|
|
12
|
+
single-command shape. Any decorated or chained form falls outside this contract’s baseline
|
|
13
|
+
guarantee; behavior is then host/config-dependent — it may prompt, an opt-in read-lane may
|
|
14
|
+
auto-approve a vetted compound, and command substitution may even slip past a prefix allow
|
|
15
|
+
rule. The plain single-command shape is the only shell fallback this contract treats as
|
|
16
|
+
promptless by construction.
|
|
17
|
+
- **Improvised file writes ride the host's file-edit tools** (Write/Edit or the equivalent) —
|
|
18
|
+
never an ad-hoc heredoc or shell-redirect write.
|
|
19
|
+
|
|
20
|
+
**Scope — improvised shapes only.** The executable commands a mode doc itself prescribes (the
|
|
21
|
+
`node …/tools/…` dispatch lines, `--apply` lanes, install/symlink steps) are OUTSIDE this
|
|
22
|
+
contract: run them exactly as prescribed, as plain single invocations.
|
|
@@ -43,7 +43,7 @@ duplicate anchor → **STOP with an error**, never edit (the file is left byte-f
|
|
|
43
43
|
exceed the 100-line cap, the engine is too old to ship its fragment, or (autonomy only) its anchor
|
|
44
44
|
(the orchestration pair) is absent: every prior pointer still lands and the upgrade continues.
|
|
45
45
|
|
|
46
|
-
**Agent-rules lens refresh (runs in BOTH paths).** Its precondition is its own target file — run it
|
|
46
|
+
**Agent-rules lens + Communication refresh (runs in BOTH paths).** Its precondition is its own target file — run it
|
|
47
47
|
only after `<project>/docs/ai/agent_rules.md` exists: after the substrate deploy in the
|
|
48
48
|
**delegated** path, after the fallback-template copy in the **kit** path (never anchored to the
|
|
49
49
|
`AGENTS.md` step above). ONE command:
|
|
@@ -53,4 +53,9 @@ prior canonical body is refreshed from the installed engine; a current seed repo
|
|
|
53
53
|
current* (zero-diff). Relay its outcome in plain language (*refreshed* / *already current* /
|
|
54
54
|
*custom edit preserved + note* / *file absent — skipped* / *engine too old — skipped* / *over the
|
|
55
55
|
line cap — refused*); a fully absent/invalid engine is the same loud STOP + one-line install
|
|
56
|
-
command as the pointer reconcile.
|
|
56
|
+
command as the pointer reconcile. The same invocation also reconciles the **Communication
|
|
57
|
+
(user-facing messages)** section from the kit's own bundled template canon (no engine involved):
|
|
58
|
+
a body matching the current canon is reported *already current*; a body matching a known prior
|
|
59
|
+
is refreshed; a custom body is preserved + noted; an absent section is a stated note (never an
|
|
60
|
+
insert); an over-cap refresh is refused; and an unreadable bundled template canon is its own
|
|
61
|
+
loud STOP naming the kit reinstall command.
|
|
@@ -63,7 +63,16 @@ Before proposing changes or committing, review against:
|
|
|
63
63
|
### 2.4. Quality Gates
|
|
64
64
|
- Always run type-checker, linter, and all tests before committing.
|
|
65
65
|
|
|
66
|
-
### 2.5.
|
|
66
|
+
### 2.5. Communication (user-facing messages)
|
|
67
|
+
Apply this as part of §2 before any user-facing summary:
|
|
68
|
+
- **Plain language.** User-facing narration is short, clear, plain words of the dialogue language; when the dialogue language is not English, transliterated English jargon is banned — an English term survives only as the NAME of a thing (a flag / command / file / test), glossed in plain words when helpful; plain English stays plain for English-dialogue users.
|
|
69
|
+
- **Deliver the artifact IN the message** — paste the prompt / diff / version / command inline; never "see §X / open the file / run it and you'll see" as a *substitute* for showing what was asked.
|
|
70
|
+
- **Lead with the result**, then the details; show exactly what was asked — no deflection, no "almost done" when the ask was the finished thing.
|
|
71
|
+
- **No condescension, no filler.** Own a miss plainly and fix it in the same message.
|
|
72
|
+
- **Large artifact (≈>100 lines):** deliver a real summary or the key excerpt inline **and** link the file — never flood the reader with a 2000-line paste, never hide the answer behind a bare pointer.
|
|
73
|
+
- **Live host/session facts are tool-composed only.** Any claim 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**; 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.
|
|
74
|
+
|
|
75
|
+
### 2.6. Planning, review & process-fidelity invariants
|
|
67
76
|
Apply these when authoring a plan, reviewing, folding a finding, or editing code — the layer read **before any code change**. (Full canon: the project's planning / workflow-methodology + orchestration canon. This section is rendered from that canon and refreshed on upgrade; a custom edit is preserved verbatim, but flagged.)
|
|
68
77
|
- **Fold by code, not prose.** Before folding a code-touching finding into a plan or change, read the cited `file:line` and cite it — a prose fold drifts from the code and seeds the next bug.
|
|
69
78
|
- **Right altitude.** Pin intent + invariants + acceptance criteria (named tests); leave fine code-mechanics to Execute, where prose cannot diverge from reality.
|
|
@@ -254,7 +254,7 @@ const renderDiagnosis = ({ plan, probeResult, log }) => {
|
|
|
254
254
|
}
|
|
255
255
|
};
|
|
256
256
|
|
|
257
|
-
// The untrusted degrade every lane runs FIRST
|
|
257
|
+
// The untrusted degrade every lane runs FIRST: while any sandbox binary resolves only
|
|
258
258
|
// outside the trusted dirs, no offer, no consent invitation, and no privileged path may proceed —
|
|
259
259
|
// an install cannot make such a host verify-ready.
|
|
260
260
|
const refuseUntrusted = ({ plan, log }) => {
|
|
@@ -148,6 +148,17 @@ export const duplicateKeys = (parsed) => [...parsed.byKey.entries()].filter(([,
|
|
|
148
148
|
|
|
149
149
|
// ── effective-value resolution (env > file > built-in default) ────────────────────────
|
|
150
150
|
|
|
151
|
+
// The env keys the wrappers resolver-validate AFTER precedence (AD-061 aw_effective_timeout) —
|
|
152
|
+
// exactly these mirror the wrapper's invalid-env → warn + built-in-default lane.
|
|
153
|
+
const RESOLVER_VALIDATED_ENV = new Set(['CODEX_HARD_TIMEOUT', 'AGY_HARD_TIMEOUT']);
|
|
154
|
+
// A control byte (C0 range + DEL) in a screened knob makes the wrapper EXIT 2 pre-spend — the
|
|
155
|
+
// advisor must report that refusal, never a benign fallback, and never echo the raw byte into
|
|
156
|
+
// the report (a newline could otherwise forge a `review posture:`/status line). safeShow escapes
|
|
157
|
+
// every note value: JSON.stringify handles C0, an explicit pass handles DEL (JSON leaves it raw).
|
|
158
|
+
const hasControlByte = (s) => /[\x01-\x1f\x7f]/.test(s);
|
|
159
|
+
const safeShow = (s) => JSON.stringify(s).slice(1, -1).replace(/\x7f/g, '\\u007f');
|
|
160
|
+
const REFUSED = (key, v) => ({ value: null, source: 'default', note: `env value "${safeShow(v)}" carries control bytes for ${key} — the wrapper REFUSES the run pre-spend` });
|
|
161
|
+
|
|
151
162
|
// The effective value of one knob + where it comes from. Fact-only; never a model claim.
|
|
152
163
|
export const effectiveOf = (entry, parsed, getenv) => {
|
|
153
164
|
const key = entry.key;
|
|
@@ -159,13 +170,22 @@ export const effectiveOf = (entry, parsed, getenv) => {
|
|
|
159
170
|
// add-dir knobs fall to their real built-in default). Report the manifest default (null ⇒ "wrapper
|
|
160
171
|
// built-in applies"), not a misleading null-for-everything.
|
|
161
172
|
if (v === '') return { value: entry.default, source: 'default', note: 'the env KEY= suppresses the file override — the wrapper built-in applies' };
|
|
173
|
+
// A control byte in a SCREENED knob (the tier + the resolver-validated timeouts) is a pre-spend
|
|
174
|
+
// refusal in the wrapper — report it as such (the default value is inert; the run never happens).
|
|
175
|
+
if (hasControlByte(v) && (entry.kind === 'enum' || RESOLVER_VALIDATED_ENV.has(key))) {
|
|
176
|
+
return REFUSED(key, v);
|
|
177
|
+
}
|
|
162
178
|
// Mirror the wrapper: an ENUM knob (the service tier) validates its ENV value too — codex accepts
|
|
163
179
|
// any `-c service_tier` string silently, so the wrapper drops an unsupported one to the built-in
|
|
164
|
-
// default (codex-exec.sh:188).
|
|
165
|
-
//
|
|
166
|
-
//
|
|
180
|
+
// default (codex-exec.sh:188). The TIMEOUT knobs are resolver-validated env INCLUDED since AD-061
|
|
181
|
+
// (aw_effective_timeout closed the old env bypass, with a 7-digit integer-part overflow bound) —
|
|
182
|
+
// the advisor must never display a dead override as active. Every OTHER non-enum env value stays
|
|
183
|
+
// the operator's RAW override (the wrappers run no resolver on it), shown as-is.
|
|
167
184
|
if (entry.kind === 'enum' && !settingValueValid(entry, v)) {
|
|
168
|
-
return { value: entry.default, source: 'default', note: `env value "${v}" is not a supported ${key} — the wrapper runs the built-in default` };
|
|
185
|
+
return { value: entry.default, source: 'default', note: `env value "${safeShow(v)}" is not a supported ${key} — the wrapper runs the built-in default` };
|
|
186
|
+
}
|
|
187
|
+
if (RESOLVER_VALIDATED_ENV.has(key) && (!settingValueValid(entry, v) || v.match(/^\d*/)[0].length > 7)) {
|
|
188
|
+
return { value: entry.default, source: 'default', note: `env value "${safeShow(v)}" is invalid for ${key} — the wrapper falls back to the built-in default` };
|
|
169
189
|
}
|
|
170
190
|
return { value: v, source: 'env' };
|
|
171
191
|
}
|
|
@@ -173,7 +193,7 @@ export const effectiveOf = (entry, parsed, getenv) => {
|
|
|
173
193
|
if (fileEntries && fileEntries.length) {
|
|
174
194
|
const v = fileEntries[fileEntries.length - 1].value; // last occurrence wins
|
|
175
195
|
if (settingValueValid(entry, v)) return { value: v, source: 'file' };
|
|
176
|
-
return { value: entry.default, source: 'default', note: `file value "${v}" is invalid — falls back to the built-in default` };
|
|
196
|
+
return { value: entry.default, source: 'default', note: `file value "${safeShow(v)}" is invalid — falls back to the built-in default` };
|
|
177
197
|
}
|
|
178
198
|
return { value: entry.default, source: 'default' };
|
|
179
199
|
};
|
|
@@ -37,7 +37,7 @@ export const classifyChangedPath = (rel) => {
|
|
|
37
37
|
|
|
38
38
|
// Strip the quotes and decode escapes BYTE-wise (octal escapes are UTF-8 bytes) — an unparsed
|
|
39
39
|
// quoted path compares unequal to its classifier/testId form and would silently escape the
|
|
40
|
-
// coverage/cap surface (
|
|
40
|
+
// coverage/cap surface (AD-047).
|
|
41
41
|
const CQUOTE_SIMPLE = { n: 10, t: 9, r: 13, f: 12, v: 11, b: 8, a: 7, '"': 34, '\\': 92 };
|
|
42
42
|
export const unquoteDiffPath = (p) => {
|
|
43
43
|
if (!(p.length >= 2 && p.startsWith('"') && p.endsWith('"'))) return p;
|
|
@@ -47,7 +47,7 @@ export const unquoteDiffPath = (p) => {
|
|
|
47
47
|
const c = inner[i];
|
|
48
48
|
if (c !== '\\') {
|
|
49
49
|
// Consume a full CODE POINT — 16-bit-unit iteration would split a surrogate pair (an
|
|
50
|
-
// unescaped non-BMP char, reachable under core.quotepath=false) into replacement bytes
|
|
50
|
+
// unescaped non-BMP char, reachable under core.quotepath=false) into replacement bytes.
|
|
51
51
|
const ch = String.fromCodePoint(inner.codePointAt(i));
|
|
52
52
|
for (const b of Buffer.from(ch, 'utf8')) bytes.push(b);
|
|
53
53
|
i += ch.length - 1;
|
|
@@ -89,7 +89,7 @@ export const parseUnifiedDiff = (diffText) => {
|
|
|
89
89
|
}
|
|
90
90
|
if (inHeader && line.startsWith('--- ')) continue;
|
|
91
91
|
if (inHeader && line.startsWith('+++ ')) {
|
|
92
|
-
const p = unquoteDiffPath(line.slice(4).replace(/[\t\r]+$/, '')); // TAB/CR are git artifacts, never filename bytes
|
|
92
|
+
const p = unquoteDiffPath(line.slice(4).replace(/[\t\r]+$/, '')); // TAB/CR are git artifacts, never filename bytes
|
|
93
93
|
current = p === '/dev/null' ? null : p.startsWith('b/') ? p.slice(2) : p;
|
|
94
94
|
inHeader = false;
|
|
95
95
|
continue;
|
|
@@ -114,7 +114,7 @@ export const parseUnifiedDiff = (diffText) => {
|
|
|
114
114
|
// ── the changed surface (git-driven; the ONE computation both consumers read) ─────────────────────
|
|
115
115
|
|
|
116
116
|
// The one diff-invocation shape every surface pass uses. The a/ b/ prefixes are pinned EXPLICITLY
|
|
117
|
-
//
|
|
117
|
+
// A user's global diff.noprefix=true would otherwise drop them and the parsers would eat
|
|
118
118
|
// a real directory named "a" — user git config must never bend the parse.
|
|
119
119
|
export const DIFF_FLAGS = ['--unified=0', '--no-color', '--no-ext-diff', '--no-renames', '--src-prefix=a/', '--dst-prefix=b/'];
|
|
120
120
|
|
|
@@ -134,7 +134,7 @@ export const readFileSafe = (path) => {
|
|
|
134
134
|
};
|
|
135
135
|
// A changed LEAF is read only if it is a REGULAR file. lstat (no-follow): a symlinked or non-regular
|
|
136
136
|
// leaf must NEVER be read/followed — following it could read outside the work tree or HANG on a
|
|
137
|
-
// FIFO/device (
|
|
137
|
+
// FIFO/device (AD-046). A non-regular leaf fails closed (routed to `unsupported`).
|
|
138
138
|
export const isRegularLeaf = (abs) => {
|
|
139
139
|
try {
|
|
140
140
|
return lstatSync(abs).isFile();
|
|
@@ -150,13 +150,13 @@ export const isRegularLeaf = (abs) => {
|
|
|
150
150
|
// pure deletions cost nothing, subtractive folds stay free); an untracked file is wholly new, so all
|
|
151
151
|
// its lines are "changed". `unsupportedLines` is the D4 cap's view of the SAME pass: unsupported
|
|
152
152
|
// SOURCE files carry their new-side lines too (excluding them would gift a large-TS-fold bypass,
|
|
153
|
-
//
|
|
153
|
+
// BUGFREE-2 D4); an unreadable/non-regular leaf counts 0 lines but stays LISTED (the
|
|
154
154
|
// coverage gate still fails closed on it). excluded-test and out-of-domain never carry lines.
|
|
155
155
|
export const computeChangedSurface = (root) => {
|
|
156
156
|
// Unborn branch (no HEAD yet): the plain diff alone misses files STAGED for the initial commit —
|
|
157
157
|
// they sit in the index, so they are neither worktree-vs-index changes nor untracked. Merge the
|
|
158
158
|
// --cached diff (index vs the empty tree) with the plain one; parseUnifiedDiff unions per-file
|
|
159
|
-
// lines across concatenated diffs
|
|
159
|
+
// lines across concatenated diffs.
|
|
160
160
|
const trackedDiff = gitStdout(['diff', 'HEAD', ...DIFF_FLAGS], root)
|
|
161
161
|
?? `${gitStdout(['diff', '--cached', ...DIFF_FLAGS], root) ?? ''}\n${gitStdout(['diff', ...DIFF_FLAGS], root) ?? ''}`;
|
|
162
162
|
const trackedLines = parseUnifiedDiff(trackedDiff);
|
|
@@ -207,7 +207,7 @@ export const computeChangedSurface = (root) => {
|
|
|
207
207
|
// ── the shared fail-closed positive-integer knob parser (AD-047 precedent) ───────────────────────
|
|
208
208
|
|
|
209
209
|
// Zero / negative / fractional / non-numeric values are refusals by name — the parseInt(...)||default
|
|
210
|
-
// idiom would silently accept bad truthy values (
|
|
210
|
+
// idiom would silently accept bad truthy values (AD-046). Unset → the default; set → a
|
|
211
211
|
// positive integer, exactly. `makeError` lets each caller throw its OWN typed STOP.
|
|
212
212
|
export const parsePositiveIntKnob = (env, name, fallback, makeError = (m) => new Error(m)) => {
|
|
213
213
|
const raw = env[name];
|
package/tools/commands.mjs
CHANGED
|
@@ -231,6 +231,15 @@ const CATALOG = [
|
|
|
231
231
|
kind: READ_ONLY,
|
|
232
232
|
oneLine: 'Check that the documented contract tokens still match the live code constants they describe — a read-only lint that fails closed on drift; --check turns it into a gate exit code.',
|
|
233
233
|
},
|
|
234
|
+
{
|
|
235
|
+
// NEVER `guarded` — that kind promises dry-run-first, which these writers do not have; the
|
|
236
|
+
// honest strongest caution is `writer` with the destructive arm named in the line itself.
|
|
237
|
+
key: 'worktrees',
|
|
238
|
+
invocation: invocationOf('worktrees'),
|
|
239
|
+
group: 'Orchestrate',
|
|
240
|
+
kind: WRITER,
|
|
241
|
+
oneLine: 'Run features in parallel git worktrees: provision an isolated sibling copy, list them, stage a finished one back onto clean main (the commit still asks in dialogue), and remove a live-verified landed one. No preview step; list is read-only; cleanup --abandon destroys unlanded work.',
|
|
242
|
+
},
|
|
234
243
|
];
|
|
235
244
|
|
|
236
245
|
// Deep-freeze: freeze the array AND every entry, so the catalog is genuinely immutable at runtime
|
package/tools/delegation.mjs
CHANGED
|
@@ -89,7 +89,7 @@ export const handoffPlan = (delegate) =>
|
|
|
89
89
|
memoryWrites: ['docs/ai/', 'AGENTS.md', 'docs/ai/.memory-version'],
|
|
90
90
|
// The lens region runs AFTER the substrate deploy (its own precondition: the file exists)
|
|
91
91
|
// — it converges a stale-memory seed to the installed engine's canon (AD-041).
|
|
92
|
-
kitWrites: ['AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens
|
|
92
|
+
kitWrites: ['AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens + Communication regions', 'docs/ai/.workflow-version'],
|
|
93
93
|
stampsPresent: ['.memory-version', '.workflow-version'],
|
|
94
94
|
memoryRaisesCommitGate: false,
|
|
95
95
|
commitGate: 'kit-only-after-injection',
|
|
@@ -101,7 +101,7 @@ export const handoffPlan = (delegate) =>
|
|
|
101
101
|
// the kit reconciles them (ensure-slot + inject-because-empty) exactly like the delegate
|
|
102
102
|
// path — so both paths end with FILLED slots, not inline methodology. The lens region
|
|
103
103
|
// runs after the fallback-template copy of docs/ai (same reconcile, both paths — AD-041).
|
|
104
|
-
kitWrites: ['docs/ai/', 'AGENTS.md', 'AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens
|
|
104
|
+
kitWrites: ['docs/ai/', 'AGENTS.md', 'AGENTS.md pointer slots (methodology / orchestration / autonomy)', 'docs/ai/agent_rules.md lens + Communication regions', 'docs/ai/.workflow-version'],
|
|
105
105
|
stampsPresent: ['.workflow-version'],
|
|
106
106
|
memoryRaisesCommitGate: false,
|
|
107
107
|
commitGate: 'kit-only-after-injection',
|
|
@@ -82,6 +82,10 @@ const RAW_BACKENDS = [
|
|
|
82
82
|
},
|
|
83
83
|
notes: [
|
|
84
84
|
'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',
|
|
85
|
+
'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',
|
|
86
|
+
'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',
|
|
87
|
+
'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',
|
|
88
|
+
'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
|
|
85
89
|
],
|
|
86
90
|
},
|
|
87
91
|
review: {
|
|
@@ -92,6 +96,10 @@ const RAW_BACKENDS = [
|
|
|
92
96
|
grounding: 'automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags',
|
|
93
97
|
continue: [],
|
|
94
98
|
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',
|
|
99
|
+
notes: [
|
|
100
|
+
'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',
|
|
101
|
+
'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
|
|
102
|
+
],
|
|
95
103
|
},
|
|
96
104
|
},
|
|
97
105
|
bin: 'codex',
|
|
@@ -128,6 +136,8 @@ const RAW_BACKENDS = [
|
|
|
128
136
|
receipt: "side effect — a successful review appends one JSON receipt line to <git dir>/agent-workflow-review-receipts.jsonl (AW_REVIEW_RECEIPTS overrides; plan/diff outside a git tree: warn + skip unless overridden): fingerprint = sha256 over the canonical uncommitted-state payload (staged diff + unstaged diff + untracked-not-ignored contents — the review-payload domain; never-committable untracked paths — character/block devices, FIFOs, sockets — are excluded from the domain entirely, untracked symlinks/directories ride as name-only notes) in code mode, the artifact-file sha256 in plan/diff mode; verdict recorded verbatim from the mandated '### Verdict' section (SHIP / SHIP WITH NITS / REWORK); grounded = whether a NON-EMPTY --facts payload was supplied (code mode refuses pre-spend without one — no run, no receipt — unless --ungrounded/AGY_PROBE=1; in plan/diff an empty payload records grounded:false — fail-closed, the state gate rejects it), factsHash = sha256 of the facts payload; a continuation receipt is fresh:false (informational-only — it cannot attest the folded tree); probe = whether the run relaxed the quality guards (AGY_PROBE=1), written on EVERY receipt so it self-declares — the kit's review-state gate rejects a probe-marked receipt (a probe review never attests) and equally rejects an unmarked one (silence is not a declaration); posture = the ACTUAL run posture {model} (agy has no 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, an ATTESTING review with AGY_MODEL explicitly emptied refuses pre-spend, and a model string carrying control bytes refuses pre-spend in every mode; a run whose output carries NO recognized '### Verdict' section — empty 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",
|
|
129
137
|
notes: [
|
|
130
138
|
'pre-dispatch host-diff: before the FIRST dispatch of this bridge, diff its declared networkHosts against the live sandbox allow-list — a missing host is surfaced to the maintainer BEFORE dispatching, never fired into a known prompt',
|
|
139
|
+
'the review posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration agy-run hands 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',
|
|
140
|
+
'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
|
|
131
141
|
],
|
|
132
142
|
},
|
|
133
143
|
},
|
package/tools/grounding.mjs
CHANGED
|
@@ -102,7 +102,7 @@ export const assembleGrounding = ({ constraintsText = null, autonomyText = null,
|
|
|
102
102
|
// Trim `payload` to `budget` bytes, tail-first, with a loud in-band marker. The budget is a HARD
|
|
103
103
|
// ceiling: when it is smaller than the marker itself, the marker is truncated too (the output may
|
|
104
104
|
// never exceed `budget` — a facts file over the reserved share would push the final wrapper prompt
|
|
105
|
-
// past the argv ceiling
|
|
105
|
+
// past the argv ceiling). Returns { text, trimmedBytes }. Never a silent cut.
|
|
106
106
|
export const trimToBudget = (payload, budget) => {
|
|
107
107
|
const bytes = Buffer.byteLength(payload, 'utf8');
|
|
108
108
|
if (bytes <= budget) return { text: payload, trimmedBytes: 0 };
|
|
@@ -164,17 +164,17 @@ const gitLine = (args, cwd) => {
|
|
|
164
164
|
// EXISTING in-repo file, even gitignored, is a project file — the .env clobber class). Refused:
|
|
165
165
|
// tracked paths, in-repo not-ignored paths (a new untracked file would move the fingerprint),
|
|
166
166
|
// every other outside-repo destination, symlink and existing non-regular leaves, and any
|
|
167
|
-
// unverifiable lstat. The check runs on the REAL destination, never the lexical path
|
|
167
|
+
// unverifiable lstat. The check runs on the REAL destination, never the lexical path:
|
|
168
168
|
// the parent directory is realpath-resolved first, so a symlink can never route the write onto a
|
|
169
169
|
// tracked/in-repo file. Returns { path, kind: 'temp' | 'repo-fresh' } — a repo-fresh caller MUST
|
|
170
|
-
// write with the exclusive flag ('wx'), sealing the guard→write race
|
|
170
|
+
// write with the exclusive flag ('wx'), sealing the guard→write race.
|
|
171
171
|
export const assertScratchDestination = (outPath, cwd) => {
|
|
172
172
|
const lexical = isAbsolute(outPath) ? outPath : resolve(cwd, outPath);
|
|
173
173
|
let leaf = null;
|
|
174
174
|
try {
|
|
175
175
|
leaf = lstatSync(lexical);
|
|
176
176
|
} catch (err) {
|
|
177
|
-
// ONLY an absent leaf is a fresh file; any other lstat failure fails CLOSED (
|
|
177
|
+
// ONLY an absent leaf is a fresh file; any other lstat failure fails CLOSED (this
|
|
178
178
|
// writer is bridge-tier auto-allowable, so an unverifiable leaf must never be written).
|
|
179
179
|
if (err?.code !== 'ENOENT') {
|
|
180
180
|
throw fail(1, `--out cannot inspect the destination (${outPath}: ${err?.code ?? err?.message ?? err}) — refusing to write an unverifiable leaf`);
|
|
@@ -185,7 +185,7 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
185
185
|
throw fail(1, `--out refuses a symlink destination (${outPath}) — the write would follow it onto another file; name the real scratch path`);
|
|
186
186
|
}
|
|
187
187
|
if (leaf != null && !leaf.isFile()) {
|
|
188
|
-
throw fail(1, `--out refuses an existing non-regular destination (${outPath}) — a FIFO/device/directory write would hang or land on a non-file
|
|
188
|
+
throw fail(1, `--out refuses an existing non-regular destination (${outPath}) — a FIFO/device/directory write would hang or land on a non-file; name a fresh or regular scratch path`);
|
|
189
189
|
}
|
|
190
190
|
let realParent;
|
|
191
191
|
try {
|
|
@@ -194,8 +194,8 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
194
194
|
throw fail(1, `--out parent directory does not exist (${dirname(lexical)}) — create the scratch dir first`);
|
|
195
195
|
}
|
|
196
196
|
const full = join(realParent, basename(lexical));
|
|
197
|
-
// An out-of-repo (or no-repo) destination is scratch ONLY under a system temp root
|
|
198
|
-
//
|
|
197
|
+
// An out-of-repo (or no-repo) destination is scratch ONLY under a system temp root: the bridge
|
|
198
|
+
// tier auto-allows this tool with an args wildcard, so "anything outside
|
|
199
199
|
// the repo is scratch" would let an unattended run overwrite e.g. ~/.bashrc promptless.
|
|
200
200
|
// $TMPDIR / os.tmpdir() / /tmp are the scratch surface; everything else refuses loudly.
|
|
201
201
|
const assertTempScratch = () => {
|
|
@@ -215,7 +215,7 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
215
215
|
if (top == null || top.status !== 0) return { path: assertTempScratch().path, kind: 'temp' }; // no git tree → temp-only scratch
|
|
216
216
|
const root = top.stdout.replace(/\r?\n$/, '');
|
|
217
217
|
const rel = relative(root, full);
|
|
218
|
-
// Segment-safe outside test (the Issue-004 class
|
|
218
|
+
// Segment-safe outside test (the Issue-004 class): an in-repo file literally named
|
|
219
219
|
// `..facts` has rel `..facts` — `startsWith('..')` would misread it as outside and BYPASS the
|
|
220
220
|
// tracked/ignored refusals below. Only `..` exactly or a `..${sep}`-prefixed rel is outside.
|
|
221
221
|
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return { path: assertTempScratch().path, kind: 'temp' };
|
|
@@ -227,11 +227,11 @@ export const assertScratchDestination = (outPath, cwd) => {
|
|
|
227
227
|
if (ignored == null || ignored.status !== 0) {
|
|
228
228
|
throw fail(1, `--out refuses an in-repo path that is not gitignored (${rel}) — a new untracked file would move the review fingerprint; use a gitignored path or a location outside the repo`);
|
|
229
229
|
}
|
|
230
|
-
// An in-repo destination must be a FRESH file
|
|
230
|
+
// An in-repo destination must be a FRESH file: even a gitignored existing file is a
|
|
231
231
|
// project file (.env is the canonical victim) — an auto-allowable writer must never clobber one.
|
|
232
232
|
// The rewritable scratch surface is the system temp; in-repo gitignored writes are create-only —
|
|
233
233
|
// the CALLER must open with the exclusive flag ('wx'): the kind seals the pre-check against the
|
|
234
|
-
// guard→write race
|
|
234
|
+
// guard→write race.
|
|
235
235
|
if (leaf != null) {
|
|
236
236
|
throw fail(1, `--out refuses to OVERWRITE an existing in-repo file (${rel}) — even gitignored, it is a project file (the .env class); write rewritable scratch under $TMPDIR//tmp, or remove the file first`);
|
|
237
237
|
}
|
|
@@ -328,7 +328,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
328
328
|
};
|
|
329
329
|
const constraintsText = constraints ? readOrStop('AGENTS.md', 'root AGENTS.md') : null;
|
|
330
330
|
const autonomyText = autonomy ? resolveAutonomyFacts({ cwd }) : null;
|
|
331
|
-
// --plan is CONFINED to the git work tree
|
|
331
|
+
// --plan is CONFINED to the git work tree: the bridge tier auto-allows this tool
|
|
332
332
|
// with an args wildcard, so an unattended invocation could otherwise point --plan at ANY
|
|
333
333
|
// readable file and ship it into the facts payload. Plans are in-repo by contract
|
|
334
334
|
// (docs/plans); an outside-tree path is a loud refusal, never a silent read.
|
|
@@ -360,7 +360,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
360
360
|
if (out != null) {
|
|
361
361
|
const dest = assertScratchDestination(out, cwd);
|
|
362
362
|
// repo-fresh writes are EXCLUSIVE ('wx'): the create-only pre-check would otherwise race a
|
|
363
|
-
// file created between the guard and this write
|
|
363
|
+
// file created between the guard and this write; temp scratch stays rewritable.
|
|
364
364
|
// ctx.writeFile is a TEST seam (the EEXIST race arm is not constructible without it).
|
|
365
365
|
const writeFile = ctx.writeFile ?? writeFileSync;
|
|
366
366
|
try {
|
|
@@ -380,7 +380,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
380
380
|
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
381
381
|
if (isDirectRun) {
|
|
382
382
|
const r = main(process.argv.slice(2));
|
|
383
|
-
// Exact writes + a natural exit
|
|
383
|
+
// Exact writes + a natural exit: console.log would append a stray newline to the
|
|
384
384
|
// byte-exact facts payload, and process.exit() can truncate large piped stdout before Node
|
|
385
385
|
// flushes. The payload already ends with '\n' (sliceSection normalizes); only a non-payload
|
|
386
386
|
// message (help / the --out report) gains the terminating newline it lacks.
|