mandrel 2.42.0 → 2.44.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/agentrc-reference.json +4 -0
- package/.agents/docs/configuration.md +4 -1
- package/.agents/docs/workflows.md +1 -1
- package/.agents/schemas/agentrc.schema.json +20 -1
- package/.agents/scripts/lib/ITicketingProvider.js +27 -0
- package/.agents/scripts/lib/config-settings-schema.js +29 -1
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches-detect.js +187 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +37 -90
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +115 -19
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +10 -2
- package/.agents/scripts/lib/orchestration/plan-context.js +4 -0
- package/.agents/scripts/lib/orchestration/plan-run-labels/reap.js +327 -0
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +9 -1
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +159 -55
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +117 -36
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-outcome.js +12 -7
- package/.agents/scripts/providers/github/labels.js +88 -0
- package/.agents/scripts/providers/github.js +2 -0
- package/.agents/scripts/prune-plan-run-labels.js +218 -0
- package/.agents/workflows/memory-consolidate.md +18 -6
- package/docs/CHANGELOG.md +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// .agents/scripts/prune-plan-run-labels.js — Story #5189.
|
|
4
|
+
//
|
|
5
|
+
// Sweep the repository's `plan-run::<id>` cohort labels and delete the ones
|
|
6
|
+
// that are provably spent.
|
|
7
|
+
//
|
|
8
|
+
// The close tail reaps incrementally — one Story's own labels, as it lands —
|
|
9
|
+
// which keeps a healthy repository flat but does nothing about a pile that
|
|
10
|
+
// already exists, and nothing about a cohort whose last Story was closed by
|
|
11
|
+
// hand rather than by a close. This is the surface that burns an accumulated
|
|
12
|
+
// pile down: it reads the whole label vocabulary through the paginating
|
|
13
|
+
// listing port, so what it can see is bounded by the repository rather than by
|
|
14
|
+
// an API page.
|
|
15
|
+
//
|
|
16
|
+
// The decision is not made here — `lib/orchestration/plan-run-labels/reap.js`
|
|
17
|
+
// owns it, so this sweep and the close-path reap cannot come to different
|
|
18
|
+
// conclusions about when a label is spent. In short: reapable means the label
|
|
19
|
+
// carries at least one issue and every one of them is closed. A label carrying
|
|
20
|
+
// zero issues is NOT reapable by default, because that shape is exactly what
|
|
21
|
+
// an in-flight `plan-persist` looks like between minting its label and
|
|
22
|
+
// creating its Stories; `--include-unreferenced` is the explicit opt-in for an
|
|
23
|
+
// operator who knows no persist is running.
|
|
24
|
+
//
|
|
25
|
+
// Exit codes:
|
|
26
|
+
// 0 nothing reapable (or, without `--check`, the reap was performed)
|
|
27
|
+
// 1 `--check` found at least one label that would be reaped
|
|
28
|
+
// 2 the sweep could not run
|
|
29
|
+
|
|
30
|
+
// Fail-fast if the framework's runtime deps are not installed — must be the
|
|
31
|
+
// first import so the check runs before any third-party-importing sibling
|
|
32
|
+
// module is evaluated (Story #3432).
|
|
33
|
+
import './lib/runtime-deps/ensure-installed.js';
|
|
34
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
35
|
+
import { resolveConfig } from './lib/config-resolver.js';
|
|
36
|
+
import { Logger } from './lib/Logger.js';
|
|
37
|
+
import {
|
|
38
|
+
REAP_REASONS,
|
|
39
|
+
sweepCohortLabels,
|
|
40
|
+
} from './lib/orchestration/plan-run-labels/reap.js';
|
|
41
|
+
import { createProvider } from './lib/provider-factory.js';
|
|
42
|
+
|
|
43
|
+
const EXIT_CLEAN = 0;
|
|
44
|
+
const EXIT_WOULD_REAP = 1;
|
|
45
|
+
const EXIT_CANNOT_RUN = 2;
|
|
46
|
+
|
|
47
|
+
const HELP = {
|
|
48
|
+
invocation:
|
|
49
|
+
'node .agents/scripts/prune-plan-run-labels.js [--check] [--json] [--include-unreferenced] [--cwd <dir>]',
|
|
50
|
+
summary:
|
|
51
|
+
'Delete the plan-run:: cohort labels whose Stories are all closed. Never touches any other label axis, and never changes how a cohort label is minted.',
|
|
52
|
+
flags: [
|
|
53
|
+
['--check', 'Report what would be reaped, delete nothing, exit 1 if any.'],
|
|
54
|
+
['--json', 'Emit the report as JSON instead of text.'],
|
|
55
|
+
[
|
|
56
|
+
'--include-unreferenced',
|
|
57
|
+
'Also reap cohort labels carrying zero issues. Off by default: that shape is indistinguishable from a label an in-flight plan-persist just minted.',
|
|
58
|
+
],
|
|
59
|
+
['--cwd <dir>', 'Repository root to sweep. Default: process.cwd().'],
|
|
60
|
+
],
|
|
61
|
+
notes: [
|
|
62
|
+
'Reapable = the label carries at least one issue AND every issue carrying it\nis closed. One open issue anywhere in the cohort keeps the label.',
|
|
63
|
+
'Exit codes:\n 0 clean, or reaped\n 1 --check found reapable labels\n 2 the sweep could not run',
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse argv into an options bag. An unknown flag is an error, never a silent
|
|
69
|
+
* no-op — a typo'd `--dry-run` must not read as "delete the labels".
|
|
70
|
+
*
|
|
71
|
+
* @param {string[]} argv
|
|
72
|
+
* @returns {{ check: boolean, json: boolean, includeUnreferenced: boolean, cwd: string }}
|
|
73
|
+
*/
|
|
74
|
+
export function parseArgs(argv = []) {
|
|
75
|
+
const out = {
|
|
76
|
+
check: false,
|
|
77
|
+
json: false,
|
|
78
|
+
includeUnreferenced: false,
|
|
79
|
+
cwd: null,
|
|
80
|
+
};
|
|
81
|
+
let i = 0;
|
|
82
|
+
while (i < argv.length) {
|
|
83
|
+
const arg = argv[i];
|
|
84
|
+
const value = argv[i + 1];
|
|
85
|
+
i += 1;
|
|
86
|
+
if (arg === '--check') out.check = true;
|
|
87
|
+
else if (arg === '--json') out.json = true;
|
|
88
|
+
else if (arg === '--include-unreferenced') out.includeUnreferenced = true;
|
|
89
|
+
else if (arg === '--cwd') {
|
|
90
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
91
|
+
throw new Error('--cwd requires a directory');
|
|
92
|
+
}
|
|
93
|
+
out.cwd = value;
|
|
94
|
+
i += 1;
|
|
95
|
+
} else throw new Error(`unknown flag "${arg}" (try --help)`);
|
|
96
|
+
}
|
|
97
|
+
out.cwd = out.cwd ?? process.cwd();
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* One human-readable line per cohort label, naming the reason it was kept or
|
|
103
|
+
* reaped. The reason is printed for every label, not only the reapable ones —
|
|
104
|
+
* an operator auditing a pile of 235 needs to see why the other 234 stayed.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} decision
|
|
107
|
+
* @param {boolean} check
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
function renderDecision(decision, check) {
|
|
111
|
+
if (!decision.reapable) {
|
|
112
|
+
const suffix =
|
|
113
|
+
decision.reason === REAP_REASONS.OPEN_STORIES
|
|
114
|
+
? ` (open: ${decision.openIssues.join(', ') || 'unknown'})`
|
|
115
|
+
: '';
|
|
116
|
+
return ` · keep ${decision.label} — ${decision.reason}${suffix}`;
|
|
117
|
+
}
|
|
118
|
+
const verb = check ? 'would reap' : 'reaped';
|
|
119
|
+
return ` · ${verb} ${decision.label} — ${decision.reason} (${decision.issueCount} closed issue(s))`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Render the whole report as text.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} report
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function formatReport(report) {
|
|
129
|
+
const verb = report.check ? 'would reap' : 'reaped';
|
|
130
|
+
const count = report.check ? report.reapable.length : report.deleted.length;
|
|
131
|
+
const lines = [
|
|
132
|
+
`[prune-plan-run-labels] ${verb} ${count} of ${report.evaluated} cohort ` +
|
|
133
|
+
`label(s) (${report.totalLabels} label(s) in the repository)`,
|
|
134
|
+
...report.decisions.map((d) => renderDecision(d, report.check)),
|
|
135
|
+
];
|
|
136
|
+
for (const failure of report.failed) {
|
|
137
|
+
lines.push(` ! failed ${failure.label} — ${failure.detail}`);
|
|
138
|
+
}
|
|
139
|
+
if (report.check && report.reapable.length > 0) {
|
|
140
|
+
lines.push(
|
|
141
|
+
'',
|
|
142
|
+
'Run without --check to delete them:',
|
|
143
|
+
' node .agents/scripts/prune-plan-run-labels.js',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The whole sweep — argv in, exit code out, report written through `writeFn`.
|
|
151
|
+
*
|
|
152
|
+
* Exported with its provider/config/output surfaces as default-parameter
|
|
153
|
+
* seams (`rules/test-seams.md`) so a test drives the *real* argv parsing,
|
|
154
|
+
* report rendering and exit-code decision against a stub provider. Without
|
|
155
|
+
* that, the only testable thing here would be a re-implementation of the
|
|
156
|
+
* decision, which is precisely the copy that drifts.
|
|
157
|
+
*
|
|
158
|
+
* @param {string[]} argv
|
|
159
|
+
* @param {{
|
|
160
|
+
* createProviderFn?: Function,
|
|
161
|
+
* resolveConfigFn?: Function,
|
|
162
|
+
* writeFn?: (text: string) => void,
|
|
163
|
+
* warnFn?: (message: string) => void,
|
|
164
|
+
* }} [seams]
|
|
165
|
+
* @returns {Promise<number>} the process exit code.
|
|
166
|
+
*/
|
|
167
|
+
export async function runSweep(
|
|
168
|
+
argv,
|
|
169
|
+
{
|
|
170
|
+
createProviderFn = createProvider,
|
|
171
|
+
resolveConfigFn = resolveConfig,
|
|
172
|
+
writeFn = (text) => process.stdout.write(text),
|
|
173
|
+
warnFn = (message) => Logger.warn(`[prune-plan-run-labels] ${message}`),
|
|
174
|
+
} = {},
|
|
175
|
+
) {
|
|
176
|
+
let opts;
|
|
177
|
+
let report;
|
|
178
|
+
try {
|
|
179
|
+
opts = parseArgs(argv);
|
|
180
|
+
report = await sweepCohortLabels({
|
|
181
|
+
provider: createProviderFn(resolveConfigFn({ cwd: opts.cwd })),
|
|
182
|
+
includeUnreferenced: opts.includeUnreferenced,
|
|
183
|
+
check: opts.check,
|
|
184
|
+
onWarn: warnFn,
|
|
185
|
+
});
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const message = err?.message ?? String(err);
|
|
188
|
+
writeFn(
|
|
189
|
+
`${JSON.stringify({ schemaVersion: '1', error: message }, null, 2)}\n`,
|
|
190
|
+
);
|
|
191
|
+
return EXIT_CANNOT_RUN;
|
|
192
|
+
}
|
|
193
|
+
writeFn(
|
|
194
|
+
opts.json
|
|
195
|
+
? `${JSON.stringify(report, null, 2)}\n`
|
|
196
|
+
: `${formatReport(report)}\n`,
|
|
197
|
+
);
|
|
198
|
+
return report.check && report.reapable.length > 0
|
|
199
|
+
? EXIT_WOULD_REAP
|
|
200
|
+
: EXIT_CLEAN;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* CLI entry point. Returns its exit code rather than calling `process.exit()`,
|
|
205
|
+
* so `runAsCli`'s `propagateExitCode` path settles it through `flushStdio` and
|
|
206
|
+
* an unbounded report is not truncated at a pipe boundary (Story #4783).
|
|
207
|
+
*
|
|
208
|
+
* @returns {Promise<number>}
|
|
209
|
+
*/
|
|
210
|
+
async function main() {
|
|
211
|
+
return runSweep(process.argv.slice(2));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
runAsCli(import.meta.url, main, {
|
|
215
|
+
source: 'prune-plan-run-labels',
|
|
216
|
+
usage: HELP,
|
|
217
|
+
propagateExitCode: true,
|
|
218
|
+
});
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
description: >-
|
|
3
3
|
Attended consolidation pass over this project's agent memory pool — merge
|
|
4
4
|
duplicates, verify claims against the current tree, prune with operator
|
|
5
|
-
confirmation, rewrite the index, and stamp the pool
|
|
6
|
-
|
|
5
|
+
confirmation, rewrite the index, and stamp the pool with the date and entry
|
|
6
|
+
count the /mandrel-plan advisory measures its next nudge against.
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# /memory-consolidate [--dry-run]
|
|
@@ -93,14 +93,26 @@ pointers only, never memory content.
|
|
|
93
93
|
Then write the receipt to `.consolidation-stamp.json` in the pool root:
|
|
94
94
|
|
|
95
95
|
```json
|
|
96
|
-
{ "lastConsolidatedAt": "<ISO-8601 timestamp>" }
|
|
96
|
+
{ "lastConsolidatedAt": "<ISO-8601 timestamp>", "entryCount": 42 }
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
`entryCount` is the surviving non-index `*.md` count **after** the rewrite —
|
|
100
|
+
count the directory, never the plan. It is the baseline the next run measures
|
|
101
|
+
growth against, so a wrong number silently mis-arms the nudge.
|
|
102
|
+
|
|
103
|
+
The `/mandrel-plan` Phase 0 advisory re-arms on exactly two conditions: the
|
|
104
|
+
stamp aging past `planning.memoryPool.staleAfterDays` (30), or
|
|
105
|
+
`planning.memoryPool.growthDelta` (25) entries written since that count. Pool
|
|
106
|
+
size alone never triggers it — a pass that keeps every entry still quiets the
|
|
107
|
+
nudge. A stamp with no `entryCount` leaves growth unmeasured, and only the age
|
|
108
|
+
arm can speak until the next pass writes one.
|
|
109
|
+
|
|
110
|
+
Write it **only** after Gate #2 — the stamp asserts an operator reviewed the
|
|
111
|
+
pass, so writing it early makes it a lie.
|
|
102
112
|
|
|
103
113
|
Close with counts: entries read, corrected, merged, pruned, and the new total.
|
|
114
|
+
Then the forecast the operator would otherwise derive by hand: when the
|
|
115
|
+
advisory next fires, and which arm reaches it first.
|
|
104
116
|
|
|
105
117
|
## Constraints
|
|
106
118
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,26 @@ All notable changes to this project will be documented in this file.
|
|
|
15
15
|
-->
|
|
16
16
|
<!-- markdownlint-disable-file MD004 MD012 MD037 -->
|
|
17
17
|
|
|
18
|
+
## [2.44.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.43.0...mandrel-v2.44.0) (2026-09-07)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
* reap a plan-run cohort label once every Story carrying it is closed ([#5189](https://github.com/dsj1984/mandrel/issues/5189)) ([#5194](https://github.com/dsj1984/mandrel/issues/5194)) ([43ecacb](https://github.com/dsj1984/mandrel/commit/43ecacb7e2fa8469813174623307149cfe59a60a))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
* **git-cleanup:** record an outcome for every remote-only branch, including the no-PR ones ([#5188](https://github.com/dsj1984/mandrel/issues/5188)) ([#5190](https://github.com/dsj1984/mandrel/issues/5190)) ([8016137](https://github.com/dsj1984/mandrel/commit/8016137e0c5e3b06fb608674cbb68a8fbf212ca8))
|
|
29
|
+
* **review:** disk-probe the scoped-lint code runner so an absent biome degrades by name (refs [#5193](https://github.com/dsj1984/mandrel/issues/5193)) ([#5195](https://github.com/dsj1984/mandrel/issues/5195)) ([cc4bb41](https://github.com/dsj1984/mandrel/commit/cc4bb41a5877366f7e4e488d6d522fcfa3c2f8cd))
|
|
30
|
+
|
|
31
|
+
## [2.43.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.42.0...mandrel-v2.43.0) (2026-09-07)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
* memory-pool advisory measures growth since the last consolidation instead of an absolute entry ceiling ([#5182](https://github.com/dsj1984/mandrel/issues/5182)) ([#5183](https://github.com/dsj1984/mandrel/issues/5183)) ([cccdd69](https://github.com/dsj1984/mandrel/commit/cccdd69546e5dfb4c6d1e205423ff52f07982ade))
|
|
37
|
+
|
|
18
38
|
## [2.42.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.41.0...mandrel-v2.42.0) (2026-09-06)
|
|
19
39
|
|
|
20
40
|
|
package/package.json
CHANGED