mandrel 1.87.0 โ†’ 1.88.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.
@@ -891,11 +891,23 @@ watch / auto-merge / cleanup tail that drives the PR to merge:
891
891
  fixed on a hotfix branch and re-merged into the Epic.
892
892
  2. **Audit (Phase 4).** The change-set audit lenses run against the Epic
893
893
  diff; findings flow through as advisory signal to inform the code
894
- review that follows.
894
+ review that follows. Remediation routing is **threshold-aware**
895
+ (`delivery.epicAudit.autoFixSeverity`, default `medium`): at `medium`
896
+ the host LLM fixes ๐Ÿ”ด/๐ŸŸ /๐ŸŸก findings on-branch (Mediums batched per
897
+ lens) while ๐ŸŸข Suggestions graduate to follow-up issues; `high`
898
+ reproduces the older Critical/High-only routing. Findings fixed
899
+ on-branch are recorded under the `audit-results` comment's
900
+ `## Fixed on-branch` section, which the graduator skips so they never
901
+ spawn duplicate follow-up issues.
895
902
  3. **Code-review (Phase 5).** `lib/orchestration/code-review.js` (extracted
896
903
  from the `code-review.md` helper) audits the diff and posts the
897
- findings as a `code-review` structured comment on the Epic. ๐Ÿ”ด Critical
898
- findings halt the run; ๐ŸŸ /๐ŸŸก/๐ŸŸข findings flow through as non-blocking.
904
+ findings as a `code-review` structured comment on the Epic. Focused-fix
905
+ routing is threshold-aware in the same way
906
+ (`delivery.codeReview.autoFixSeverity`, default `medium` โ€” fixes
907
+ ๐Ÿ”ด/๐ŸŸ /๐ŸŸก on-branch, ๐ŸŸข stays on the comment), and fixed findings land
908
+ under the comment's `## Fixed on-branch` section so the graduator skips
909
+ them. The severity gate is unchanged: surviving ๐Ÿ”ด Critical findings
910
+ halt the run; surviving ๐ŸŸ /๐ŸŸก/๐ŸŸข flow through as non-blocking.
899
911
  4. **Retro (Phase 6).** `lib/orchestration/retro-runner.js` (extracted from the old
900
912
  retro helper) aggregates perf signals, friction counts, hotfix counts,
901
913
  recut counts, parked counts, and HITL count using
@@ -275,12 +275,14 @@ top-level keys are validation errors.
275
275
  | `epicAudit` | No | `object` | โ€” | Nested configuration block. |
276
276
  | `epicAudit.maxFixAttempts` | No | `integer` | โ€” | Maximum auto-fix retry attempts per finding in /deliver Phase 4 (epic-audit). 0 disables auto-fix. Default 3. |
277
277
  | `epicAudit.maxFixScopeFiles` | No | `integer` | โ€” | Maximum file count a single auto-fix may modify before escalating to agent::blocked. Default 5. |
278
+ | `epicAudit.autoFixSeverity` | No | `"high"` \| `"medium"` | `"medium"` | Severity threshold for on-branch remediation in /deliver Phase 4 (epic-audit). `medium` (default) routes ๐Ÿ”ด/๐ŸŸ /๐ŸŸก findings into the host-LLM remediation loop (Mediums batched per lens: one commit per lens, a single validation + overlapping-lens rescan at the end) while ๐ŸŸข suggestions still graduate to follow-up issues; `high` reproduces the pre-4399 Critical/High-only routing. Hard cutover โ€” no back-compat flag. |
278
279
  | `codeReview` | No | `object` | โ€” | Nested configuration block. |
279
280
  | `codeReview.provider` | No | `"native"` \| `"codex"` \| `"security-review"` | `"native"` | Legacy single-adapter selection. ReviewProvider that produces the Finding[] consumed by runCodeReview(). Story #2833 registered `native` (in-process maintainability/lint); Story #2830 added `codex` (invokes `/codex:review` plugin); Story #2871 added `security-review` (shells out to `claude --print /security-review`). When `providers` (chain shape) is set this field is ignored with a warning. Selecting an adapter whose probe fails hard-fails at factory construction unless declared `optional: true` in the chain. |
280
281
  | `codeReview.providers[]` | No | `array<object>` | โ€” | Multi-provider chain (Story #2871). When set and non-empty, takes precedence over the legacy `provider` field. The orchestrator iterates inline entries in declaration order and merges their Finding[] before posting one structured comment; manual-prompt entries (e.g. ultrareview) contribute a trailing 'Manual review suggestions' section. Each item has: name, scopes, optional, manualPrompt, when. |
281
282
  | `codeReview.providerConfig` | No | `object` | โ€” | Optional escape hatch for adapter-specific configuration. No documented keys in Epic #2815; reserved so future adapters can be configured without another schema migration. |
282
283
  | `codeReview.maxFixAttempts` | No | `integer` | โ€” | Maximum auto-fix retry attempts per finding in /deliver Phase 5 (code-review). 0 disables auto-fix. Default 3. |
283
284
  | `codeReview.maxFixScopeFiles` | No | `integer` | โ€” | Maximum file count a single auto-fix may modify before escalating to agent::blocked. Default 5. |
285
+ | `codeReview.autoFixSeverity` | No | `"high"` \| `"medium"` | `"medium"` | Severity threshold for on-branch remediation in /deliver Phase 5 (code-review). `medium` (default) routes ๐Ÿ”ด/๐ŸŸ /๐ŸŸก findings into the host-LLM focused-fix routing (Mediums batched per lens: one commit per lens, a single validation + rescan at the end) while ๐ŸŸข suggestions still graduate to follow-up issues; `high` reproduces the pre-4399 Critical/High-only routing. Hard cutover โ€” no back-compat flag. |
284
286
  | `retro` | No | `object` | โ€” | Story #3042 (Epic #3019). Operator-tunable retro behaviour. Currently exposes `perfThresholds`, the gates the retro perf-signals classifier uses to decide which signals to surface in the `## Performance Signals` / `## Recommended Follow-Ons` retro sections. |
285
287
  | `retro.perfThresholds` | No | `object` | โ€” | Gates for `classifyPerfSignals` (lib/orchestration/retro-perf-heuristics.js). Defaults are 0.6 / 0.4 / 2. |
286
288
  | `retro.perfThresholds.utilisation` | No | `number` | โ€” | Per-wave utilisation threshold. Waves whose `utilisation` is strictly below this value emit a `low-utilisation` signal. Default 0.6. |
@@ -542,6 +542,12 @@
542
542
  "type": "integer",
543
543
  "minimum": 1,
544
544
  "description": "Maximum file count a single auto-fix may modify before escalating to agent::blocked. Default 5."
545
+ },
546
+ "autoFixSeverity": {
547
+ "type": "string",
548
+ "enum": ["high", "medium"],
549
+ "default": "medium",
550
+ "description": "Severity threshold for on-branch remediation in /deliver Phase 4 (epic-audit). `medium` (default) routes ๐Ÿ”ด/๐ŸŸ /๐ŸŸก findings into the host-LLM remediation loop (Mediums batched per lens: one commit per lens, a single validation + overlapping-lens rescan at the end) while ๐ŸŸข suggestions still graduate to follow-up issues; `high` reproduces the pre-4399 Critical/High-only routing. Hard cutover โ€” no back-compat flag."
545
551
  }
546
552
  },
547
553
  "additionalProperties": false
@@ -624,6 +630,12 @@
624
630
  "type": "integer",
625
631
  "minimum": 1,
626
632
  "description": "Maximum file count a single auto-fix may modify before escalating to agent::blocked. Default 5."
633
+ },
634
+ "autoFixSeverity": {
635
+ "type": "string",
636
+ "enum": ["high", "medium"],
637
+ "default": "medium",
638
+ "description": "Severity threshold for on-branch remediation in /deliver Phase 5 (code-review). `medium` (default) routes ๐Ÿ”ด/๐ŸŸ /๐ŸŸก findings into the host-LLM focused-fix routing (Mediums batched per lens: one commit per lens, a single validation + rescan at the end) while ๐ŸŸข suggestions still graduate to follow-up issues; `high` reproduces the pre-4399 Critical/High-only routing. Hard cutover โ€” no back-compat flag."
627
639
  }
628
640
  },
629
641
  "additionalProperties": false
@@ -196,6 +196,10 @@ const KEY_MEANINGS = Object.freeze({
196
196
  'Maximum auto-fix attempts the code-review phase makes.',
197
197
  'delivery.codeReview.maxFixScopeFiles':
198
198
  'Maximum files an auto-fix may touch in one attempt.',
199
+ 'delivery.codeReview.autoFixSeverity':
200
+ 'Severity threshold for on-branch code-review remediation (medium fixes ๐Ÿ”ด/๐ŸŸ /๐ŸŸก, high fixes ๐Ÿ”ด/๐ŸŸ  only; default medium).',
201
+ 'delivery.epicAudit.autoFixSeverity':
202
+ 'Severity threshold for on-branch epic-audit remediation (medium fixes ๐Ÿ”ด/๐ŸŸ /๐ŸŸก, high fixes ๐Ÿ”ด/๐ŸŸ  only; default medium).',
199
203
  'delivery.refactorStage.enabled':
200
204
  'Whether a dedicated refactor stage runs during delivery.',
201
205
  'delivery.acceptanceEval.maxRounds':
@@ -48,15 +48,22 @@ const DEFAULT_DELIVER_RUNNER = Object.freeze({
48
48
  * and Phase 5 (code-review). Operators override via
49
49
  * `delivery.epicAudit.*` and `delivery.codeReview.*` in `.agentrc.json`
50
50
  * (Story #2611, Epic #2586).
51
+ *
52
+ * `autoFixSeverity` (Story #4399) defaults to `'medium'` โ€” the phase
53
+ * remediates ๐Ÿ”ด/๐ŸŸ /๐ŸŸก findings on-branch while ๐ŸŸข suggestions graduate to
54
+ * follow-up issues. `'high'` reproduces the pre-4399 Critical/High-only
55
+ * routing. Hard cutover per `rules/git-conventions.md` โ€” no back-compat flag.
51
56
  */
52
57
  export const DEFAULT_EPIC_AUDIT = Object.freeze({
53
58
  maxFixAttempts: 3,
54
59
  maxFixScopeFiles: 5,
60
+ autoFixSeverity: 'medium',
55
61
  });
56
62
 
57
63
  export const DEFAULT_CODE_REVIEW = Object.freeze({
58
64
  maxFixAttempts: 3,
59
65
  maxFixScopeFiles: 5,
66
+ autoFixSeverity: 'medium',
60
67
  });
61
68
 
62
69
  /**
@@ -65,8 +72,8 @@ export const DEFAULT_CODE_REVIEW = Object.freeze({
65
72
  * @param {object | null | undefined} config
66
73
  * @returns {{
67
74
  * deliverRunner: { concurrencyCap: number, progressReportIntervalSec: number, verifyConcurrencyCap: number },
68
- * epicAudit: { maxFixAttempts: number, maxFixScopeFiles: number },
69
- * codeReview: { maxFixAttempts: number, maxFixScopeFiles: number },
75
+ * epicAudit: { maxFixAttempts: number, maxFixScopeFiles: number, autoFixSeverity: 'high'|'medium' },
76
+ * codeReview: { maxFixAttempts: number, maxFixScopeFiles: number, autoFixSeverity: 'high'|'medium' },
70
77
  * storyMergeRetry: { maxAttempts: number, backoffMs: readonly number[] },
71
78
  * decomposer: { concurrencyCap: number },
72
79
  * }}
@@ -92,12 +99,16 @@ export function getRunners(config) {
92
99
  epicAuditUser.maxFixAttempts ?? DEFAULT_EPIC_AUDIT.maxFixAttempts,
93
100
  maxFixScopeFiles:
94
101
  epicAuditUser.maxFixScopeFiles ?? DEFAULT_EPIC_AUDIT.maxFixScopeFiles,
102
+ autoFixSeverity:
103
+ epicAuditUser.autoFixSeverity ?? DEFAULT_EPIC_AUDIT.autoFixSeverity,
95
104
  },
96
105
  codeReview: {
97
106
  maxFixAttempts:
98
107
  codeReviewUser.maxFixAttempts ?? DEFAULT_CODE_REVIEW.maxFixAttempts,
99
108
  maxFixScopeFiles:
100
109
  codeReviewUser.maxFixScopeFiles ?? DEFAULT_CODE_REVIEW.maxFixScopeFiles,
110
+ autoFixSeverity:
111
+ codeReviewUser.autoFixSeverity ?? DEFAULT_CODE_REVIEW.autoFixSeverity,
101
112
  },
102
113
  storyMergeRetry: DEFAULT_STORY_MERGE_RETRY,
103
114
  decomposer: DEFAULT_DECOMPOSER,
@@ -210,12 +210,19 @@ const MERGE_WATCH_SCHEMA = {
210
210
  * `agent::blocked` (default 5) โ€” a deliberately narrow bound for
211
211
  * unattended auto-fixes, independent of the Story-sizing thresholds in
212
212
  * `ticket-validator-sizing.js`.
213
+ *
214
+ * `autoFixSeverity` (Story #4399) is the threshold that governs which
215
+ * findings the Phase 4 host-LLM remediation loop fixes on-branch: `medium`
216
+ * (the default) routes ๐Ÿ”ด/๐ŸŸ /๐ŸŸก into remediation while ๐ŸŸข still graduates;
217
+ * `high` reproduces the pre-4399 Critical/High-only routing. It is a hard
218
+ * cutover per `rules/git-conventions.md` โ€” there is no back-compat flag.
213
219
  */
214
220
  const EPIC_AUDIT_SCHEMA = {
215
221
  type: 'object',
216
222
  properties: {
217
223
  maxFixAttempts: { type: 'integer', minimum: 0 },
218
224
  maxFixScopeFiles: { type: 'integer', minimum: 1 },
225
+ autoFixSeverity: { type: 'string', enum: ['high', 'medium'] },
219
226
  },
220
227
  additionalProperties: false,
221
228
  };
@@ -116,6 +116,12 @@ export const QUALITY_SCHEMA = {
116
116
  /**
117
117
  * `delivery.codeReview` โ€” sibling to `delivery.epicAudit`. Same bounded
118
118
  * retry + scope cap, applied to /deliver Phase 5 (code-review).
119
+ *
120
+ * `autoFixSeverity` (Story #4399) is the sibling of
121
+ * `delivery.epicAudit.autoFixSeverity`: the threshold that governs which
122
+ * Phase 5 findings the host-LLM focused-fix routing remediates on-branch โ€”
123
+ * `medium` (default) routes ๐Ÿ”ด/๐ŸŸ /๐ŸŸก while ๐ŸŸข still graduates, `high`
124
+ * reproduces the pre-4399 Critical/High-only routing.
119
125
  */
120
126
  export const CODE_REVIEW_SCHEMA = {
121
127
  type: 'object',
@@ -173,6 +179,7 @@ export const CODE_REVIEW_SCHEMA = {
173
179
  providerConfig: { type: 'object', additionalProperties: true },
174
180
  maxFixAttempts: { type: 'integer', minimum: 0 },
175
181
  maxFixScopeFiles: { type: 'integer', minimum: 1 },
182
+ autoFixSeverity: { type: 'string', enum: ['high', 'medium'] },
176
183
  },
177
184
  additionalProperties: false,
178
185
  };
@@ -98,6 +98,13 @@ export function buildIdempotencyMarker(epicId, index) {
98
98
  * emoji and embeds the cited path inside backticks. ๐Ÿ”ด critical findings
99
99
  * are filtered out (they're blocking โ€” the Epic stops on those).
100
100
  *
101
+ * Findings the Phase 4 remediation loop already fixed on-branch are
102
+ * rendered under a **"Fixed on-branch"** heading (Story #4399) with a โœ…
103
+ * prefix so they no longer parse as open findings. As a belt-and-suspenders
104
+ * guard the parser also skips every line inside a Fixed-on-branch section
105
+ * outright, so a remediated ๐ŸŸก Medium never spawns a ghost follow-up issue
106
+ * even if its line retains its original severity emoji.
107
+ *
101
108
  * Pure. Exported so the parser can be unit-tested in isolation.
102
109
  *
103
110
  * @param {string} body
@@ -109,18 +116,25 @@ export function parseFindings(body) {
109
116
  const lines = body.split(/\r?\n/);
110
117
  let idx = 0;
111
118
  let lens = 'unknown';
119
+ let inFixedSection = false;
112
120
  for (const rawLine of lines) {
113
121
  const trimmed = rawLine.trim();
114
122
  if (trimmed.length === 0) continue;
115
123
 
116
- // Detect a lens heading. We accept any heading-prefixed line that
117
- // names a known audit family (`audit-*`).
118
- const lensMatch = trimmed.match(/^#{2,6}\s+(audit-[a-z0-9-]+)/i);
119
- if (lensMatch) {
120
- lens = lensMatch[1];
124
+ // Any markdown heading resets the Fixed-on-branch guard and, when it
125
+ // names a known audit family (`audit-*`), sets the active lens. A
126
+ // "Fixed on-branch" heading opens a section whose entries never
127
+ // graduate (Story #4399).
128
+ const headingMatch = trimmed.match(/^#{2,6}\s+(.+)$/);
129
+ if (headingMatch) {
130
+ inFixedSection = /fixed on-branch/i.test(headingMatch[1]);
131
+ const lensMatch = headingMatch[1].match(/^(audit-[a-z0-9-]+)/i);
132
+ if (lensMatch) lens = lensMatch[1];
121
133
  continue;
122
134
  }
123
135
 
136
+ if (inFixedSection) continue;
137
+
124
138
  let severity = null;
125
139
  if (trimmed.startsWith('๐Ÿ”ด')) {
126
140
  // Critical Blocker โ€” skip; never graduates.
@@ -104,6 +104,13 @@ export function buildIdempotencyMarker(epicId, index) {
104
104
  * filter ๐Ÿ”ด (Critical Blocker โ€” blocking) out; ๐ŸŸ /๐ŸŸก/๐ŸŸข are non-blocking
105
105
  * and graduate to follow-up issues.
106
106
  *
107
+ * Findings the Phase 5 focused-fix routing already fixed on-branch are
108
+ * rendered under a **"Fixed on-branch"** heading (Story #4399) with a โœ…
109
+ * prefix so they no longer parse as open findings. As a belt-and-suspenders
110
+ * guard the parser also skips every line inside a Fixed-on-branch section
111
+ * outright, so a remediated ๐ŸŸก Medium never spawns a ghost follow-up issue
112
+ * even if its line retains its original severity emoji.
113
+ *
107
114
  * @param {string} body
108
115
  * @returns {Array<{ severity: 'high'|'medium'|'low', path: string, summary: string, index: number }>}
109
116
  */
@@ -112,9 +119,19 @@ export function parseFindings(body) {
112
119
  const findings = [];
113
120
  const lines = body.split(/\r?\n/);
114
121
  let idx = 0;
122
+ let inFixedSection = false;
115
123
  for (const line of lines) {
116
124
  const trimmed = line.trim();
117
125
  if (trimmed.length === 0) continue;
126
+ // Any markdown heading resets the Fixed-on-branch guard; a
127
+ // "Fixed on-branch" heading opens a section whose entries never
128
+ // graduate (Story #4399).
129
+ const headingMatch = trimmed.match(/^#{1,6}\s+(.+)$/);
130
+ if (headingMatch) {
131
+ inFixedSection = /fixed on-branch/i.test(headingMatch[1]);
132
+ continue;
133
+ }
134
+ if (inFixedSection) continue;
118
135
  let severity = null;
119
136
  if (trimmed.startsWith('๐ŸŸ ')) severity = 'high';
120
137
  else if (trimmed.startsWith('๐ŸŸก')) severity = 'medium';
@@ -293,12 +293,63 @@ For every finding, provide:
293
293
  fix worked. Keep it tight (โ‰ค 5 sentences); the sub-agent will read the
294
294
  surrounding code itself.
295
295
 
296
+ ### The `## Fixed on-branch` section (Story #4399)
297
+
298
+ Findings that Step 4.5 remediated on `[HEAD_REF]` MUST be rendered under a
299
+ dedicated **`## Fixed on-branch`** heading, **not** in the severity groups
300
+ above. This is the contract seam that keeps remediated findings from
301
+ spawning ghost follow-up issues: the
302
+ [`code-review` graduator](../../scripts/lib/feedback-loop/code-review-graduator.js)
303
+ skips every entry inside this section (both because a fixed entry is
304
+ rendered with a **โœ… prefix** โ€” so it carries no leading severity emoji the
305
+ parser would match โ€” and because the parser has an explicit
306
+ Fixed-on-branch section guard).
307
+
308
+ Render each fixed finding as a `โœ…`-prefixed line naming its original
309
+ severity, the file path in backticks, and the remediating commit SHA, e.g.:
310
+
311
+ ```markdown
312
+ ## Fixed on-branch
313
+
314
+ - โœ… ๐ŸŸก Medium: `src/lib/foo.js` โ€” missing edge-case guard added (a1b2c3d)
315
+ - โœ… ๐ŸŸ  High: `src/api/users.js` โ€” ownership check added (d4e5f6a)
316
+ ```
317
+
318
+ Open (escalated / unfixed) findings stay in their severity group with
319
+ their leading severity emoji so the graduator still files them.
320
+
296
321
  ## Step 4.5 โ€” Focused-fix Routing (host LLM, no automated loop)
297
322
 
298
323
  There is **no runtime auto-fix function** at this phase. The host LLM is
299
- the executor: for each ๐Ÿ”ด / ๐ŸŸ  finding from Step 4, decide between two
300
- paths and keep the `code-review` structured comment authoritative for
301
- anything not fixed in-place.
324
+ the executor: it decides, per finding, between a focused fix on
325
+ `[HEAD_REF]` and leaving the finding on the `code-review` structured
326
+ comment for the operator.
327
+
328
+ ### Resolve the remediation threshold (Story #4399)
329
+
330
+ Read `delivery.codeReview.autoFixSeverity` from the resolved `.agentrc.json`
331
+ (default **`medium`**; the resolver in
332
+ [`config/runners.js`](../../scripts/lib/config/runners.js) supplies the
333
+ default when the key is absent). The threshold governs **which severities
334
+ route into on-branch remediation** โ€” it never changes the halting rule (a
335
+ surviving ๐Ÿ”ด still stops) or the escalation classes:
336
+
337
+ - **`medium`** (default) โ€” route ๐Ÿ”ด Critical, ๐ŸŸ  High, **and ๐ŸŸก Medium**
338
+ findings into remediation. ๐ŸŸข Suggestions stay on the comment (never
339
+ auto-fixed).
340
+ - **`high`** โ€” route only ๐Ÿ”ด Critical and ๐ŸŸ  High findings, reproducing
341
+ the pre-4399 behavior exactly. ๐ŸŸก Medium and ๐ŸŸข Suggestion findings stay
342
+ on the comment.
343
+
344
+ Hard cutover per
345
+ [`rules/git-conventions.md`](../../rules/git-conventions.md) ยง Contract
346
+ Cutovers โ€” no back-compat flag; `high` is opt-in to the old routing.
347
+
348
+ ### ๐Ÿ”ด / ๐ŸŸ  findings โ€” per-finding ceremony (unchanged)
349
+
350
+ For each ๐Ÿ”ด / ๐ŸŸ  finding from Step 4, decide between two paths and keep the
351
+ `code-review` structured comment authoritative for anything not fixed
352
+ in-place.
302
353
 
303
354
  1. **Apply a focused fix on `[HEAD_REF]`.** Permitted only when the
304
355
  finding is unambiguously *fixable* (clean remediation, no scope
@@ -328,11 +379,36 @@ anything not fixed in-place.
328
379
  attempt (the equivalent of the prior loop's
329
380
  `validation-regression` / `thrash-detected` exits).
330
381
 
382
+ ### ๐ŸŸก Medium findings โ€” batched per-lens ceremony (only when `autoFixSeverity: medium`)
383
+
384
+ When the threshold is `medium`, remediate the fixable ๐ŸŸก Medium findings in
385
+ a **batch keyed by owning review lens/pillar** rather than the per-finding
386
+ ceremony above:
387
+
388
+ 1. Group the fixable Mediums by owning lens (the pillar or audit family
389
+ that produced them). A Medium is fixable on the same terms as a ๐ŸŸ ; a
390
+ Medium in any escalation class stays on the comment exactly like a ๐ŸŸ .
391
+ 2. For each lens, call `assert-branch.js --expected [HEAD_REF]`, stage
392
+ explicit paths only, and make **one focused conventional commit per
393
+ lens** (`fix(<scope>): <description> (review findings batch)`).
394
+ 3. Bounded-attempt semantics extend to the batch: each finding gets **at
395
+ most one** attempt, and a lens's batch commit that would exceed
396
+ `delivery.codeReview.maxFixScopeFiles` routes that lens's findings to
397
+ escalation (`scope-exceeded`) instead of committing.
398
+ 4. After **all** lens batches are committed, run a **single** validation
399
+ pass (`npm run lint` plus the relevant `npm test` slice) and a
400
+ **single** targeted rescan over the touched files. Surviving batched
401
+ findings stay on the comment for Step 5.
402
+
403
+ Record every remediated finding (๐ŸŸ  or ๐ŸŸก) in the **"Fixed on-branch"**
404
+ section of the `code-review` comment (Step 4) so it does not graduate to a
405
+ follow-up issue.
406
+
331
407
  Do not invent a programmatic retry budget. The host LLM applies *at most
332
- one* focused-fix attempt per finding before escalating to the operator.
333
- Escalated findings remain on the `code-review` structured comment with
334
- their reason recorded, so Step 5 (and downstream consumers) see exactly
335
- why each one was not auto-remediated.
408
+ one* focused-fix attempt per finding (or per batched finding) before
409
+ escalating to the operator. Escalated findings remain on the `code-review`
410
+ structured comment with their reason recorded, so Step 5 (and downstream
411
+ consumers) see exactly why each one was not auto-remediated.
336
412
 
337
413
  ## Step 4.6 โ€” Cross-phase re-check trigger
338
414
 
@@ -489,6 +489,16 @@ therefore auto-runs its mapped lenses (e.g. a `security`-axis Epic runs
489
489
  low-risk Epic adds nothing. Findings are persisted as an `audit-results`
490
490
  structured comment on the Epic.
491
491
 
492
+ The helper's Step 3 remediation is **threshold-aware** (Story #4399): it
493
+ reads `delivery.epicAudit.autoFixSeverity` (default **`medium`**) and, at
494
+ `medium`, routes ๐Ÿ”ด/๐ŸŸ /**๐ŸŸก** findings into on-branch remediation (Mediums
495
+ batched per lens โ€” one commit per lens, a single validation + overlapping-
496
+ lens rescan at the end) while ๐ŸŸข Suggestions still graduate; `high`
497
+ reproduces the pre-4399 Critical/High-only routing. Remediated findings are
498
+ rendered under the comment's `## Fixed on-branch` section so they never
499
+ graduate to follow-up issues. The severity gate below is **unchanged** โ€”
500
+ it keys off the surviving (unfixed) findings.
501
+
492
502
  The helper walks the selected roster **serially in-context by default**; when
493
503
  the roster carries more than one lens it **may delegate the walk to a single
494
504
  audit-orchestrator sub-agent** that fans the already-selected lenses out as
@@ -500,8 +510,8 @@ per-lens cost gate is preserved, and the seven sequential-only lenses are **not*
500
510
  batch-converted โ€” the fan-out parallelizes across lenses only and never changes
501
511
  how any single lens runs internally.
502
512
 
503
- - **Any ๐Ÿ”ด Critical Blocker** โ€” STOP. Relay to the operator.
504
- - **Only ๐ŸŸ /๐ŸŸก/๐ŸŸข** โ€” log as non-blocking and continue.
513
+ - **Any surviving ๐Ÿ”ด Critical Blocker** โ€” STOP. Relay to the operator.
514
+ - **Only ๐ŸŸ /๐ŸŸก/๐ŸŸข surviving** โ€” log as non-blocking and continue.
505
515
  - **Selector reports `degraded: true`** โ€” STOP. Propagate the
506
516
  `reason`/`detail`, post a friction comment, do not fall back to a
507
517
  full-roster audit.
@@ -533,8 +543,18 @@ emit so a high-risk Epic gets a deeper adversarial pass and a low-risk one a
533
543
  lighter one. Depth is **input-only** โ€” it never changes the findings envelope
534
544
  or the posted comment shape.
535
545
 
536
- - **Any ๐Ÿ”ด Critical Blocker** โ€” STOP. Relay to the operator.
537
- - **Only ๐ŸŸ /๐ŸŸก/๐ŸŸข** โ€” log as non-blocking and continue.
546
+ The helper's Step 4.5 focused-fix routing is **threshold-aware**
547
+ (Story #4399): it reads `delivery.codeReview.autoFixSeverity` (default
548
+ **`medium`**) and, at `medium`, routes ๐Ÿ”ด/๐ŸŸ /**๐ŸŸก** findings into on-branch
549
+ remediation (Mediums batched per lens โ€” one commit per lens, a single
550
+ validation + rescan at the end) while ๐ŸŸข Suggestions stay on the comment;
551
+ `high` reproduces the pre-4399 Critical/High-only routing. Remediated
552
+ findings are rendered under the comment's `## Fixed on-branch` section so
553
+ they never graduate to follow-up issues. The severity gate below is
554
+ **unchanged** โ€” it keys off the surviving (unfixed) findings.
555
+
556
+ - **Any surviving ๐Ÿ”ด Critical Blocker** โ€” STOP. Relay to the operator.
557
+ - **Only ๐ŸŸ /๐ŸŸก/๐ŸŸข surviving** โ€” log as non-blocking and continue.
538
558
 
539
559
  ---
540
560
 
@@ -263,9 +263,32 @@ envelope" bullet. Until then, the host LLM is the gate.
263
263
  ## Step 3 โ€” Remediation Routing (host LLM, no automated loop)
264
264
 
265
265
  There is **no runtime auto-fix function** at this phase. The host LLM is
266
- the executor: it inspects the aggregated ๐Ÿ”ด / ๐ŸŸ  findings from Step 2 and
267
- either applies a focused fix on the Epic branch or escalates the finding
268
- to the operator via the `audit-results` comment in Step 4.
266
+ the executor: it inspects the aggregated findings from Step 2 and either
267
+ applies a focused fix on the Epic branch or escalates the finding to the
268
+ operator via the `audit-results` comment in Step 4.
269
+
270
+ ### Resolve the remediation threshold (Story #4399)
271
+
272
+ Read `delivery.epicAudit.autoFixSeverity` from the resolved `.agentrc.json`
273
+ (default **`medium`**; the resolver in
274
+ [`config/runners.js`](../../scripts/lib/config/runners.js) supplies the
275
+ default when the key is absent). The threshold governs **which severities
276
+ route into on-branch remediation** โ€” it never changes the halting rule
277
+ (a surviving ๐Ÿ”ด still stops Phase 4 in Step 4) or the escalation classes:
278
+
279
+ - **`medium`** (default) โ€” route ๐Ÿ”ด Critical, ๐ŸŸ  High, **and ๐ŸŸก Medium**
280
+ findings into remediation. ๐ŸŸข Suggestions still graduate to follow-up
281
+ issues (never auto-fixed).
282
+ - **`high`** โ€” route only ๐Ÿ”ด Critical and ๐ŸŸ  High findings into
283
+ remediation, reproducing the pre-4399 behavior exactly. ๐ŸŸก Medium and
284
+ ๐ŸŸข Suggestion findings graduate to follow-up issues untouched.
285
+
286
+ This is a hard cutover per
287
+ [`rules/git-conventions.md`](../../rules/git-conventions.md) ยง Contract
288
+ Cutovers โ€” there is no back-compat flag; `high` is opt-in to the old
289
+ routing, `medium` is the shipped default.
290
+
291
+ ### ๐Ÿ”ด / ๐ŸŸ  findings โ€” per-finding ceremony (unchanged)
269
292
 
270
293
  For each ๐Ÿ”ด / ๐ŸŸ  finding, the host LLM MUST decide between two paths:
271
294
 
@@ -297,10 +320,42 @@ For each ๐Ÿ”ด / ๐ŸŸ  finding, the host LLM MUST decide between two paths:
297
320
  attempt (the equivalent of the prior loop's
298
321
  `validation-regression` / `thrash-detected` exits).
299
322
 
323
+ ### ๐ŸŸก Medium findings โ€” batched per-lens ceremony (only when `autoFixSeverity: medium`)
324
+
325
+ When the threshold is `medium`, remediate the fixable ๐ŸŸก Medium findings
326
+ in a **batch keyed by owning lens** rather than the per-finding ceremony
327
+ above โ€” the per-finding rescan is disproportionate for the volume of
328
+ Mediums a wide change set surfaces:
329
+
330
+ 1. Group the fixable Mediums by their owning lens. A Medium is fixable on
331
+ the same terms as a ๐ŸŸ  (clean remediation, no escalation class); a
332
+ Medium that falls into any escalation class (`spec-deviation`,
333
+ `secrets`, `test-deletion`, `scope-exceeded`) routes to Step 4
334
+ untouched exactly like a ๐ŸŸ .
335
+ 2. For each lens, call `assert-branch.js --expected [EPIC_BRANCH]`, stage
336
+ explicit paths only, and make **one focused conventional commit per
337
+ lens** carrying all that lens's Medium fixes
338
+ (`fix(<scope>): <description> (audit findings batch)`).
339
+ 3. The bounded-attempt semantics extend to the batch: each finding in the
340
+ batch gets **at most one** attempt, and a lens's batch commit that
341
+ would exceed `delivery.epicAudit.maxFixScopeFiles` routes that lens's
342
+ findings to escalation (`scope-exceeded`) instead of committing.
343
+ 4. After **all** lens batches are committed, run a **single** validation
344
+ pass (`npm run lint` plus the relevant `npm test` slice) and a
345
+ **single** rescan of the **overlapping lenses only** (re-invoke
346
+ `run-audit-suite.js` for the lenses whose findings were touched).
347
+ Confirm the batched findings are gone. If a batched finding survives
348
+ the rescan or validation regresses, route the surviving finding(s) to
349
+ escalation and record the attempt context in Step 4.
350
+
351
+ Record every remediated finding (๐ŸŸ  or ๐ŸŸก) in the **"Fixed on-branch"**
352
+ section of the `audit-results` comment (Step 4) so it does not graduate to
353
+ a follow-up issue.
354
+
300
355
  Do not invent a programmatic retry budget. The host LLM applies *at most
301
- one* focused-fix attempt per finding before escalating; any further
302
- remediation is the operator's call after reading the `audit-results`
303
- comment.
356
+ one* focused-fix attempt per finding (or per batched finding) before
357
+ escalating; any further remediation is the operator's call after reading
358
+ the `audit-results` comment.
304
359
 
305
360
  Escalated findings flow through to Step 4 unchanged with their
306
361
  escalation reason recorded โ€” the audit pass does not delete them, it
@@ -332,12 +387,40 @@ The body MUST include:
332
387
  - a link to the per-lens artifact files under `<auditOutputDir>` so the
333
388
  operator (and downstream retro) can re-read the full prompt body.
334
389
 
390
+ ### The `## Fixed on-branch` section (Story #4399)
391
+
392
+ Findings that Step 3 remediated on the Epic branch MUST be rendered under a
393
+ dedicated **`## Fixed on-branch`** heading, **not** under their lens's
394
+ open-findings group. This is the contract seam that keeps remediated
395
+ findings from spawning ghost follow-up issues: the
396
+ [`audit-results` graduator](../../scripts/lib/feedback-loop/audit-results-graduator.js)
397
+ skips every entry inside this section (both because a fixed entry is
398
+ rendered with a **โœ… prefix** โ€” so it carries no leading severity emoji the
399
+ parser would match โ€” and because the parser has an explicit
400
+ Fixed-on-branch section guard).
401
+
402
+ Render each fixed finding as a `โœ…`-prefixed line naming its original
403
+ severity, the file path in backticks, and the remediating commit SHA, e.g.:
404
+
405
+ ```markdown
406
+ ## Fixed on-branch
407
+
408
+ - โœ… ๐ŸŸก Medium (audit-clean-code): `.agents/scripts/foo.js` โ€” dead branch removed (a1b2c3d)
409
+ - โœ… ๐ŸŸ  High (audit-security): `src/api/users.js` โ€” ownership check added (d4e5f6a)
410
+ ```
411
+
412
+ Open (escalated / unfixed) findings stay under their lens heading with
413
+ their leading severity emoji so the graduator still files them.
414
+
335
415
  ### Severity gating
336
416
 
337
- - **Any ๐Ÿ”ด Critical Blocker** โ†’ STOP. Relay to the operator and let
338
- `/deliver` Phase 4 record a manual intervention.
339
- - **Only ๐ŸŸ /๐ŸŸก/๐ŸŸข** โ†’ log as non-blocking and return to `/deliver`
340
- Phase 5 (code-review).
417
+ The gate is unchanged by the threshold โ€” it keys off the **surviving**
418
+ (unfixed) findings after Step 3:
419
+
420
+ - **Any surviving ๐Ÿ”ด Critical Blocker** โ†’ STOP. Relay to the operator and
421
+ let `/deliver` Phase 4 record a manual intervention.
422
+ - **Only ๐ŸŸ /๐ŸŸก/๐ŸŸข surviving** โ†’ log as non-blocking and return to
423
+ `/deliver` Phase 5 (code-review).
341
424
 
342
425
  ## Constraints
343
426
 
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.88.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.87.0...mandrel-v1.88.0) (2026-07-08)
6
+
7
+
8
+ ### Added
9
+
10
+ * **delivery:** threshold-aware auto-fix remediates Medium findings on-branch (refs [#4399](https://github.com/dsj1984/mandrel/issues/4399)) ([#4400](https://github.com/dsj1984/mandrel/issues/4400)) ([34964b3](https://github.com/dsj1984/mandrel/commit/34964b3665852bf43081f6d893430ff9a0a1f630))
11
+
5
12
  ## [1.87.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.86.0...mandrel-v1.87.0) (2026-07-08)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "1.87.0",
3
+ "version": "1.88.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, personas, skills, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",