mandrel 2.46.0 → 2.48.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 (32) hide show
  1. package/.agents/docs/configuration.md +1 -0
  2. package/.agents/docs/quality-gates.md +48 -0
  3. package/.agents/schemas/story-deliver-terminal.schema.json +6 -1
  4. package/.agents/scripts/lib/baselines/kernel.js +19 -0
  5. package/.agents/scripts/lib/baselines/kinds/bundle-size.js +12 -0
  6. package/.agents/scripts/lib/baselines/kinds/coverage.js +1 -0
  7. package/.agents/scripts/lib/baselines/kinds/crap.js +21 -5
  8. package/.agents/scripts/lib/baselines/kinds/duplication.js +1 -0
  9. package/.agents/scripts/lib/baselines/kinds/kind-factory.js +26 -1
  10. package/.agents/scripts/lib/baselines/kinds/lighthouse.js +1 -0
  11. package/.agents/scripts/lib/baselines/kinds/lint.js +12 -0
  12. package/.agents/scripts/lib/baselines/kinds/maintainability.js +1 -0
  13. package/.agents/scripts/lib/baselines/kinds/mutation.js +1 -0
  14. package/.agents/scripts/lib/baselines/merge-envelopes.js +272 -0
  15. package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +175 -0
  16. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +8 -2
  17. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  18. package/.agents/scripts/lib/orchestration/column-sync.js +26 -2
  19. package/.agents/scripts/lib/orchestration/epic-container.js +56 -21
  20. package/.agents/scripts/lib/orchestration/epic-expansion.js +28 -6
  21. package/.agents/scripts/lib/orchestration/epic-rollup.js +460 -0
  22. package/.agents/scripts/lib/orchestration/run-epilogue.js +44 -104
  23. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +60 -1
  24. package/.agents/scripts/merge-baseline.js +238 -0
  25. package/.agents/scripts/providers/github/errors.js +66 -10
  26. package/.agents/scripts/providers/github/sub-issues.js +8 -1
  27. package/.agents/scripts/single-story-init.js +35 -0
  28. package/.agents/workflows/helpers/deliver-reference.md +31 -9
  29. package/.agents/workflows/mandrel-deliver.md +3 -3
  30. package/docs/CHANGELOG.md +19 -0
  31. package/lib/cli/registry.js +63 -0
  32. package/package.json +1 -1
@@ -55,6 +55,7 @@ import { getStoryBranch, gitSpawn, gitSync } from './lib/git-utils.js';
55
55
  import { Logger } from './lib/Logger.js';
56
56
  import { TYPE_LABELS } from './lib/label-constants.js';
57
57
  import { emitTerseResult } from './lib/observability/terse-result.js';
58
+ import { rollUpEpicForStory } from './lib/orchestration/epic-rollup.js';
58
59
  import {
59
60
  executeFastForward,
60
61
  planFastForward,
@@ -223,6 +224,34 @@ async function flipStoryToExecuting(provider, storyId, story) {
223
224
  }
224
225
  }
225
226
 
227
+ /**
228
+ * Roll any container Epic listing this Story up from its children.
229
+ *
230
+ * A container carries no `agent::*` label, so the Status sync that follows
231
+ * the flip above cannot reach it — its column is derived from its children
232
+ * instead. This is the edge where the first child of an Epic starts moving,
233
+ * which is what puts the Epic on the board as In Progress with an owner.
234
+ *
235
+ * Best-effort by construction: the rollup never throws, and a container left
236
+ * at a stale column must never cost the Story its init.
237
+ *
238
+ * @param {object} provider
239
+ * @param {number} storyId
240
+ * @param {object} config
241
+ * @returns {Promise<void>}
242
+ */
243
+ async function rollUpContainerEpic(provider, storyId, config) {
244
+ const outcome = await rollUpEpicForStory({ storyId, provider, config });
245
+ for (const epic of outcome.epics) {
246
+ if (!epic.column) continue;
247
+ progress(
248
+ 'EPIC',
249
+ `🗃️ Epic #${epic.epicId} → ${epic.column}` +
250
+ (epic.assigned ? ' (assigned)' : ''),
251
+ );
252
+ }
253
+ }
254
+
226
255
  /**
227
256
  * Undo this run's claim when provisioning fails after the early
228
257
  * `agent::executing` flip: revert the label to `agent::ready` and release the
@@ -675,6 +704,12 @@ export async function runSingleStoryInit({
675
704
  // install window instead of reading agent::ready and double-dispatching.
676
705
  await flipStoryToExecuting(provider, storyId, story);
677
706
 
707
+ // Story #5205 — the child is now in flight, so any container Epic listing
708
+ // it is too. Fired here rather than after provisioning so the board shows
709
+ // In Progress for the whole install window, and after the flip so the
710
+ // rollup reads the state it derives from.
711
+ await rollUpContainerEpic(provider, storyId, config);
712
+
678
713
  // Any failure from here on leaves a claimed, executing-labelled Story with
679
714
  // no live run behind it — revert the label and release the lease so the
680
715
  // Story is not stranded as phantom-executing.
@@ -292,18 +292,40 @@ This executes, in order:
292
292
  (files issues when auto-file is on; posts `follow-ups`).
293
293
  - `sibling-coherence` — Spec/Acceptance coherence check across sibling bodies
294
294
  (`plan-run-sibling-coherence`).
295
- - `epic-close` — closes a container Epic once **every** child Story is
296
- `agent::done`, as `completed`. This is the only completion cascade v2 has:
297
- it closes the container and nothing else — no child status roll-up, no label
298
- inheritance, no reopening. Because linkage is parent→child only, the parent
299
- is found by scanning open `type::epic` issues, and only an Epic containing
300
- one of *this run's* Stories is considered, so an unrelated container is
301
- never swept. An Epic with an outstanding child is reported `pending` and
302
- left open.
295
+ - `epic-close` — **reports** which container Epics this run closed and which
296
+ are still pending. It derives nothing itself: every step here and the
297
+ per-Story land tail alike delegate to `epic-rollup.js`, so one rule decides
298
+ a container's state.
303
299
 
304
300
  A single-Story run skips the epilogue — follow-ups are captured on merge
305
301
  confirm instead (`captureStoryFollowUps`).
306
302
 
303
+ ## Container-Epic rollup (every N)
304
+
305
+ A container Epic is never delivered, so nothing used to write to it during
306
+ the run it was the subject of. `epic-rollup.js` derives its state from its
307
+ children at both per-Story lifecycle edges — the `agent::executing` flip in
308
+ `single-story-init.js` and the post-land tail (reported as the tail's
309
+ `epicRollup` step) — which is why it holds at **N=1**, where no epilogue runs.
310
+
311
+ - **Status** follows the children's composition (`deriveParentState` mapped
312
+ onto the board's three options): any child executing or blocked → `In
313
+ Progress`, every child `agent::done` or closed → `Done`. It is written
314
+ **directly**, never via a label: the container carries no `agent::*` label
315
+ by construction, which is what keeps it out of the bare `/mandrel-deliver`
316
+ ready list.
317
+ - **Owner** — `github.operatorHandle` is added to the Epic while any child is
318
+ in flight, through the additive assignees endpoint, and is never removed.
319
+ - **Closure** is one-way: every child landed closes the container as
320
+ `completed`; a reopened child moves Status back to `In Progress` and does
321
+ **not** reopen it.
322
+ - The parent lookup scans open `type::epic` issues, because linkage is
323
+ parent→child only, and reads children as the body checklist **union** the
324
+ native sub-issue edges — the same reader `/mandrel-deliver`'s expansion
325
+ uses, so an Epic can never be expandable but unclosable.
326
+ - Every step is best-effort and never throws: a stale container costs
327
+ tidiness, not a landed Story's envelope.
328
+
307
329
  ## Ceremony (profiles + two scopes)
308
330
 
309
331
  Ceremony depth is selected by `delivery.routing.ceremonyProfile`
@@ -323,7 +345,7 @@ sensitive-path classes in `audit-rules.json`
323
345
  | **Per-Story (always)** | Gates, branch discipline, close-and-land | `deliver-story` / `single-story-close` |
324
346
  | **Per-Story (profile + derived level)** | Acceptance critic mode; review depth | `ceremony-routing.js` + `review-depth.js` + `code-review.js` |
325
347
  | **Per-run (N>1)** | Audit roster · follow-up roll-up · sibling coherence | `plan-run-epilogue.js` once at run end |
326
- | **Per-Story land tail** | Follow-up capture · status resync · ref cleanup · base fast-forward | `single-story-close/phases/post-land.js` (in-process, per-step reported) |
348
+ | **Per-Story land tail** | Follow-up capture · status resync · Epic rollup · ref cleanup · base fast-forward | `single-story-close/phases/post-land.js` (in-process, per-step reported) |
327
349
 
328
350
  ## Async merge-confirm mode (`delivery.mergeWatch.mode: "async"`)
329
351
 
@@ -102,9 +102,9 @@ to an attended run.
102
102
 
103
103
  4. **Close each hand-off** (§ Closing what the workers hand back), then, with
104
104
  every Story landed, run the **per-run epilogue (N>1)**:
105
- `node .agents/scripts/plan-run-epilogue.js --stories 101,102`, which also
106
- closes a container Epic whose children all landed. N=1 skips it
107
- ([reference](helpers/deliver-reference.md)).
105
+ `node .agents/scripts/plan-run-epilogue.js --stories 101,102`; N=1 has none
106
+ ([reference](helpers/deliver-reference.md)). Every close rolls its container
107
+ Epic up from its children.
108
108
 
109
109
  5. **Correct what the change invalidated.** If a memory you recalled this
110
110
  session is now wrong — a trap this landed, a budget it moved — fix that entry
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,25 @@ 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.48.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.47.0...mandrel-v2.48.0) (2026-09-08)
19
+
20
+
21
+ ### Added
22
+
23
+ * baselines: merge concurrent refreshes by row identity with a git merge driver ([#5215](https://github.com/dsj1984/mandrel/issues/5215)) ([#5216](https://github.com/dsj1984/mandrel/issues/5216)) ([bcc7057](https://github.com/dsj1984/mandrel/commit/bcc7057d8aad647ec4dae0dc859dd35865fe40a5))
24
+
25
+
26
+ ### Fixed
27
+
28
+ * never close a container Epic on a degraded child read, and stop flattening gh transport failures to permanent ([#5210](https://github.com/dsj1984/mandrel/issues/5210)) ([#5212](https://github.com/dsj1984/mandrel/issues/5212)) ([08520d7](https://github.com/dsj1984/mandrel/commit/08520d7b5ca0962f98042ca4b103cf0c295516cf))
29
+
30
+ ## [2.47.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.46.0...mandrel-v2.47.0) (2026-09-07)
31
+
32
+
33
+ ### Added
34
+
35
+ * roll a container Epic's board status, assignee and closure up from its child Stories ([#5205](https://github.com/dsj1984/mandrel/issues/5205)) ([#5206](https://github.com/dsj1984/mandrel/issues/5206)) ([38aae8b](https://github.com/dsj1984/mandrel/commit/38aae8bb4c273b258b420b463781970df18f58e1))
36
+
18
37
  ## [2.46.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.45.0...mandrel-v2.46.0) (2026-09-07)
19
38
 
20
39
 
@@ -25,6 +25,11 @@ import fs from 'node:fs';
25
25
  import { createRequire } from 'node:module';
26
26
  import path from 'node:path';
27
27
  import { fileURLToPath } from 'node:url';
28
+ import {
29
+ BASELINE_MERGE_DRIVER_CONFIG_KEY,
30
+ BASELINE_MERGE_DRIVER_REMEDY,
31
+ declaresBaselineMergeDriver,
32
+ } from '../../.agents/scripts/lib/bootstrap/baseline-merge-driver.js';
28
33
  import {
29
34
  REQUIRED_NODE_CEILING_MAJOR,
30
35
  REQUIRED_NODE_FLOOR,
@@ -1074,6 +1079,60 @@ function runVersionCurrent({ cachePath, installedVersion, fsImpl = fs } = {}) {
1074
1079
  // Registry
1075
1080
  // ---------------------------------------------------------------------------
1076
1081
 
1082
+ /**
1083
+ * Is this clone's `baselines/*.json` merge driver actually registered?
1084
+ *
1085
+ * The two halves of that registration live in different places on purpose.
1086
+ * `.gitattributes` is tracked, so the "use the driver" half ships with the
1087
+ * repo; the driver COMMAND is per-clone `git config`, because git will not
1088
+ * execute a command chosen by whoever wrote the repository. A fresh clone
1089
+ * therefore has the first half and not the second — and git reports nothing
1090
+ * at all, it just quietly falls back to text-merging baselines, which is the
1091
+ * behaviour the driver exists to replace.
1092
+ *
1093
+ * Silent degradation is why this is a doctor check rather than a one-time
1094
+ * install step. It is scoped to repos that opted in: when `.gitattributes`
1095
+ * does not declare the driver, the check passes as skipped, so a consumer
1096
+ * who never installed the quality surface is not told to fix something they
1097
+ * did not ask for.
1098
+ *
1099
+ * @param {{cwd?: () => string, fsImpl?: typeof fs, runner?: typeof spawn}} [opts]
1100
+ * @returns {{ ok: boolean, detail: string, remedy?: string }}
1101
+ */
1102
+ export function runMergeDriver({ cwd, fsImpl = fs, runner = spawn } = {}) {
1103
+ const projectRoot = (cwd ?? (() => process.cwd()))();
1104
+ const attributesPath = path.join(projectRoot, '.gitattributes');
1105
+
1106
+ let attributes = '';
1107
+ try {
1108
+ attributes = fsImpl.readFileSync(attributesPath, 'utf8');
1109
+ } catch {
1110
+ attributes = '';
1111
+ }
1112
+ if (!declaresBaselineMergeDriver(attributes)) {
1113
+ return {
1114
+ ok: true,
1115
+ detail:
1116
+ 'skipped — .gitattributes does not route baselines/*.json through the mandrel merge driver',
1117
+ };
1118
+ }
1119
+
1120
+ const configured = runner('git', [
1121
+ 'config',
1122
+ '--get',
1123
+ BASELINE_MERGE_DRIVER_CONFIG_KEY,
1124
+ ]);
1125
+ if (configured.status === 0 && configured.stdout.trim() !== '') {
1126
+ return { ok: true, detail: configured.stdout.trim() };
1127
+ }
1128
+
1129
+ return {
1130
+ ok: false,
1131
+ detail: `${BASELINE_MERGE_DRIVER_CONFIG_KEY} is unset — baselines/*.json will fall back to git's text merge, which conflicts on the generatedAt stamp and can splice rows neither branch scored`,
1132
+ remedy: BASELINE_MERGE_DRIVER_REMEDY,
1133
+ };
1134
+ }
1135
+
1077
1136
  /**
1078
1137
  * Ordered array of doctor checks. Each entry follows the
1079
1138
  * `{ name: string, run(opts?): { ok: boolean, detail: string, remedy?: string } }` contract.
@@ -1123,6 +1182,10 @@ export const registry = [
1123
1182
  name: 'agents-drift',
1124
1183
  run: (opts) => runAgentsDrift(opts),
1125
1184
  },
1185
+ {
1186
+ name: 'merge-driver',
1187
+ run: (opts) => runMergeDriver(opts),
1188
+ },
1126
1189
  {
1127
1190
  name: 'pin-current',
1128
1191
  // Fatal, unlike version-current below (Story #4525/#4530): a pin/install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.46.0",
3
+ "version": "2.48.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/",