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,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loop-health.js — read-only retro-scope check for the feedback-loop
|
|
3
|
+
* substrate (Epic #4406 / Story #4419, the terminal slice).
|
|
4
|
+
*
|
|
5
|
+
* The original feedback-loop drift shipped silently because an empty
|
|
6
|
+
* feedback report is indistinguishable from a healthy one. This standing
|
|
7
|
+
* self-check closes that gap: at retro time it samples the on-disk signal
|
|
8
|
+
* substrate the sibling Stories established and surfaces the three ways the
|
|
9
|
+
* repaired loop can regress without anyone noticing:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Schema-invalid signal lines.** It tails the most recent
|
|
12
|
+
* {@link MAX_SAMPLE_LINES} lines of every `signals.ndjson` stream under
|
|
13
|
+
* the Epic temp tree and validates each against the canonical
|
|
14
|
+
* `signal-event.schema.json` (via `validateSignal`, the same validator
|
|
15
|
+
* the writer uses — no hand-rolled drift).
|
|
16
|
+
* 2. **Persisted write-time rejects.** It reads the per-Epic reject tally
|
|
17
|
+
* (`temp/epic-<id>/signal-rejects.json`, written by Story #4413's
|
|
18
|
+
* signals-writer) so records that were dropped at write time — and thus
|
|
19
|
+
* never appear in the stream — are still counted.
|
|
20
|
+
* 3. **Un-actioned retro proposals.** It reads the retro mirror
|
|
21
|
+
* (`temp/epic-<id>/retro.md`, Story #4418) and flags any actionable
|
|
22
|
+
* "Proposed issues" item that carries neither a filed-issue reference
|
|
23
|
+
* (`Filed: [#N](url)`) nor lives under the explicit "One-off /
|
|
24
|
+
* discarded" record.
|
|
25
|
+
*
|
|
26
|
+
* Contract:
|
|
27
|
+
* - Scope `retro`, `autoCorrect: 'refuse-and-print'` — read-only by
|
|
28
|
+
* construction; the runner refuses `autoFix` under the retro scope.
|
|
29
|
+
* - A clean substrate (valid lines, zero rejects, every proposal filed or
|
|
30
|
+
* discarded) yields **zero findings**, preserving the compact retro
|
|
31
|
+
* shape. Only when a concern is non-zero does `detect` return a single
|
|
32
|
+
* combined finding naming every non-clean dimension.
|
|
33
|
+
* - No new provider plumbing: `detect(state)` reads `state.cwd` from the
|
|
34
|
+
* existing cwd-scoped checks-registry state, anchors it to the main
|
|
35
|
+
* checkout root, and reads the temp tree directly.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
|
|
41
|
+
import { mainCheckoutRoot, tempRootFrom } from '../config/temp-paths.js';
|
|
42
|
+
import { validateSignal } from '../observability/signal-validator.js';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Lines sampled from the tail of each `signals.ndjson` stream. The check is
|
|
46
|
+
* a health probe, not an exhaustive audit — the most recent window is a
|
|
47
|
+
* representative sample that keeps the read bounded on long streams.
|
|
48
|
+
*/
|
|
49
|
+
export const MAX_SAMPLE_LINES = 200;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Locate the most-recently-touched `epic-<id>` temp tree under
|
|
53
|
+
* `<baseDir>/<tempRoot>`. Returns `{ epicId, epicDir }` or `null` when no
|
|
54
|
+
* Epic temp tree exists (a fresh checkout, or a non-Epic context).
|
|
55
|
+
*
|
|
56
|
+
* @param {string} baseDir The main checkout root.
|
|
57
|
+
* @param {{ tempRoot?: string, fsImpl?: { readdirSync: typeof readdirSync, statSync: typeof statSync } }} [opts]
|
|
58
|
+
* @returns {{ epicId: number, epicDir: string } | null}
|
|
59
|
+
*/
|
|
60
|
+
export function resolveEpicTempTree(
|
|
61
|
+
baseDir,
|
|
62
|
+
{ tempRoot = 'temp', fsImpl } = {},
|
|
63
|
+
) {
|
|
64
|
+
const readdir = fsImpl?.readdirSync ?? readdirSync;
|
|
65
|
+
const stat = fsImpl?.statSync ?? statSync;
|
|
66
|
+
const tempDir = path.join(baseDir, tempRoot);
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = readdir(tempDir);
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
let best = null;
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const match = /^epic-(\d+)$/.exec(entry);
|
|
76
|
+
if (!match) continue;
|
|
77
|
+
const full = path.join(tempDir, entry);
|
|
78
|
+
let st;
|
|
79
|
+
try {
|
|
80
|
+
st = stat(full);
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!st.isDirectory()) continue;
|
|
85
|
+
if (!best || st.mtimeMs > best.mtimeMs) {
|
|
86
|
+
best = { epicId: Number(match[1]), epicDir: full, mtimeMs: st.mtimeMs };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return best ? { epicId: best.epicId, epicDir: best.epicDir } : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Enumerate every `signals.ndjson` stream under an Epic temp tree: the
|
|
94
|
+
* Epic-level wave-lifecycle stream plus each per-Story stream.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} epicDir
|
|
97
|
+
* @param {{ fsImpl?: { readdirSync: typeof readdirSync, statSync: typeof statSync } }} [opts]
|
|
98
|
+
* @returns {string[]}
|
|
99
|
+
*/
|
|
100
|
+
export function findSignalStreams(epicDir, { fsImpl } = {}) {
|
|
101
|
+
const readdir = fsImpl?.readdirSync ?? readdirSync;
|
|
102
|
+
const stat = fsImpl?.statSync ?? statSync;
|
|
103
|
+
const streams = [];
|
|
104
|
+
const isFile = (p) => {
|
|
105
|
+
try {
|
|
106
|
+
return stat(p).isFile();
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const epicSignals = path.join(epicDir, 'signals.ndjson');
|
|
112
|
+
if (isFile(epicSignals)) streams.push(epicSignals);
|
|
113
|
+
const storiesDir = path.join(epicDir, 'stories');
|
|
114
|
+
let storyEntries;
|
|
115
|
+
try {
|
|
116
|
+
storyEntries = readdir(storiesDir);
|
|
117
|
+
} catch {
|
|
118
|
+
storyEntries = [];
|
|
119
|
+
}
|
|
120
|
+
for (const entry of storyEntries) {
|
|
121
|
+
if (!/^story-\d+$/.test(entry)) continue;
|
|
122
|
+
const storySignals = path.join(storiesDir, entry, 'signals.ndjson');
|
|
123
|
+
if (isFile(storySignals)) streams.push(storySignals);
|
|
124
|
+
}
|
|
125
|
+
return streams;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Tail `maxLines` of a single stream and count how many sampled lines fail
|
|
130
|
+
* the canonical schema (a JSON parse failure counts as invalid). A missing
|
|
131
|
+
* or unreadable stream contributes zero — absence is not invalidity.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} streamPath
|
|
134
|
+
* @param {{ validate?: typeof validateSignal, maxLines?: number, readImpl?: typeof readFileSync }} [opts]
|
|
135
|
+
* @returns {{ sampled: number, invalid: number }}
|
|
136
|
+
*/
|
|
137
|
+
export function sampleStreamInvalidCount(
|
|
138
|
+
streamPath,
|
|
139
|
+
{
|
|
140
|
+
validate = validateSignal,
|
|
141
|
+
maxLines = MAX_SAMPLE_LINES,
|
|
142
|
+
readImpl = readFileSync,
|
|
143
|
+
} = {},
|
|
144
|
+
) {
|
|
145
|
+
let raw;
|
|
146
|
+
try {
|
|
147
|
+
raw = readImpl(streamPath, 'utf8');
|
|
148
|
+
} catch {
|
|
149
|
+
return { sampled: 0, invalid: 0 };
|
|
150
|
+
}
|
|
151
|
+
const lines = raw
|
|
152
|
+
.split('\n')
|
|
153
|
+
.map((l) => l.trim())
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
const tail = lines.slice(-maxLines);
|
|
156
|
+
let invalid = 0;
|
|
157
|
+
for (const line of tail) {
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(line);
|
|
161
|
+
} catch {
|
|
162
|
+
invalid += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!validate(parsed).valid) invalid += 1;
|
|
166
|
+
}
|
|
167
|
+
return { sampled: tail.length, invalid };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read the per-Epic persisted reject count from `signal-rejects.json`.
|
|
172
|
+
* Returns 0 when the tally is absent or unreadable.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} epicDir
|
|
175
|
+
* @param {{ readImpl?: typeof readFileSync }} [opts]
|
|
176
|
+
* @returns {number}
|
|
177
|
+
*/
|
|
178
|
+
export function readRejectTally(epicDir, { readImpl = readFileSync } = {}) {
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(
|
|
181
|
+
readImpl(path.join(epicDir, 'signal-rejects.json'), 'utf8'),
|
|
182
|
+
);
|
|
183
|
+
return parsed && Number.isFinite(parsed.count) ? parsed.count : 0;
|
|
184
|
+
} catch {
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Scan a retro mirror body for actionable proposals that were neither filed
|
|
191
|
+
* nor discarded. An actionable proposal is a `- **Title**` item under a
|
|
192
|
+
* `### Proposed issues` heading whose body carries a paste-ready
|
|
193
|
+
* `gh issue create` stanza (the un-filed fallback) instead of a
|
|
194
|
+
* `Filed: [#N](url)` reference. Discarded proposals live under the separate
|
|
195
|
+
* "### One-off / discarded" heading and are never scanned here.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} retroText
|
|
198
|
+
* @returns {string[]} Titles of unfiled actionable proposals.
|
|
199
|
+
*/
|
|
200
|
+
export function scanRetroMirror(retroText) {
|
|
201
|
+
if (typeof retroText !== 'string' || retroText.length === 0) return [];
|
|
202
|
+
const ACTION_HEADING = /^###\s+Proposed issues\b/;
|
|
203
|
+
const ANY_HEADING = /^#{1,6}\s+/;
|
|
204
|
+
const ITEM = /^-\s+\*\*(.+?)\*\*\s*$/;
|
|
205
|
+
const unfiled = [];
|
|
206
|
+
let inSection = false;
|
|
207
|
+
let current = null;
|
|
208
|
+
const flush = () => {
|
|
209
|
+
if (current && current.actionable && !current.filed) {
|
|
210
|
+
unfiled.push(current.title);
|
|
211
|
+
}
|
|
212
|
+
current = null;
|
|
213
|
+
};
|
|
214
|
+
for (const line of retroText.split('\n')) {
|
|
215
|
+
if (ANY_HEADING.test(line)) {
|
|
216
|
+
flush();
|
|
217
|
+
inSection = ACTION_HEADING.test(line);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (!inSection) continue;
|
|
221
|
+
const itemMatch = ITEM.exec(line);
|
|
222
|
+
if (itemMatch) {
|
|
223
|
+
flush();
|
|
224
|
+
current = { title: itemMatch[1].trim(), filed: false, actionable: false };
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (!current) continue;
|
|
228
|
+
if (/^\s*Filed:/.test(line)) current.filed = true;
|
|
229
|
+
if (/gh issue create/.test(line)) current.actionable = true;
|
|
230
|
+
}
|
|
231
|
+
flush();
|
|
232
|
+
return unfiled;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Core detection: locate the Epic temp tree under `baseDir`, sample its
|
|
237
|
+
* signal streams, read its reject tally, and scan its retro mirror. Returns
|
|
238
|
+
* a single combined finding when any dimension is non-clean, else `null`.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} baseDir
|
|
241
|
+
* @param {{
|
|
242
|
+
* tempRoot?: string,
|
|
243
|
+
* validate?: typeof validateSignal,
|
|
244
|
+
* maxLines?: number,
|
|
245
|
+
* scope?: string,
|
|
246
|
+
* fsImpl?: object,
|
|
247
|
+
* readImpl?: typeof readFileSync,
|
|
248
|
+
* }} [opts]
|
|
249
|
+
* @returns {import('./index.js').Finding | null}
|
|
250
|
+
*/
|
|
251
|
+
export function detectLoopHealth(
|
|
252
|
+
baseDir,
|
|
253
|
+
{
|
|
254
|
+
tempRoot = 'temp',
|
|
255
|
+
validate = validateSignal,
|
|
256
|
+
maxLines = MAX_SAMPLE_LINES,
|
|
257
|
+
scope = 'retro',
|
|
258
|
+
fsImpl,
|
|
259
|
+
readImpl = readFileSync,
|
|
260
|
+
} = {},
|
|
261
|
+
) {
|
|
262
|
+
const tree = resolveEpicTempTree(baseDir, { tempRoot, fsImpl });
|
|
263
|
+
if (!tree) return null;
|
|
264
|
+
const { epicId, epicDir } = tree;
|
|
265
|
+
|
|
266
|
+
const streams = findSignalStreams(epicDir, { fsImpl });
|
|
267
|
+
let invalidCount = 0;
|
|
268
|
+
let sampled = 0;
|
|
269
|
+
for (const stream of streams) {
|
|
270
|
+
const r = sampleStreamInvalidCount(stream, {
|
|
271
|
+
validate,
|
|
272
|
+
maxLines,
|
|
273
|
+
readImpl,
|
|
274
|
+
});
|
|
275
|
+
invalidCount += r.invalid;
|
|
276
|
+
sampled += r.sampled;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const rejectCount = readRejectTally(epicDir, { readImpl });
|
|
280
|
+
|
|
281
|
+
let retroText = '';
|
|
282
|
+
try {
|
|
283
|
+
retroText = readImpl(path.join(epicDir, 'retro.md'), 'utf8');
|
|
284
|
+
} catch {
|
|
285
|
+
retroText = '';
|
|
286
|
+
}
|
|
287
|
+
const unfiledProposals = scanRetroMirror(retroText);
|
|
288
|
+
|
|
289
|
+
const signalConcern = invalidCount > 0 || rejectCount > 0;
|
|
290
|
+
const proposalConcern = unfiledProposals.length > 0;
|
|
291
|
+
if (!signalConcern && !proposalConcern) return null;
|
|
292
|
+
|
|
293
|
+
const summaryParts = [];
|
|
294
|
+
const detailLines = [];
|
|
295
|
+
if (signalConcern) {
|
|
296
|
+
summaryParts.push(
|
|
297
|
+
`${invalidCount} schema-invalid signal sample(s), ${rejectCount} persisted reject(s)`,
|
|
298
|
+
);
|
|
299
|
+
detailLines.push(
|
|
300
|
+
`Sampled ${sampled} line(s) across ${streams.length} signals.ndjson stream(s) (last ${maxLines} per stream):`,
|
|
301
|
+
` schema-invalid samples: ${invalidCount}`,
|
|
302
|
+
` persisted reject tally (signal-rejects.json): ${rejectCount}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (proposalConcern) {
|
|
306
|
+
summaryParts.push(
|
|
307
|
+
`${unfiledProposals.length} unfiled actionable proposal(s)`,
|
|
308
|
+
);
|
|
309
|
+
detailLines.push(
|
|
310
|
+
'Retro proposals with neither a filed-issue reference nor a discard record:',
|
|
311
|
+
...unfiledProposals.map((title) => ` - ${title}`),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
id: 'loop-health',
|
|
317
|
+
severity: 'warning',
|
|
318
|
+
scope,
|
|
319
|
+
summary: `Loop-health (epic-${epicId}): ${summaryParts.join('; ')}.`,
|
|
320
|
+
detail: detailLines.join('\n'),
|
|
321
|
+
fixCommand:
|
|
322
|
+
'Inspect temp/epic-<id>/{signals.ndjson,signal-rejects.json,retro.md}; fix the signal producer or file/discard the surfaced proposals.',
|
|
323
|
+
autoCorrectable: false,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default {
|
|
328
|
+
id: 'loop-health',
|
|
329
|
+
severity: 'warning',
|
|
330
|
+
scope: ['retro'],
|
|
331
|
+
autoCorrect: 'refuse-and-print',
|
|
332
|
+
detect(state) {
|
|
333
|
+
const cwd = state?.cwd ?? process.cwd();
|
|
334
|
+
const baseDir = mainCheckoutRoot(cwd) ?? cwd;
|
|
335
|
+
return detectLoopHealth(baseDir, {
|
|
336
|
+
tempRoot: tempRootFrom(state?.config),
|
|
337
|
+
scope: state?.scope ?? 'retro',
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
};
|
|
@@ -58,6 +58,8 @@ export function parseSprintArgs(args = process.argv) {
|
|
|
58
58
|
'skip-sync': { type: 'boolean', default: false },
|
|
59
59
|
'no-auto-merge': { type: 'boolean', default: false },
|
|
60
60
|
'no-full-scope-crap': { type: 'boolean', default: false },
|
|
61
|
+
'wait-merge': { type: 'boolean', default: false },
|
|
62
|
+
'no-wait-merge': { type: 'boolean', default: false },
|
|
61
63
|
executor: { type: 'string' },
|
|
62
64
|
cwd: { type: 'string' },
|
|
63
65
|
'recut-of': { type: 'string' },
|
|
@@ -79,6 +81,12 @@ export function parseSprintArgs(args = process.argv) {
|
|
|
79
81
|
skipSync: coerceBooleanFlag(values['skip-sync']),
|
|
80
82
|
noAutoMerge: coerceBooleanFlag(values['no-auto-merge']),
|
|
81
83
|
noFullScopeCrap: coerceBooleanFlag(values['no-full-scope-crap']),
|
|
84
|
+
// Story #4428 — headless must-land signal for `single-story-close.js`:
|
|
85
|
+
// `--wait-merge` opts a headless run into polling to merge confirmation
|
|
86
|
+
// instead of resting at `agent::closing`; `--no-wait-merge` is the
|
|
87
|
+
// explicit opt-out (see `single-story-close/phases/options.js`).
|
|
88
|
+
waitForMerge: coerceBooleanFlag(values['wait-merge']),
|
|
89
|
+
noWaitForMerge: coerceBooleanFlag(values['no-wait-merge']),
|
|
82
90
|
executor: values.executor ?? null,
|
|
83
91
|
// Resolve worktree cwd from flag or env. Empty string/whitespace → null.
|
|
84
92
|
cwd:
|
|
@@ -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,30 @@ 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` is **tier-aware** (Story #4412, Epic #4405). The
|
|
53
|
+
* Epic-close audit tier (`DEFAULT_EPIC_AUDIT`) defaults to `'high'` —
|
|
54
|
+
* remediating only 🔴 Critical + 🟠 High findings on-branch while 🟡 Medium
|
|
55
|
+
* and 🟢 Suggestion findings graduate to follow-up issues. This is the
|
|
56
|
+
* three-tier model's slim outermost tier: 🟡 Medium code-quality concerns are
|
|
57
|
+
* already routed into on-branch remediation shift-left (the write-time
|
|
58
|
+
* checklist threading of Story #4410 and the Story-scope local-lens pass of
|
|
59
|
+
* Story #4409), so the Epic-close tier stops paying to re-remediate them where
|
|
60
|
+
* a fix is most expensive. The code-review tier (`DEFAULT_CODE_REVIEW`) keeps
|
|
61
|
+
* the `'medium'` default introduced by Story #4399. Both are hard cutovers per
|
|
62
|
+
* `rules/git-conventions.md` — no back-compat flag; an operator opts back into
|
|
63
|
+
* the wider routing by setting `delivery.epicAudit.autoFixSeverity: 'medium'`.
|
|
51
64
|
*/
|
|
52
65
|
export const DEFAULT_EPIC_AUDIT = Object.freeze({
|
|
53
66
|
maxFixAttempts: 3,
|
|
54
67
|
maxFixScopeFiles: 5,
|
|
68
|
+
autoFixSeverity: 'high',
|
|
55
69
|
});
|
|
56
70
|
|
|
57
71
|
export const DEFAULT_CODE_REVIEW = Object.freeze({
|
|
58
72
|
maxFixAttempts: 3,
|
|
59
73
|
maxFixScopeFiles: 5,
|
|
74
|
+
autoFixSeverity: 'medium',
|
|
60
75
|
});
|
|
61
76
|
|
|
62
77
|
/**
|
|
@@ -65,8 +80,8 @@ export const DEFAULT_CODE_REVIEW = Object.freeze({
|
|
|
65
80
|
* @param {object | null | undefined} config
|
|
66
81
|
* @returns {{
|
|
67
82
|
* deliverRunner: { concurrencyCap: number, progressReportIntervalSec: number, verifyConcurrencyCap: number },
|
|
68
|
-
* epicAudit: { maxFixAttempts: number, maxFixScopeFiles: number },
|
|
69
|
-
* codeReview: { maxFixAttempts: number, maxFixScopeFiles: number },
|
|
83
|
+
* epicAudit: { maxFixAttempts: number, maxFixScopeFiles: number, autoFixSeverity: 'high'|'medium' },
|
|
84
|
+
* codeReview: { maxFixAttempts: number, maxFixScopeFiles: number, autoFixSeverity: 'high'|'medium' },
|
|
70
85
|
* storyMergeRetry: { maxAttempts: number, backoffMs: readonly number[] },
|
|
71
86
|
* decomposer: { concurrencyCap: number },
|
|
72
87
|
* }}
|
|
@@ -92,12 +107,16 @@ export function getRunners(config) {
|
|
|
92
107
|
epicAuditUser.maxFixAttempts ?? DEFAULT_EPIC_AUDIT.maxFixAttempts,
|
|
93
108
|
maxFixScopeFiles:
|
|
94
109
|
epicAuditUser.maxFixScopeFiles ?? DEFAULT_EPIC_AUDIT.maxFixScopeFiles,
|
|
110
|
+
autoFixSeverity:
|
|
111
|
+
epicAuditUser.autoFixSeverity ?? DEFAULT_EPIC_AUDIT.autoFixSeverity,
|
|
95
112
|
},
|
|
96
113
|
codeReview: {
|
|
97
114
|
maxFixAttempts:
|
|
98
115
|
codeReviewUser.maxFixAttempts ?? DEFAULT_CODE_REVIEW.maxFixAttempts,
|
|
99
116
|
maxFixScopeFiles:
|
|
100
117
|
codeReviewUser.maxFixScopeFiles ?? DEFAULT_CODE_REVIEW.maxFixScopeFiles,
|
|
118
|
+
autoFixSeverity:
|
|
119
|
+
codeReviewUser.autoFixSeverity ?? DEFAULT_CODE_REVIEW.autoFixSeverity,
|
|
101
120
|
},
|
|
102
121
|
storyMergeRetry: DEFAULT_STORY_MERGE_RETRY,
|
|
103
122
|
decomposer: DEFAULT_DECOMPOSER,
|
|
@@ -258,6 +258,30 @@ export function signalsFile(eid, sid, config) {
|
|
|
258
258
|
return path.join(storyTempDir(eid, sid, config), 'signals.ndjson');
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* `temp/epic-<eid>/stories/story-<sid>/lifecycle.ndjson` — the story-scope
|
|
263
|
+
* ledger destination for lifecycle events emitted directly by a Story
|
|
264
|
+
* (rather than routed through the Epic-scoped bus ledger). Story #4426
|
|
265
|
+
* (Epic #4425) introduces the first consumer: a standalone
|
|
266
|
+
* `single-story-close` run (no parent Epic) emitting `merge.unlanded`
|
|
267
|
+
* needs an on-disk home even though there is no `epic-<id>/` directory to
|
|
268
|
+
* anchor the event to.
|
|
269
|
+
*
|
|
270
|
+
* Mirrors `epicLedgerPath` exactly, one level down: `eid === null` routes
|
|
271
|
+
* through `storyTempDir`'s standalone branch to
|
|
272
|
+
* `<tempRoot>/standalone/stories/story-<sid>/lifecycle.ndjson`; a real
|
|
273
|
+
* `eid` routes to `<tempRoot>/epic-<eid>/stories/story-<sid>/lifecycle.ndjson`,
|
|
274
|
+
* so an Epic-attached Story's story-scope ledger sits alongside its
|
|
275
|
+
* `signals.ndjson` sibling.
|
|
276
|
+
*
|
|
277
|
+
* @param {number|null} eid
|
|
278
|
+
* @param {number} sid
|
|
279
|
+
* @param {object} [config]
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
export const storyLedgerPath = (eid, sid, config) =>
|
|
283
|
+
storyArtifactPath(eid, sid, 'lifecycle.ndjson', config);
|
|
284
|
+
|
|
261
285
|
/**
|
|
262
286
|
* Escape hatch for an Epic-level artifact whose name isn't part of the
|
|
263
287
|
* canonical layout (one of the per-Epic perf surfaces, retro mirror, etc.).
|
|
@@ -210,12 +210,25 @@ 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 findings
|
|
215
|
+
* the Epic-close host-LLM remediation loop fixes on-branch. It is **tier-aware**
|
|
216
|
+
* (Story #4412, Epic #4405): the Epic-close audit tier defaults to `high`
|
|
217
|
+
* (route only 🔴 Critical + 🟠 High into remediation; 🟡 Medium + 🟢 Suggestion
|
|
218
|
+
* graduate to follow-up issues) because 🟡 Medium code-quality concerns are
|
|
219
|
+
* already remediated shift-left at the write-time and Story-scope tiers — the
|
|
220
|
+
* slim outermost tier stops re-paying to fix them where a fix is most
|
|
221
|
+
* expensive. Setting `medium` opts back into routing 🔴/🟠/🟡 on-branch. The
|
|
222
|
+
* enum is the single validation seam: a configured value round-trips through
|
|
223
|
+
* the resolver, and any value outside `['high', 'medium']` is rejected at load.
|
|
224
|
+
* Hard cutover per `rules/git-conventions.md` — there is no back-compat flag.
|
|
213
225
|
*/
|
|
214
226
|
const EPIC_AUDIT_SCHEMA = {
|
|
215
227
|
type: 'object',
|
|
216
228
|
properties: {
|
|
217
229
|
maxFixAttempts: { type: 'integer', minimum: 0 },
|
|
218
230
|
maxFixScopeFiles: { type: 'integer', minimum: 1 },
|
|
231
|
+
autoFixSeverity: { type: 'string', enum: ['high', 'medium'] },
|
|
219
232
|
},
|
|
220
233
|
additionalProperties: false,
|
|
221
234
|
};
|
|
@@ -324,15 +337,22 @@ const ACCEPTANCE_EVAL_SCHEMA = {
|
|
|
324
337
|
/**
|
|
325
338
|
* `delivery.feedbackLoop` — opt-out toggles consumed by the Epic finalize
|
|
326
339
|
* listener's auto-file graduators (`lib/feedback-loop/*-graduator.js`, read
|
|
327
|
-
* via `graduator-core.js#makeIsAutoFileEnabled`).
|
|
328
|
-
* (auto-file on); set
|
|
340
|
+
* via `graduator-core.js#makeIsAutoFileEnabled`). All default to `true`
|
|
341
|
+
* (auto-file on); set any to `false` to suppress auto-filing the
|
|
329
342
|
* corresponding non-blocking findings as follow-up issues.
|
|
343
|
+
*
|
|
344
|
+
* `retroProposals` (Story #4418) governs the retro auto-filer: when true
|
|
345
|
+
* (default) the retro's actionable routed proposals are filed as
|
|
346
|
+
* `meta::<framework-gap|consumer-improvement>` + `friction::<category>`
|
|
347
|
+
* issues via the graduator pre-parsed-findings seam, and the rendered retro
|
|
348
|
+
* sections list the filed issue numbers instead of paste-ready `gh` command
|
|
349
|
+
* stanzas; set it to `false` to fall back to the command stanzas.
|
|
330
350
|
*/
|
|
331
351
|
const FEEDBACK_LOOP_SCHEMA = {
|
|
332
352
|
type: 'object',
|
|
333
353
|
properties: {
|
|
334
|
-
codeReviewAutoFile: { type: 'boolean' },
|
|
335
354
|
auditResultsAutoFile: { type: 'boolean' },
|
|
355
|
+
retroProposals: { type: 'boolean' },
|
|
336
356
|
},
|
|
337
357
|
additionalProperties: false,
|
|
338
358
|
};
|
|
@@ -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
|
};
|