fapony 0.1.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/LICENSE +21 -0
- package/README.md +473 -0
- package/fapony.ts +78 -0
- package/package.json +42 -0
- package/skill/git-commit-conventional/SKILL.md +68 -0
- package/skill/git-ship/SKILL.md +144 -0
- package/skill/move-to-done/SKILL.md +126 -0
- package/skill/plan-with-pony/SKILL.md +263 -0
- package/skill/review-pony/SKILL.md +254 -0
- package/src/analyze.ts +517 -0
- package/src/context/index.ts +11 -0
- package/src/context/projectHealth.ts +359 -0
- package/src/conventions-seed.ts +420 -0
- package/src/db/defaults.ts +26 -0
- package/src/db/getters.ts +33 -0
- package/src/db/index.ts +7 -0
- package/src/db/load.ts +57 -0
- package/src/db/store.ts +286 -0
- package/src/db/types.ts +79 -0
- package/src/debt.ts +667 -0
- package/src/digest/cli.ts +75 -0
- package/src/digest/collect.ts +625 -0
- package/src/digest/html.ts +208 -0
- package/src/digest/text.ts +191 -0
- package/src/gate.ts +153 -0
- package/src/gates.ts +194 -0
- package/src/hook.ts +436 -0
- package/src/init-mem.ts +71 -0
- package/src/init.ts +237 -0
- package/src/install/claude.ts +361 -0
- package/src/install/codex.ts +61 -0
- package/src/install/cursor.ts +167 -0
- package/src/install/detect.ts +78 -0
- package/src/install/opencode.ts +234 -0
- package/src/install/skills.ts +106 -0
- package/src/install/types.ts +69 -0
- package/src/install/utils.ts +29 -0
- package/src/install/zcode.ts +120 -0
- package/src/install.ts +176 -0
- package/src/lint-baseline.ts +260 -0
- package/src/map.ts +320 -0
- package/src/math.ts +13 -0
- package/src/mcp/evidence.ts +332 -0
- package/src/mcp/primitives.ts +316 -0
- package/src/mcp/tools/check.ts +243 -0
- package/src/mcp/tools/collect.ts +157 -0
- package/src/mcp/tools/context.ts +66 -0
- package/src/mcp/tools/index.ts +309 -0
- package/src/mcp/tools/mem.ts +95 -0
- package/src/mcp/tools/plans.ts +255 -0
- package/src/mcp/tools/report.ts +285 -0
- package/src/mcp/tools/stats.ts +96 -0
- package/src/mcp/tools/usage.ts +211 -0
- package/src/mcp/tools/verdict.ts +148 -0
- package/src/mcp/transport.ts +241 -0
- package/src/mcp/types.ts +54 -0
- package/src/mcp/worktree.ts +27 -0
- package/src/memory.ts +264 -0
- package/src/parse.ts +71 -0
- package/src/plan-seed.ts +599 -0
- package/src/price/fetch.ts +146 -0
- package/src/price/index.ts +8 -0
- package/src/price/resolve.ts +213 -0
- package/src/report/cli.ts +92 -0
- package/src/report/format.ts +37 -0
- package/src/report/index.ts +4 -0
- package/src/report/render.ts +206 -0
- package/src/review-seed.ts +932 -0
- package/src/safety.ts +18 -0
- package/src/session/activeSession.ts +153 -0
- package/src/session/claude-code.ts +412 -0
- package/src/session/codex.ts +347 -0
- package/src/session/findModel.ts +376 -0
- package/src/session/helpers.ts +640 -0
- package/src/session/index.ts +31 -0
- package/src/session/opencode.ts +167 -0
- package/src/session/registry.ts +45 -0
- package/src/session/types.ts +128 -0
- package/src/session/zcode.ts +151 -0
- package/src/setup.ts +242 -0
- package/src/stats/cli.ts +44 -0
- package/src/stats/data.ts +1019 -0
- package/src/stats/format.ts +584 -0
- package/src/stats/index.ts +19 -0
- package/src/telemetry.ts +364 -0
- package/src/test.ts +2 -0
- package/src/update.ts +212 -0
- package/src/usage/cache.ts +125 -0
- package/src/usage/cli.ts +120 -0
- package/src/usage/format.ts +29 -0
- package/src/usage/index.ts +4 -0
- package/src/usage/render.ts +523 -0
- package/src/usage/scan.ts +161 -0
- package/src/util.ts +32 -0
- package/src/web/html.ts +33 -0
- package/templates/PLAN.md +90 -0
- package/templates/SPEC.md +30 -0
- package/templates/mem/commands/plan.ts +360 -0
- package/templates/mem/commands/read.ts +194 -0
- package/templates/mem/commands/rotate.ts +59 -0
- package/templates/mem/commands/selftest.ts +450 -0
- package/templates/mem/commands/write.ts +214 -0
- package/templates/mem/mem.ts +68 -0
- package/templates/mem/render.ts +63 -0
- package/templates/mem/selectors.ts +144 -0
- package/templates/mem/store.ts +285 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// src/context/projectHealth.ts — project-health context block builder
|
|
2
|
+
//
|
|
3
|
+
// Composes the cross-run knowledge queries (PLAN-project-health-context §2)
|
|
4
|
+
// into a short plain-text block fed to `plan-with-pony` as "known patterns"
|
|
5
|
+
// before drafting. Framed as "watch for", never "must follow" (overfitting
|
|
6
|
+
// guard — PLAN §5). Pure function over StatsData: no DB, no I/O.
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ReasonCodeCount,
|
|
10
|
+
RecentVerdictNote,
|
|
11
|
+
StatsData,
|
|
12
|
+
} from "../stats/data.js";
|
|
13
|
+
import { computeFrontier } from "../stats/format.js";
|
|
14
|
+
|
|
15
|
+
export interface HealthContextOptions {
|
|
16
|
+
/** Scope to one worktree path. Global across worktrees when omitted. */
|
|
17
|
+
worktree?: string;
|
|
18
|
+
/** Max reason_code rows (default 3 — PLAN §5 escape hatch against prompt bloat). */
|
|
19
|
+
topReasons?: number;
|
|
20
|
+
/** Min runs before trends are reported (default 5 — PLAN §5 sample-size guard). */
|
|
21
|
+
minRuns?: number;
|
|
22
|
+
/** Filter recentVerdictNotes to only those mentioning these files. */
|
|
23
|
+
files?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Project decisions from the mem log, surfaced first. Reading the log is I/O,
|
|
26
|
+
* so it happens in the tool handler — this builder stays pure over its inputs.
|
|
27
|
+
*/
|
|
28
|
+
memDecisions?: MemDecision[];
|
|
29
|
+
/**
|
|
30
|
+
* Structural hubs among the files being touched, computed at the handler
|
|
31
|
+
* from the live import graph (I/O) — this builder stays pure and just
|
|
32
|
+
* renders. Files below the dependents threshold never reach here.
|
|
33
|
+
*/
|
|
34
|
+
hubs?: HubEntry[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One structural hub from the live import graph (dependents ≥ threshold). */
|
|
38
|
+
export interface HubEntry {
|
|
39
|
+
file: string;
|
|
40
|
+
dependents: number;
|
|
41
|
+
tested: boolean;
|
|
42
|
+
/** Transitive (multi-hop) dependent count — omitted when equal to dependents. */
|
|
43
|
+
transitive?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Files imported by at least this many direct dependents are structural hubs —
|
|
47
|
+
// a break there drags every dependent with it (PLAN-hub-signal §4: 5 is the
|
|
48
|
+
// noise floor guess). Threshold gates on direct dependents only; transitive
|
|
49
|
+
// count is additive context on the same line, not a second gate.
|
|
50
|
+
export const HUB_DEPENDENTS_MIN = 5;
|
|
51
|
+
// One line, few entries — the mechanical 15-line cap already bounds the block.
|
|
52
|
+
const HUB_MAX = 3;
|
|
53
|
+
|
|
54
|
+
/** One mem-log decision distilled for the block. */
|
|
55
|
+
export interface MemDecision {
|
|
56
|
+
text: string;
|
|
57
|
+
spec?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Best-scoring model for one regime, from real graded work. */
|
|
61
|
+
export interface ModelFit {
|
|
62
|
+
regime: string;
|
|
63
|
+
model: string;
|
|
64
|
+
gates: number;
|
|
65
|
+
failRate: number;
|
|
66
|
+
avgQuality: number;
|
|
67
|
+
tokensPerPass: number | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Model right-sizing: below this many graded touches a bucket is noise, not a
|
|
71
|
+
// recommendation (same sample-size guard the trend lines use).
|
|
72
|
+
const MIN_MODEL_FIT_N = 5;
|
|
73
|
+
// Most regimes are 4 (code|fix|review|plan); cap protects the 15-line budget.
|
|
74
|
+
const MAX_MODEL_FIT = 4;
|
|
75
|
+
// Mem decisions shown ahead of the less-specific lines below.
|
|
76
|
+
const MEM_DECISION_MAX = 3;
|
|
77
|
+
const MEM_DECISION_CHARS = 140;
|
|
78
|
+
// Free-text verdict notes can be a whole paragraph — one 4k-char note once
|
|
79
|
+
// made the whole block 5,001 chars (measured 2026-09-17). Cap ~200, same trim
|
|
80
|
+
// shape as the mem-decision line above.
|
|
81
|
+
const RECENT_NOTE_CHARS = 200;
|
|
82
|
+
|
|
83
|
+
function fmtShortTokens(n: number): string {
|
|
84
|
+
return n >= 1000 ? `${Math.round(n / 1000)}k` : `${Math.round(n)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function truncate(s: string, max: number): string {
|
|
88
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// `none` asserts "checked, nothing was wrong" — it is not a missing value, and
|
|
92
|
+
// verdict_submit's schema says to send it on every clean pass rather than
|
|
93
|
+
// `other`. Either way it prints as no tag at all: a `[none]` prefix carries
|
|
94
|
+
// nothing and was what made three 1.4k-char notes read as a wall. A real reason
|
|
95
|
+
// tag stays — it is the KPI axis the trend line counts by.
|
|
96
|
+
function fmtRecentNote(n: RecentVerdictNote): string {
|
|
97
|
+
const body = truncate(n.note, RECENT_NOTE_CHARS);
|
|
98
|
+
return n.reason === "none" ? body : `[${n.reason}] ${body}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Per-worktree rows repeat the same reason_code across worktrees — without a
|
|
102
|
+
// worktree scope, merge counts by reason BEFORE slicing, or the line reads
|
|
103
|
+
// "spec_gap (5×), scope_mismatch (2×), spec_gap (2×)" (measured) instead of
|
|
104
|
+
// one entry per reason with the true total.
|
|
105
|
+
function topReasonRows(
|
|
106
|
+
rows: ReasonCodeCount[],
|
|
107
|
+
cap: number,
|
|
108
|
+
mergeAcrossWorktrees: boolean,
|
|
109
|
+
): ReasonCodeCount[] {
|
|
110
|
+
if (!mergeAcrossWorktrees) return rows.slice(0, cap);
|
|
111
|
+
const merged = new Map<string, number>();
|
|
112
|
+
for (const r of rows)
|
|
113
|
+
merged.set(r.reason, (merged.get(r.reason) ?? 0) + r.count);
|
|
114
|
+
return [...merged.entries()]
|
|
115
|
+
.map(([reason, count]) => ({ worktree: "(all)", reason, count }))
|
|
116
|
+
.sort((a, b) => b.count - a.count)
|
|
117
|
+
.slice(0, cap);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hubLine(hubs: HubEntry[]): string | null {
|
|
121
|
+
if (hubs.length === 0) return null;
|
|
122
|
+
const list = hubs
|
|
123
|
+
.slice(0, HUB_MAX)
|
|
124
|
+
.map((h) => {
|
|
125
|
+
const transitiveBit =
|
|
126
|
+
h.transitive !== undefined && h.transitive > h.dependents
|
|
127
|
+
? `, ${h.transitive} transitively`
|
|
128
|
+
: "";
|
|
129
|
+
return `${h.file} (imported by ${h.dependents} files${transitiveBit}${h.tested ? "" : "; untested"})`;
|
|
130
|
+
})
|
|
131
|
+
.join(" · ");
|
|
132
|
+
return `- Hubs you are touching (high blast radius): ${list}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pick the best model per regime from real graded work, on the same Pareto
|
|
137
|
+
* frontier `stats --mode verdict` renders: quality up, tokens/pass down.
|
|
138
|
+
*
|
|
139
|
+
* This used to sort by failRate first. That axis is dead in practice — work is
|
|
140
|
+
* self-graded and nearly every bucket sits at 0 fails — so the sort fell
|
|
141
|
+
* through to tokens and almost never read quality at all. failRate is still
|
|
142
|
+
* reported, just no longer the ranking key.
|
|
143
|
+
*
|
|
144
|
+
* Buckets under `minN` are skipped: one lucky verdict is not a recommendation.
|
|
145
|
+
*/
|
|
146
|
+
export function computeModelFit(
|
|
147
|
+
byRegime: StatsData["byRegime"],
|
|
148
|
+
worktree?: string,
|
|
149
|
+
minN = MIN_MODEL_FIT_N,
|
|
150
|
+
): ModelFit[] {
|
|
151
|
+
const eligible = byRegime.filter(
|
|
152
|
+
(r) =>
|
|
153
|
+
r.regime !== "—" &&
|
|
154
|
+
r.model !== "—" &&
|
|
155
|
+
r.gates >= minN &&
|
|
156
|
+
(worktree ? r.worktree === worktree : true),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const byName = new Map<string, typeof eligible>();
|
|
160
|
+
for (const r of eligible) {
|
|
161
|
+
const list = byName.get(r.regime) ?? [];
|
|
162
|
+
list.push(r);
|
|
163
|
+
byName.set(r.regime, list);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const out: ModelFit[] = [];
|
|
167
|
+
for (const list of byName.values()) {
|
|
168
|
+
const { frontier } = computeFrontier(
|
|
169
|
+
list.map((r) => ({
|
|
170
|
+
model: r.model,
|
|
171
|
+
gates: r.gates,
|
|
172
|
+
fails: r.fails,
|
|
173
|
+
avgQuality: r.avgQuality,
|
|
174
|
+
tokensPerPass: r.tokensPerPass,
|
|
175
|
+
})),
|
|
176
|
+
minN,
|
|
177
|
+
);
|
|
178
|
+
// The frontier is built cheapest-first with quality rising, so its last row
|
|
179
|
+
// is the best quality nothing beats outright. An empty frontier means no
|
|
180
|
+
// bucket had token attribution — then quality alone decides.
|
|
181
|
+
const best =
|
|
182
|
+
frontier[frontier.length - 1]?.model ??
|
|
183
|
+
[...list].sort(
|
|
184
|
+
(a, b) => b.avgQuality - a.avgQuality || a.failRate - b.failRate,
|
|
185
|
+
)[0].model;
|
|
186
|
+
const row = list.find((r) => r.model === best)!;
|
|
187
|
+
out.push({
|
|
188
|
+
regime: row.regime,
|
|
189
|
+
model: row.model,
|
|
190
|
+
gates: row.gates,
|
|
191
|
+
failRate: row.failRate,
|
|
192
|
+
avgQuality: row.avgQuality,
|
|
193
|
+
tokensPerPass: row.tokensPerPass,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return out.slice(0, MAX_MODEL_FIT);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Build the "known patterns" block (spec §3 shape, capped ~15 lines).
|
|
201
|
+
* Low-history scopes get an explicit "not enough history yet" line instead
|
|
202
|
+
* of noise from n=1 patterns looking like trends.
|
|
203
|
+
*/
|
|
204
|
+
export function buildProjectHealthContext(
|
|
205
|
+
data: StatsData,
|
|
206
|
+
opts?: HealthContextOptions,
|
|
207
|
+
): string {
|
|
208
|
+
const worktree = opts?.worktree;
|
|
209
|
+
const topReasons = opts?.topReasons ?? 3;
|
|
210
|
+
const minRuns = opts?.minRuns ?? 5;
|
|
211
|
+
const files = opts?.files;
|
|
212
|
+
|
|
213
|
+
const total = worktree
|
|
214
|
+
? (data.byWorktree.find((w) => w.worktree === worktree)?.runs ?? 0)
|
|
215
|
+
: data.runs.total;
|
|
216
|
+
const scope = worktree ?? "all worktrees";
|
|
217
|
+
const header = `## Known patterns for this project (from fapony history, N=${total} runs, ${scope})`;
|
|
218
|
+
|
|
219
|
+
// Lead with the two things no code-exploration tool can produce: what this
|
|
220
|
+
// project already decided (mem) and which model actually holds up for each
|
|
221
|
+
// task shape (the ledger). Everything below is the older, weaker watch-fors.
|
|
222
|
+
const lead: string[] = [];
|
|
223
|
+
const memDecisions = (opts?.memDecisions ?? []).slice(0, MEM_DECISION_MAX);
|
|
224
|
+
if (memDecisions.length > 0) {
|
|
225
|
+
const list = memDecisions
|
|
226
|
+
.map((d) => `"${truncate(d.text, MEM_DECISION_CHARS)}"`)
|
|
227
|
+
.join(" · ");
|
|
228
|
+
lead.push(`- Decisions on record (mem): ${list}`);
|
|
229
|
+
}
|
|
230
|
+
for (const f of computeModelFit(data.byRegime, worktree)) {
|
|
231
|
+
const bits = [
|
|
232
|
+
`failRate ${Math.round(f.failRate * 100)}%`,
|
|
233
|
+
`quality ${f.avgQuality.toFixed(1)}`,
|
|
234
|
+
`N=${f.gates}`,
|
|
235
|
+
];
|
|
236
|
+
if (f.tokensPerPass !== null) {
|
|
237
|
+
bits.push(`${fmtShortTokens(f.tokensPerPass)} tok/pass`);
|
|
238
|
+
}
|
|
239
|
+
lead.push(
|
|
240
|
+
`- Model fit: regime=${f.regime} → ${f.model} (${bits.join(", ")})`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Recent free-text notes carry signal from N=1 (a specific "worked around
|
|
245
|
+
// X" beats a count) — unlike the trend lines below, not gated by minRuns.
|
|
246
|
+
// Order matters: filter (worktree, then files) BEFORE slicing, so a match
|
|
247
|
+
// sitting past the top-3 cutoff still surfaces when files[] is given.
|
|
248
|
+
let notes = worktree
|
|
249
|
+
? data.recentVerdictNotes.filter((n) => n.worktree === worktree)
|
|
250
|
+
: data.recentVerdictNotes;
|
|
251
|
+
// When files[] is provided, keep only notes that mention at least one of
|
|
252
|
+
// the target files (substring match on note text or stored files array).
|
|
253
|
+
if (files && files.length > 0) {
|
|
254
|
+
const fileSet = new Set(files.map((f) => f.toLowerCase()));
|
|
255
|
+
notes = notes.filter((n) => {
|
|
256
|
+
// Check stored files array first (reliable, from gate event data).
|
|
257
|
+
if (n.files && n.files.length > 0) {
|
|
258
|
+
return n.files.some((f) => fileSet.has(f.toLowerCase()));
|
|
259
|
+
}
|
|
260
|
+
// Fallback: substring match on note text.
|
|
261
|
+
const noteLower = n.note.toLowerCase();
|
|
262
|
+
return files.some((f) => noteLower.includes(f.toLowerCase()));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
notes = notes.slice(0, 3);
|
|
266
|
+
|
|
267
|
+
// File risk for exactly the files being touched. Unlike the trend lines
|
|
268
|
+
// below this is NOT gated by minRuns: "this file failed last time" is
|
|
269
|
+
// actionable at n=1, and the count is printed so the reader can weigh it.
|
|
270
|
+
let riskLine: string | null = null;
|
|
271
|
+
if (files && files.length > 0) {
|
|
272
|
+
const fileSet = new Set(files.map((f) => f.toLowerCase()));
|
|
273
|
+
const hits = data.byFile
|
|
274
|
+
.filter((f) => (worktree ? f.worktree === worktree : true))
|
|
275
|
+
.filter((f) => f.fails > 0 && fileSet.has(f.file.toLowerCase()))
|
|
276
|
+
.slice(0, 3);
|
|
277
|
+
if (hits.length > 0) {
|
|
278
|
+
riskLine = `- Files you are touching that failed before: ${hits
|
|
279
|
+
.map(
|
|
280
|
+
(h) =>
|
|
281
|
+
`${h.file} (${h.fails}/${h.gates} graded touches failed${h.lastReason ? `, last: ${h.lastReason}` : ""})`,
|
|
282
|
+
)
|
|
283
|
+
.join(" · ")}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Structural hubs (computed at the handler — pure here) warn before an
|
|
288
|
+
// edit: same watch-for framing as riskLine, but deterministic, not history.
|
|
289
|
+
const hub = hubLine(opts?.hubs ?? []);
|
|
290
|
+
|
|
291
|
+
if (total < minRuns) {
|
|
292
|
+
const lines = [
|
|
293
|
+
header,
|
|
294
|
+
...lead,
|
|
295
|
+
`- Not enough history yet (${total} runs, need ${minRuns}+) for recurring patterns; draft freely.`,
|
|
296
|
+
];
|
|
297
|
+
if (riskLine) lines.push(riskLine);
|
|
298
|
+
if (hub) lines.push(hub);
|
|
299
|
+
if (notes.length > 0) {
|
|
300
|
+
lines.push(
|
|
301
|
+
`- Recent verdict notes: ${notes.map(fmtRecentNote).join(" · ")}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return lines.slice(0, 15).join("\n");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const reasons = topReasonRows(
|
|
308
|
+
worktree
|
|
309
|
+
? data.byReasonCode.filter((r) => r.worktree === worktree)
|
|
310
|
+
: data.byReasonCode,
|
|
311
|
+
Math.max(topReasons, 0),
|
|
312
|
+
!worktree,
|
|
313
|
+
);
|
|
314
|
+
const escalated = worktree
|
|
315
|
+
? data.escalatedRuns.filter((e) => e.worktree === worktree)
|
|
316
|
+
: data.escalatedRuns;
|
|
317
|
+
const passing = (
|
|
318
|
+
worktree
|
|
319
|
+
? data.bestPassing.filter((b) => b.worktree === worktree)
|
|
320
|
+
: data.bestPassing
|
|
321
|
+
).slice(0, 3);
|
|
322
|
+
|
|
323
|
+
const lines = [header, ...lead];
|
|
324
|
+
if (riskLine) lines.push(riskLine);
|
|
325
|
+
if (hub) lines.push(hub);
|
|
326
|
+
if (reasons.length > 0) {
|
|
327
|
+
const list = reasons.map((r) => `${r.reason} (${r.count}×)`).join(", ");
|
|
328
|
+
lines.push(
|
|
329
|
+
`- Recurring fail reasons (non-pass gates): ${list} — watch for these in the new plan.`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (escalated.length > 0) {
|
|
333
|
+
// Top-1 concrete example only (PLAN §5: never dump full history).
|
|
334
|
+
const ex = escalated[0];
|
|
335
|
+
const planBit = ex.plan
|
|
336
|
+
? ` (e.g. plan "${ex.plan}", round ${ex.round})`
|
|
337
|
+
: "";
|
|
338
|
+
lines.push(
|
|
339
|
+
`- ${escalated.length} run${escalated.length === 1 ? "" : "s"} escalated past the round cap${planBit} — likely the plan was underspecified, not the code.`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (passing.length > 0) {
|
|
343
|
+
const list = passing.map((b) => `"${b.plan}"`).join(", ");
|
|
344
|
+
lines.push(
|
|
345
|
+
`- Passed round 1 before: ${list} — shapes worth reusing when they fit.`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (notes.length > 0) {
|
|
349
|
+
lines.push(
|
|
350
|
+
`- Recent verdict notes: ${notes.map(fmtRecentNote).join(" · ")}`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (lines.length === 1) {
|
|
354
|
+
lines.push(
|
|
355
|
+
"- No recurring failure or escalation patterns observed — draft freely, keep the scope tight.",
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return lines.slice(0, 15).join("\n");
|
|
359
|
+
}
|