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.
Files changed (25) hide show
  1. package/.agents/docs/agentrc-reference.json +4 -0
  2. package/.agents/docs/configuration.md +4 -1
  3. package/.agents/docs/workflows.md +1 -1
  4. package/.agents/schemas/agentrc.schema.json +20 -1
  5. package/.agents/scripts/lib/ITicketingProvider.js +27 -0
  6. package/.agents/scripts/lib/config-settings-schema.js +29 -1
  7. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  8. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  9. package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches-detect.js +187 -0
  10. package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +37 -90
  11. package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +115 -19
  12. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +10 -2
  13. package/.agents/scripts/lib/orchestration/plan-context.js +4 -0
  14. package/.agents/scripts/lib/orchestration/plan-run-labels/reap.js +327 -0
  15. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +9 -1
  16. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +159 -55
  17. package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +117 -36
  18. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +81 -0
  19. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-outcome.js +12 -7
  20. package/.agents/scripts/providers/github/labels.js +88 -0
  21. package/.agents/scripts/providers/github.js +2 -0
  22. package/.agents/scripts/prune-plan-run-labels.js +218 -0
  23. package/.agents/workflows/memory-consolidate.md +18 -6
  24. package/docs/CHANGELOG.md +20 -0
  25. 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 so the /mandrel-plan advisory
6
- goes quiet.
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
- The `/mandrel-plan` Phase 0 advisory reads this file; until it is written, the nudge
100
- keeps firing. Write it **only** after Gate #2 — the stamp asserts an operator
101
- reviewed the pass, so writing it early makes it a lie.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.42.0",
3
+ "version": "2.44.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",