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.
@@ -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
- const leaseReleased = await releaseLease(leaseArgs);
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
- leaseReleased,
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
- leaseReleased,
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: number | null } }
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). Parse failures inside escomplex still resolve to
14
- * `0` to preserve byte-for-byte parity with the pre-pool serial path
15
- * (calculateForSource swallows parse errors and returns 0).
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 { calculateForFile } from '../maintainability-engine.js';
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 | null }} [deps]
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 ?? calculateForFile;
47
+ const scoreFn = deps.score ?? scoreFile;
44
48
  try {
45
- const score = scoreFn(filePath);
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, score } },
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
- Exit `3` (`blocked: true`) means the landed diff exceeds the light ceilings
140
- (file count or a sensitive-path class). STOP, flip `agent::blocked`, and
141
- escalate to `/plan` do not land. This is the pass that actually bounds
142
- size, which is why the prediction gate above can afford to be coarse.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.23.0",
3
+ "version": "2.24.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/",