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.
- package/.agents/README.md +18 -13
- package/.agents/audit-checklists/architecture.md +24 -0
- package/.agents/audit-checklists/clean-code.md +24 -0
- package/.agents/audit-checklists/dependencies.md +14 -0
- package/.agents/audit-checklists/devops.md +17 -0
- package/.agents/audit-checklists/documentation.md +22 -0
- package/.agents/audit-checklists/lighthouse.md +15 -0
- package/.agents/audit-checklists/navigability.md +14 -0
- package/.agents/audit-checklists/performance.md +22 -0
- package/.agents/audit-checklists/privacy.md +21 -0
- package/.agents/audit-checklists/quality.md +18 -0
- package/.agents/audit-checklists/security.md +22 -0
- package/.agents/audit-checklists/seo.md +16 -0
- package/.agents/audit-checklists/sre.md +24 -0
- package/.agents/audit-checklists/ux-ui.md +21 -0
- package/.agents/docs/SDLC.md +63 -16
- package/.agents/docs/configuration.md +5 -3
- package/.agents/instructions.md +51 -21
- package/.agents/personas/architect.md +10 -7
- package/.agents/personas/engineer.md +4 -3
- package/.agents/personas/project-manager.md +5 -2
- package/.agents/personas/refactorer.md +5 -3
- package/.agents/rules/git-conventions.md +77 -0
- package/.agents/schemas/agentrc.schema.json +16 -4
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +38 -0
- package/.agents/schemas/signal-event.schema.json +28 -13
- package/.agents/scripts/acceptance-spec-reconciler.js +6 -4
- package/.agents/scripts/check-context-budget.js +320 -0
- package/.agents/scripts/diagnose-friction.js +4 -4
- package/.agents/scripts/epic-audit-prepare.js +30 -2
- package/.agents/scripts/epic-audit-recheck.js +46 -13
- package/.agents/scripts/epic-deliver-prepare.js +80 -8
- package/.agents/scripts/epic-plan-spec.js +4 -8
- package/.agents/scripts/generate-lens-checklists.js +180 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +300 -0
- package/.agents/scripts/lib/audit-suite/findings.js +27 -0
- package/.agents/scripts/lib/audit-suite/index.js +9 -0
- package/.agents/scripts/lib/audit-suite/lens-checklist.js +212 -0
- package/.agents/scripts/lib/audit-suite/selector.js +136 -5
- package/.agents/scripts/lib/checks/loop-health.js +340 -0
- package/.agents/scripts/lib/cli-args.js +8 -0
- package/.agents/scripts/lib/config/explain.js +4 -0
- package/.agents/scripts/lib/config/runners.js +21 -2
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +23 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +7 -0
- package/.agents/scripts/lib/doc-tiers.js +291 -0
- package/.agents/scripts/lib/epic-body-sections.js +5 -2
- package/.agents/scripts/lib/epic-merge-lock.js +83 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +3 -1
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +66 -20
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +395 -86
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +299 -72
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +438 -0
- package/.agents/scripts/lib/gates/friction.js +15 -5
- package/.agents/scripts/lib/observability/perf-aggregator.js +30 -104
- package/.agents/scripts/lib/observability/perf-report-readers.js +1 -1
- package/.agents/scripts/lib/observability/signal-validator.js +204 -0
- package/.agents/scripts/lib/observability/signals-writer.js +157 -54
- package/.agents/scripts/lib/observability/tool-trace-hook.js +42 -4
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +74 -4
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +213 -0
- package/.agents/scripts/lib/orchestration/doc-reader.js +4 -96
- package/.agents/scripts/lib/orchestration/docs-digest.js +34 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +56 -19
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +22 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +188 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +69 -8
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +11 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +258 -14
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +6 -0
- package/.agents/scripts/lib/orchestration/merge-block-class.js +218 -0
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +1 -1
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -3
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +63 -34
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +167 -52
- package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +49 -2
- package/.agents/scripts/lib/orchestration/retro-proposals.js +12 -55
- package/.agents/scripts/lib/orchestration/retro-runner.js +9 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +419 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +35 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +353 -69
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +66 -4
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +60 -9
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +7 -5
- package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +24 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +167 -8
- package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +163 -0
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +20 -9
- package/.agents/scripts/lib/planning-corpus.js +306 -0
- package/.agents/scripts/lib/signals/detectors/common.js +10 -10
- package/.agents/scripts/lib/signals/detectors/index.js +4 -4
- package/.agents/scripts/lib/signals/detectors/retry.js +19 -18
- package/.agents/scripts/lib/signals/detectors/rework.js +1 -1
- package/.agents/scripts/lib/signals/schema.js +56 -81
- package/.agents/scripts/lib/signals/span-tree.js +6 -5
- package/.agents/scripts/lib/story-plan.js +3 -0
- package/.agents/scripts/lib/wave-runner/tick.js +10 -2
- package/.agents/scripts/lifecycle-emit.js +39 -8
- package/.agents/scripts/providers/github/issues.js +12 -1
- package/.agents/scripts/resolve-doc-tiers.js +83 -0
- package/.agents/scripts/retro-run.js +51 -0
- package/.agents/scripts/signals-view.js +1 -1
- package/.agents/scripts/single-story-close.js +20 -1
- package/.agents/scripts/standalone-feedback-rollup.js +188 -0
- package/.agents/scripts/story-close.js +48 -0
- package/.agents/scripts/story-plan.js +51 -12
- package/.agents/scripts/validate-docs-freshness.js +69 -15
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +58 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +20 -7
- package/.agents/skills/core/scope-triage/SKILL.md +61 -0
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/workflows/audit-documentation.md +82 -2
- package/.agents/workflows/helpers/code-review.md +193 -44
- package/.agents/workflows/helpers/deliver-epic.md +128 -39
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -283
- package/.agents/workflows/helpers/epic-deliver-story.md +14 -0
- package/.agents/workflows/helpers/epic-plan-decompose.md +18 -200
- package/.agents/workflows/helpers/epic-plan-spec.md +18 -180
- package/.agents/workflows/helpers/plan-epic.md +141 -105
- package/.agents/workflows/helpers/plan-story.md +32 -0
- package/.agents/workflows/helpers/single-story-deliver.md +43 -0
- package/.agents/workflows/loops/nightly-audit.md +9 -7
- package/docs/CHANGELOG.md +29 -0
- package/lib/cli/doctor.js +44 -0
- package/package.json +4 -3
- package/.agents/scripts/epic-plan-spec-validate.js +0 -111
- package/.agents/scripts/lib/feedback-loop/code-review-graduator.js +0 -207
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
- 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
|
|
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
|
-
|
|
122
|
+
ts: new Date().toISOString(),
|
|
123
123
|
epicId: Number(epicId),
|
|
124
124
|
storyId: Number(storyId),
|
|
125
125
|
category: 'reap-failure',
|
|
126
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
476
|
-
* emit order (consumer → framework →
|
|
477
|
-
*
|
|
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',
|