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,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: ratchet-down gate on provenance citations in workflow prose.
|
|
3
|
+
*
|
|
4
|
+
* Workflow documents ride resident in an executing agent's context. A
|
|
5
|
+
* `(Story #1234)` aside costs the same tokens as instruction and teaches
|
|
6
|
+
* the model Mandrel's own history instead of the task in front of it. The
|
|
7
|
+
* citations that survive are the ones a reader must follow to act; the
|
|
8
|
+
* rest belong in the commit trail and `docs/decisions.md`.
|
|
9
|
+
*
|
|
10
|
+
* Nothing stops the tax re-accumulating one well-meaning aside at a time,
|
|
11
|
+
* so this gate holds the line: count every issue-shaped reference across
|
|
12
|
+
* `.agents/workflows/**\/*.md` and fail when the total rises above the
|
|
13
|
+
* committed baseline at `baselines/workflow-citations.json`.
|
|
14
|
+
*
|
|
15
|
+
* The counted token is the bare `#NNNN` form rather than the
|
|
16
|
+
* `(Story|Epic|issue|refs) #NNNN` phrasing, because the same citation is
|
|
17
|
+
* written both ways — `(Story #4593)` and a bare `(#4593)` — and a gate
|
|
18
|
+
* that only saw the prefixed form would wave the other one through.
|
|
19
|
+
*
|
|
20
|
+
* Ratchet semantics mirror `check-arch-cycles.js`:
|
|
21
|
+
* - Total above the baseline → exit 1, with the files that grew.
|
|
22
|
+
* - Total below the baseline → printed as shrinkage, exit 0. Refresh the
|
|
23
|
+
* baseline with `--update` to lock the reduction in.
|
|
24
|
+
* - Equal → exit 0.
|
|
25
|
+
*
|
|
26
|
+
* Flags:
|
|
27
|
+
* --baseline <path> override the baseline path (default
|
|
28
|
+
* `baselines/workflow-citations.json`, resolved from cwd)
|
|
29
|
+
* --root <path> scan a different workflow root (default
|
|
30
|
+
* `.agents/workflows`)
|
|
31
|
+
* --update rewrite the baseline from the live count
|
|
32
|
+
* --json write the structured envelope to stdout
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
import process from 'node:process';
|
|
38
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
39
|
+
|
|
40
|
+
/** Default workflow root, relative to the repository root. */
|
|
41
|
+
const DEFAULT_ROOT = path.join('.agents', 'workflows');
|
|
42
|
+
|
|
43
|
+
/** Default baseline path, relative to the repository root. */
|
|
44
|
+
const DEFAULT_BASELINE = path.join('baselines', 'workflow-citations.json');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Issue-shaped reference: a `#` followed by 3–5 digits. Narrow enough to
|
|
48
|
+
* skip markdown headings and anchor links, wide enough to catch both the
|
|
49
|
+
* `(Story #1234)` and bare `(#1234)` spellings of one citation.
|
|
50
|
+
*/
|
|
51
|
+
const CITATION_RE = /#\d{3,5}\b/g;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse argv. Exported so unit tests can pin the parser.
|
|
55
|
+
*
|
|
56
|
+
* @param {string[]} argv
|
|
57
|
+
* @returns {{ baselinePath: string | null, rootPath: string | null, update: boolean, json: boolean }}
|
|
58
|
+
*/
|
|
59
|
+
export function parseArgv(argv = []) {
|
|
60
|
+
const out = {
|
|
61
|
+
baselinePath: null,
|
|
62
|
+
rootPath: null,
|
|
63
|
+
update: false,
|
|
64
|
+
json: false,
|
|
65
|
+
};
|
|
66
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
67
|
+
const flag = argv[i];
|
|
68
|
+
const next = argv[i + 1];
|
|
69
|
+
const takesValue = next && !next.startsWith('--');
|
|
70
|
+
if (flag === '--baseline' && takesValue) {
|
|
71
|
+
out.baselinePath = next;
|
|
72
|
+
i += 1;
|
|
73
|
+
} else if (flag === '--root' && takesValue) {
|
|
74
|
+
out.rootPath = next;
|
|
75
|
+
i += 1;
|
|
76
|
+
} else if (flag === '--update') {
|
|
77
|
+
out.update = true;
|
|
78
|
+
} else if (flag === '--json') {
|
|
79
|
+
out.json = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Recursively collect `.md` files under `rootDir`. Returns absolute paths,
|
|
87
|
+
* sorted for determinism.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} rootDir
|
|
90
|
+
* @returns {string[]}
|
|
91
|
+
*/
|
|
92
|
+
export function collectMarkdownFiles(rootDir) {
|
|
93
|
+
const out = [];
|
|
94
|
+
const walk = (dir) => {
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const full = path.join(dir, entry.name);
|
|
103
|
+
if (entry.isDirectory()) walk(full);
|
|
104
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
walk(rootDir);
|
|
108
|
+
return out.sort();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Pure helper: count citations in source text.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} source
|
|
115
|
+
* @returns {number}
|
|
116
|
+
*/
|
|
117
|
+
export function countCitations(source) {
|
|
118
|
+
return (String(source ?? '').match(CITATION_RE) ?? []).length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Count citations across a file set, relativizing paths against `cwd` so
|
|
123
|
+
* the report serializes identically on every platform. Files with zero
|
|
124
|
+
* citations are omitted — the baseline records where the tax lives.
|
|
125
|
+
*
|
|
126
|
+
* @param {string[]} files absolute paths
|
|
127
|
+
* @param {string} cwd
|
|
128
|
+
* @param {{ readFile?: (p: string) => string }} [opts]
|
|
129
|
+
* @returns {{ total: number, files: Array<{ path: string, count: number }> }}
|
|
130
|
+
*/
|
|
131
|
+
export function tallyCitations(files, cwd, { readFile } = {}) {
|
|
132
|
+
const read = readFile ?? ((p) => fs.readFileSync(p, 'utf-8'));
|
|
133
|
+
const rows = [];
|
|
134
|
+
let total = 0;
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
let count;
|
|
137
|
+
try {
|
|
138
|
+
count = countCitations(read(file));
|
|
139
|
+
} catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (count === 0) continue;
|
|
143
|
+
total += count;
|
|
144
|
+
rows.push({
|
|
145
|
+
path: path.relative(cwd, file).split(path.sep).join('/'),
|
|
146
|
+
count,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return { total, files: rows.sort((a, b) => a.path.localeCompare(b.path)) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Pure helper: read the baseline envelope. Returns `null` when the file is
|
|
154
|
+
* missing or unparseable — the baseline is not distributed to consumers, so
|
|
155
|
+
* its absence degrades to "no ceiling" rather than a false failure.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} baselinePath
|
|
158
|
+
* @returns {{ total?: number, files?: Array<{ path: string, count: number }> } | null}
|
|
159
|
+
*/
|
|
160
|
+
export function loadBaseline(baselinePath) {
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
163
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Pure helper: diff the live tally against the baseline. `grew` names the
|
|
171
|
+
* files whose per-file count rose, so a failure points at the edit rather
|
|
172
|
+
* than only at the total.
|
|
173
|
+
*
|
|
174
|
+
* @param {{ total?: number, files?: Array<{ path: string, count: number }> } | null} baseline
|
|
175
|
+
* @param {{ total: number, files: Array<{ path: string, count: number }> }} tally
|
|
176
|
+
* @returns {{ baselineTotal: number | null, delta: number | null, grew: Array<{ path: string, from: number, to: number }> }}
|
|
177
|
+
*/
|
|
178
|
+
export function diffTally(baseline, tally) {
|
|
179
|
+
const baselineTotal =
|
|
180
|
+
typeof baseline?.total === 'number' ? baseline.total : null;
|
|
181
|
+
const prior = new Map(
|
|
182
|
+
(baseline?.files ?? []).map((row) => [row.path, row.count]),
|
|
183
|
+
);
|
|
184
|
+
const grew = tally.files
|
|
185
|
+
.map((row) => ({
|
|
186
|
+
path: row.path,
|
|
187
|
+
from: prior.get(row.path) ?? 0,
|
|
188
|
+
to: row.count,
|
|
189
|
+
}))
|
|
190
|
+
.filter((row) => row.to > row.from);
|
|
191
|
+
return {
|
|
192
|
+
baselineTotal,
|
|
193
|
+
delta: baselineTotal === null ? null : tally.total - baselineTotal,
|
|
194
|
+
grew,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Pure helper: render the human-readable diff, mirroring the `+` / `-`
|
|
200
|
+
* vocabulary of the sibling ratchets.
|
|
201
|
+
*
|
|
202
|
+
* @param {{ baselineTotal: number | null, delta: number | null, grew: Array<{ path: string, from: number, to: number }> }} diff
|
|
203
|
+
* @param {{ total: number }} tally
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
export function renderDiff(diff, tally) {
|
|
207
|
+
const lines = [];
|
|
208
|
+
for (const row of diff.grew) {
|
|
209
|
+
lines.push(`+ ${row.path}: ${row.from} -> ${row.to}`);
|
|
210
|
+
}
|
|
211
|
+
if (diff.delta !== null && diff.delta < 0) {
|
|
212
|
+
lines.push(
|
|
213
|
+
`[workflow-citations] ⚠ ${-diff.delta} citation(s) below baseline — refresh with --update to lock the reduction in`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const tag = diff.delta !== null && diff.delta > 0 ? '(gate fail)' : '(ok)';
|
|
217
|
+
lines.push(
|
|
218
|
+
`[workflow-citations] total=${tally.total} baseline=${diff.baselineTotal ?? 'none'} ${tag}`,
|
|
219
|
+
);
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Top-level CLI entry. Exported so tests can drive the pipeline against a
|
|
225
|
+
* tmpdir fixture corpus.
|
|
226
|
+
*
|
|
227
|
+
* @param {{
|
|
228
|
+
* argv?: string[],
|
|
229
|
+
* cwd?: string,
|
|
230
|
+
* stdout?: { write: (s: string) => void },
|
|
231
|
+
* stderr?: { write: (s: string) => void },
|
|
232
|
+
* }} [opts]
|
|
233
|
+
* @returns {Promise<number>} 0 = at or below baseline; 1 = regression
|
|
234
|
+
*/
|
|
235
|
+
export async function runCli({
|
|
236
|
+
argv = process.argv.slice(2),
|
|
237
|
+
cwd = process.cwd(),
|
|
238
|
+
stdout = process.stdout,
|
|
239
|
+
stderr = process.stderr,
|
|
240
|
+
} = {}) {
|
|
241
|
+
const { baselinePath, rootPath, update, json } = parseArgv(argv);
|
|
242
|
+
const root = path.resolve(cwd, rootPath ?? DEFAULT_ROOT);
|
|
243
|
+
const resolvedBaselinePath = path.resolve(
|
|
244
|
+
cwd,
|
|
245
|
+
baselinePath ?? DEFAULT_BASELINE,
|
|
246
|
+
);
|
|
247
|
+
if (!fs.existsSync(root)) {
|
|
248
|
+
throw new Error(`[workflow-citations] workflow root not found: ${root}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const tally = tallyCitations(collectMarkdownFiles(root), cwd);
|
|
252
|
+
|
|
253
|
+
if (update) {
|
|
254
|
+
const envelope = {
|
|
255
|
+
$schema: 'https://mandrel.dev/baselines/workflow-citations.schema.json',
|
|
256
|
+
generatedAt: new Date().toISOString(),
|
|
257
|
+
total: tally.total,
|
|
258
|
+
files: tally.files,
|
|
259
|
+
};
|
|
260
|
+
fs.mkdirSync(path.dirname(resolvedBaselinePath), { recursive: true });
|
|
261
|
+
fs.writeFileSync(
|
|
262
|
+
resolvedBaselinePath,
|
|
263
|
+
`${JSON.stringify(envelope, null, 2)}\n`,
|
|
264
|
+
);
|
|
265
|
+
stdout.write(
|
|
266
|
+
`[workflow-citations] baseline written: ${resolvedBaselinePath} (total=${tally.total})\n`,
|
|
267
|
+
);
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const baseline = loadBaseline(resolvedBaselinePath);
|
|
272
|
+
const diff = diffTally(baseline, tally);
|
|
273
|
+
const exitCode = diff.delta !== null && diff.delta > 0 ? 1 : 0;
|
|
274
|
+
|
|
275
|
+
if (json) {
|
|
276
|
+
stdout.write(
|
|
277
|
+
`${JSON.stringify(
|
|
278
|
+
{
|
|
279
|
+
kind: 'workflow-citations-report',
|
|
280
|
+
root,
|
|
281
|
+
baselinePath: resolvedBaselinePath,
|
|
282
|
+
total: tally.total,
|
|
283
|
+
baselineTotal: diff.baselineTotal,
|
|
284
|
+
delta: diff.delta,
|
|
285
|
+
files: tally.files,
|
|
286
|
+
grew: diff.grew,
|
|
287
|
+
exitCode,
|
|
288
|
+
},
|
|
289
|
+
null,
|
|
290
|
+
2,
|
|
291
|
+
)}\n`,
|
|
292
|
+
);
|
|
293
|
+
return exitCode;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!baseline) {
|
|
297
|
+
stderr.write(
|
|
298
|
+
`[workflow-citations] ⚠ baseline not found at ${resolvedBaselinePath} — no ceiling enforced\n`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
stdout.write(`\n--- workflow-citations preview ---\n`);
|
|
302
|
+
stdout.write(`${renderDiff(diff, tally)}\n`);
|
|
303
|
+
return exitCode;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function main() {
|
|
307
|
+
return runCli();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
runAsCli(import.meta.url, main, {
|
|
311
|
+
source: 'workflow-citations',
|
|
312
|
+
propagateExitCode: true,
|
|
313
|
+
errorPrefix: '[workflow-citations] ❌ Fatal error',
|
|
314
|
+
usage: {
|
|
315
|
+
invocation:
|
|
316
|
+
'node .agents/scripts/check-workflow-citations.js [--baseline <path>] [--root <dir>] [--update] [--json]',
|
|
317
|
+
summary:
|
|
318
|
+
'Ratchet on provenance citations in workflow prose: count issue-shaped references across .agents/workflows/** and fail when the total rises above the recorded baseline.',
|
|
319
|
+
flags: [
|
|
320
|
+
[
|
|
321
|
+
'--baseline <path>',
|
|
322
|
+
'Baseline file (default: baselines/workflow-citations.json).',
|
|
323
|
+
],
|
|
324
|
+
['--root <dir>', 'Workflow root to scan (default: .agents/workflows).'],
|
|
325
|
+
['--update', 'Rewrite the baseline from the live count.'],
|
|
326
|
+
['--json', 'Emit the comparison envelope as JSON.'],
|
|
327
|
+
],
|
|
328
|
+
notes: [
|
|
329
|
+
'Exit codes:\n 0 at or below the baseline\n 1 the citation total regressed above the baseline',
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
});
|
|
@@ -79,18 +79,29 @@ import { createProvider } from './lib/provider-factory.js';
|
|
|
79
79
|
const HELP = `\
|
|
80
80
|
Usage:
|
|
81
81
|
deliver-light.js --prompt <text> [--creates csv] [--refactors csv]
|
|
82
|
-
[--acceptance n] [--
|
|
82
|
+
[--acceptance n] [--kinds csv] [--magnitude m]
|
|
83
|
+
[--uncertainty u] [--route lite|full] [--reason <text>]
|
|
83
84
|
[--amends '#id'] [--yes]
|
|
84
85
|
deliver-light.js --backstop --story <id>
|
|
85
86
|
|
|
86
87
|
The thin /deliver-light entry point: suitability gate → inline receipt Story →
|
|
87
88
|
the same single-story-init.js / single-story-close.js engine /deliver uses.
|
|
88
89
|
|
|
90
|
+
The gate judges EFFORT and RISK, not artifact counts: N instances of one
|
|
91
|
+
mechanical edit is one kind at N sites. It rejects only clearly-epic work; the
|
|
92
|
+
--backstop pass enforces size against the actual diff.
|
|
93
|
+
|
|
89
94
|
Gate options:
|
|
90
95
|
--prompt <text> Operator prompt describing the change. Required for the gate.
|
|
91
96
|
--creates <csv> Predicted NEW file paths (comma-separated).
|
|
92
97
|
--refactors <csv> Predicted edited/existing file paths (comma-separated).
|
|
93
|
-
--acceptance <n> Predicted acceptance-criteria count (default 1).
|
|
98
|
+
--acceptance <n> Predicted acceptance-criteria count (default 1). Not capped.
|
|
99
|
+
--kinds <csv> Distinct KINDS of change (default: one per assumption, so
|
|
100
|
+
N same-shaped edits count once).
|
|
101
|
+
--magnitude <m> Coarse effort bucket: trivial | moderate | substantial
|
|
102
|
+
(default moderate; substantial routes to /plan).
|
|
103
|
+
--uncertainty <u> determined (the request fixes the shape) | needs-design
|
|
104
|
+
(default determined; needs-design routes to /plan).
|
|
94
105
|
--route <r> Ledgered model verdict route: lite | full.
|
|
95
106
|
--reason <text> Recorded reason for a lite verdict (required for lite).
|
|
96
107
|
--amends <#id> Mark this as an amendment of an existing issue.
|
|
@@ -166,17 +177,25 @@ export function synthesizeAcceptance(count) {
|
|
|
166
177
|
* creates?: string[],
|
|
167
178
|
* refactors?: string[],
|
|
168
179
|
* acceptance?: number,
|
|
180
|
+
* kinds?: string[],
|
|
181
|
+
* magnitude?: string,
|
|
182
|
+
* uncertainty?: string,
|
|
169
183
|
* route?: string,
|
|
170
184
|
* reason?: string,
|
|
171
185
|
* yes?: boolean,
|
|
172
186
|
* injectedRules?: object,
|
|
173
|
-
* }} args
|
|
187
|
+
* }} args `kinds` / `magnitude` / `uncertainty` are the declared effort-and-risk
|
|
188
|
+
* axes the gate judges (Story #4764); omitting them declares no signal, not a
|
|
189
|
+
* small one — an unrecognized bucket fails closed.
|
|
174
190
|
* @returns {{ action: string, suitability: object, outcome: object }}
|
|
175
191
|
*/
|
|
176
192
|
export function runLightGate({
|
|
177
193
|
creates = [],
|
|
178
194
|
refactors = [],
|
|
179
195
|
acceptance,
|
|
196
|
+
kinds,
|
|
197
|
+
magnitude,
|
|
198
|
+
uncertainty,
|
|
180
199
|
route,
|
|
181
200
|
reason,
|
|
182
201
|
yes = false,
|
|
@@ -186,6 +205,9 @@ export function runLightGate({
|
|
|
186
205
|
const suitability = deriveLightSuitability({
|
|
187
206
|
predictedChanges,
|
|
188
207
|
predictedAcceptance: synthesizeAcceptance(acceptance),
|
|
208
|
+
predictedKinds: kinds,
|
|
209
|
+
predictedMagnitude: magnitude,
|
|
210
|
+
predictedUncertainty: uncertainty,
|
|
189
211
|
verdict: { route, reason },
|
|
190
212
|
injectedRules,
|
|
191
213
|
});
|
|
@@ -355,6 +377,9 @@ export async function runGateMode(values, deps = {}) {
|
|
|
355
377
|
acceptance: values.acceptance
|
|
356
378
|
? Number.parseInt(String(values.acceptance), 10)
|
|
357
379
|
: 1,
|
|
380
|
+
kinds: parseCsvPaths(values.kinds),
|
|
381
|
+
magnitude: values.magnitude,
|
|
382
|
+
uncertainty: values.uncertainty,
|
|
358
383
|
route: values.route,
|
|
359
384
|
reason: values.reason,
|
|
360
385
|
yes: values.yes === true,
|
|
@@ -417,6 +442,9 @@ async function main() {
|
|
|
417
442
|
creates: { type: 'string' },
|
|
418
443
|
refactors: { type: 'string' },
|
|
419
444
|
acceptance: { type: 'string' },
|
|
445
|
+
kinds: { type: 'string' },
|
|
446
|
+
magnitude: { type: 'string' },
|
|
447
|
+
uncertainty: { type: 'string' },
|
|
420
448
|
route: { type: 'string' },
|
|
421
449
|
reason: { type: 'string' },
|
|
422
450
|
amends: { type: 'string' },
|
|
@@ -443,4 +471,5 @@ async function main() {
|
|
|
443
471
|
runAsCli(import.meta.url, main, {
|
|
444
472
|
source: 'deliver-light',
|
|
445
473
|
propagateExitCode: true,
|
|
474
|
+
usage: HELP,
|
|
446
475
|
});
|
|
@@ -136,4 +136,7 @@ export async function runDeliverRecover({
|
|
|
136
136
|
return { success: true, result: recovery };
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
runAsCli(import.meta.url, runDeliverRecover, {
|
|
139
|
+
runAsCli(import.meta.url, runDeliverRecover, {
|
|
140
|
+
source: 'deliver-recover',
|
|
141
|
+
usage: HELP,
|
|
142
|
+
});
|
|
@@ -248,4 +248,20 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
248
248
|
|
|
249
249
|
import { runAsCli } from './lib/cli-utils.js';
|
|
250
250
|
|
|
251
|
-
runAsCli(import.meta.url, main, {
|
|
251
|
+
runAsCli(import.meta.url, main, {
|
|
252
|
+
source: 'DiagnoseFriction',
|
|
253
|
+
usage: {
|
|
254
|
+
invocation:
|
|
255
|
+
'node .agents/scripts/diagnose-friction.js [--story <id>] [--epic <id>] --cmd <command with args...>',
|
|
256
|
+
summary:
|
|
257
|
+
'Run a command through the diagnostic interceptor: stream its output, then append a local friction signal describing the failure. Never posts to the ticket.',
|
|
258
|
+
flags: [
|
|
259
|
+
['--story <id>', 'Story the friction belongs to.'],
|
|
260
|
+
['--epic <id>', 'Epic the friction belongs to.'],
|
|
261
|
+
[
|
|
262
|
+
'--cmd <command...>',
|
|
263
|
+
'The command to execute; everything after it is the argv (required).',
|
|
264
|
+
],
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
});
|
|
@@ -47,6 +47,21 @@ import { runAsCli } from './lib/cli-utils.js';
|
|
|
47
47
|
|
|
48
48
|
const DEFAULT_SCOPE = 'diagnose';
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The `--help` block. Single-homed here so the CLI gate in `runAsCli` and the
|
|
52
|
+
* `validateDiagnoseArgs` HELP-sentinel branch print the same bytes.
|
|
53
|
+
*/
|
|
54
|
+
const HELP = [
|
|
55
|
+
'Usage: diagnose [--scope <scope>] [--fail-on-blocker] [--json]',
|
|
56
|
+
'',
|
|
57
|
+
'Options:',
|
|
58
|
+
' --scope <s> Filter checks by scope (default: diagnose).',
|
|
59
|
+
' Use `all` to run every registered check.',
|
|
60
|
+
' --fail-on-blocker Exit 2 when at least one finding is a blocker.',
|
|
61
|
+
' --json Emit findings as a single line of JSON.',
|
|
62
|
+
'',
|
|
63
|
+
].join('\n');
|
|
64
|
+
|
|
50
65
|
/**
|
|
51
66
|
* Parse the CLI argv slice (i.e. argv without the leading node/script
|
|
52
67
|
* elements). Exported for direct unit testing — keeps the parse pure so
|
|
@@ -203,19 +218,7 @@ export async function runDiagnose({
|
|
|
203
218
|
*/
|
|
204
219
|
export function validateDiagnoseArgs(err) {
|
|
205
220
|
if (err && err.message === 'HELP') {
|
|
206
|
-
return {
|
|
207
|
-
kind: 'help',
|
|
208
|
-
text: [
|
|
209
|
-
'Usage: diagnose [--scope <scope>] [--fail-on-blocker] [--json]',
|
|
210
|
-
'',
|
|
211
|
-
'Options:',
|
|
212
|
-
' --scope <s> Filter checks by scope (default: diagnose).',
|
|
213
|
-
' Use `all` to run every registered check.',
|
|
214
|
-
' --fail-on-blocker Exit 2 when at least one finding is a blocker.',
|
|
215
|
-
' --json Emit findings as a single line of JSON.',
|
|
216
|
-
'',
|
|
217
|
-
].join('\n'),
|
|
218
|
-
};
|
|
221
|
+
return { kind: 'help', text: HELP };
|
|
219
222
|
}
|
|
220
223
|
const message = err?.message ? err.message : String(err);
|
|
221
224
|
return { kind: 'error', text: `[diagnose] ${message}\n`, exitCode: 1 };
|
|
@@ -237,4 +240,7 @@ async function main() {
|
|
|
237
240
|
}
|
|
238
241
|
}
|
|
239
242
|
|
|
240
|
-
runAsCli(import.meta.url, main, {
|
|
243
|
+
runAsCli(import.meta.url, main, {
|
|
244
|
+
source: 'diagnose',
|
|
245
|
+
usage: HELP,
|
|
246
|
+
});
|
|
@@ -144,4 +144,23 @@ async function main() {
|
|
|
144
144
|
);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
runAsCli(import.meta.url, main, {
|
|
147
|
+
runAsCli(import.meta.url, main, {
|
|
148
|
+
source: 'drain-pending-cleanup',
|
|
149
|
+
usage: {
|
|
150
|
+
invocation:
|
|
151
|
+
'node .agents/scripts/drain-pending-cleanup.js [--dry-run] [--no-escalate] [--worktree-root <path>]',
|
|
152
|
+
summary:
|
|
153
|
+
'Drain the pending-worktree-cleanup manifest, removing trees whose holders have exited.',
|
|
154
|
+
flags: [
|
|
155
|
+
['--dry-run', 'Report each entry and its holders; remove nothing.'],
|
|
156
|
+
[
|
|
157
|
+
'--no-escalate',
|
|
158
|
+
'Do not escalate to a forced removal for stuck entries.',
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
'--worktree-root <path>',
|
|
162
|
+
'Worktree root (default: delivery.worktreeIsolation.root).',
|
|
163
|
+
],
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
});
|
|
@@ -238,4 +238,23 @@ async function main() {
|
|
|
238
238
|
await runEvidenceGate({ ...args, runnerArgs });
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
runAsCli(import.meta.url, main, {
|
|
241
|
+
runAsCli(import.meta.url, main, {
|
|
242
|
+
source: 'evidence-gate',
|
|
243
|
+
usage: {
|
|
244
|
+
invocation:
|
|
245
|
+
'node .agents/scripts/evidence-gate.js --scope-id <id> --gate <name> [--standalone] [--no-evidence] [--cwd <path>] [--worktree <path>]',
|
|
246
|
+
summary:
|
|
247
|
+
'Run one named gate, reusing a prior evidence stamp for the same HEAD instead of re-running it.',
|
|
248
|
+
flags: [
|
|
249
|
+
['--scope-id <id>', 'Story id the evidence is scoped to (required).'],
|
|
250
|
+
['--gate <name>', 'Gate to run (e.g. lint, typecheck) (required).'],
|
|
251
|
+
['--standalone', 'Run outside a close pipeline and stamp the evidence.'],
|
|
252
|
+
[
|
|
253
|
+
'--no-evidence',
|
|
254
|
+
'Ignore and do not write evidence — always run the gate.',
|
|
255
|
+
],
|
|
256
|
+
['--cwd <path>', 'Repository root (default: project root).'],
|
|
257
|
+
['--worktree <path>', 'Worktree the gate runs in.'],
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
});
|
|
@@ -608,4 +608,17 @@ export {
|
|
|
608
608
|
spliceRegion,
|
|
609
609
|
};
|
|
610
610
|
|
|
611
|
-
runAsCli(import.meta.url, main, {
|
|
611
|
+
runAsCli(import.meta.url, main, {
|
|
612
|
+
source: 'generate-config-docs',
|
|
613
|
+
usage: {
|
|
614
|
+
invocation: 'node .agents/scripts/generate-config-docs.js [--check]',
|
|
615
|
+
summary:
|
|
616
|
+
'Regenerate the configuration reference from the .agentrc schema. Writes only when the generated content differs.',
|
|
617
|
+
flags: [
|
|
618
|
+
[
|
|
619
|
+
'--check',
|
|
620
|
+
'Verify the doc is current and fail if stale; write nothing.',
|
|
621
|
+
],
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
});
|
|
@@ -221,4 +221,17 @@ export {
|
|
|
221
221
|
spliceRegion,
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
-
runAsCli(import.meta.url, main, {
|
|
224
|
+
runAsCli(import.meta.url, main, {
|
|
225
|
+
source: 'generate-lifecycle-docs',
|
|
226
|
+
usage: {
|
|
227
|
+
invocation: 'node .agents/scripts/generate-lifecycle-docs.js [--check]',
|
|
228
|
+
summary:
|
|
229
|
+
'Regenerate the lifecycle-event table in docs/LIFECYCLE.md from the event schemas. Writes only when the generated content differs.',
|
|
230
|
+
flags: [
|
|
231
|
+
[
|
|
232
|
+
'--check',
|
|
233
|
+
'Verify the doc is current and fail if stale; write nothing.',
|
|
234
|
+
],
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
});
|
|
@@ -198,4 +198,17 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
198
198
|
|
|
199
199
|
export { DOC_PATH, WORKFLOWS_DIR };
|
|
200
200
|
|
|
201
|
-
runAsCli(import.meta.url, main, {
|
|
201
|
+
runAsCli(import.meta.url, main, {
|
|
202
|
+
source: 'generate-workflows-doc',
|
|
203
|
+
usage: {
|
|
204
|
+
invocation: 'node .agents/scripts/generate-workflows-doc.js [--check]',
|
|
205
|
+
summary:
|
|
206
|
+
'Regenerate the workflow catalog from .agents/workflows/. Writes only when the generated content differs.',
|
|
207
|
+
flags: [
|
|
208
|
+
[
|
|
209
|
+
'--check',
|
|
210
|
+
'Verify the doc is current and fail if stale; write nothing.',
|
|
211
|
+
],
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
});
|
|
@@ -129,4 +129,35 @@ async function main() {
|
|
|
129
129
|
process.exit(exitCode);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
runAsCli(import.meta.url, main, {
|
|
132
|
+
runAsCli(import.meta.url, main, {
|
|
133
|
+
source: 'git-cleanup',
|
|
134
|
+
usage: {
|
|
135
|
+
invocation:
|
|
136
|
+
'node .agents/scripts/git-cleanup.js [--execute] [--yes] [--json] [phase flags] [filters]',
|
|
137
|
+
summary:
|
|
138
|
+
'Tidy the local checkout in four phases — fast-forward the base branch, prune stale remote refs, reap merged branches, triage stashes. Dry-run unless --execute.',
|
|
139
|
+
flags: [
|
|
140
|
+
['--execute', 'Perform the mutations (default is a dry run).'],
|
|
141
|
+
['--dry-run', 'Force a dry run even alongside --execute.'],
|
|
142
|
+
['--yes', 'Skip the interactive confirmation prompts.'],
|
|
143
|
+
['--json', 'Emit the plan/result envelope as JSON.'],
|
|
144
|
+
['--remote', 'Also delete the matching remote branches.'],
|
|
145
|
+
['--fast-forward-main', 'Run only the fast-forward-base phase.'],
|
|
146
|
+
['--prune-remotes', 'Run only the prune-remotes phase.'],
|
|
147
|
+
['--branches', 'Run only the merged-branch reap phase.'],
|
|
148
|
+
['--stashes', 'Run only the stash-triage phase.'],
|
|
149
|
+
[
|
|
150
|
+
'--include <glob>',
|
|
151
|
+
'Only consider branches matching the glob (repeatable).',
|
|
152
|
+
],
|
|
153
|
+
[
|
|
154
|
+
'--exclude <glob>',
|
|
155
|
+
'Never consider branches matching the glob (repeatable).',
|
|
156
|
+
],
|
|
157
|
+
['--drop-stashes <ref>', 'Stash ref approved for dropping (repeatable).'],
|
|
158
|
+
['--base <branch>', 'Base branch (default: project.baseBranch).'],
|
|
159
|
+
['--cwd <path>', 'Repository root (default: process cwd).'],
|
|
160
|
+
],
|
|
161
|
+
notes: ['With no phase flag, every phase runs in order.'],
|
|
162
|
+
},
|
|
163
|
+
});
|