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.
- package/.agents/docs/configuration.md +1 -0
- package/.agents/docs/quality-gates.md +48 -0
- package/.agents/schemas/story-deliver-terminal.schema.json +6 -1
- package/.agents/scripts/lib/baselines/kernel.js +19 -0
- package/.agents/scripts/lib/baselines/kinds/bundle-size.js +12 -0
- package/.agents/scripts/lib/baselines/kinds/coverage.js +1 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +21 -5
- package/.agents/scripts/lib/baselines/kinds/duplication.js +1 -0
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +26 -1
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +1 -0
- package/.agents/scripts/lib/baselines/kinds/lint.js +12 -0
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +1 -0
- package/.agents/scripts/lib/baselines/kinds/mutation.js +1 -0
- package/.agents/scripts/lib/baselines/merge-envelopes.js +272 -0
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +175 -0
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +8 -2
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/column-sync.js +26 -2
- package/.agents/scripts/lib/orchestration/epic-container.js +56 -21
- package/.agents/scripts/lib/orchestration/epic-expansion.js +28 -6
- package/.agents/scripts/lib/orchestration/epic-rollup.js +460 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +44 -104
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +60 -1
- package/.agents/scripts/merge-baseline.js +238 -0
- package/.agents/scripts/providers/github/errors.js +66 -10
- package/.agents/scripts/providers/github/sub-issues.js +8 -1
- package/.agents/scripts/single-story-init.js +35 -0
- package/.agents/workflows/helpers/deliver-reference.md +31 -9
- package/.agents/workflows/mandrel-deliver.md +3 -3
- package/docs/CHANGELOG.md +19 -0
- package/lib/cli/registry.js +63 -0
- 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` —
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
package/lib/cli/registry.js
CHANGED
|
@@ -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