mandrel 1.87.0 → 1.89.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 (140) hide show
  1. package/.agents/README.md +18 -13
  2. package/.agents/audit-checklists/architecture.md +24 -0
  3. package/.agents/audit-checklists/clean-code.md +24 -0
  4. package/.agents/audit-checklists/dependencies.md +14 -0
  5. package/.agents/audit-checklists/devops.md +17 -0
  6. package/.agents/audit-checklists/documentation.md +22 -0
  7. package/.agents/audit-checklists/lighthouse.md +15 -0
  8. package/.agents/audit-checklists/navigability.md +14 -0
  9. package/.agents/audit-checklists/performance.md +22 -0
  10. package/.agents/audit-checklists/privacy.md +21 -0
  11. package/.agents/audit-checklists/quality.md +18 -0
  12. package/.agents/audit-checklists/security.md +22 -0
  13. package/.agents/audit-checklists/seo.md +16 -0
  14. package/.agents/audit-checklists/sre.md +24 -0
  15. package/.agents/audit-checklists/ux-ui.md +21 -0
  16. package/.agents/docs/SDLC.md +63 -16
  17. package/.agents/docs/configuration.md +5 -3
  18. package/.agents/instructions.md +51 -21
  19. package/.agents/personas/architect.md +10 -7
  20. package/.agents/personas/engineer.md +4 -3
  21. package/.agents/personas/project-manager.md +5 -2
  22. package/.agents/personas/refactorer.md +5 -3
  23. package/.agents/rules/git-conventions.md +77 -0
  24. package/.agents/schemas/agentrc.schema.json +16 -4
  25. package/.agents/schemas/audit-rules.json +16 -2
  26. package/.agents/schemas/audit-rules.schema.json +7 -6
  27. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +38 -0
  28. package/.agents/schemas/signal-event.schema.json +28 -13
  29. package/.agents/scripts/acceptance-spec-reconciler.js +6 -4
  30. package/.agents/scripts/check-context-budget.js +320 -0
  31. package/.agents/scripts/diagnose-friction.js +4 -4
  32. package/.agents/scripts/epic-audit-prepare.js +30 -2
  33. package/.agents/scripts/epic-audit-recheck.js +46 -13
  34. package/.agents/scripts/epic-deliver-prepare.js +80 -8
  35. package/.agents/scripts/epic-plan-spec.js +4 -8
  36. package/.agents/scripts/generate-lens-checklists.js +180 -0
  37. package/.agents/scripts/lib/audit-suite/checklist-threading.js +300 -0
  38. package/.agents/scripts/lib/audit-suite/findings.js +27 -0
  39. package/.agents/scripts/lib/audit-suite/index.js +9 -0
  40. package/.agents/scripts/lib/audit-suite/lens-checklist.js +212 -0
  41. package/.agents/scripts/lib/audit-suite/selector.js +136 -5
  42. package/.agents/scripts/lib/checks/loop-health.js +340 -0
  43. package/.agents/scripts/lib/cli-args.js +8 -0
  44. package/.agents/scripts/lib/config/explain.js +4 -0
  45. package/.agents/scripts/lib/config/runners.js +21 -2
  46. package/.agents/scripts/lib/config/temp-paths.js +24 -0
  47. package/.agents/scripts/lib/config-settings-schema-delivery.js +23 -3
  48. package/.agents/scripts/lib/config-settings-schema-quality.js +7 -0
  49. package/.agents/scripts/lib/doc-tiers.js +291 -0
  50. package/.agents/scripts/lib/epic-body-sections.js +5 -2
  51. package/.agents/scripts/lib/epic-merge-lock.js +83 -0
  52. package/.agents/scripts/lib/epic-plan-clarity.js +3 -1
  53. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +66 -20
  54. package/.agents/scripts/lib/feedback-loop/graduator-core.js +395 -86
  55. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +299 -72
  56. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +438 -0
  57. package/.agents/scripts/lib/gates/friction.js +15 -5
  58. package/.agents/scripts/lib/observability/perf-aggregator.js +30 -104
  59. package/.agents/scripts/lib/observability/perf-report-readers.js +1 -1
  60. package/.agents/scripts/lib/observability/signal-validator.js +204 -0
  61. package/.agents/scripts/lib/observability/signals-writer.js +157 -54
  62. package/.agents/scripts/lib/observability/tool-trace-hook.js +42 -4
  63. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  64. package/.agents/scripts/lib/orchestration/code-review.js +74 -4
  65. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +213 -0
  66. package/.agents/scripts/lib/orchestration/doc-reader.js +4 -96
  67. package/.agents/scripts/lib/orchestration/docs-digest.js +34 -0
  68. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +56 -19
  69. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +22 -0
  70. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +188 -0
  71. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
  72. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +69 -8
  73. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +11 -2
  74. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
  75. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +39 -3
  76. package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +144 -0
  77. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +258 -14
  78. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +6 -0
  79. package/.agents/scripts/lib/orchestration/merge-block-class.js +218 -0
  80. package/.agents/scripts/lib/orchestration/plan-review-routing.js +1 -1
  81. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -3
  82. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +63 -34
  83. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +167 -52
  84. package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +49 -2
  85. package/.agents/scripts/lib/orchestration/retro-proposals.js +12 -55
  86. package/.agents/scripts/lib/orchestration/retro-runner.js +9 -0
  87. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -0
  88. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +419 -0
  89. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +35 -2
  90. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +353 -69
  91. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +66 -4
  92. package/.agents/scripts/lib/orchestration/spec-section-validator.js +60 -9
  93. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +7 -5
  94. package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +24 -2
  95. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +167 -8
  96. package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +163 -0
  97. package/.agents/scripts/lib/orchestration/ticketing/reads.js +20 -9
  98. package/.agents/scripts/lib/planning-corpus.js +306 -0
  99. package/.agents/scripts/lib/signals/detectors/common.js +10 -10
  100. package/.agents/scripts/lib/signals/detectors/index.js +4 -4
  101. package/.agents/scripts/lib/signals/detectors/retry.js +19 -18
  102. package/.agents/scripts/lib/signals/detectors/rework.js +1 -1
  103. package/.agents/scripts/lib/signals/schema.js +56 -81
  104. package/.agents/scripts/lib/signals/span-tree.js +6 -5
  105. package/.agents/scripts/lib/story-plan.js +3 -0
  106. package/.agents/scripts/lib/wave-runner/tick.js +10 -2
  107. package/.agents/scripts/lifecycle-emit.js +39 -8
  108. package/.agents/scripts/providers/github/issues.js +12 -1
  109. package/.agents/scripts/resolve-doc-tiers.js +83 -0
  110. package/.agents/scripts/retro-run.js +51 -0
  111. package/.agents/scripts/signals-view.js +1 -1
  112. package/.agents/scripts/single-story-close.js +20 -1
  113. package/.agents/scripts/standalone-feedback-rollup.js +188 -0
  114. package/.agents/scripts/story-close.js +48 -0
  115. package/.agents/scripts/story-plan.js +51 -12
  116. package/.agents/scripts/validate-docs-freshness.js +69 -15
  117. package/.agents/skills/core/documentation-and-adrs/SKILL.md +58 -0
  118. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
  119. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +20 -7
  120. package/.agents/skills/core/scope-triage/SKILL.md +61 -0
  121. package/.agents/skills/skills.index.json +3 -3
  122. package/.agents/workflows/audit-documentation.md +82 -2
  123. package/.agents/workflows/helpers/code-review.md +193 -44
  124. package/.agents/workflows/helpers/deliver-epic.md +128 -39
  125. package/.agents/workflows/helpers/deliver-stories.md +26 -0
  126. package/.agents/workflows/helpers/epic-audit.md +116 -283
  127. package/.agents/workflows/helpers/epic-deliver-story.md +14 -0
  128. package/.agents/workflows/helpers/epic-plan-decompose.md +18 -200
  129. package/.agents/workflows/helpers/epic-plan-spec.md +18 -180
  130. package/.agents/workflows/helpers/plan-epic.md +141 -105
  131. package/.agents/workflows/helpers/plan-story.md +32 -0
  132. package/.agents/workflows/helpers/single-story-deliver.md +43 -0
  133. package/.agents/workflows/loops/nightly-audit.md +9 -7
  134. package/docs/CHANGELOG.md +29 -0
  135. package/lib/cli/doctor.js +44 -0
  136. package/package.json +4 -3
  137. package/.agents/scripts/epic-plan-spec-validate.js +0 -111
  138. package/.agents/scripts/lib/feedback-loop/code-review-graduator.js +0 -207
  139. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
  140. package/.agents/scripts/lib/signals/detectors/hotspot.js +0 -292
@@ -0,0 +1,218 @@
1
+ // .agents/scripts/lib/orchestration/merge-block-class.js
2
+ /**
3
+ * merge-block-class.js — Story #4426 (Epic #4425, slice 1: foundation).
4
+ *
5
+ * Shared block-class classifier consumed by BOTH delivery paths — the
6
+ * epic-path must-land terminal step and the standalone
7
+ * `single-story-close` must-land terminal step (the not-yet-landed
8
+ * follow-on Stories under Epic #4425) — so a headless delivery run that
9
+ * finishes its work without a confirmed merge is attributable to exactly
10
+ * one of four classes from the SAME decision logic, instead of each path
11
+ * inventing its own ad hoc diagnosis.
12
+ *
13
+ * Block classes (Epic #4425 Goal):
14
+ * - `checks-pending-timeout` The watch/poll budget was
15
+ * exhausted while required checks
16
+ * were still pending/running — not
17
+ * a hard block, the run simply ran
18
+ * out of time.
19
+ * - `branch-protection-human-required` GitHub reports the PR needs a
20
+ * human action: a required review
21
+ * that hasn't been granted, or a
22
+ * branch-protection rule the
23
+ * automation cannot satisfy on its
24
+ * own.
25
+ * - `arm-failure` The arm call itself (`gh pr merge
26
+ * --auto` or equivalent) failed for
27
+ * a reason that is NOT branch
28
+ * protection — auth, rate limit, an
29
+ * already-merged race, a network
30
+ * error.
31
+ * - `api-race-other` Fallback for anything that does
32
+ * not cleanly fit the above three —
33
+ * a transient GraphQL/API error, an
34
+ * ambiguous probe result, or a
35
+ * genuinely novel condition.
36
+ *
37
+ * Pure function, no I/O: callers pass in the already-observed
38
+ * arm-result / PR-probe / budget signals (from `AutomergeArmer`,
39
+ * `MergeWatcher`, a raw `gh pr view` read, or the standalone
40
+ * `single-story-confirm-merge.js` poll) and get back a
41
+ * `{ blockClass, reason }` verdict ready to hand to `emitMergeUnlanded`
42
+ * (`emit-merge-unlanded.js`).
43
+ */
44
+
45
+ /**
46
+ * The four block classes named in the Epic #4425 Goal. Order is the
47
+ * evaluation priority documented on `classifyMergeBlock` below, NOT an
48
+ * arbitrary listing — earlier entries are checked first when a real input
49
+ * happens to satisfy more than one heuristic.
50
+ */
51
+ export const BLOCK_CLASSES = Object.freeze([
52
+ 'checks-pending-timeout',
53
+ 'branch-protection-human-required',
54
+ 'arm-failure',
55
+ 'api-race-other',
56
+ ]);
57
+
58
+ const BLOCK_CLASS_SET = new Set(BLOCK_CLASSES);
59
+
60
+ /**
61
+ * @param {string} value
62
+ * @returns {boolean} `true` iff `value` is one of the four canonical
63
+ * block classes.
64
+ */
65
+ export function isValidBlockClass(value) {
66
+ return BLOCK_CLASS_SET.has(value);
67
+ }
68
+
69
+ /**
70
+ * Substrings that identify a branch-protection / human-review rejection
71
+ * surfaced through an arm call's stderr or reason text. Matched
72
+ * case-insensitively against the whole string.
73
+ */
74
+ const BRANCH_PROTECTION_MARKERS = Object.freeze([
75
+ 'review',
76
+ 'required_status_checks',
77
+ 'protected branch',
78
+ 'branch protection',
79
+ 'approval',
80
+ ]);
81
+
82
+ function textIncludesAny(text, markers) {
83
+ const lower = String(text ?? '').toLowerCase();
84
+ return markers.some((marker) => lower.includes(marker));
85
+ }
86
+
87
+ /**
88
+ * Build the `api-race-other` fallback reason from whatever signal is
89
+ * available, so the emitted event still carries a specific-as-possible
90
+ * explanation rather than a bare "unknown".
91
+ */
92
+ function describeApiRaceFallback(prProbe, budget) {
93
+ if (prProbe?.error) {
94
+ return `PR probe error: ${prProbe.error}`;
95
+ }
96
+ if (budget && budget.exhausted === true) {
97
+ return `watch budget exhausted with an unrecognised checks status (${prProbe?.checksStatus ?? 'unknown'})`;
98
+ }
99
+ return 'no definitive block signal observed; classified as a transient API race or other condition';
100
+ }
101
+
102
+ /**
103
+ * Classify why a delivery run finished without a confirmed merge.
104
+ *
105
+ * Evaluation order (first match wins):
106
+ * 1. Arm failure — the arm call itself did not succeed. A failed arm
107
+ * means there is no "armed but stuck" PR left to probe, so this is
108
+ * checked before any PR-probe or budget signal. A branch-protection
109
+ * rejection surfaced AT arm time still routes to
110
+ * `branch-protection-human-required` rather than the generic
111
+ * `arm-failure`.
112
+ * 2. Budget exhaustion while checks were still in flight —
113
+ * `checks-pending-timeout`. Evaluated BEFORE the human-required
114
+ * probe signals because on a protected branch GitHub reports
115
+ * `mergeStateStatus: 'BLOCKED'` for the entire time required checks
116
+ * are still running — a slow-CI timeout would otherwise always
117
+ * misclassify as `branch-protection-human-required` and the
118
+ * headless once-only budget extension could never engage.
119
+ * 3. PR-probe human-required signals — `reviewDecision` reporting a
120
+ * required review, or `mergeStateStatus: 'BLOCKED'` with checks NOT
121
+ * in flight (green/failed checks + BLOCKED = a genuinely human
122
+ * gate, e.g. a missing approval).
123
+ * 4. Fallback — `api-race-other`.
124
+ *
125
+ * @param {object} input
126
+ * @param {object} [input.armResult] Outcome of the arm call.
127
+ * @param {boolean} [input.armResult.armed] `false` when the arm call
128
+ * itself failed (a non-zero `gh pr merge` exit, or arming was refused
129
+ * up-front).
130
+ * @param {string} [input.armResult.reason] Free-form failure detail (e.g.
131
+ * `gh` stderr) — inspected for branch-protection markers.
132
+ * @param {string} [input.armResult.error] Alternate free-form failure
133
+ * detail field, checked when `reason` is absent.
134
+ * @param {object} [input.prProbe] Latest `gh pr view` read.
135
+ * @param {string} [input.prProbe.reviewDecision] GitHub review decision
136
+ * (`REVIEW_REQUIRED`, `APPROVED`, …).
137
+ * @param {string} [input.prProbe.mergeStateStatus] GitHub merge-state
138
+ * status (`BLOCKED`, `BEHIND`, `CLEAN`, …).
139
+ * @param {string} [input.prProbe.checksStatus] Aggregate required-check
140
+ * status observed on the last probe (`success` | `pending` |
141
+ * `still-running` | `failure` | `unknown`).
142
+ * @param {string} [input.prProbe.error] Set when the probe call itself
143
+ * errored (network / API failure reading the PR).
144
+ * @param {object} [input.budget] Poll-budget accounting.
145
+ * @param {boolean} [input.budget.exhausted] `true` once the watch loop hit
146
+ * its budget without observing a confirmed merge.
147
+ * @param {number} [input.budget.elapsedSeconds] Elapsed watch time in
148
+ * seconds, folded into the `reason` text.
149
+ * @returns {{ blockClass: string, reason: string }}
150
+ */
151
+ export function classifyMergeBlock(input) {
152
+ const { armResult, prProbe, budget } = input ?? {};
153
+
154
+ // 1. Arm call failure.
155
+ if (armResult && armResult.armed === false) {
156
+ const detail = armResult.reason ?? armResult.error ?? '';
157
+ if (textIncludesAny(detail, BRANCH_PROTECTION_MARKERS)) {
158
+ return {
159
+ blockClass: 'branch-protection-human-required',
160
+ reason:
161
+ detail ||
162
+ 'arm call rejected: branch protection requires a human action',
163
+ };
164
+ }
165
+ return {
166
+ blockClass: 'arm-failure',
167
+ reason: detail || 'arm call failed for an unspecified reason',
168
+ };
169
+ }
170
+
171
+ // Positive in-flight evidence from the latest probe. Only `pending` /
172
+ // `still-running` count — `unknown` (empty rollup: a checks-less repo
173
+ // or a probe race) routes to the api-race re-arm below, and
174
+ // `undefined` (no probe at all) keeps its budget-timeout mapping in
175
+ // step 2 without suppressing the step-3 human-required verdict.
176
+ const checksStatus = prProbe?.checksStatus;
177
+ const checksPendingEvidence =
178
+ checksStatus === 'pending' || checksStatus === 'still-running';
179
+
180
+ // 2. Budget exhausted while checks were still in flight. Ordered
181
+ // before the human-required probe signals: `mergeStateStatus:
182
+ // 'BLOCKED'` is the steady state on a protected branch while required
183
+ // checks run, so a slow-CI timeout must not read as human-required —
184
+ // it must consume the headless once-only budget extension instead.
185
+ if (
186
+ budget &&
187
+ budget.exhausted === true &&
188
+ (checksPendingEvidence || checksStatus === undefined)
189
+ ) {
190
+ return {
191
+ blockClass: 'checks-pending-timeout',
192
+ reason: `watch budget exhausted after ${budget.elapsedSeconds ?? 'an unknown number of'} seconds with required checks still pending`,
193
+ };
194
+ }
195
+
196
+ // 3. PR-probe human-required signals. A BLOCKED merge state counts
197
+ // only without positive checks-in-flight evidence —
198
+ // BLOCKED-with-settled-checks is a genuinely human gate (e.g. a
199
+ // missing required approval), whereas BLOCKED-while-checks-run is the
200
+ // protected-branch steady state.
201
+ if (prProbe) {
202
+ if (
203
+ prProbe.reviewDecision === 'REVIEW_REQUIRED' ||
204
+ (prProbe.mergeStateStatus === 'BLOCKED' && !checksPendingEvidence)
205
+ ) {
206
+ return {
207
+ blockClass: 'branch-protection-human-required',
208
+ reason: `PR requires human action (reviewDecision=${prProbe.reviewDecision ?? 'n/a'}, mergeStateStatus=${prProbe.mergeStateStatus ?? 'n/a'})`,
209
+ };
210
+ }
211
+ }
212
+
213
+ // 4. Fallback.
214
+ return {
215
+ blockClass: 'api-race-other',
216
+ reason: describeApiRaceFallback(prProbe, budget),
217
+ };
218
+ }
@@ -24,7 +24,7 @@ const AUTO_PROCEED_MESSAGE =
24
24
  'Planning risk is low — auto-proceeding to Phase 8 decomposition after spec validation. Context tickets remain open until Epic delivery finalizes.';
25
25
 
26
26
  const REVIEW_REQUIRED_MESSAGE =
27
- 'Planning risk requires operator review — STOP before Phase 8. Review the PRD, Tech Spec, and Acceptance Spec on GitHub and confirm in this session before decomposition.';
27
+ 'Planning risk requires operator review — STOP before Phase 8. Review the Tech Spec sections and the Acceptance Table — both managed sections of the Epic body — on GitHub and confirm in this session before decomposition.';
28
28
 
29
29
  const FORCE_REVIEW_MESSAGE =
30
30
  'Operator override — forcing review stop before Phase 8 despite low planning risk.';
@@ -119,12 +119,12 @@ async function emitReapFailureFriction({
119
119
  storyId: Number(storyId),
120
120
  signal: {
121
121
  kind: 'friction',
122
- timestamp: new Date().toISOString(),
122
+ ts: new Date().toISOString(),
123
123
  epicId: Number(epicId),
124
124
  storyId: Number(storyId),
125
125
  category: 'reap-failure',
126
- source: { tool: 'story-close.js' },
127
- details: `Worktree reap failed: ${reason}`,
126
+ emitter: { tool: 'story-close.js' },
127
+ details: { message: `Worktree reap failed: ${reason}` },
128
128
  epicBranch,
129
129
  worktreePath: wtPath,
130
130
  reason,
@@ -26,6 +26,37 @@ export function normalizeInterventionCount(value) {
26
26
  return Math.trunc(value);
27
27
  }
28
28
 
29
+ /**
30
+ * Pure: does `routedProposals` carry at least one actionable item (a
31
+ * framework- or consumer-routed `gh issue create` proposal)? Story #4417 —
32
+ * the compact retro shape (and therefore the `automerge-verdict`
33
+ * `cleanSprint: true` flag it drives) MUST be suppressed whenever an
34
+ * actionable proposal exists, regardless of `forceFull` or the
35
+ * comment-derived counts: the clean-sprint verdict is an auto-merge input
36
+ * and must never read "clean" while actionable friction sits in the routed
37
+ * proposals. Discarded (single-occurrence) items do NOT count — they are
38
+ * the retro's explicit "nothing to file" bucket.
39
+ *
40
+ * @param {{ framework?: object[], consumer?: object[] } | null | undefined} routedProposals
41
+ * @returns {boolean}
42
+ */
43
+ export function hasActionableProposals(routedProposals) {
44
+ if (
45
+ !routedProposals ||
46
+ typeof routedProposals !== 'object' ||
47
+ Array.isArray(routedProposals)
48
+ ) {
49
+ return false;
50
+ }
51
+ const framework = Array.isArray(routedProposals.framework)
52
+ ? routedProposals.framework
53
+ : [];
54
+ const consumer = Array.isArray(routedProposals.consumer)
55
+ ? routedProposals.consumer
56
+ : [];
57
+ return framework.length > 0 || consumer.length > 0;
58
+ }
59
+
29
60
  /**
30
61
  * Pure: derive the recurring-defect-class signal from the routed-proposal
31
62
  * sections (Story #4135 / Epic #4131, F11).
@@ -125,7 +156,15 @@ export function composeRetroBody(input) {
125
156
  } = input;
126
157
 
127
158
  const interventions = normalizeInterventionCount(counts?.interventions);
128
- const compact = !forceFull && isCleanManifest({ ...counts, interventions });
159
+ // Story #4417 — the compact shape requires BOTH a clean manifest AND zero
160
+ // actionable routed proposals. The routed proposals and the counts are
161
+ // derived from the same unified signals scan (gather-signals.js), so this
162
+ // guard closes the window where the comment-derived counts read "clean"
163
+ // while the ndjson-derived routed proposals hold an actionable item.
164
+ const compact =
165
+ !forceFull &&
166
+ isCleanManifest({ ...counts, interventions }) &&
167
+ !hasActionableProposals(routedProposals);
129
168
  const heading = `## 🪞 Sprint Retrospective — Epic #${epicId}: ${epicTitle}`;
130
169
  const generatedLine = `_Generated ${timestamp}_`;
131
170
  const scorecardRows = [
@@ -214,12 +253,11 @@ export function composeRetroBody(input) {
214
253
  legacyActionItems.length > 0 ? legacyActionItems.join('\n') : '_None._';
215
254
 
216
255
  // Story #2558 — routed-proposals mode. When routedProposals is supplied
217
- // AND any of the four buckets is non-empty, render the four explicit
256
+ // AND any of the three buckets is non-empty, render the three explicit
218
257
  // sections in deterministic order ABOVE the retro-complete marker:
219
258
  // 1. Proposed issues — consumer repo
220
259
  // 2. Proposed issues — framework repo
221
- // 3. Proposed memory updates
222
- // 4. One-off / discarded
260
+ // 3. One-off / discarded
223
261
  // Otherwise the legacy "Action Items for Next Epic" section renders.
224
262
  const routedSectionsBlock = renderRoutedSections(routedProposals);
225
263
 
@@ -419,7 +457,10 @@ function shellEscape(s) {
419
457
  * Render the body lines (everything below a section heading and its trailing
420
458
  * blank) for a "proposed issues" bucket — the consumer and framework sections
421
459
  * share this shape. Empty buckets collapse to a single `_None._`; populated
422
- * buckets emit one fenced `gh issue create` stanza per item.
460
+ * buckets emit, per item, either the **filed issue reference** (when the
461
+ * retro auto-filer already filed it — Story #4418, stamped as
462
+ * `item.filedIssue`) or the paste-ready fenced `gh issue create` stanza (the
463
+ * toggle-OFF / filing-skipped fallback).
423
464
  *
424
465
  * @param {object[]} items
425
466
  * @returns {string[]}
@@ -430,32 +471,21 @@ function renderIssueBucket(items) {
430
471
  for (const item of items) {
431
472
  lines.push(`- **${item.title ?? item.category}**`);
432
473
  lines.push('');
433
- lines.push('```sh');
434
- lines.push(String(item.command ?? ''));
435
- lines.push('```');
474
+ if (item.filedIssue && item.filedIssue.url) {
475
+ const ref = Number.isInteger(item.filedIssue.number)
476
+ ? `#${item.filedIssue.number}`
477
+ : 'issue';
478
+ lines.push(` Filed: [${ref}](${item.filedIssue.url})`);
479
+ } else {
480
+ lines.push('```sh');
481
+ lines.push(String(item.command ?? ''));
482
+ lines.push('```');
483
+ }
436
484
  lines.push('');
437
485
  }
438
486
  return lines;
439
487
  }
440
488
 
441
- /**
442
- * Render the body lines for the "proposed memory updates" bucket — a plain
443
- * instruction prelude followed by one bullet per insight, or `_None._` when
444
- * empty. Deliberately NOT YAML frontmatter (asserted by the routed-sections
445
- * contract test).
446
- *
447
- * @param {object[]} items
448
- * @returns {string[]}
449
- */
450
- function renderMemoryBucket(items) {
451
- if (items.length === 0) return ['_None._'];
452
- return [
453
- 'update your memory with the following insights:',
454
- '',
455
- ...items.map((m) => `- ${m.insight}`),
456
- ];
457
- }
458
-
459
489
  /**
460
490
  * Render the body lines for the "one-off / discarded" bucket — one bullet per
461
491
  * discarded class naming its occurrence count and source, or `_None._`.
@@ -472,12 +502,16 @@ function renderDiscardedBucket(items) {
472
502
  }
473
503
 
474
504
  /**
475
- * Descriptor table for the four routed-proposal sections, in deterministic
476
- * emit order (consumer → framework → memory → discarded). Each descriptor
477
- * pairs a heading, the `routedProposals` field it reads, and a body renderer.
505
+ * Descriptor table for the three routed-proposal sections, in deterministic
506
+ * emit order (consumer → framework → discarded). Each descriptor pairs a
507
+ * heading, the `routedProposals` field it reads, and a body renderer.
478
508
  * {@link renderRoutedSections} walks the table once, so reordering or adding a
479
509
  * section is a data edit here rather than another copy-pasted emit block.
480
510
  *
511
+ * The former "Proposed memory updates" section was deleted in the Epic
512
+ * #4406 signal-contract cutover — no writer ever produced the memory-pane
513
+ * records it rendered.
514
+ *
481
515
  * @type {Array<{ heading: string, field: string, renderBucket: (items: object[]) => string[] }>}
482
516
  */
483
517
  const ROUTED_SECTIONS = [
@@ -491,11 +525,6 @@ const ROUTED_SECTIONS = [
491
525
  field: 'framework',
492
526
  renderBucket: renderIssueBucket,
493
527
  },
494
- {
495
- heading: '### Proposed memory updates',
496
- field: 'memory',
497
- renderBucket: renderMemoryBucket,
498
- },
499
528
  {
500
529
  heading: '### One-off / discarded',
501
530
  field: 'discarded',