mandrel 2.14.0 → 2.16.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/.agents/audit-checklists/navigability.md +1 -1
- package/.agents/docs/workflows.md +4 -4
- package/.agents/scripts/acceptance-eval.js +18 -1
- package/.agents/scripts/agents-bootstrap-github.js +22 -1
- package/.agents/scripts/apply-quality-bootstrap.js +6 -0
- package/.agents/scripts/audit-labels-bootstrap.js +15 -1
- package/.agents/scripts/audit-to-stories.js +26 -1
- package/.agents/scripts/boot-sweep.js +4 -1
- package/.agents/scripts/bootstrap.js +1 -0
- package/.agents/scripts/check-arch-cycles.js +20 -0
- package/.agents/scripts/check-baselines.js +8 -2
- package/.agents/scripts/check-context-budget.js +40 -5
- package/.agents/scripts/check-dead-exports.js +21 -0
- package/.agents/scripts/check-doc-links.js +12 -1
- package/.agents/scripts/check-lifecycle-doc-drift.js +9 -0
- package/.agents/scripts/check-workflow-citations.js +332 -0
- package/.agents/scripts/deliver-light.js +32 -3
- package/.agents/scripts/deliver-recover.js +4 -1
- package/.agents/scripts/diagnose-friction.js +17 -1
- package/.agents/scripts/diagnose.js +20 -14
- package/.agents/scripts/drain-pending-cleanup.js +20 -1
- package/.agents/scripts/evidence-gate.js +20 -1
- package/.agents/scripts/generate-config-docs.js +14 -1
- package/.agents/scripts/generate-lifecycle-docs.js +14 -1
- package/.agents/scripts/generate-workflows-doc.js +14 -1
- package/.agents/scripts/git-cleanup.js +32 -1
- package/.agents/scripts/lib/cli-usage.js +174 -0
- package/.agents/scripts/lib/cli-utils.js +12 -0
- package/.agents/scripts/lib/close-validation/process.js +61 -15
- package/.agents/scripts/lib/doc-tiers.js +53 -10
- package/.agents/scripts/lib/orchestration/complexity-gate.js +307 -89
- package/.agents/scripts/lib/orchestration/light-suitability.js +31 -9
- package/.agents/scripts/lib/orchestration/plan-context.js +205 -20
- package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +87 -13
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +24 -15
- package/.agents/scripts/lib/workflow-closure.js +431 -0
- package/.agents/scripts/mandrel-update-preflight.js +9 -0
- package/.agents/scripts/nav-registry-diff.js +13 -0
- package/.agents/scripts/plan-context.js +21 -2
- package/.agents/scripts/plan-critics.js +10 -0
- package/.agents/scripts/plan-persist.js +33 -1
- package/.agents/scripts/plan-run-epilogue.js +12 -1
- package/.agents/scripts/quality-preview.js +17 -1
- package/.agents/scripts/resolve-doc-tiers.js +13 -0
- package/.agents/scripts/resolve-stories.js +1 -0
- package/.agents/scripts/resync-status-column.js +4 -1
- package/.agents/scripts/signals-view.js +11 -0
- package/.agents/scripts/single-story-close.js +24 -0
- package/.agents/scripts/single-story-confirm-merge.js +16 -0
- package/.agents/scripts/single-story-init.js +21 -1
- package/.agents/scripts/stories-wave-tick.js +1 -0
- package/.agents/scripts/sync-agentrc.js +16 -4
- package/.agents/scripts/update-ticket-state.js +23 -2
- package/.agents/workflows/audit-navigability.md +2 -2
- package/.agents/workflows/audit-to-stories.md +1 -1
- package/.agents/workflows/deliver.md +80 -81
- package/.agents/workflows/git-cleanup.md +9 -14
- package/.agents/workflows/helpers/acceptance-self-eval.md +14 -13
- package/.agents/workflows/helpers/audit-lens-core.md +2 -2
- package/.agents/workflows/helpers/code-review.md +11 -11
- package/.agents/workflows/helpers/deliver-digest.md +7 -8
- package/.agents/workflows/helpers/deliver-light.md +222 -0
- package/.agents/workflows/helpers/deliver-reference.md +46 -14
- package/.agents/workflows/helpers/deliver-story-reference.md +55 -63
- package/.agents/workflows/helpers/deliver-story.md +22 -22
- package/.agents/workflows/helpers/mandrel-sync-config.md +2 -2
- package/.agents/workflows/helpers/parallel-tooling.md +1 -2
- package/.agents/workflows/helpers/plan-reference.md +96 -17
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -4
- package/.agents/workflows/mandrel-update.md +6 -6
- package/.agents/workflows/plan.md +90 -85
- package/.agents/workflows/prototype.md +104 -0
- package/docs/CHANGELOG.md +32 -0
- package/package.json +2 -1
- package/.agents/workflows/deliver-light.md +0 -148
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.agents/scripts/lib/cli-usage.js` — the one implementation of `--help` for
|
|
3
|
+
* every top-level script under `.agents/scripts/`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why
|
|
6
|
+
*
|
|
7
|
+
* A script's flag contract used to live in the workflow prose that invoked it
|
|
8
|
+
* (`deliver-story.md` Step 0 restating `single-story-init.js`'s `--dry-run` /
|
|
9
|
+
* `--steal`, and so on). Two homes for one contract is a drift class: the
|
|
10
|
+
* script changes, the prose does not, and the next agent runs a flag that no
|
|
11
|
+
* longer exists. Moving the enumeration into the script's own `--help` gives
|
|
12
|
+
* the contract a single home that ships with the code that implements it.
|
|
13
|
+
*
|
|
14
|
+
* ## Contract
|
|
15
|
+
*
|
|
16
|
+
* `--help` (or `-h`) is a **query**, never an error path:
|
|
17
|
+
*
|
|
18
|
+
* - writes non-empty text to **stdout** and exits **0**;
|
|
19
|
+
* - performs no GitHub write, acquires no lease, mutates no working tree.
|
|
20
|
+
*
|
|
21
|
+
* stdout — not `Logger.info` — because help output must survive
|
|
22
|
+
* `AGENT_LOG_LEVEL=silent` and carry no `[Orchestrator]` decoration; this is
|
|
23
|
+
* the same `process.stdout.write` carve-out that machine-parsable envelopes
|
|
24
|
+
* use (see `tests/enforcement/no-console.test.js`).
|
|
25
|
+
*
|
|
26
|
+
* The short-circuit itself lives in `runAsCli` (`lib/cli-utils.js`), which
|
|
27
|
+
* fires this module **before** the script's `main` runs — so the "no side
|
|
28
|
+
* effects" half of the contract holds structurally rather than by each
|
|
29
|
+
* script remembering to check first.
|
|
30
|
+
*
|
|
31
|
+
* ## Usage
|
|
32
|
+
*
|
|
33
|
+
* runAsCli(import.meta.url, main, {
|
|
34
|
+
* source: 'my-script',
|
|
35
|
+
* usage: {
|
|
36
|
+
* invocation: 'node .agents/scripts/my-script.js --story <id> [--json]',
|
|
37
|
+
* summary: 'One line on what the script does.',
|
|
38
|
+
* flags: [
|
|
39
|
+
* ['--story <id>', 'GitHub issue number of the Story (required).'],
|
|
40
|
+
* ['--json', 'Emit the envelope as JSON instead of prose.'],
|
|
41
|
+
* ],
|
|
42
|
+
* },
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* A pre-rendered string is accepted too, so a script that already shipped a
|
|
46
|
+
* hand-written `HELP` block adopts the shared gate without its observable
|
|
47
|
+
* text changing (`usage: HELP`).
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** Tokens that request help. */
|
|
51
|
+
export const HELP_FLAGS = Object.freeze(['--help', '-h']);
|
|
52
|
+
|
|
53
|
+
/** Left column width for the flag table; longer flags wrap to their own line. */
|
|
54
|
+
const FLAG_COLUMN = 22;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Did the caller ask for help? Scans up to the `--` end-of-flags separator so
|
|
58
|
+
* a positional literally named `--help` after `--` is not mistaken for a
|
|
59
|
+
* request.
|
|
60
|
+
*
|
|
61
|
+
* @param {string[]} argv Argument vector without the node/script entries.
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
export function wantsHelp(argv = []) {
|
|
65
|
+
if (!Array.isArray(argv)) return false;
|
|
66
|
+
for (const token of argv) {
|
|
67
|
+
if (token === '--') return false;
|
|
68
|
+
if (HELP_FLAGS.includes(token)) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Normalize one flag entry into `{ flag, description }`. Accepts a
|
|
75
|
+
* `[flag, description]` pair or an object, so call sites can use whichever
|
|
76
|
+
* reads better next to their option table.
|
|
77
|
+
*
|
|
78
|
+
* @param {[string, string]|{flag: string, description?: string}} entry
|
|
79
|
+
* @returns {{ flag: string, description: string }}
|
|
80
|
+
*/
|
|
81
|
+
function normalizeFlag(entry) {
|
|
82
|
+
if (Array.isArray(entry)) {
|
|
83
|
+
return {
|
|
84
|
+
flag: String(entry[0] ?? ''),
|
|
85
|
+
description: String(entry[1] ?? ''),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
flag: String(entry?.flag ?? ''),
|
|
90
|
+
description: String(entry?.description ?? ''),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Render one ` --flag description` row, wrapping onto a second line when
|
|
96
|
+
* the flag itself is wider than the column.
|
|
97
|
+
*
|
|
98
|
+
* @param {{ flag: string, description: string }} row
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
function renderFlagRow({ flag, description }) {
|
|
102
|
+
if (!description) return ` ${flag}`;
|
|
103
|
+
if (flag.length >= FLAG_COLUMN) {
|
|
104
|
+
return ` ${flag}\n ${' '.repeat(FLAG_COLUMN)}${description}`;
|
|
105
|
+
}
|
|
106
|
+
return ` ${flag.padEnd(FLAG_COLUMN)}${description}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Append the `--help` row unless the spec already documents it, so every
|
|
111
|
+
* rendered block is self-describing without each call site repeating it.
|
|
112
|
+
*
|
|
113
|
+
* @param {Array<{ flag: string, description: string }>} rows
|
|
114
|
+
* @returns {Array<{ flag: string, description: string }>}
|
|
115
|
+
*/
|
|
116
|
+
function withHelpRow(rows) {
|
|
117
|
+
const documented = rows.some((r) => r.flag.split(/[\s,]/)[0] === '--help');
|
|
118
|
+
if (documented) return rows;
|
|
119
|
+
return [...rows, { flag: '--help', description: 'Show this message.' }];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Render a usage spec into the text `--help` prints.
|
|
124
|
+
*
|
|
125
|
+
* @param {{
|
|
126
|
+
* invocation: string,
|
|
127
|
+
* summary?: string,
|
|
128
|
+
* flags?: Array<[string, string]|{flag: string, description?: string}>,
|
|
129
|
+
* notes?: string[],
|
|
130
|
+
* }} spec
|
|
131
|
+
* @returns {string} Text block, newline-terminated.
|
|
132
|
+
*/
|
|
133
|
+
export function formatUsage(spec) {
|
|
134
|
+
const rows = withHelpRow((spec?.flags ?? []).map(normalizeFlag));
|
|
135
|
+
const blocks = [`Usage: ${spec?.invocation ?? ''}`.trim()];
|
|
136
|
+
if (spec?.summary) blocks.push(spec.summary);
|
|
137
|
+
blocks.push(['Flags:', ...rows.map(renderFlagRow)].join('\n'));
|
|
138
|
+
for (const note of spec?.notes ?? []) blocks.push(note);
|
|
139
|
+
return `${blocks.join('\n\n')}\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Coerce either accepted `usage` shape — a spec object or a pre-rendered
|
|
144
|
+
* string — into the text to print.
|
|
145
|
+
*
|
|
146
|
+
* @param {object|string} usage
|
|
147
|
+
* @returns {string}
|
|
148
|
+
*/
|
|
149
|
+
export function renderUsage(usage) {
|
|
150
|
+
if (typeof usage === 'string') {
|
|
151
|
+
return usage.endsWith('\n') ? usage : `${usage}\n`;
|
|
152
|
+
}
|
|
153
|
+
return formatUsage(usage);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Answer a help request. Returns `true` when help was requested (and printed),
|
|
158
|
+
* `false` when the caller should carry on with its normal path.
|
|
159
|
+
*
|
|
160
|
+
* Never throws on a missing/blank `usage`: a script whose spec renders empty
|
|
161
|
+
* still owes the contract non-empty stdout, so a minimal line is emitted
|
|
162
|
+
* rather than a silent exit that reads as a broken CLI.
|
|
163
|
+
*
|
|
164
|
+
* @param {string[]} argv
|
|
165
|
+
* @param {object|string} usage
|
|
166
|
+
* @param {{ write: (s: string) => void }} [out] Defaults to `process.stdout`.
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
export function respondToHelp(argv, usage, out = process.stdout) {
|
|
170
|
+
if (!wantsHelp(argv)) return false;
|
|
171
|
+
const text = renderUsage(usage ?? '');
|
|
172
|
+
out.write(text.trim().length > 0 ? text : 'Usage: (no flags documented)\n');
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { respondToHelp } from './cli-usage.js';
|
|
19
20
|
import { formatCliError } from './error-redactor.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -37,12 +38,21 @@ export function isDirectInvocation(importMetaUrl) {
|
|
|
37
38
|
* `main` is funnelled through either the caller-supplied `onError` callback
|
|
38
39
|
* or the default handler (prefixed stderr line + `process.exit(exitCode)`).
|
|
39
40
|
*
|
|
41
|
+
* A `usage` option makes the script self-describing: when the argv carries
|
|
42
|
+
* `--help` / `-h`, the rendered usage block goes to stdout and `main` is
|
|
43
|
+
* **never invoked**. That ordering is the whole point — it makes "`--help`
|
|
44
|
+
* performs no GitHub write, acquires no lease, and mutates no working tree"
|
|
45
|
+
* structurally true for every adopting script, rather than something each
|
|
46
|
+
* `main` has to remember to check before its first side effect.
|
|
47
|
+
*
|
|
40
48
|
* @param {string} importMetaUrl Caller's `import.meta.url`.
|
|
41
49
|
* @param {() => Promise<unknown>} main The CLI's main function.
|
|
42
50
|
* @param {object} [options]
|
|
43
51
|
* @param {string} [options.source='CLI'] Prefix used in the default error message.
|
|
44
52
|
* @param {number} [options.exitCode=1] Exit code used by the default error handler.
|
|
45
53
|
* @param {(err: Error) => void} [options.onError] Full override of the error handler.
|
|
54
|
+
* @param {object|string} [options.usage] Usage spec (or pre-rendered
|
|
55
|
+
* text) printed for `--help`; see `lib/cli-usage.js`.
|
|
46
56
|
*/
|
|
47
57
|
export function runAsCli(importMetaUrl, main, options = {}) {
|
|
48
58
|
if (!isDirectInvocation(importMetaUrl)) return;
|
|
@@ -52,7 +62,9 @@ export function runAsCli(importMetaUrl, main, options = {}) {
|
|
|
52
62
|
onError,
|
|
53
63
|
propagateExitCode = false,
|
|
54
64
|
errorPrefix,
|
|
65
|
+
usage,
|
|
55
66
|
} = options;
|
|
67
|
+
if (usage && respondToHelp(process.argv.slice(2), usage)) return;
|
|
56
68
|
const promise = main();
|
|
57
69
|
if (propagateExitCode) {
|
|
58
70
|
promise.then((code) => process.exit(code ?? 0));
|
|
@@ -11,22 +11,48 @@ import { spawn } from 'node:child_process';
|
|
|
11
11
|
* Pipe a child stream's output line-by-line through `emit`, prepending
|
|
12
12
|
* `prefix` to each line. Tail bytes without a trailing newline flush on
|
|
13
13
|
* `end` so the operator never loses the last line of a gate's output.
|
|
14
|
+
*
|
|
15
|
+
* ## The drain must stay cheap (Story #4766)
|
|
16
|
+
*
|
|
17
|
+
* This handler runs on the reader side of the child's stdout/stderr pipe.
|
|
18
|
+
* Every microsecond spent here is a microsecond the pipe is not being read,
|
|
19
|
+
* and once the OS pipe buffer fills, the child's own write blocks — or, on a
|
|
20
|
+
* non-blocking pipe, fails outright with `EAGAIN`. A gate child is not
|
|
21
|
+
* obliged to survive that: Biome's `biome_console` `.unwrap()`s the error and
|
|
22
|
+
* aborts the process with exit 101, so a green lint verdict presents as a
|
|
23
|
+
* failed close. Two consequences bind everything on this path:
|
|
24
|
+
*
|
|
25
|
+
* 1. Splitting is O(chunk), not O(chunk × lines) — the scan advances a
|
|
26
|
+
* `start` index instead of re-slicing the buffer once per line, so a
|
|
27
|
+
* 64KB chunk carrying 500 lines does not copy 16MB.
|
|
28
|
+
* 2. **`emit` MUST NOT block.** A synchronous per-line file write is
|
|
29
|
+
* exactly the stall this path cannot afford; the close path's capture
|
|
30
|
+
* sink (`single-story-close/gate-log.js`) buffers to an async stream for
|
|
31
|
+
* that reason.
|
|
14
32
|
*/
|
|
15
33
|
function pipePrefixed(stream, prefix, emit) {
|
|
16
34
|
let buf = '';
|
|
17
35
|
stream.setEncoding('utf8');
|
|
18
36
|
stream.on('data', (chunk) => {
|
|
19
37
|
buf += chunk;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
emit(prefix + buf.slice(
|
|
24
|
-
|
|
38
|
+
let start = 0;
|
|
39
|
+
let nl = buf.indexOf('\n', start);
|
|
40
|
+
while (nl !== -1) {
|
|
41
|
+
emit(prefix + buf.slice(start, nl));
|
|
42
|
+
start = nl + 1;
|
|
43
|
+
nl = buf.indexOf('\n', start);
|
|
25
44
|
}
|
|
45
|
+
if (start > 0) buf = buf.slice(start);
|
|
26
46
|
});
|
|
27
47
|
stream.on('end', () => {
|
|
28
|
-
if (buf.length > 0)
|
|
48
|
+
if (buf.length > 0) {
|
|
49
|
+
emit(prefix + buf);
|
|
50
|
+
buf = '';
|
|
51
|
+
}
|
|
29
52
|
});
|
|
53
|
+
// A pipe-level error (EIO on a vanished child) must not become an
|
|
54
|
+
// unhandled 'error' event that takes the whole close down.
|
|
55
|
+
stream.on('error', () => {});
|
|
30
56
|
}
|
|
31
57
|
|
|
32
58
|
/** Wire the AbortSignal so an abort kills the child. Returns the cleanup fn. */
|
|
@@ -68,6 +94,18 @@ export function gateExitCode(code, sig) {
|
|
|
68
94
|
const BIOME_NO_FILES_PROCESSED =
|
|
69
95
|
'No files were processed in the specified paths';
|
|
70
96
|
|
|
97
|
+
/**
|
|
98
|
+
* How many trailing gate lines the "No files were processed" probe retains.
|
|
99
|
+
*
|
|
100
|
+
* The marker only ever appears when biome processed nothing, and in that case
|
|
101
|
+
* its whole output is a handful of lines — so a bounded tail always contains
|
|
102
|
+
* it when it is there at all. Retaining a tail rather than the full transcript
|
|
103
|
+
* keeps the drain path's per-line work O(1) in the volume of gate output
|
|
104
|
+
* (Story #4766): the previous `captured += line` grew a string without limit,
|
|
105
|
+
* on the one gate (biome/format) whose output is the loudest.
|
|
106
|
+
*/
|
|
107
|
+
const MARKER_PROBE_TAIL_LINES = 32;
|
|
108
|
+
|
|
71
109
|
/**
|
|
72
110
|
* Whether biome's combined gate output carries the "No files were processed"
|
|
73
111
|
* marker. Pure function — no I/O. Exported for unit coverage (Story #4292).
|
|
@@ -85,8 +123,11 @@ export function isBiomeNoFilesProcessed(output) {
|
|
|
85
123
|
* Default async gate runner — used by `runCloseValidation` when no `runner`
|
|
86
124
|
* is injected. Spawns the gate via `child_process.spawn`, prefixes every
|
|
87
125
|
* stdout/stderr line with `[gate-name] ` (so concurrent gates don't bleed
|
|
88
|
-
* into each other in the operator's terminal), and resolves only
|
|
89
|
-
* child
|
|
126
|
+
* into each other in the operator's terminal), and resolves only once the
|
|
127
|
+
* child has exited and both stdio pipes are drained.
|
|
128
|
+
*
|
|
129
|
+
* `opts.log` is the drain sink and **must not block** — see `pipePrefixed`
|
|
130
|
+
* above for what a synchronous per-line write costs the child (Story #4766).
|
|
90
131
|
*
|
|
91
132
|
* Honours `opts.signal`: a TERM is delivered to the child the moment the
|
|
92
133
|
* signal fires, so a sibling gate's failure aborts the rest of the wave
|
|
@@ -119,13 +160,14 @@ export function defaultGateRunner(cmd, args, opts = {}) {
|
|
|
119
160
|
const prefix = gateName ? `[${gateName}] ` : '';
|
|
120
161
|
const emit =
|
|
121
162
|
typeof log === 'function' ? log : (m) => process.stdout.write(`${m}\n`);
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
163
|
+
// Retain a bounded tail only when we may need to inspect it for the biome
|
|
164
|
+
// "No files were processed" marker — otherwise the stream is purely piped
|
|
165
|
+
// through to the operator (no retained buffer).
|
|
166
|
+
const recent = [];
|
|
126
167
|
const tap = tolerateNoFilesProcessed
|
|
127
168
|
? (line) => {
|
|
128
|
-
|
|
169
|
+
recent.push(line);
|
|
170
|
+
if (recent.length > MARKER_PROBE_TAIL_LINES) recent.shift();
|
|
129
171
|
emit(line);
|
|
130
172
|
}
|
|
131
173
|
: emit;
|
|
@@ -133,13 +175,17 @@ export function defaultGateRunner(cmd, args, opts = {}) {
|
|
|
133
175
|
pipePrefixed(child.stderr, prefix, tap);
|
|
134
176
|
const detach = attachGateAbortHandler(child, signal);
|
|
135
177
|
return new Promise((resolve) => {
|
|
136
|
-
|
|
178
|
+
// 'close', not 'exit' (Story #4766): 'close' fires only once the child has
|
|
179
|
+
// exited AND both stdio pipes have been fully drained and closed, so no
|
|
180
|
+
// gate ever reports its status while lines are still in flight. Resolving
|
|
181
|
+
// on 'exit' raced the tail of a high-volume gate's output.
|
|
182
|
+
child.on('close', (code, sig) => {
|
|
137
183
|
detach();
|
|
138
184
|
const status = gateExitCode(code, sig);
|
|
139
185
|
if (
|
|
140
186
|
status !== 0 &&
|
|
141
187
|
tolerateNoFilesProcessed &&
|
|
142
|
-
isBiomeNoFilesProcessed(
|
|
188
|
+
isBiomeNoFilesProcessed(recent.join('\n'))
|
|
143
189
|
) {
|
|
144
190
|
emit(
|
|
145
191
|
`${prefix}↳ biome processed zero files (all changed paths are config-ignored); treating as a clean skip`,
|
|
@@ -29,11 +29,19 @@
|
|
|
29
29
|
* converted spawn boots on **instead of** the always-loaded
|
|
30
30
|
* closure, so it is budgeted independently (per-file ≤8KB
|
|
31
31
|
* ceiling gated by `check-context-budget.js`).
|
|
32
|
+
* - `workflow` / — the `.agents/workflows/**` read-tier (Story #4752),
|
|
33
|
+
* `workflowOnDemand` resolved as each entry point's transitive
|
|
34
|
+
* markdown-link closure by
|
|
35
|
+
* [`workflow-closure.js`](workflow-closure.js): the files
|
|
36
|
+
* an entry point's `mandatoryReads:` frontmatter forces
|
|
37
|
+
* you to read (`workflow`, gated) versus the reachable
|
|
38
|
+
* remainder (`workflowOnDemand`, recorded only).
|
|
32
39
|
*
|
|
33
40
|
* A file that could appear in more than one tier is kept in its **highest**
|
|
34
41
|
* tier only (alwaysLoaded > mandatoryRead > digestVisible > onDemand), so the
|
|
35
42
|
* arrays partition the doc set with no double-counting. `agentBoot` is disjoint
|
|
36
|
-
* from the read-tiers (it lives under `.agents/agents/`, not the doc/rules set)
|
|
43
|
+
* from the read-tiers (it lives under `.agents/agents/`, not the doc/rules set),
|
|
44
|
+
* and so are the workflow tiers (confined to `.agents/workflows/`).
|
|
37
45
|
*
|
|
38
46
|
* The closure is discovered by parsing `@`-import references and following
|
|
39
47
|
* them recursively (cycle-safe via a visited set). A candidate `@`-token only
|
|
@@ -47,6 +55,7 @@
|
|
|
47
55
|
|
|
48
56
|
import nodeFs from 'node:fs';
|
|
49
57
|
import path from 'node:path';
|
|
58
|
+
import { resolveWorkflowClosures } from './workflow-closure.js';
|
|
50
59
|
|
|
51
60
|
/**
|
|
52
61
|
* Basename of the always-loaded entry document (the root of the closure).
|
|
@@ -212,18 +221,31 @@ export function docsContextPaths(config) {
|
|
|
212
221
|
}
|
|
213
222
|
|
|
214
223
|
/**
|
|
215
|
-
* Resolve the
|
|
224
|
+
* Resolve the documentation read-tiers, each entry `{ path, bytes }`,
|
|
216
225
|
* partitioned so no path appears in more than one tier (highest tier wins).
|
|
226
|
+
* `workflowClosure` rides alongside the tiers as the per-entry-point workflow
|
|
227
|
+
* measurement (Story #4752) — its `reachableTotalBytes` is a recorded drift
|
|
228
|
+
* signal, never a gate.
|
|
217
229
|
*
|
|
218
230
|
* @param {object} config resolved config (`resolveConfig()` output)
|
|
219
231
|
* @param {{ root?: string, fs?: FsLike }} [opts]
|
|
220
|
-
* @returns {{
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
232
|
+
* @returns {{
|
|
233
|
+
* tiers: {
|
|
234
|
+
* alwaysLoaded: Array<{ path: string, bytes: number }>,
|
|
235
|
+
* mandatoryRead: Array<{ path: string, bytes: number }>,
|
|
236
|
+
* digestVisible: Array<{ path: string, bytes: number }>,
|
|
237
|
+
* onDemand: Array<{ path: string, bytes: number }>,
|
|
238
|
+
* agentBoot: Array<{ path: string, bytes: number }>,
|
|
239
|
+
* workflow: Array<{ path: string, bytes: number }>,
|
|
240
|
+
* workflowOnDemand: Array<{ path: string, bytes: number }>,
|
|
241
|
+
* },
|
|
242
|
+
* workflowClosure: {
|
|
243
|
+
* mandatoryTotalBytes: number,
|
|
244
|
+
* reachableTotalBytes: number,
|
|
245
|
+
* entryPoints: Array<{ path: string, mandatoryBytes: number, reachableBytes: number }>,
|
|
246
|
+
* },
|
|
247
|
+
* }}
|
|
248
|
+
* @throws {Error} on an unresolvable `mandatoryReads` entry or a mandatory cycle
|
|
227
249
|
*/
|
|
228
250
|
export function resolveDocTiers(
|
|
229
251
|
config,
|
|
@@ -265,8 +287,29 @@ export function resolveDocTiers(
|
|
|
265
287
|
// are standalone system prompts, disjoint from the doc read-tiers.
|
|
266
288
|
const agentBoot = collect(listAgentDefs(root, fs));
|
|
267
289
|
|
|
290
|
+
// 6. workflow: each entry point's transitive markdown-link closure (#4752),
|
|
291
|
+
// split into the gated mandatory set and the recorded on-demand remainder.
|
|
292
|
+
// The walk is confined to `.agents/workflows/**`, and `collect` still
|
|
293
|
+
// de-dupes, so no path is counted against two tiers.
|
|
294
|
+
const closure = resolveWorkflowClosures(root, { fs });
|
|
295
|
+
const workflow = collect(closure.mandatoryFiles.map((e) => e.path));
|
|
296
|
+
const workflowOnDemand = collect(closure.onDemandFiles.map((e) => e.path));
|
|
297
|
+
|
|
268
298
|
return {
|
|
269
|
-
tiers: {
|
|
299
|
+
tiers: {
|
|
300
|
+
alwaysLoaded,
|
|
301
|
+
mandatoryRead,
|
|
302
|
+
digestVisible,
|
|
303
|
+
onDemand,
|
|
304
|
+
agentBoot,
|
|
305
|
+
workflow,
|
|
306
|
+
workflowOnDemand,
|
|
307
|
+
},
|
|
308
|
+
workflowClosure: {
|
|
309
|
+
mandatoryTotalBytes: closure.mandatoryTotalBytes,
|
|
310
|
+
reachableTotalBytes: closure.reachableTotalBytes,
|
|
311
|
+
entryPoints: closure.entryPoints,
|
|
312
|
+
},
|
|
270
313
|
};
|
|
271
314
|
}
|
|
272
315
|
|