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,208 @@
|
|
|
1
|
+
// src/digest/html.ts — render digest as a single HTML page
|
|
2
|
+
//
|
|
3
|
+
// คอลัมน์เดียว max-width: 68rem · ไม่มี tab ไม่มีปุ่มพับ ไม่มี JS
|
|
4
|
+
// ใช้ DARK_THEME_CSS + TABLE_CSS จาก src/web/html.ts
|
|
5
|
+
|
|
6
|
+
import { DARK_THEME_CSS, esc, TABLE_CSS } from "../web/html.js";
|
|
7
|
+
import type {
|
|
8
|
+
CostRow,
|
|
9
|
+
DigestData,
|
|
10
|
+
MemRow,
|
|
11
|
+
PlanRow,
|
|
12
|
+
RegimeModelRow,
|
|
13
|
+
Tally,
|
|
14
|
+
} from "./collect.js";
|
|
15
|
+
|
|
16
|
+
function fmtDate(iso: string): string {
|
|
17
|
+
return iso.slice(0, 10);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fmtTokens(n: number): string {
|
|
21
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
22
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
|
|
23
|
+
return String(Math.round(n));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function memRow(r: MemRow): string {
|
|
27
|
+
const date = r.ts.slice(5, 10);
|
|
28
|
+
return `<tr>
|
|
29
|
+
<td class="muted">${esc(date)}</td>
|
|
30
|
+
<td>${esc(r.agent)}</td>
|
|
31
|
+
<td>${esc(r.text.replace(/\n/g, " ").slice(0, 120))}</td>
|
|
32
|
+
</tr>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function planRow(p: PlanRow): string {
|
|
36
|
+
const name = esc(p.file.replace(/\.md$/, ""));
|
|
37
|
+
const status = esc(p.status ?? "active");
|
|
38
|
+
const progress = p.total > 0 ? `${p.done}/${p.total}` : "\u2014";
|
|
39
|
+
return `<tr>
|
|
40
|
+
<td>${name}</td>
|
|
41
|
+
<td class="muted">${status}</td>
|
|
42
|
+
<td class="muted">${progress}</td>
|
|
43
|
+
</tr>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function costRow(c: CostRow): string {
|
|
47
|
+
const name = c.provider ? `${esc(c.provider)}/${esc(c.model)}` : esc(c.model);
|
|
48
|
+
const costStr =
|
|
49
|
+
c.imputed > 0
|
|
50
|
+
? `~$${c.imputed.toFixed(4)} imputed`
|
|
51
|
+
: c.cost > 0
|
|
52
|
+
? `$${c.cost.toFixed(4)} recorded`
|
|
53
|
+
: "unpriced";
|
|
54
|
+
return `<tr>
|
|
55
|
+
<td>${name}</td>
|
|
56
|
+
<td class="muted">${c.sessions}</td>
|
|
57
|
+
<td>${fmtTokens(c.input)}</td>
|
|
58
|
+
<td>${fmtTokens(c.output)}</td>
|
|
59
|
+
<td>${costStr}</td>
|
|
60
|
+
</tr>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function tallyRow(t: Tally): string {
|
|
64
|
+
return `<tr><td>${esc(t.key)}</td><td>${t.count}</td></tr>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function regimeModelRow(r: RegimeModelRow): string {
|
|
68
|
+
return `<tr>
|
|
69
|
+
<td class="muted">${esc(r.regime)}</td>
|
|
70
|
+
<td>${esc(r.model)}</td>
|
|
71
|
+
<td>${r.gates}</td>
|
|
72
|
+
<td>${r.fails}</td>
|
|
73
|
+
</tr>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function section(title: string, content: string, cls = ""): string {
|
|
77
|
+
return `<h2 class="${cls}">${esc(title)}</h2>\n${content}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function renderDigestHtml(d: DigestData): string {
|
|
81
|
+
const units = d.verdicts.units_graded;
|
|
82
|
+
const costBits: string[] = [];
|
|
83
|
+
if (d.cost.imputed_usd > 0)
|
|
84
|
+
costBits.push(`~$${d.cost.imputed_usd.toFixed(4)} (imputed)`);
|
|
85
|
+
if (d.cost.total_usd > 0)
|
|
86
|
+
costBits.push(`$${d.cost.total_usd.toFixed(4)} (recorded)`);
|
|
87
|
+
const costLabel = costBits.length > 0 ? costBits.join(" · ") : "$0";
|
|
88
|
+
|
|
89
|
+
// decisions table
|
|
90
|
+
const decisionsHtml =
|
|
91
|
+
d.decisions.length > 0
|
|
92
|
+
? `<table><thead><tr><th>Date</th><th>Agent</th><th>Decision</th></tr></thead><tbody>${d.decisions.map(memRow).join("")}</tbody></table>`
|
|
93
|
+
: '<p class="muted">(none this period)</p>';
|
|
94
|
+
|
|
95
|
+
// open bugs
|
|
96
|
+
const bugsOpenHtml =
|
|
97
|
+
d.bugs.open.length > 0
|
|
98
|
+
? `<table><thead><tr><th>Date</th><th>Agent</th><th>Bug</th></tr></thead><tbody>${d.bugs.open.map(memRow).join("")}</tbody></table>`
|
|
99
|
+
: '<p class="muted">(none)</p>';
|
|
100
|
+
const closedNote =
|
|
101
|
+
d.bugs.closed.length > 0
|
|
102
|
+
? `<p class="muted">${d.bugs.closed.length} closed this period</p>`
|
|
103
|
+
: "";
|
|
104
|
+
|
|
105
|
+
// plans
|
|
106
|
+
const inFlight = d.plans.pending.filter(
|
|
107
|
+
(p) => p.status === "active" || !p.status,
|
|
108
|
+
);
|
|
109
|
+
const plansHtml =
|
|
110
|
+
inFlight.length > 0
|
|
111
|
+
? `<table><thead><tr><th>Plan</th><th>Status</th><th>Progress</th></tr></thead><tbody>${inFlight.map(planRow).join("")}</tbody></table>`
|
|
112
|
+
: '<p class="muted">(none)</p>';
|
|
113
|
+
const shippedNote =
|
|
114
|
+
d.plans.shipped.length > 0
|
|
115
|
+
? `<p class="muted">${d.plans.shipped.length} shipped this period</p>`
|
|
116
|
+
: "";
|
|
117
|
+
|
|
118
|
+
// cost
|
|
119
|
+
const costHtml =
|
|
120
|
+
d.cost.by_model.length > 0
|
|
121
|
+
? `<div style="overflow-x:auto"><table><thead><tr><th>Model</th><th>Sessions</th><th>In</th><th>Out</th><th>Cost</th></tr></thead><tbody>${d.cost.by_model.map(costRow).join("")}</tbody></table></div>` +
|
|
122
|
+
(d.cost.unpriced_sessions > 0
|
|
123
|
+
? `<p class="muted">${d.cost.unpriced_sessions} sessions unpriced (no rate found — not counted as free)</p>`
|
|
124
|
+
: "")
|
|
125
|
+
: '<p class="muted">(no usage data)</p>';
|
|
126
|
+
|
|
127
|
+
// verdicts — by_grade
|
|
128
|
+
const gradeHtml =
|
|
129
|
+
d.verdicts.by_grade.length > 0
|
|
130
|
+
? `<table><thead><tr><th>Grade</th><th>Count</th></tr></thead><tbody>${d.verdicts.by_grade.map(tallyRow).join("")}</tbody></table>`
|
|
131
|
+
: "";
|
|
132
|
+
|
|
133
|
+
// verdicts — by_reason_code
|
|
134
|
+
const reasonHtml =
|
|
135
|
+
d.verdicts.by_reason_code.length > 0
|
|
136
|
+
? `<table><thead><tr><th>Reason</th><th>Count</th></tr></thead><tbody>${d.verdicts.by_reason_code.map(tallyRow).join("")}</tbody></table>`
|
|
137
|
+
: '<p class="muted">no graded fails in this period</p>';
|
|
138
|
+
|
|
139
|
+
// verdicts — by_regime_model
|
|
140
|
+
const regimeHtml =
|
|
141
|
+
d.verdicts.by_regime_model.length > 0
|
|
142
|
+
? `<div style="overflow-x:auto"><table><thead><tr><th>Regime</th><th>Model</th><th>Gates</th><th>Fails</th></tr></thead><tbody>${d.verdicts.by_regime_model.map(regimeModelRow).join("")}</tbody></table></div>`
|
|
143
|
+
: "";
|
|
144
|
+
|
|
145
|
+
// sources
|
|
146
|
+
const sourcesHtml = `<table><thead><tr><th>Source</th><th>Status</th><th>Detail</th></tr></thead><tbody>${d.sources
|
|
147
|
+
.map(
|
|
148
|
+
(s) =>
|
|
149
|
+
`<tr><td>${esc(s.name)}</td><td class="${s.ok ? "pass" : "fail"}">${s.ok ? "ok" : "!!"}</td><td class="muted">${esc(s.detail)}</td></tr>`,
|
|
150
|
+
)
|
|
151
|
+
.join("")}</tbody></table>`;
|
|
152
|
+
|
|
153
|
+
const malformedNote =
|
|
154
|
+
d.skipped_malformed > 0
|
|
155
|
+
? `<p class="muted">${d.skipped_malformed} malformed log lines skipped</p>`
|
|
156
|
+
: "";
|
|
157
|
+
|
|
158
|
+
return `<!DOCTYPE html>
|
|
159
|
+
<html lang="en">
|
|
160
|
+
<head>
|
|
161
|
+
<meta charset="utf-8">
|
|
162
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
163
|
+
<title>fapony digest — ${fmtDate(d.since)} → ${fmtDate(d.generated_at)}</title>
|
|
164
|
+
<style>
|
|
165
|
+
${DARK_THEME_CSS}
|
|
166
|
+
${TABLE_CSS}
|
|
167
|
+
body { max-width: 68rem; }
|
|
168
|
+
.headline { font-size: 1.1rem; margin: 1rem 0 1.5rem; }
|
|
169
|
+
.scope-note { color: var(--muted); font-size: 0.85rem; margin-bottom: 1.5rem; }
|
|
170
|
+
table { margin-bottom: 1.5rem; }
|
|
171
|
+
td:first-child { font-weight: 600; }
|
|
172
|
+
</style>
|
|
173
|
+
</head>
|
|
174
|
+
<body>
|
|
175
|
+
|
|
176
|
+
<h1>fapony digest — ${esc(fmtDate(d.since))} → ${esc(fmtDate(d.generated_at))}</h1>
|
|
177
|
+
<div class="scope-note">${esc(d.worktree)}<br>${esc(d.scope_note)}</div>
|
|
178
|
+
|
|
179
|
+
<div class="headline">
|
|
180
|
+
<strong>${units}</strong> unit${units === 1 ? "" : "s"} graded · <strong>${d.verdicts.round1_pct}%</strong> passed round 1 · <strong>${esc(costLabel)}</strong>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
${section("Decisions", decisionsHtml)}
|
|
184
|
+
|
|
185
|
+
${section(
|
|
186
|
+
`Notes (${d.notes.length})`,
|
|
187
|
+
d.notes.length > 0
|
|
188
|
+
? `<table><thead><tr><th>Date</th><th>Agent</th><th>Note</th></tr></thead><tbody>${d.notes.map(memRow).join("")}</tbody></table>`
|
|
189
|
+
: '<p class="muted">(none this period)</p>',
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
${section(`Open Bugs (${d.bugs.open.length})`, bugsOpenHtml + closedNote)}
|
|
193
|
+
|
|
194
|
+
${section(`In Flight (${inFlight.length})`, plansHtml + shippedNote)}
|
|
195
|
+
|
|
196
|
+
${section("Cost", costHtml)}
|
|
197
|
+
|
|
198
|
+
${section("How Work Failed", reasonHtml)}
|
|
199
|
+
${gradeHtml ? `\n${section("By Grade", gradeHtml)}` : ""}
|
|
200
|
+
${regimeHtml ? `\n${section("By Regime × Model", regimeHtml)}` : ""}
|
|
201
|
+
|
|
202
|
+
${section("Sources", sourcesHtml)}
|
|
203
|
+
|
|
204
|
+
${malformedNote}
|
|
205
|
+
|
|
206
|
+
</body>
|
|
207
|
+
</html>`;
|
|
208
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// src/digest/text.ts — render digest as terminal text
|
|
2
|
+
//
|
|
3
|
+
// ลำดับ section ตาม SPEC §3 (เล่าเรื่อง ไม่ใช่ dashboard)
|
|
4
|
+
|
|
5
|
+
import type { CostRow, DigestData, MemRow, PlanRow } from "./collect.js";
|
|
6
|
+
|
|
7
|
+
function fmtDate(iso: string): string {
|
|
8
|
+
return iso.slice(0, 10);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function fmtTime(iso: string): string {
|
|
12
|
+
return iso.slice(5, 10); // MM-DD
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function section(name: string, lines: string[]): string {
|
|
16
|
+
return `${name}\n${lines.map((l) => ` ${l}`).join("\n")}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function memLine(r: MemRow): string {
|
|
20
|
+
const date = fmtTime(r.ts);
|
|
21
|
+
return `${date} ${r.agent} ${r.text.replace(/\n/g, " ").slice(0, 80)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function planLine(p: PlanRow): string {
|
|
25
|
+
const name = p.file.replace(/\.md$/, "");
|
|
26
|
+
const status = p.status ?? "active";
|
|
27
|
+
const progress = p.total > 0 ? `${p.done}/${p.total}` : "—";
|
|
28
|
+
return `${name.padEnd(30)} ${status.padEnd(12)} ${progress}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fmtTokens(n: number): string {
|
|
32
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
33
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
|
|
34
|
+
return String(Math.round(n));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function costLine(c: CostRow): string {
|
|
38
|
+
const name = c.provider ? `${c.provider}/${c.model}` : c.model;
|
|
39
|
+
const costStr =
|
|
40
|
+
c.imputed > 0
|
|
41
|
+
? `$${c.imputed.toFixed(4)} imputed`
|
|
42
|
+
: c.cost > 0
|
|
43
|
+
? `$${c.cost.toFixed(4)} recorded`
|
|
44
|
+
: "unpriced";
|
|
45
|
+
return `${name.padEnd(45)} ${String(c.sessions).padStart(5)} sessions ${fmtTokens(c.input).padStart(7)} in ${fmtTokens(c.output).padStart(7)} out ${costStr}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function renderDigestText(d: DigestData): string {
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
|
|
51
|
+
// 1. Header
|
|
52
|
+
lines.push(
|
|
53
|
+
`fapony digest — ${fmtDate(d.since)} → ${fmtDate(d.generated_at)}`,
|
|
54
|
+
);
|
|
55
|
+
lines.push(` ${d.worktree}`);
|
|
56
|
+
lines.push(` scope: ${d.scope_note}`);
|
|
57
|
+
lines.push("");
|
|
58
|
+
|
|
59
|
+
// 2. Headline — run-based counts from collect (gate tallies are not units)
|
|
60
|
+
const units = d.verdicts.units_graded;
|
|
61
|
+
const costBits: string[] = [];
|
|
62
|
+
if (d.cost.imputed_usd > 0)
|
|
63
|
+
costBits.push(`$${d.cost.imputed_usd.toFixed(4)} (imputed)`);
|
|
64
|
+
if (d.cost.total_usd > 0)
|
|
65
|
+
costBits.push(`$${d.cost.total_usd.toFixed(4)} (recorded)`);
|
|
66
|
+
const costLabel = costBits.length > 0 ? costBits.join(" · ") : "$0";
|
|
67
|
+
|
|
68
|
+
lines.push(
|
|
69
|
+
` ${units} unit${units === 1 ? "" : "s"} graded · ${d.verdicts.round1_pct}% passed round 1 · ${costLabel}`,
|
|
70
|
+
);
|
|
71
|
+
lines.push("");
|
|
72
|
+
|
|
73
|
+
// 3. Decisions
|
|
74
|
+
lines.push(
|
|
75
|
+
section(`DECISIONS (${d.decisions.length})`, [
|
|
76
|
+
...d.decisions.map(memLine),
|
|
77
|
+
...(d.decisions.length === 0 ? ["(none this period)"] : []),
|
|
78
|
+
]),
|
|
79
|
+
);
|
|
80
|
+
lines.push("");
|
|
81
|
+
|
|
82
|
+
// 3b. Notes — mem rows like decisions; the sync payload must stay visible
|
|
83
|
+
lines.push(
|
|
84
|
+
section(`NOTES (${d.notes.length})`, [
|
|
85
|
+
...d.notes.map(memLine),
|
|
86
|
+
...(d.notes.length === 0 ? ["(none this period)"] : []),
|
|
87
|
+
]),
|
|
88
|
+
);
|
|
89
|
+
lines.push("");
|
|
90
|
+
|
|
91
|
+
// 4. Open bugs
|
|
92
|
+
const openBugLines = d.bugs.open.map(memLine);
|
|
93
|
+
const closedCount = d.bugs.closed.length;
|
|
94
|
+
lines.push(
|
|
95
|
+
section(`OPEN BUGS (${d.bugs.open.length})`, [
|
|
96
|
+
...openBugLines,
|
|
97
|
+
...(d.bugs.open.length === 0 ? ["(none)"] : []),
|
|
98
|
+
...(closedCount > 0 ? [`(${closedCount} closed this period)`] : []),
|
|
99
|
+
]),
|
|
100
|
+
);
|
|
101
|
+
lines.push("");
|
|
102
|
+
|
|
103
|
+
// 5. In flight
|
|
104
|
+
const inFlight = d.plans.pending.filter(
|
|
105
|
+
(p) => p.status === "active" || !p.status,
|
|
106
|
+
);
|
|
107
|
+
const shipped = d.plans.shipped;
|
|
108
|
+
lines.push(
|
|
109
|
+
section(`IN FLIGHT (${inFlight.length})`, [
|
|
110
|
+
...inFlight.map(planLine),
|
|
111
|
+
...(inFlight.length === 0 ? ["(none)"] : []),
|
|
112
|
+
...(shipped.length > 0 ? [`${shipped.length} shipped this period`] : []),
|
|
113
|
+
]),
|
|
114
|
+
);
|
|
115
|
+
lines.push("");
|
|
116
|
+
|
|
117
|
+
// 6. Cost
|
|
118
|
+
if (d.cost.by_model.length > 0) {
|
|
119
|
+
lines.push(
|
|
120
|
+
section("COST", [
|
|
121
|
+
"model".padEnd(45) +
|
|
122
|
+
"sessions".padStart(5) +
|
|
123
|
+
" in".padStart(9) +
|
|
124
|
+
" out".padStart(9) +
|
|
125
|
+
" cost",
|
|
126
|
+
...d.cost.by_model.map(costLine),
|
|
127
|
+
...(d.cost.unpriced_sessions > 0
|
|
128
|
+
? [
|
|
129
|
+
`${d.cost.unpriced_sessions} sessions unpriced (no rate found — not counted as free)`,
|
|
130
|
+
]
|
|
131
|
+
: []),
|
|
132
|
+
]),
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
lines.push(section("COST", ["(no usage data)"]));
|
|
136
|
+
}
|
|
137
|
+
lines.push("");
|
|
138
|
+
|
|
139
|
+
// 7. How work failed — by_grade first (every payload field needs a place)
|
|
140
|
+
if (d.verdicts.by_grade.length > 0) {
|
|
141
|
+
lines.push(
|
|
142
|
+
section("BY GRADE", [
|
|
143
|
+
...d.verdicts.by_grade.map((t) => `${t.key.padEnd(20)} ${t.count}`),
|
|
144
|
+
]),
|
|
145
|
+
);
|
|
146
|
+
lines.push("");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (d.verdicts.by_reason_code.length > 0) {
|
|
150
|
+
lines.push(
|
|
151
|
+
section("HOW WORK FAILED", [
|
|
152
|
+
...d.verdicts.by_reason_code.map(
|
|
153
|
+
(t) => `${t.key.padEnd(20)} ${t.count}`,
|
|
154
|
+
),
|
|
155
|
+
]),
|
|
156
|
+
);
|
|
157
|
+
} else {
|
|
158
|
+
lines.push(section("HOW WORK FAILED", ["no graded fails in this period"]));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (d.verdicts.by_regime_model.length > 0) {
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push(
|
|
164
|
+
section("BY REGIME × MODEL", [
|
|
165
|
+
`${"regime".padEnd(10)}${"model".padEnd(40)}gates fails`,
|
|
166
|
+
...d.verdicts.by_regime_model.map(
|
|
167
|
+
(r) =>
|
|
168
|
+
`${r.regime.padEnd(10)}${r.model.padEnd(40)}${String(r.gates).padStart(5)} ${String(r.fails).padStart(5)}`,
|
|
169
|
+
),
|
|
170
|
+
]),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
lines.push("");
|
|
174
|
+
|
|
175
|
+
// 8. Sources
|
|
176
|
+
lines.push(
|
|
177
|
+
section(
|
|
178
|
+
"SOURCES",
|
|
179
|
+
d.sources.map(
|
|
180
|
+
(s) => `${s.name.padEnd(10)} ${s.ok ? "ok" : "!!"} ${s.detail}`,
|
|
181
|
+
),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (d.skipped_malformed > 0) {
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push(` (${d.skipped_malformed} malformed log lines skipped)`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
package/src/gate.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// gateOnce is the core review-verdict logic — still live, called by the MCP
|
|
2
|
+
// verdict_submit tool (src/mcp/tools/verdict.ts). The CLI wrapper (cmdGate)
|
|
3
|
+
// was removed with the rest of the execute→review→fix loop (Wave 2).
|
|
4
|
+
import {
|
|
5
|
+
addEvent,
|
|
6
|
+
getRun,
|
|
7
|
+
incrementRound,
|
|
8
|
+
loadConfig,
|
|
9
|
+
openDb,
|
|
10
|
+
setStatus,
|
|
11
|
+
} from "./db/index.js";
|
|
12
|
+
import { closeMemory, kickoffMemory } from "./memory.js";
|
|
13
|
+
import { isPassFamily, type VerdictGrade } from "./parse.js";
|
|
14
|
+
|
|
15
|
+
export interface GateResult {
|
|
16
|
+
runId: number;
|
|
17
|
+
status: "passed" | "fixing" | "stopped" | "stalled";
|
|
18
|
+
round?: number;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Core gate logic — does NOT call process.exit.
|
|
24
|
+
* CLI wrapper (cmdGate) is responsible for exit codes.
|
|
25
|
+
*
|
|
26
|
+
* Verdict routing (3 groups):
|
|
27
|
+
* pass-family (pass-excellent|pass-good|pass-adequate|pass) → passed
|
|
28
|
+
* fail → fixing (+ round cap check → stalled)
|
|
29
|
+
* uncertain → stopped (same shape as round-cap path)
|
|
30
|
+
*/
|
|
31
|
+
export function gateOnce(
|
|
32
|
+
runId: number,
|
|
33
|
+
verdict: VerdictGrade,
|
|
34
|
+
note: string,
|
|
35
|
+
files?: string[],
|
|
36
|
+
): GateResult {
|
|
37
|
+
const db = openDb();
|
|
38
|
+
const run = getRun(db, runId);
|
|
39
|
+
if (!run) {
|
|
40
|
+
return { runId, status: "stopped", error: `run ${runId} not found` };
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
run.status === "passed" ||
|
|
44
|
+
run.status === "stopped" ||
|
|
45
|
+
run.status === "stalled"
|
|
46
|
+
) {
|
|
47
|
+
return {
|
|
48
|
+
runId,
|
|
49
|
+
status: run.status,
|
|
50
|
+
round: run.round,
|
|
51
|
+
error: `run ${runId} is already ${run.status}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
// runs.worktree เก็บ absolute path (SERVER_INSTRUCTIONS บังคับ) — ไม่ใช่ key ของ config.worktrees
|
|
57
|
+
// การ lookup จึงพลาดเสมอ และ fallback เดิม "." ทำให้คำสั่ง memory ของโปรเจกต์ไปรันใน cwd ของ
|
|
58
|
+
// MCP server แทนที่จะเป็นโปรเจกต์นั้น · lookup ยังอยู่เผื่อ row เก่าที่เคยเก็บเป็น key
|
|
59
|
+
const worktree = config.worktrees[run.worktree] ?? run.worktree;
|
|
60
|
+
|
|
61
|
+
// --- pass family (4 grades) ---
|
|
62
|
+
if (isPassFamily(verdict)) {
|
|
63
|
+
setStatus(db, runId, "passed");
|
|
64
|
+
// round = run's current round at review time (pre-increment for fail).
|
|
65
|
+
addEvent(db, runId, "gate", {
|
|
66
|
+
verdict,
|
|
67
|
+
note,
|
|
68
|
+
round: run.round,
|
|
69
|
+
...(files && files.length > 0 ? { files } : {}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (run.mem_id && config.memory) {
|
|
73
|
+
closeMemory(config, worktree, run.mem_id, note || `run ${runId} passed`);
|
|
74
|
+
addEvent(db, runId, "memory_claim_closed", { mem_id: run.mem_id });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const kickoff = kickoffMemory(config, worktree);
|
|
78
|
+
if (kickoff) console.log(kickoff);
|
|
79
|
+
|
|
80
|
+
return { runId, status: "passed", round: run.round };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- uncertain → stop (plan problem, same as round-cap) ---
|
|
84
|
+
if (verdict === "uncertain") {
|
|
85
|
+
setStatus(db, runId, "stopped");
|
|
86
|
+
addEvent(db, runId, "gate", {
|
|
87
|
+
verdict,
|
|
88
|
+
note,
|
|
89
|
+
round: run.round,
|
|
90
|
+
...(files && files.length > 0 ? { files } : {}),
|
|
91
|
+
});
|
|
92
|
+
addEvent(db, runId, "stop", { reason: "verdict_uncertain" });
|
|
93
|
+
|
|
94
|
+
if (run.mem_id) {
|
|
95
|
+
closeMemory(
|
|
96
|
+
config,
|
|
97
|
+
worktree,
|
|
98
|
+
run.mem_id,
|
|
99
|
+
`run ${runId} stopped — uncertain verdict`,
|
|
100
|
+
);
|
|
101
|
+
addEvent(db, runId, "memory_claim_closed", { mem_id: run.mem_id });
|
|
102
|
+
}
|
|
103
|
+
return { runId, status: "stopped", round: run.round };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- fail → back to executor, one more round ---
|
|
107
|
+
incrementRound(db, runId);
|
|
108
|
+
setStatus(db, runId, "fixing");
|
|
109
|
+
addEvent(db, runId, "gate", {
|
|
110
|
+
verdict,
|
|
111
|
+
note,
|
|
112
|
+
round: run.round,
|
|
113
|
+
...(files && files.length > 0 ? { files } : {}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const updated = getRun(db, runId);
|
|
117
|
+
if (!updated) {
|
|
118
|
+
setStatus(db, runId, "stopped");
|
|
119
|
+
return {
|
|
120
|
+
runId,
|
|
121
|
+
status: "stopped",
|
|
122
|
+
error: `run ${runId} disappeared after update`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (updated.round > config.review.maxRounds) {
|
|
127
|
+
// Round cap reached — the plan is the problem, stall for real. Persist it
|
|
128
|
+
// (else the run sits in 'fixing' forever) and release the memory claim.
|
|
129
|
+
// stalled (not stopped): tells the caller to take it back to the human
|
|
130
|
+
// instead of sending the agent for another fix round.
|
|
131
|
+
setStatus(db, runId, "stalled");
|
|
132
|
+
addEvent(db, runId, "stop", {
|
|
133
|
+
reason: `round ${updated.round} > maxRounds ${config.review.maxRounds}`,
|
|
134
|
+
});
|
|
135
|
+
if (run.mem_id) {
|
|
136
|
+
closeMemory(
|
|
137
|
+
config,
|
|
138
|
+
worktree,
|
|
139
|
+
run.mem_id,
|
|
140
|
+
`run ${runId} stalled at round cap`,
|
|
141
|
+
);
|
|
142
|
+
addEvent(db, runId, "memory_claim_closed", { mem_id: run.mem_id });
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
runId,
|
|
146
|
+
status: "stalled",
|
|
147
|
+
round: updated.round,
|
|
148
|
+
error: `round ${updated.round} > maxRounds ${config.review.maxRounds} — stop fixing and take it back to the human, the plan likely has a problem`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { runId, status: "fixing", round: updated.round };
|
|
153
|
+
}
|