mandrel 1.88.0 → 1.90.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 +62 -27
- package/.agents/docs/configuration.md +5 -4
- 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 +10 -6
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/epic.blocked.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +39 -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/coverage-capture.js +17 -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/close-validation/gates.js +64 -24
- package/.agents/scripts/lib/config/ci.js +12 -1
- package/.agents/scripts/lib/config/runners.js +13 -5
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -8
- 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 +47 -15
- 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/npm-scripts.js +55 -0
- 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 +193 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +248 -13
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +109 -12
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +46 -4
- 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 +246 -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/close-validation.js +5 -1
- 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/pre-merge-validation.js +8 -1
- 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 +116 -43
- package/.agents/workflows/helpers/deliver-epic.md +123 -54
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -366
- 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 -224
- 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:
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* runner (`INDEPENDENT_GATE_NAMES` / `partitionGates`).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { hasNpmScript, readPackageScripts } from '../npm-scripts.js';
|
|
9
10
|
import {
|
|
10
11
|
buildFormatHint,
|
|
11
12
|
FORMAT_CHECK_FALLBACK,
|
|
@@ -87,17 +88,30 @@ function isCrapGateEnabled(config) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
91
|
+
* The gates run in the Story worktree, whose `package.json` is the committed
|
|
92
|
+
* one the consumer ships — the presence of a `test:coverage` script is a
|
|
93
|
+
* committed fact, so probing at the gate cwd is authoritative. See
|
|
94
|
+
* `lib/npm-scripts.js` for the shared reader.
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Conditionally produce the standalone `test` gate entry.
|
|
95
99
|
*
|
|
96
|
-
*
|
|
100
|
+
* The plain `test` gate is the canonical test runner UNLESS the
|
|
101
|
+
* coverage-capture gate is taking that role — which happens only when the
|
|
102
|
+
* CRAP gate is enabled (Story #1798) AND the consumer actually ships a
|
|
103
|
+
* `test:coverage` script for coverage-capture to run (#4473). When CRAP is
|
|
104
|
+
* enabled but `test:coverage` is absent, coverage-capture is dropped from
|
|
105
|
+
* the gate list, so the `test` gate MUST come back — otherwise the consumer
|
|
106
|
+
* has NO working test gate at all. Splitting this out keeps
|
|
107
|
+
* `buildDefaultGates` flat for the CRAP-cyclomatic gate.
|
|
108
|
+
*
|
|
109
|
+
* @param {boolean} coverageCaptureActive - Whether the coverage-capture gate
|
|
110
|
+
* is registered as the test runner for this build.
|
|
97
111
|
* @returns {Gate[]}
|
|
98
112
|
*/
|
|
99
|
-
function buildTestGateEntry(
|
|
100
|
-
if (
|
|
113
|
+
function buildTestGateEntry(coverageCaptureActive) {
|
|
114
|
+
if (coverageCaptureActive) return [];
|
|
101
115
|
return [{ name: 'test', cmd: 'npm', args: ['test'] }];
|
|
102
116
|
}
|
|
103
117
|
|
|
@@ -105,10 +119,13 @@ function buildTestGateEntry(config) {
|
|
|
105
119
|
* Build the canonical close-validation gate list.
|
|
106
120
|
*
|
|
107
121
|
* Ordering (cheapest fast-fail first): typecheck → lint → [test] →
|
|
108
|
-
* format → coverage-capture → check-baselines. The standalone `test`
|
|
109
|
-
* gate is dropped when
|
|
110
|
-
*
|
|
111
|
-
*
|
|
122
|
+
* format → [coverage-capture] → check-baselines. The standalone `test`
|
|
123
|
+
* gate is dropped when coverage-capture is the active test runner — i.e.
|
|
124
|
+
* `crap.enabled === true` (Story #1798) AND a `test:coverage` script
|
|
125
|
+
* exists (Story #4473) — because coverage-capture then carries
|
|
126
|
+
* test-failure signalling under c8. When CRAP is on but `test:coverage` is
|
|
127
|
+
* absent, coverage-capture is dropped and the `test` gate is restored so
|
|
128
|
+
* there is always a working test gate.
|
|
112
129
|
*
|
|
113
130
|
* `typecheck` is mandatory; consumers may customise the command via
|
|
114
131
|
* `project.commands.typecheck` (default `npm run typecheck`).
|
|
@@ -127,15 +144,34 @@ function buildTestGateEntry(config) {
|
|
|
127
144
|
* re-discovered inherited main-vs-epic drift in untouched files as phantom
|
|
128
145
|
* regressions and worked around it by hand-setting `BASELINE_REF`.
|
|
129
146
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
147
|
+
* Story #4473 — the coverage-capture gate spawns `npm run test:coverage`,
|
|
148
|
+
* so it is registered ONLY when the consumer actually ships that script.
|
|
149
|
+
* When CRAP is enabled but `test:coverage` is absent, coverage-capture is
|
|
150
|
+
* dropped and the plain `test` gate is restored (see `buildTestGateEntry`),
|
|
151
|
+
* so a consumer without a coverage script gets a working degraded test gate
|
|
152
|
+
* instead of a deterministic close failure with no test gate at all. The
|
|
153
|
+
* probe reads `package.json` at `cwd` (the gate execution directory).
|
|
154
|
+
*
|
|
155
|
+
* @param {{ config?: object, epicBranch?: string, cwd?: string, packageScripts?: Record<string, string> }} [opts]
|
|
156
|
+
* `config` is the canonical resolved config (`{ project, delivery, ... }`);
|
|
157
|
+
* gate commands resolve from `project.commands` and the CRAP toggle from
|
|
133
158
|
* `delivery.quality.gates.crap.enabled`. `epicBranch` is the close run's
|
|
134
159
|
* integration branch (`epic/<id>` for Epic-attached Stories, the base
|
|
135
|
-
* branch for standalone Stories).
|
|
160
|
+
* branch for standalone Stories). `cwd` is where the `package.json`
|
|
161
|
+
* coverage-script probe reads from (defaults to `process.cwd()`);
|
|
162
|
+
* `packageScripts` injects the scripts map directly (tests) and short-
|
|
163
|
+
* circuits the disk read.
|
|
136
164
|
* @returns {Gate[]}
|
|
137
165
|
*/
|
|
138
|
-
export function buildDefaultGates({
|
|
166
|
+
export function buildDefaultGates({
|
|
167
|
+
config,
|
|
168
|
+
epicBranch,
|
|
169
|
+
cwd,
|
|
170
|
+
packageScripts,
|
|
171
|
+
} = {}) {
|
|
172
|
+
const scripts = packageScripts ?? readPackageScripts(cwd);
|
|
173
|
+
const coverageCaptureActive =
|
|
174
|
+
isCrapGateEnabled(config) && hasNpmScript(scripts, 'test:coverage');
|
|
139
175
|
const typecheckCmdString = resolveTypecheckCommand(config);
|
|
140
176
|
const [typecheckCmd, ...typecheckArgs] = typecheckCmdString
|
|
141
177
|
.split(/\s+/)
|
|
@@ -158,7 +194,7 @@ export function buildDefaultGates({ config, epicBranch } = {}) {
|
|
|
158
194
|
hint: TYPECHECK_HINT,
|
|
159
195
|
},
|
|
160
196
|
{ name: 'lint', cmd: 'npm', args: ['run', 'lint'] },
|
|
161
|
-
...buildTestGateEntry(
|
|
197
|
+
...buildTestGateEntry(coverageCaptureActive),
|
|
162
198
|
{
|
|
163
199
|
// Gate name kept generic ("format") so the close-orchestrator log line
|
|
164
200
|
// and the per-gate phase-timer key don't shift when a repo swaps biome
|
|
@@ -172,12 +208,16 @@ export function buildDefaultGates({ config, epicBranch } = {}) {
|
|
|
172
208
|
? { changedFileScope: formatChangedFileScope }
|
|
173
209
|
: {}),
|
|
174
210
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
211
|
+
...(coverageCaptureActive
|
|
212
|
+
? [
|
|
213
|
+
{
|
|
214
|
+
name: 'coverage-capture',
|
|
215
|
+
cmd: 'node',
|
|
216
|
+
args: ['.agents/scripts/coverage-capture.js'],
|
|
217
|
+
hint: 'Coverage capture failed — `npm run test:coverage` exited non-zero. Fix failing tests or coverage-threshold breaches, then re-run close.',
|
|
218
|
+
},
|
|
219
|
+
]
|
|
220
|
+
: []),
|
|
181
221
|
{
|
|
182
222
|
// Story #2210 — unified `check-baselines` gate is the only path for
|
|
183
223
|
// per-kind regression enforcement. The legacy per-kind in-process
|
|
@@ -12,12 +12,19 @@
|
|
|
12
12
|
* waves run; `watch` tunes the merge/CI watch poll loop; and `autoMerge`
|
|
13
13
|
* (default `"trust-ci"`) selects the merge posture — `"trust-ci"` merges once
|
|
14
14
|
* required checks pass, `"strict"` additionally requires a clean review gate.
|
|
15
|
+
*
|
|
16
|
+
* Story #4472 adds `requireChecks` (default `false`): when `true` the
|
|
17
|
+
* AutomergePredicate treats a checks-less repo ("no checks reported") as a
|
|
18
|
+
* hard block rather than green, so a consumer that wants fail-closed-without-
|
|
19
|
+
* checks as policy opts into it explicitly instead of the framework blocking
|
|
20
|
+
* implicitly.
|
|
15
21
|
*/
|
|
16
22
|
|
|
17
23
|
export const CI_DELIVERY_DEFAULTS = Object.freeze({
|
|
18
24
|
skipForStoryPushes: true,
|
|
19
25
|
earlyPr: true,
|
|
20
26
|
autoMerge: 'trust-ci',
|
|
27
|
+
requireChecks: false,
|
|
21
28
|
});
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -28,7 +35,7 @@ export const CI_DELIVERY_DEFAULTS = Object.freeze({
|
|
|
28
35
|
* defaults; only the scalar knobs carry framework defaults here.
|
|
29
36
|
*
|
|
30
37
|
* @param {object | null | undefined} config
|
|
31
|
-
* @returns {{ skipForStoryPushes: boolean, earlyPr: boolean, autoMerge: 'trust-ci' | 'strict', watch: object | undefined }}
|
|
38
|
+
* @returns {{ skipForStoryPushes: boolean, earlyPr: boolean, autoMerge: 'trust-ci' | 'strict', requireChecks: boolean, watch: object | undefined }}
|
|
32
39
|
*/
|
|
33
40
|
export function getCiDelivery(config) {
|
|
34
41
|
const ci = config?.delivery?.ci ?? config?.ci ?? config ?? {};
|
|
@@ -45,6 +52,10 @@ export function getCiDelivery(config) {
|
|
|
45
52
|
ci.autoMerge === 'trust-ci' || ci.autoMerge === 'strict'
|
|
46
53
|
? ci.autoMerge
|
|
47
54
|
: CI_DELIVERY_DEFAULTS.autoMerge,
|
|
55
|
+
requireChecks:
|
|
56
|
+
typeof ci.requireChecks === 'boolean'
|
|
57
|
+
? ci.requireChecks
|
|
58
|
+
: CI_DELIVERY_DEFAULTS.requireChecks,
|
|
48
59
|
watch:
|
|
49
60
|
ci.watch && typeof ci.watch === 'object' ? { ...ci.watch } : undefined,
|
|
50
61
|
};
|
|
@@ -49,15 +49,23 @@ const DEFAULT_DELIVER_RUNNER = Object.freeze({
|
|
|
49
49
|
* `delivery.epicAudit.*` and `delivery.codeReview.*` in `.agentrc.json`
|
|
50
50
|
* (Story #2611, Epic #2586).
|
|
51
51
|
*
|
|
52
|
-
* `autoFixSeverity` (Story #
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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'`.
|
|
56
64
|
*/
|
|
57
65
|
export const DEFAULT_EPIC_AUDIT = Object.freeze({
|
|
58
66
|
maxFixAttempts: 3,
|
|
59
67
|
maxFixScopeFiles: 5,
|
|
60
|
-
autoFixSeverity: '
|
|
68
|
+
autoFixSeverity: 'high',
|
|
61
69
|
});
|
|
62
70
|
|
|
63
71
|
export const DEFAULT_CODE_REVIEW = Object.freeze({
|
|
@@ -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.).
|
|
@@ -211,11 +211,17 @@ const MERGE_WATCH_SCHEMA = {
|
|
|
211
211
|
* unattended auto-fixes, independent of the Story-sizing thresholds in
|
|
212
212
|
* `ticket-validator-sizing.js`.
|
|
213
213
|
*
|
|
214
|
-
* `autoFixSeverity` (Story #4399) is the threshold that governs which
|
|
215
|
-
*
|
|
216
|
-
* (
|
|
217
|
-
*
|
|
218
|
-
*
|
|
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.
|
|
219
225
|
*/
|
|
220
226
|
const EPIC_AUDIT_SCHEMA = {
|
|
221
227
|
type: 'object',
|
|
@@ -258,6 +264,13 @@ const CI_DELIVERY_SCHEMA = {
|
|
|
258
264
|
earlyPr: { type: 'boolean' },
|
|
259
265
|
watch: CI_WATCH_SCHEMA,
|
|
260
266
|
autoMerge: { type: 'string', enum: ['trust-ci', 'strict'] },
|
|
267
|
+
// Story #4472 — fail-closed-without-checks policy. When `true`, the
|
|
268
|
+
// AutomergePredicate refuses to arm merge in a repo that reports zero
|
|
269
|
+
// required checks ("no checks reported"), treating the absence of a CI
|
|
270
|
+
// gate as a hard block instead of green. Defaults to `false` so a
|
|
271
|
+
// checks-less repo with green close-validation gates lands headlessly
|
|
272
|
+
// rather than parking on the operator-merges path.
|
|
273
|
+
requireChecks: { type: 'boolean' },
|
|
261
274
|
},
|
|
262
275
|
additionalProperties: false,
|
|
263
276
|
};
|
|
@@ -331,15 +344,22 @@ const ACCEPTANCE_EVAL_SCHEMA = {
|
|
|
331
344
|
/**
|
|
332
345
|
* `delivery.feedbackLoop` — opt-out toggles consumed by the Epic finalize
|
|
333
346
|
* listener's auto-file graduators (`lib/feedback-loop/*-graduator.js`, read
|
|
334
|
-
* via `graduator-core.js#makeIsAutoFileEnabled`).
|
|
335
|
-
* (auto-file on); set
|
|
347
|
+
* via `graduator-core.js#makeIsAutoFileEnabled`). All default to `true`
|
|
348
|
+
* (auto-file on); set any to `false` to suppress auto-filing the
|
|
336
349
|
* corresponding non-blocking findings as follow-up issues.
|
|
350
|
+
*
|
|
351
|
+
* `retroProposals` (Story #4418) governs the retro auto-filer: when true
|
|
352
|
+
* (default) the retro's actionable routed proposals are filed as
|
|
353
|
+
* `meta::<framework-gap|consumer-improvement>` + `friction::<category>`
|
|
354
|
+
* issues via the graduator pre-parsed-findings seam, and the rendered retro
|
|
355
|
+
* sections list the filed issue numbers instead of paste-ready `gh` command
|
|
356
|
+
* stanzas; set it to `false` to fall back to the command stanzas.
|
|
337
357
|
*/
|
|
338
358
|
const FEEDBACK_LOOP_SCHEMA = {
|
|
339
359
|
type: 'object',
|
|
340
360
|
properties: {
|
|
341
|
-
codeReviewAutoFile: { type: 'boolean' },
|
|
342
361
|
auditResultsAutoFile: { type: 'boolean' },
|
|
362
|
+
retroProposals: { type: 'boolean' },
|
|
343
363
|
},
|
|
344
364
|
additionalProperties: false,
|
|
345
365
|
};
|