mandrel 2.23.0 → 2.24.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/schemas/story-deliver-terminal.schema.json +6 -1
- package/.agents/scripts/deliver-light.js +23 -45
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +10 -25
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +20 -32
- package/.agents/scripts/lib/escomplex-ast-compat.js +360 -0
- package/.agents/scripts/lib/maintainability-engine.js +83 -11
- package/.agents/scripts/lib/maintainability-unscorable.js +60 -0
- package/.agents/scripts/lib/maintainability-utils.js +14 -5
- package/.agents/scripts/lib/observability/runtime-friction.js +8 -0
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +283 -0
- package/.agents/scripts/lib/orchestration/light-backstop.js +107 -0
- package/.agents/scripts/lib/orchestration/light-escalation.js +169 -0
- package/.agents/scripts/lib/orchestration/light-suitability.js +151 -46
- package/.agents/scripts/lib/orchestration/plan-context.js +12 -13
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +70 -2
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +23 -5
- package/.agents/scripts/lib/workers/maintainability-worker.js +14 -9
- package/.agents/workflows/helpers/deliver-light.md +21 -4
- package/docs/CHANGELOG.md +9 -0
- package/package.json +1 -1
|
@@ -353,8 +353,8 @@ function closeResult({
|
|
|
353
353
|
note: waitedForMerge
|
|
354
354
|
? 'Close-and-land: PR merge confirmed. Story flipped agent::closing → agent::done, the issue closed (confirmStoryMerged), and the post-land tail ran.'
|
|
355
355
|
: autoMergeEnabled
|
|
356
|
-
? 'PR open against baseBranch with auto-merge enabled. Story rests at agent::closing (issue stays OPEN). GitHub will squash-merge when required checks pass; run single-story-confirm-merge.js after the merge confirms to flip agent::done and close the issue (the Closes #<id> footer also auto-closes it).'
|
|
357
|
-
: 'PR open against baseBranch. Story rests at agent::closing (issue stays OPEN). Operator merges via GitHub UI; run single-story-confirm-merge.js after the merge confirms to flip agent::done (the Closes #<id> footer also auto-closes the issue).',
|
|
356
|
+
? 'PR open against baseBranch with auto-merge enabled. Story rests at agent::closing (issue stays OPEN and assigned to the operator). GitHub will squash-merge when required checks pass; run single-story-confirm-merge.js after the merge confirms to flip agent::done, release the lease, and close the issue (the Closes #<id> footer also auto-closes it).'
|
|
357
|
+
: 'PR open against baseBranch. Story rests at agent::closing (issue stays OPEN and assigned to the operator). Operator merges via GitHub UI; run single-story-confirm-merge.js after the merge confirms to flip agent::done and release the lease (the Closes #<id> footer also auto-closes the issue).',
|
|
358
358
|
};
|
|
359
359
|
}
|
|
360
360
|
|
|
@@ -552,7 +552,18 @@ async function runClosePipeline({
|
|
|
552
552
|
config,
|
|
553
553
|
progress,
|
|
554
554
|
});
|
|
555
|
-
|
|
555
|
+
// Story #4860 — the clean-path lease release USED to sit here, immediately
|
|
556
|
+
// after the arm and the `agent::closing` flip. That dropped the operator's
|
|
557
|
+
// claim the moment the PR opened, so a ticket read unassigned for the whole
|
|
558
|
+
// time its PR was in flight — and forever on the operator-merge path, where
|
|
559
|
+
// nothing downstream ever re-claimed it. The release now belongs to the
|
|
560
|
+
// post-land tail, which runs only on a CONFIRMED merge and which both
|
|
561
|
+
// landing surfaces reach. Every non-merged ending below — the
|
|
562
|
+
// `merge.unlanded` block, an exhausted wait budget, `--no-wait-merge`,
|
|
563
|
+
// `--no-auto-merge` — deliberately RETAINS the claim: the PR is open and the
|
|
564
|
+
// work still has an owner. The only releases that survive here are
|
|
565
|
+
// `releaseLeaseOnBlock`'s two throwing exits above, which fire before the PR
|
|
566
|
+
// is ever armed (Story #4257's hand-off property).
|
|
556
567
|
|
|
557
568
|
// Close-and-land (Story #4428; default since `delivery.routing.closeAndLand`
|
|
558
569
|
// — Story #4539): poll the just-armed PR to merge confirmation, or block
|
|
@@ -625,7 +636,11 @@ async function runClosePipeline({
|
|
|
625
636
|
autoMergeEnabled,
|
|
626
637
|
autoMergeReason,
|
|
627
638
|
worktreeReaped,
|
|
628
|
-
|
|
639
|
+
// Story #4860 — the release is a post-land tail step now, so the tail's
|
|
640
|
+
// own per-step boolean IS the answer. A wait that ended anything other
|
|
641
|
+
// than landed never ran the tail, and correctly reports `false`: the
|
|
642
|
+
// claim is still held, by design.
|
|
643
|
+
leaseReleased: waitOutcome.tail?.leaseRelease === true,
|
|
629
644
|
localCleanupDeferred,
|
|
630
645
|
directMerged,
|
|
631
646
|
waitedForMerge: true,
|
|
@@ -663,7 +678,10 @@ async function runClosePipeline({
|
|
|
663
678
|
autoMergeEnabled,
|
|
664
679
|
autoMergeReason,
|
|
665
680
|
worktreeReaped,
|
|
666
|
-
|
|
681
|
+
// Story #4860 — this is the no-wait ending: the PR is open and a human
|
|
682
|
+
// owns the merge, so the Story stays assigned until the confirm-merge
|
|
683
|
+
// surface lands it and runs the tail.
|
|
684
|
+
leaseReleased: false,
|
|
667
685
|
localCleanupDeferred,
|
|
668
686
|
directMerged,
|
|
669
687
|
});
|
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
* Message contract — see lib/cpu-pool.js:
|
|
8
8
|
* IN : { item: string } — absolute file path to score
|
|
9
9
|
* { exit: true } — drain & terminate
|
|
10
|
-
* OUT : { ok: true, result: { filePath, score
|
|
10
|
+
* OUT : { ok: true, result: { filePath, score, unscorable?, reason? } }
|
|
11
11
|
*
|
|
12
12
|
* `score` is `null` only when the file genuinely cannot be read (ENOENT
|
|
13
|
-
* or other I/O error).
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* or other I/O error).
|
|
14
|
+
*
|
|
15
|
+
* A file the kernel cannot analyse comes back as `unscorable: true` with the
|
|
16
|
+
* kernel's own `reason`, rather than as a bare `0`. The `0` is still carried in
|
|
17
|
+
* `score` for wire compatibility, but it is no longer the only signal — the
|
|
18
|
+
* point of the flag is that the caller can *report* the file instead of
|
|
19
|
+
* silently dropping it (see `maintainability-engine.js`'s `UNSCORABLE`).
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { parentPort } from 'node:worker_threads';
|
|
19
|
-
import {
|
|
23
|
+
import { scoreFile } from '../maintainability-engine.js';
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
26
|
* Pure handler for a single inbound worker message. Exported so unit
|
|
@@ -24,7 +28,7 @@ import { calculateForFile } from '../maintainability-engine.js';
|
|
|
24
28
|
* without spawning a real `Worker` thread.
|
|
25
29
|
*
|
|
26
30
|
* @param {unknown} msg
|
|
27
|
-
* @param {{ score?: (filePath: string) => number |
|
|
31
|
+
* @param {{ score?: (filePath: string) => { score: number, unscorable: boolean, reason: string|null } }} [deps]
|
|
28
32
|
* @returns {{kind: 'exit'} | {kind: 'reply', message: object}}
|
|
29
33
|
*/
|
|
30
34
|
export function handleMaintainabilityWorkerMessage(msg, deps = {}) {
|
|
@@ -40,12 +44,13 @@ export function handleMaintainabilityWorkerMessage(msg, deps = {}) {
|
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
46
|
const filePath = msg.item;
|
|
43
|
-
const scoreFn = deps.score ??
|
|
47
|
+
const scoreFn = deps.score ?? scoreFile;
|
|
44
48
|
try {
|
|
45
|
-
|
|
49
|
+
// `scoreFn` returns `{ score, unscorable, reason }` — spread so the flag
|
|
50
|
+
// and its reason reach the pool caller intact.
|
|
46
51
|
return {
|
|
47
52
|
kind: 'reply',
|
|
48
|
-
message: { ok: true, result: { filePath,
|
|
53
|
+
message: { ok: true, result: { filePath, ...scoreFn(filePath) } },
|
|
49
54
|
};
|
|
50
55
|
} catch (err) {
|
|
51
56
|
// I/O or other unexpected error — surface as a per-item null score
|
|
@@ -48,6 +48,11 @@ Because the predicted footprint is a *declaration* — a guess, and a gameable o
|
|
|
48
48
|
multi-capability enumeration). Size is enforced where ground truth is available:
|
|
49
49
|
the diff backstop in step 4. Do not talk yourself past that one.
|
|
50
50
|
|
|
51
|
+
**The backstop counts by the same principle.** It reads magnitude — changed
|
|
52
|
+
lines over implementation files — not artifacts, and exempts the test and doc
|
|
53
|
+
companions the framework itself mandates. A ceiling that punishes a repo for
|
|
54
|
+
obeying its own test-first rule is a ceiling that over-fires.
|
|
55
|
+
|
|
51
56
|
Sensitivity is the exception and stays absolute: a footprint touching an auth,
|
|
52
57
|
crypto, billing, or migration class routes `full` however small or mechanical —
|
|
53
58
|
and unlike a ceiling, it is **not overridable** (§ Recording a proceed-light
|
|
@@ -136,10 +141,22 @@ answer).
|
|
|
136
141
|
node .agents/scripts/deliver-light.js --backstop --story <storyId>
|
|
137
142
|
```
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
This is the pass that actually bounds size, which is why the prediction gate
|
|
145
|
+
above can afford to be coarse. It measures **magnitude on the change's
|
|
146
|
+
implementation half** — changed lines (additions + deletions) plus a file
|
|
147
|
+
sprawl tripwire — never raw artifact count. Tests, `docs/**`, `**/*.md`,
|
|
148
|
+
`baselines/**`, and lockfiles are exempt from the counts, because the
|
|
149
|
+
framework mandates those companions and obeying it must not inflate the
|
|
150
|
+
number that then rejects the change. They are **not** exempt from
|
|
151
|
+
sensitive-path matching, which runs over the full change set.
|
|
152
|
+
|
|
153
|
+
Exit `3` (`blocked: true`) means the diff exceeds a light ceiling or touches a
|
|
154
|
+
sensitive-path class. STOP, flip `agent::blocked`, and **recycle the receipt**
|
|
155
|
+
through the envelope's `nextCommand` (`/plan <storyId>`) — tickets mode
|
|
156
|
+
rewrites it into properly-planned Stories and closes it as superseded. Do not
|
|
157
|
+
land, and do not leave the receipt open with no successor: it already carries
|
|
158
|
+
the branch, the worktree, and the implementation, all of which are evidence
|
|
159
|
+
the plan should read.
|
|
143
160
|
|
|
144
161
|
5. **Close and land (same engine).** Exactly [`/deliver`](../deliver.md)'s close:
|
|
145
162
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.24.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.23.0...mandrel-v2.24.0) (2026-07-31)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
* **deliver:** hold the Story assignee-lease until the PR is confirmed merged (refs [#4860](https://github.com/dsj1984/mandrel/issues/4860)) ([#4861](https://github.com/dsj1984/mandrel/issues/4861)) ([d722a65](https://github.com/dsj1984/mandrel/commit/d722a6558f2fa1af6aa428d2f8549101c4bedc23))
|
|
11
|
+
* **maintainability:** score the Babel AST the escomplex kernel actually parses ([#4859](https://github.com/dsj1984/mandrel/issues/4859)) ([279b86f](https://github.com/dsj1984/mandrel/commit/279b86fa49c48454b5e8421ca1e0bb01f54a3bba))
|
|
12
|
+
* scope the light diff backstop by change magnitude, and recycle the receipt instead of orphaning it ([#4856](https://github.com/dsj1984/mandrel/issues/4856)) ([#4857](https://github.com/dsj1984/mandrel/issues/4857)) ([114b479](https://github.com/dsj1984/mandrel/commit/114b4797f248bf00064206f32f490413e28276fb))
|
|
13
|
+
|
|
5
14
|
## [2.23.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.22.0...mandrel-v2.23.0) (2026-07-30)
|
|
6
15
|
|
|
7
16
|
|
package/package.json
CHANGED