mandrel 2.17.0 → 2.19.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/docs/SDLC.md +1 -1
- package/.agents/docs/agentrc-reference.json +10 -0
- package/.agents/docs/configuration.md +8 -0
- package/.agents/schemas/agentrc.schema.json +42 -0
- package/.agents/schemas/story-deliver-terminal.schema.json +6 -1
- package/.agents/scripts/boot-sweep.js +39 -2
- package/.agents/scripts/check-doc-links.js +141 -9
- package/.agents/scripts/lib/baselines/env-overrides.js +40 -48
- package/.agents/scripts/lib/config/temp-paths.js +27 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +69 -0
- package/.agents/scripts/lib/observability/terse-result.js +7 -3
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +51 -77
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +20 -12
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +2 -1
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +19 -41
- package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +9 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +31 -1
- package/.agents/scripts/lib/single-story-sweep.js +11 -0
- package/.agents/scripts/lib/temp-retention.js +559 -0
- package/.agents/scripts/single-story-init.js +1 -1
- package/.agents/scripts/sync-branch-from-base.js +6 -1
- package/.agents/workflows/audit-performance.md +2 -2
- package/.agents/workflows/helpers/diagnose.md +1 -1
- package/.agents/workflows/helpers/signals.md +2 -2
- package/.agents/workflows/mandrel-update.md +4 -4
- package/docs/CHANGELOG.md +20 -0
- package/package.json +1 -1
package/.agents/docs/SDLC.md
CHANGED
|
@@ -528,7 +528,7 @@ new CI gate**, route the check through a `package.json` script (add it to
|
|
|
528
528
|
transitivity. **When a workflow file genuinely must change** (a new job, a
|
|
529
529
|
trigger change, a runner bump), the edit must be made by an operator with
|
|
530
530
|
`Workflows: Read and write` PAT permissions — see
|
|
531
|
-
[`docs/release-operations.md` § One-time PAT setup](
|
|
531
|
+
[`docs/release-operations.md` § One-time PAT setup](https://github.com/dsj1984/mandrel/blob/main/docs/release-operations.md#one-time-pat-setup).
|
|
532
532
|
|
|
533
533
|
### Worktree config shadow
|
|
534
534
|
|
|
@@ -128,6 +128,16 @@
|
|
|
128
128
|
"docsFreshness": {
|
|
129
129
|
"paths": ["README.md"]
|
|
130
130
|
},
|
|
131
|
+
"tempRetention": {
|
|
132
|
+
"enabled": true,
|
|
133
|
+
"staleDays": 7,
|
|
134
|
+
"classes": {
|
|
135
|
+
"orchestrationLogs": true,
|
|
136
|
+
"validationEvidence": true,
|
|
137
|
+
"auditResults": true,
|
|
138
|
+
"planDirs": true
|
|
139
|
+
}
|
|
140
|
+
},
|
|
131
141
|
"deliverRunner": {
|
|
132
142
|
"concurrencyCap": 3
|
|
133
143
|
},
|
|
@@ -125,6 +125,14 @@ top-level keys are validation errors.
|
|
|
125
125
|
| `lease.ttlMs` | No | `integer` | — | — |
|
|
126
126
|
| `docsFreshness` | No | `object` | — | Nested configuration block. |
|
|
127
127
|
| `docsFreshness.paths` | No | `array` | — | — |
|
|
128
|
+
| `tempRetention` | No | `object` | — | Story #4794. Auto-purge of spent temp artifacts once their Story lands. Classification is an allowlist: only the declared classes below are ever deleted, so operator scratch files under tempRoot are reported with their size and left alone. signals.ndjson is never purged by any path. |
|
|
129
|
+
| `tempRetention.enabled` | No | `boolean` | — | Master switch. Default true — reclaiming a landed Story's gate transcripts and validation evidence is the behaviour, and this knob turns it off. When false every purge path is a reported no-op. |
|
|
130
|
+
| `tempRetention.staleDays` | No | `integer` | — | Age floor (days, default 7) for the families no Story id can be recovered from — roster-level audit reports and abandoned plan-<slug>/ dirs. Story-keyed artifacts do not wait for it: they are purged as soon as their merge is confirmed. |
|
|
131
|
+
| `tempRetention.classes` | No | `object` | — | Per-class opt-out. Each defaults to true; set one false to keep that family while the rest are purged. |
|
|
132
|
+
| `tempRetention.classes.orchestrationLogs` | No | `boolean` | — | <tempRoot>/orchestration/*.log — close gate transcripts and terse-result detail dumps. |
|
|
133
|
+
| `tempRetention.classes.validationEvidence` | No | `boolean` | — | Per-Story validation-evidence.json, lifecycle.ndjson, and manifest.md under the standalone and per-run story trees. |
|
|
134
|
+
| `tempRetention.classes.auditResults` | No | `boolean` | — | <tempRoot>/audits/ — audit lens reports. |
|
|
135
|
+
| `tempRetention.classes.planDirs` | No | `boolean` | — | <tempRoot>/plan-<slug>/ — abandoned plan authoring dirs. Age-floored only; the current run is always excluded. |
|
|
128
136
|
| `deliverRunner` | No | `object` | — | Nested configuration block. |
|
|
129
137
|
| `deliverRunner.concurrencyCap` | No | `integer` | — | Maximum ready Stories dispatched by /deliver at once. Default 3. Moderate by design — keeps host-quota consumption predictable while allowing a small ready-set fan-out. Set 1 for strictly sequential delivery; raise further on hosts with adequate parallel-agent quota. See deliver.md for the sequencing model and throughput tradeoff. |
|
|
130
138
|
| `worktreeIsolation` | No | `object` | — | Nested configuration block. |
|
|
@@ -441,6 +441,45 @@
|
|
|
441
441
|
},
|
|
442
442
|
"additionalProperties": false
|
|
443
443
|
},
|
|
444
|
+
"tempRetention": {
|
|
445
|
+
"type": "object",
|
|
446
|
+
"description": "Story #4794. Auto-purge of spent temp artifacts once their Story lands. Classification is an allowlist: only the declared classes below are ever deleted, so operator scratch files under tempRoot are reported with their size and left alone. signals.ndjson is never purged by any path.",
|
|
447
|
+
"properties": {
|
|
448
|
+
"enabled": {
|
|
449
|
+
"type": "boolean",
|
|
450
|
+
"description": "Master switch. Default true — reclaiming a landed Story's gate transcripts and validation evidence is the behaviour, and this knob turns it off. When false every purge path is a reported no-op."
|
|
451
|
+
},
|
|
452
|
+
"staleDays": {
|
|
453
|
+
"type": "integer",
|
|
454
|
+
"minimum": 1,
|
|
455
|
+
"description": "Age floor (days, default 7) for the families no Story id can be recovered from — roster-level audit reports and abandoned plan-<slug>/ dirs. Story-keyed artifacts do not wait for it: they are purged as soon as their merge is confirmed."
|
|
456
|
+
},
|
|
457
|
+
"classes": {
|
|
458
|
+
"type": "object",
|
|
459
|
+
"description": "Per-class opt-out. Each defaults to true; set one false to keep that family while the rest are purged.",
|
|
460
|
+
"properties": {
|
|
461
|
+
"orchestrationLogs": {
|
|
462
|
+
"type": "boolean",
|
|
463
|
+
"description": "<tempRoot>/orchestration/*.log — close gate transcripts and terse-result detail dumps."
|
|
464
|
+
},
|
|
465
|
+
"validationEvidence": {
|
|
466
|
+
"type": "boolean",
|
|
467
|
+
"description": "Per-Story validation-evidence.json, lifecycle.ndjson, and manifest.md under the standalone and per-run story trees."
|
|
468
|
+
},
|
|
469
|
+
"auditResults": {
|
|
470
|
+
"type": "boolean",
|
|
471
|
+
"description": "<tempRoot>/audits/ — audit lens reports."
|
|
472
|
+
},
|
|
473
|
+
"planDirs": {
|
|
474
|
+
"type": "boolean",
|
|
475
|
+
"description": "<tempRoot>/plan-<slug>/ — abandoned plan authoring dirs. Age-floored only; the current run is always excluded."
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
"additionalProperties": false
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
"additionalProperties": false
|
|
482
|
+
},
|
|
444
483
|
"docsFreshness": {
|
|
445
484
|
"type": "object",
|
|
446
485
|
"properties": {
|
|
@@ -1324,6 +1363,9 @@
|
|
|
1324
1363
|
"docsFreshness": {
|
|
1325
1364
|
"$ref": "#/$defs/docsFreshness"
|
|
1326
1365
|
},
|
|
1366
|
+
"tempRetention": {
|
|
1367
|
+
"$ref": "#/$defs/tempRetention"
|
|
1368
|
+
},
|
|
1327
1369
|
"deliverRunner": {
|
|
1328
1370
|
"$ref": "#/$defs/deliverRunner"
|
|
1329
1371
|
},
|
|
@@ -86,13 +86,18 @@
|
|
|
86
86
|
"followUps",
|
|
87
87
|
"statusResync",
|
|
88
88
|
"refCleanup",
|
|
89
|
-
"baseFastForward"
|
|
89
|
+
"baseFastForward",
|
|
90
|
+
"tempPurge"
|
|
90
91
|
],
|
|
91
92
|
"properties": {
|
|
92
93
|
"followUps": { "type": "boolean" },
|
|
93
94
|
"statusResync": { "type": "boolean" },
|
|
94
95
|
"refCleanup": { "type": "boolean" },
|
|
95
96
|
"baseFastForward": { "type": "boolean" },
|
|
97
|
+
"tempPurge": {
|
|
98
|
+
"type": "boolean",
|
|
99
|
+
"description": "Story #4794 — the merged Story's spent temp artifacts (gate transcripts, validation evidence) were purged under delivery.tempRetention. A disabled policy reports true: the operator turned the purge off, so doing nothing IS the correct outcome. Only a real failure — an unreadable temp root, an undeletable artifact — reports false, and like every tail step that degrades the report, never the land."
|
|
100
|
+
},
|
|
96
101
|
"details": {
|
|
97
102
|
"type": "object",
|
|
98
103
|
"description": "Per-step diagnostic detail — the reason a false step reported false.",
|
|
@@ -46,6 +46,25 @@ import { Logger } from './lib/Logger.js';
|
|
|
46
46
|
import { createProvider } from './lib/provider-factory.js';
|
|
47
47
|
import { buildProtectionCtx } from './lib/single-story-sweep/protection-ctx.js';
|
|
48
48
|
import { sweepMergedBranches } from './lib/single-story-sweep.js';
|
|
49
|
+
import { sweepTempRetention } from './lib/temp-retention.js';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Recover the Story ids from the branch names a sweep reaped. Only the
|
|
53
|
+
* canonical `story-<id>` shape yields an id — an operator's ad-hoc branch that
|
|
54
|
+
* happened to match the include glob contributes nothing, so a purge can never
|
|
55
|
+
* be triggered by a name this framework did not create.
|
|
56
|
+
*
|
|
57
|
+
* @param {string[]|undefined} branches
|
|
58
|
+
* @returns {number[]}
|
|
59
|
+
*/
|
|
60
|
+
export function storyIdsFromBranches(branches) {
|
|
61
|
+
const ids = [];
|
|
62
|
+
for (const branch of Array.isArray(branches) ? branches : []) {
|
|
63
|
+
const match = /^story-(\d+)$/.exec(String(branch));
|
|
64
|
+
if (match) ids.push(Number(match[1]));
|
|
65
|
+
}
|
|
66
|
+
return ids;
|
|
67
|
+
}
|
|
49
68
|
|
|
50
69
|
const HELP = `Usage: node .agents/scripts/boot-sweep.js [options]
|
|
51
70
|
|
|
@@ -85,9 +104,11 @@ Options:
|
|
|
85
104
|
* injectedConfig?: object,
|
|
86
105
|
* injectedProvider?: object,
|
|
87
106
|
* injectedSweep?: Function,
|
|
107
|
+
* purgeFn?: Function,
|
|
88
108
|
* logger?: { info?: Function, warn?: Function },
|
|
89
109
|
* }} [args]
|
|
90
|
-
* @returns {Promise<object>} the {@link sweepMergedBranches} envelope
|
|
110
|
+
* @returns {Promise<object>} the {@link sweepMergedBranches} envelope, plus a
|
|
111
|
+
* `tempPurge` result from the Story #4794 temp-retention catch-up.
|
|
91
112
|
*/
|
|
92
113
|
export async function runBootSweep({
|
|
93
114
|
cwd,
|
|
@@ -99,6 +120,7 @@ export async function runBootSweep({
|
|
|
99
120
|
injectedConfig,
|
|
100
121
|
injectedProvider,
|
|
101
122
|
injectedSweep,
|
|
123
|
+
purgeFn = sweepTempRetention,
|
|
102
124
|
logger = Logger,
|
|
103
125
|
} = {}) {
|
|
104
126
|
const root = path.resolve(cwd ?? PROJECT_ROOT);
|
|
@@ -125,7 +147,7 @@ export async function runBootSweep({
|
|
|
125
147
|
config.delivery?.worktreeIsolation?.sweepLockMs ?? 60_000;
|
|
126
148
|
|
|
127
149
|
const sweepFn = injectedSweep ?? sweepMergedBranches;
|
|
128
|
-
|
|
150
|
+
const result = await sweepFn({
|
|
129
151
|
cwd: root,
|
|
130
152
|
baseBranch,
|
|
131
153
|
include: includeGlobs,
|
|
@@ -140,6 +162,21 @@ export async function runBootSweep({
|
|
|
140
162
|
lockPath,
|
|
141
163
|
lockTimeoutMs,
|
|
142
164
|
});
|
|
165
|
+
|
|
166
|
+
// Story #4794 — the temp-retention catch-up. Two eligibility signals, both
|
|
167
|
+
// already paid for: every branch this sweep reaped is a merge it CONFIRMED
|
|
168
|
+
// (merged PR + matching headRefOid), so those Stories' artifacts are spent;
|
|
169
|
+
// and the age floor collects everything else — the backlog from Stories
|
|
170
|
+
// merged before this existed, merged through the GitHub UI, or whose branch
|
|
171
|
+
// was already gone. Best-effort like the sweep itself: `runBootSweep`'s
|
|
172
|
+
// catch swallows any throw into the `ok: false` envelope, and exit stays 0.
|
|
173
|
+
const purge = await purgeFn({
|
|
174
|
+
config,
|
|
175
|
+
mergedStoryIds: storyIdsFromBranches(result?.reaped),
|
|
176
|
+
label: 'boot-sweep',
|
|
177
|
+
logger,
|
|
178
|
+
});
|
|
179
|
+
return { ...result, tempPurge: purge };
|
|
143
180
|
} catch (err) {
|
|
144
181
|
const msg = err?.message ?? String(err);
|
|
145
182
|
logger.warn?.(`[boot-sweep] sweep threw (host continues): ${msg}`);
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
// a retired token is always a non-zero exit even if a stale workflow
|
|
26
26
|
// file happens to exist.
|
|
27
27
|
//
|
|
28
|
+
// 4. Story #4801 — every relative link originating under `.agents/**`
|
|
29
|
+
// resolves to a target that still exists once the tree is materialized
|
|
30
|
+
// into a *consumer* project. See `escapesPayload` for the boundary rule.
|
|
31
|
+
//
|
|
28
32
|
// Exit codes:
|
|
29
33
|
// 0 every link and slash-command token resolves cleanly.
|
|
30
34
|
// 1 at least one violation; details are written to stderr (file:line).
|
|
@@ -35,6 +39,8 @@
|
|
|
35
39
|
import fs from 'node:fs';
|
|
36
40
|
import path from 'node:path';
|
|
37
41
|
import { fileURLToPath } from 'node:url';
|
|
42
|
+
import { minimatch } from 'minimatch';
|
|
43
|
+
import { parseStandardCliArgs } from './lib/cli/standard-args.js';
|
|
38
44
|
import { runAsCli } from './lib/cli-utils.js';
|
|
39
45
|
import { Logger } from './lib/Logger.js';
|
|
40
46
|
|
|
@@ -124,6 +130,63 @@ export const SLASH_ALLOWLIST = new Set([
|
|
|
124
130
|
'main',
|
|
125
131
|
]);
|
|
126
132
|
|
|
133
|
+
// --- Payload boundary (Story #4801) ----------------------------------------
|
|
134
|
+
|
|
135
|
+
// `mandrel sync` materializes ONLY the package's `.agents/` payload into a
|
|
136
|
+
// consumer's project, at `<projectRoot>/.agents` (see `lib/cli/sync.js`:
|
|
137
|
+
// `destRoot = path.join(projectRoot, '.agents')`). `bin/` and `lib/` ship
|
|
138
|
+
// inside the npm tarball but stay under `node_modules/mandrel/`, and the
|
|
139
|
+
// framework's own `tests/`, `docs/` (bar the CHANGELOG) and `.claude/` trees
|
|
140
|
+
// ship nowhere at all. So a relative link that escapes `.agents/` resolves
|
|
141
|
+
// cleanly in THIS repo and dangles in every consumer — which is exactly why
|
|
142
|
+
// the checker cannot catch this class by `fs.existsSync` alone.
|
|
143
|
+
//
|
|
144
|
+
// This is why the boundary is `.agents/` and NOT `package.json#files`: the
|
|
145
|
+
// latter lists `lib/` and `bin/`, which are packaged but never materialized
|
|
146
|
+
// at a consumer's repo root.
|
|
147
|
+
export const MATERIALIZED_ROOT = '.agents';
|
|
148
|
+
|
|
149
|
+
// Repo-root-relative paths OUTSIDE `.agents/` that a Mandrel *consumer*
|
|
150
|
+
// legitimately owns, so a doc under `.agents/**` may still link to them.
|
|
151
|
+
// Deliberately explicit rather than pattern-derived: whether a given repo-root
|
|
152
|
+
// path is consumer-owned or framework-only is a judgment per path, not a rule.
|
|
153
|
+
// A new escaping link fails closed until it is justified and added here.
|
|
154
|
+
export const CONSUMER_OWNED_PATHS = new Set([
|
|
155
|
+
'package.json',
|
|
156
|
+
'.agentrc.json',
|
|
157
|
+
'.c8rc.cjs',
|
|
158
|
+
'docs/architecture.md',
|
|
159
|
+
'docs/decisions.md',
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
// Directory prefixes (repo-root-relative, trailing slash) whose whole subtree
|
|
163
|
+
// is consumer-owned.
|
|
164
|
+
export const CONSUMER_OWNED_PREFIXES = Object.freeze(['baselines/']);
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* True when `relTarget` is unreachable from a materialized consumer tree.
|
|
168
|
+
*
|
|
169
|
+
* Only links whose SOURCE lives under `.agents/**` are subject to the rule —
|
|
170
|
+
* `docs/**` is framework-repo-only, ships nowhere, and keeps today's
|
|
171
|
+
* existence-only semantics.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} relFile repo-relative POSIX path of the linking document
|
|
174
|
+
* @param {string} relTarget repo-relative POSIX path the link resolves to
|
|
175
|
+
*/
|
|
176
|
+
export function escapesPayload(relFile, relTarget) {
|
|
177
|
+
if (!relFile.startsWith(`${MATERIALIZED_ROOT}/`)) return false;
|
|
178
|
+
if (
|
|
179
|
+
relTarget === MATERIALIZED_ROOT ||
|
|
180
|
+
relTarget.startsWith(`${MATERIALIZED_ROOT}/`)
|
|
181
|
+
) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (CONSUMER_OWNED_PATHS.has(relTarget)) return false;
|
|
185
|
+
if (CONSUMER_OWNED_PREFIXES.some((p) => relTarget.startsWith(p)))
|
|
186
|
+
return false;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
127
190
|
// --- File discovery --------------------------------------------------------
|
|
128
191
|
|
|
129
192
|
function isExcludedRelPath(relPath) {
|
|
@@ -150,14 +213,28 @@ function walkMarkdown(dirAbs, repoRoot, out) {
|
|
|
150
213
|
}
|
|
151
214
|
}
|
|
152
215
|
|
|
153
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Collect every non-excluded `*.md` under each `scanRoots` entry.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} rootAbs absolute repo root
|
|
220
|
+
* @param {string[]} scanRoots repo-relative subtrees to walk
|
|
221
|
+
* @param {string[]} [exclude] minimatch globs; a repo-relative POSIX path
|
|
222
|
+
* matching any of them is dropped from the scan
|
|
223
|
+
*/
|
|
224
|
+
export function discoverMarkdown(rootAbs, scanRoots, exclude = []) {
|
|
154
225
|
const out = [];
|
|
155
226
|
for (const sub of scanRoots) {
|
|
156
227
|
const subAbs = path.join(rootAbs, sub);
|
|
157
228
|
if (fs.existsSync(subAbs)) walkMarkdown(subAbs, rootAbs, out);
|
|
158
229
|
}
|
|
159
|
-
|
|
160
|
-
|
|
230
|
+
const filtered = exclude.length
|
|
231
|
+
? out.filter((abs) => {
|
|
232
|
+
const rel = path.relative(rootAbs, abs).split(path.sep).join('/');
|
|
233
|
+
return !exclude.some((g) => minimatch(rel, g, { dot: true }));
|
|
234
|
+
})
|
|
235
|
+
: out;
|
|
236
|
+
filtered.sort();
|
|
237
|
+
return filtered;
|
|
161
238
|
}
|
|
162
239
|
|
|
163
240
|
// --- Region masking --------------------------------------------------------
|
|
@@ -326,6 +403,26 @@ export function checkFile(absPath, repoRoot) {
|
|
|
326
403
|
} else {
|
|
327
404
|
resolved = path.resolve(fileDir, pathOnly);
|
|
328
405
|
}
|
|
406
|
+
// Payload boundary (Story #4801) takes precedence over existence: a link
|
|
407
|
+
// that escapes the materialized tree is a defect even when the target
|
|
408
|
+
// exists here, and reporting both kinds for one link would double-count.
|
|
409
|
+
const relTarget = path
|
|
410
|
+
.relative(repoRoot, resolved)
|
|
411
|
+
.split(path.sep)
|
|
412
|
+
.join('/');
|
|
413
|
+
if (escapesPayload(relFile, relTarget)) {
|
|
414
|
+
violations.push({
|
|
415
|
+
file: relFile,
|
|
416
|
+
line,
|
|
417
|
+
kind: 'payload-boundary',
|
|
418
|
+
message:
|
|
419
|
+
`link escapes the materialized payload: ${target} → ${relTarget}. ` +
|
|
420
|
+
`Only '${MATERIALIZED_ROOT}/' is materialized into a consumer project, ` +
|
|
421
|
+
'so this resolves here but dangles for every consumer. Use an absolute ' +
|
|
422
|
+
'GitHub URL or a non-link code span.',
|
|
423
|
+
});
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
329
426
|
if (!fs.existsSync(resolved)) {
|
|
330
427
|
violations.push({
|
|
331
428
|
file: relFile,
|
|
@@ -377,6 +474,8 @@ export function checkFile(absPath, repoRoot) {
|
|
|
377
474
|
|
|
378
475
|
// --- Public entry point ----------------------------------------------------
|
|
379
476
|
|
|
477
|
+
export const DEFAULT_SCAN_ROOTS = Object.freeze(['docs', '.agents']);
|
|
478
|
+
|
|
380
479
|
/**
|
|
381
480
|
* Run the checker programmatically. Returns `{ exitCode, violations }`.
|
|
382
481
|
* `exitCode` is 0 when every doc is clean, 1 otherwise.
|
|
@@ -384,11 +483,13 @@ export function checkFile(absPath, repoRoot) {
|
|
|
384
483
|
* @param {object} [options]
|
|
385
484
|
* @param {string} [options.repoRoot] Defaults to the framework repo root.
|
|
386
485
|
* @param {string[]} [options.scanRoots] Defaults to `['docs', '.agents']`.
|
|
486
|
+
* @param {string[]} [options.exclude] minimatch globs dropped from the scan.
|
|
387
487
|
*/
|
|
388
488
|
export function runCheck(options = {}) {
|
|
389
489
|
const repoRoot = options.repoRoot ?? REPO_ROOT;
|
|
390
|
-
const scanRoots = options.scanRoots ?? [
|
|
391
|
-
const
|
|
490
|
+
const scanRoots = options.scanRoots ?? [...DEFAULT_SCAN_ROOTS];
|
|
491
|
+
const exclude = options.exclude ?? [];
|
|
492
|
+
const files = discoverMarkdown(repoRoot, scanRoots, exclude);
|
|
392
493
|
const violations = [];
|
|
393
494
|
for (const abs of files) {
|
|
394
495
|
const fileViolations = checkFile(abs, repoRoot);
|
|
@@ -405,8 +506,28 @@ function formatViolation(v) {
|
|
|
405
506
|
return `${v.file}:${v.line}: [${v.kind}] ${v.message}`;
|
|
406
507
|
}
|
|
407
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Translate argv into `runCheck` options. Repeatable `--scan-root` replaces
|
|
511
|
+
* the default scan set entirely; repeatable `--exclude` filters whatever was
|
|
512
|
+
* scanned. Absent flags reproduce the pre-#4801 defaults exactly.
|
|
513
|
+
*/
|
|
514
|
+
export function parseArgs(argv) {
|
|
515
|
+
const { values } = parseStandardCliArgs({
|
|
516
|
+
argv,
|
|
517
|
+
extras: {
|
|
518
|
+
'scan-root': { type: 'string-multi', alias: 'scanRoot' },
|
|
519
|
+
exclude: { type: 'string-multi', alias: 'exclude' },
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
const scanRoots = values.scanRoot?.length
|
|
523
|
+
? values.scanRoot
|
|
524
|
+
: [...DEFAULT_SCAN_ROOTS];
|
|
525
|
+
return { scanRoots, exclude: values.exclude ?? [] };
|
|
526
|
+
}
|
|
527
|
+
|
|
408
528
|
async function main() {
|
|
409
|
-
const
|
|
529
|
+
const { scanRoots, exclude } = parseArgs(process.argv.slice(2));
|
|
530
|
+
const result = runCheck({ scanRoots, exclude });
|
|
410
531
|
if (result.violations.length === 0) {
|
|
411
532
|
Logger.info(
|
|
412
533
|
`[check-doc-links] OK — scanned ${result.scanned} active markdown file(s); no violations.`,
|
|
@@ -426,11 +547,22 @@ async function main() {
|
|
|
426
547
|
runAsCli(import.meta.url, main, {
|
|
427
548
|
source: 'check-doc-links',
|
|
428
549
|
usage: {
|
|
429
|
-
invocation:
|
|
550
|
+
invocation:
|
|
551
|
+
'node .agents/scripts/check-doc-links.js [--scan-root <path>] [--exclude <glob>]',
|
|
430
552
|
summary:
|
|
431
|
-
'Validate every relative Markdown link and /slash-command token across docs/ and .agents/,
|
|
432
|
-
flags: [
|
|
553
|
+
'Validate every relative Markdown link and /slash-command token across docs/ and .agents/, reject mentions of retired commands, and reject links that escape the materialized .agents/ payload.',
|
|
554
|
+
flags: [
|
|
555
|
+
[
|
|
556
|
+
'--scan-root <path>',
|
|
557
|
+
'Repeatable. Repo-relative subtree to scan. Replaces the default set (docs, .agents).',
|
|
558
|
+
],
|
|
559
|
+
[
|
|
560
|
+
'--exclude <glob>',
|
|
561
|
+
'Repeatable. minimatch glob; matching files are dropped from the scan.',
|
|
562
|
+
],
|
|
563
|
+
],
|
|
433
564
|
notes: [
|
|
565
|
+
'Consumers materialize only .agents/, so a relative link from .agents/**\nto a framework-repo-only path (tests/, lib/, .claude/, framework docs)\nis reported as a payload-boundary violation even though it resolves here.',
|
|
434
566
|
'Exit codes:\n 0 every link and command token resolves\n 1 at least one violation (file:line on stderr)',
|
|
435
567
|
],
|
|
436
568
|
},
|
|
@@ -83,69 +83,61 @@ export function resolveCrapEnvOverrides(crapConfig, env) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* (mirroring `CRAP_TOLERANCE`'s env-override precedent) tells
|
|
93
|
-
* `check-baselines --gate bundle-size` to treat this run's head
|
|
94
|
-
* measurements as the newly acknowledged baseline: head-vs-base
|
|
95
|
-
* regressions are demoted to `unchanged` for this invocation only. Floors
|
|
96
|
-
* still apply — an acknowledged PR can still fail on an absolute budget
|
|
97
|
-
* breach, only the ratchet-vs-`origin/main` comparison is suspended.
|
|
86
|
+
* The env var that acknowledges a deliberate baseline refresh for `kind`.
|
|
87
|
+
* Upper-snakes the kind name, so `bundle-size` → `BUNDLE_SIZE_REFRESH` and
|
|
88
|
+
* `coverage` → `COVERAGE_REFRESH`. The two names that predate the generic
|
|
89
|
+
* mechanism (`BUNDLE_SIZE_REFRESH`, Story #151; `MAINTAINABILITY_REFRESH`,
|
|
90
|
+
* Story #4731) are exactly what this rule produces, so generalizing kept
|
|
91
|
+
* both working unchanged.
|
|
98
92
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* i.e. the next PR — reverts to full strict enforcement automatically, so
|
|
102
|
-
* there is no lingering loosened tolerance to remember to reset (AC-3).
|
|
93
|
+
* Module-local: `resolveKindRefreshOverrides` is the public surface, and the
|
|
94
|
+
* naming rule is pinned through it rather than exported for its own sake.
|
|
103
95
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* @param {NodeJS.ProcessEnv} env
|
|
108
|
-
* @returns {{ acknowledged: boolean, overrides: string[] }}
|
|
96
|
+
* @param {string} kind
|
|
97
|
+
* @returns {string|null} null when `kind` is not a usable kind name
|
|
109
98
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
typeof raw === 'string' && /^(1|true)$/i.test(raw.trim());
|
|
114
|
-
const overrides = acknowledged
|
|
115
|
-
? [`acknowledged=true (BUNDLE_SIZE_REFRESH=${raw})`]
|
|
116
|
-
: [];
|
|
117
|
-
return { acknowledged, overrides };
|
|
99
|
+
function kindRefreshEnvVar(kind) {
|
|
100
|
+
if (typeof kind !== 'string' || kind.length === 0) return null;
|
|
101
|
+
return `${kind.toUpperCase().replace(/-/g, '_')}_REFRESH`;
|
|
118
102
|
}
|
|
119
103
|
|
|
120
104
|
/**
|
|
121
|
-
* Pure helper: resolve the one-shot
|
|
122
|
-
* (Story #
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
105
|
+
* Pure helper: resolve the one-shot baseline refresh/acknowledge flag for any
|
|
106
|
+
* ratcheted kind (Story #4802, generalizing Story #151 / Story #4731).
|
|
107
|
+
*
|
|
108
|
+
* `<KIND>_REFRESH=1` tells `check-baselines --gate <kind>` to demote this
|
|
109
|
+
* run's head-vs-base regressions to `unchanged` for this invocation only.
|
|
110
|
+
* Floors still apply — an acknowledged run can still fail on an absolute
|
|
111
|
+
* floor breach; only the ratchet-vs-base comparison is suspended.
|
|
112
|
+
*
|
|
113
|
+
* Why every kind needs this: a diff-scope baseline is an accretion of many
|
|
114
|
+
* partial runs, not one measurement. Replacing it with a single full-scope
|
|
115
|
+
* measurement necessarily produces row deltas in both directions that are
|
|
116
|
+
* arithmetic, not behavioural — so without an acknowledgment path the gate
|
|
117
|
+
* blocks precisely the correction it should encourage.
|
|
129
118
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* strength automatically.
|
|
119
|
+
* The flag is **not persisted** anywhere (no config write, no committed tag):
|
|
120
|
+
* the very next invocation without the env var reverts to full strict
|
|
121
|
+
* enforcement automatically, so there is no lingering loosened tolerance to
|
|
122
|
+
* remember to reset. The evaluate phase pairs this with a commit-tagged
|
|
123
|
+
* trigger that is likewise one-shot by construction.
|
|
136
124
|
*
|
|
137
|
-
* Accepted truthy values: `1`, `true` (case-insensitive)
|
|
138
|
-
*
|
|
125
|
+
* Accepted truthy values: `1`, `true` (case-insensitive), with surrounding
|
|
126
|
+
* whitespace trimmed. Anything else — including unset, empty, `0`, `false`,
|
|
127
|
+
* and non-string values — resolves to `acknowledged: false`.
|
|
139
128
|
*
|
|
129
|
+
* @param {string} kind
|
|
140
130
|
* @param {NodeJS.ProcessEnv} env
|
|
141
131
|
* @returns {{ acknowledged: boolean, overrides: string[] }}
|
|
142
132
|
*/
|
|
143
|
-
export function
|
|
144
|
-
const
|
|
133
|
+
export function resolveKindRefreshOverrides(kind, env) {
|
|
134
|
+
const varName = kindRefreshEnvVar(kind);
|
|
135
|
+
if (!varName) return { acknowledged: false, overrides: [] };
|
|
136
|
+
const raw = env?.[varName];
|
|
145
137
|
const acknowledged =
|
|
146
138
|
typeof raw === 'string' && /^(1|true)$/i.test(raw.trim());
|
|
147
139
|
const overrides = acknowledged
|
|
148
|
-
? [`acknowledged=true (
|
|
140
|
+
? [`acknowledged=true (${varName}=${raw})`]
|
|
149
141
|
: [];
|
|
150
142
|
return { acknowledged, overrides };
|
|
151
143
|
}
|
|
@@ -274,6 +274,33 @@ export function tempRootFrom(config) {
|
|
|
274
274
|
: 'temp';
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Directory segment (under `tempRoot`) holding every orchestration run log —
|
|
279
|
+
* the close gate transcripts (`close-gates-<sid>.log`) and the terse-result
|
|
280
|
+
* detail dumps (`story-init-result-<sid>.log`, `sync-result-<branch>.log`, …).
|
|
281
|
+
*
|
|
282
|
+
* Story #4794: the four writers that land here each hand-rolled the temp path
|
|
283
|
+
* from a literal `temp` segment joined onto their own cwd, which ignores
|
|
284
|
+
* `project.paths.tempRoot` entirely. On a consumer that relocates its temp
|
|
285
|
+
* root, the writers wrote to `<cwd>/temp/` while every reader — including the
|
|
286
|
+
* retention purge — resolved the configured root, so the artifacts were
|
|
287
|
+
* invisible to the tooling meant to manage them. Routing all four through this
|
|
288
|
+
* helper also picks up main-checkout anchoring for free, so a close running
|
|
289
|
+
* from a Story worktree lands its logs in the same tree the host reads.
|
|
290
|
+
*/
|
|
291
|
+
export const ORCHESTRATION_DIRNAME = 'orchestration';
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* `<tempRoot>/orchestration/` — resolved against the configured temp root and
|
|
295
|
+
* anchored to the main checkout, like every other helper in this module.
|
|
296
|
+
*
|
|
297
|
+
* @param {object} [config]
|
|
298
|
+
* @returns {string}
|
|
299
|
+
*/
|
|
300
|
+
export function orchestrationLogDir(config) {
|
|
301
|
+
return path.join(anchorTempRoot(tempRootFrom(config)), ORCHESTRATION_DIRNAME);
|
|
302
|
+
}
|
|
303
|
+
|
|
277
304
|
const runId = (id) => {
|
|
278
305
|
if (!Number.isInteger(id) || id <= 0) {
|
|
279
306
|
throw new Error(`[temp-paths] runId must be a positive integer; got ${id}`);
|
|
@@ -352,12 +352,81 @@ const AUDIT_TO_STORIES_SCHEMA = {
|
|
|
352
352
|
additionalProperties: false,
|
|
353
353
|
};
|
|
354
354
|
|
|
355
|
+
/**
|
|
356
|
+
* `delivery.tempRetention` — auto-purge of spent temp artifacts (Story #4794).
|
|
357
|
+
*
|
|
358
|
+
* `enabled` defaults to `true`: reclaiming a landed Story's gate transcripts
|
|
359
|
+
* and evidence is the behaviour, and the knob exists to turn it off. `classes`
|
|
360
|
+
* lets an operator keep one family while purging the rest; `staleDays` is the
|
|
361
|
+
* age floor for the families no Story id can be recovered from (audit reports,
|
|
362
|
+
* abandoned `plan-<slug>/` dirs).
|
|
363
|
+
*/
|
|
364
|
+
const TEMP_RETENTION_SCHEMA = {
|
|
365
|
+
type: 'object',
|
|
366
|
+
description:
|
|
367
|
+
'Story #4794. Auto-purge of spent temp artifacts once their Story lands. ' +
|
|
368
|
+
'Classification is an allowlist: only the declared classes below are ever ' +
|
|
369
|
+
'deleted, so operator scratch files under tempRoot are reported with their ' +
|
|
370
|
+
'size and left alone. signals.ndjson is never purged by any path.',
|
|
371
|
+
properties: {
|
|
372
|
+
enabled: {
|
|
373
|
+
type: 'boolean',
|
|
374
|
+
description:
|
|
375
|
+
"Master switch. Default true — reclaiming a landed Story's gate " +
|
|
376
|
+
'transcripts and validation evidence is the behaviour, and this knob ' +
|
|
377
|
+
'turns it off. When false every purge path is a reported no-op.',
|
|
378
|
+
},
|
|
379
|
+
staleDays: {
|
|
380
|
+
type: 'integer',
|
|
381
|
+
minimum: 1,
|
|
382
|
+
description:
|
|
383
|
+
'Age floor (days, default 7) for the families no Story id can be ' +
|
|
384
|
+
'recovered from — roster-level audit reports and abandoned ' +
|
|
385
|
+
'plan-<slug>/ dirs. Story-keyed artifacts do not wait for it: they are ' +
|
|
386
|
+
'purged as soon as their merge is confirmed.',
|
|
387
|
+
},
|
|
388
|
+
classes: {
|
|
389
|
+
type: 'object',
|
|
390
|
+
description:
|
|
391
|
+
'Per-class opt-out. Each defaults to true; set one false to keep that ' +
|
|
392
|
+
'family while the rest are purged.',
|
|
393
|
+
properties: {
|
|
394
|
+
orchestrationLogs: {
|
|
395
|
+
type: 'boolean',
|
|
396
|
+
description:
|
|
397
|
+
'<tempRoot>/orchestration/*.log — close gate transcripts and ' +
|
|
398
|
+
'terse-result detail dumps.',
|
|
399
|
+
},
|
|
400
|
+
validationEvidence: {
|
|
401
|
+
type: 'boolean',
|
|
402
|
+
description:
|
|
403
|
+
'Per-Story validation-evidence.json, lifecycle.ndjson, and ' +
|
|
404
|
+
'manifest.md under the standalone and per-run story trees.',
|
|
405
|
+
},
|
|
406
|
+
auditResults: {
|
|
407
|
+
type: 'boolean',
|
|
408
|
+
description: '<tempRoot>/audits/ — audit lens reports.',
|
|
409
|
+
},
|
|
410
|
+
planDirs: {
|
|
411
|
+
type: 'boolean',
|
|
412
|
+
description:
|
|
413
|
+
'<tempRoot>/plan-<slug>/ — abandoned plan authoring dirs. ' +
|
|
414
|
+
'Age-floored only; the current run is always excluded.',
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
additionalProperties: false,
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
additionalProperties: false,
|
|
421
|
+
};
|
|
422
|
+
|
|
355
423
|
export const DELIVERY_SCHEMA = {
|
|
356
424
|
type: 'object',
|
|
357
425
|
properties: {
|
|
358
426
|
execution: EXECUTION_SCHEMA,
|
|
359
427
|
lease: LEASE_SCHEMA,
|
|
360
428
|
docsFreshness: DOCS_FRESHNESS_SCHEMA,
|
|
429
|
+
tempRetention: TEMP_RETENTION_SCHEMA,
|
|
361
430
|
deliverRunner: DELIVER_RUNNER_SCHEMA,
|
|
362
431
|
worktreeIsolation: WORKTREE_ISOLATION_SCHEMA,
|
|
363
432
|
signals: SIGNALS_SCHEMA,
|