fapony 0.1.3 → 0.2.1
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/README.md +111 -46
- package/fapony.ts +35 -2
- package/package.json +3 -2
- package/skill/move-to-done/SKILL.md +18 -28
- package/skill/plan-with-pony/SKILL.md +8 -3
- package/src/analyze.ts +175 -5
- package/src/conventions-seed.ts +3 -3
- package/src/db/defaults.ts +13 -5
- package/src/db/getters.ts +11 -9
- package/src/db/load.ts +2 -2
- package/src/db/store.ts +0 -75
- package/src/db/types.ts +2 -4
- package/src/debt.ts +176 -36
- package/src/digest/collect.ts +19 -2
- package/src/digest/text.ts +25 -0
- package/src/hook.ts +665 -24
- package/src/init-mem.ts +123 -37
- package/src/init.ts +57 -39
- package/src/install/claude.ts +44 -8
- package/src/install/codex.ts +105 -13
- package/src/install/opencode.ts +169 -5
- package/src/install.ts +2 -1
- package/src/lint-baseline.ts +3 -8
- package/src/mcp/evidence.ts +15 -3
- package/src/mcp/tools/index.ts +97 -185
- package/src/mcp/tools/mem.ts +153 -11
- package/src/mcp/transport.ts +5 -13
- package/src/mem/commands/read.ts +448 -0
- package/src/mem/commands/where.ts +56 -0
- package/{templates → src}/mem/commands/write.ts +12 -5
- package/src/mem/index.ts +144 -0
- package/src/mem/store.ts +350 -0
- package/src/memory.ts +238 -40
- package/src/plan-seed.ts +26 -6
- package/src/review-seed.ts +19 -0
- package/src/setup.ts +7 -8
- package/src/stats/data.ts +40 -104
- package/src/stats/format.ts +8 -9
- package/src/stats/index.ts +0 -1
- package/templates/PLAN.md +1 -0
- package/src/mcp/tools/context.ts +0 -66
- package/src/mcp/tools/plans.ts +0 -255
- package/src/mcp/tools/stats.ts +0 -96
- package/templates/mem/commands/read.ts +0 -194
- package/templates/mem/commands/selftest.ts +0 -450
- package/templates/mem/mem.ts +0 -68
- package/templates/mem/store.ts +0 -285
- /package/{templates → src}/mem/commands/plan.ts +0 -0
- /package/{templates → src}/mem/commands/rotate.ts +0 -0
- /package/{templates → src}/mem/render.ts +0 -0
- /package/{templates → src}/mem/selectors.ts +0 -0
package/src/plan-seed.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { basename, join, relative, resolve, sep } from "node:path";
|
|
|
38
38
|
import { collectSourceFiles, isSkippedDir, SCAN_EXTS } from "./analyze.js";
|
|
39
39
|
import { computeModelFit } from "./context/projectHealth.js";
|
|
40
40
|
import { doneDir, planDir, specDir } from "./db/getters.js";
|
|
41
|
+
import { CONFIG_FILENAME } from "./db/index.js";
|
|
41
42
|
import { loadConfig } from "./db/load.js";
|
|
42
43
|
import type { Config } from "./db/types.js";
|
|
43
44
|
import { extractExports } from "./map.js";
|
|
@@ -97,7 +98,7 @@ function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
|
|
|
97
98
|
if (keys.length === 0) return placeholder;
|
|
98
99
|
|
|
99
100
|
const hits: { shipped: string; line: string }[] = [];
|
|
100
|
-
for (const dir of [doneDir(config), specDir(
|
|
101
|
+
for (const dir of [doneDir(config), specDir()]) {
|
|
101
102
|
const abs = join(cwd, dir);
|
|
102
103
|
let names: string[];
|
|
103
104
|
try {
|
|
@@ -181,6 +182,7 @@ function planTemplate(
|
|
|
181
182
|
priorArt: string,
|
|
182
183
|
contextFapony: string,
|
|
183
184
|
specLink: string | null,
|
|
185
|
+
planRel: string,
|
|
184
186
|
): string {
|
|
185
187
|
return `---
|
|
186
188
|
kind: unit
|
|
@@ -216,8 +218,20 @@ _(agent fills in)_
|
|
|
216
218
|
_(agent fills in)_
|
|
217
219
|
|
|
218
220
|
## 6. Steps (what in which order)
|
|
221
|
+
One step = one chunk = one session: finish it, close it, **stop** — starting the
|
|
222
|
+
next step in the same session is what rule 9 forbids.
|
|
223
|
+
|
|
219
224
|
1. _(agent fills in — each step must be verifiable)_
|
|
220
225
|
|
|
226
|
+
**Closing a step:** tick its TL;DR box with the sha · \`git commit\` this step's
|
|
227
|
+
files only · \`verdict_submit\` (MCP) with this step's \`regime\` · then hand off:
|
|
228
|
+
|
|
229
|
+
\`\`\`bash
|
|
230
|
+
fapony mem add note "<what chunk N+1 must know>" --files <f1,f2> ${planRel}
|
|
231
|
+
\`\`\`
|
|
232
|
+
|
|
233
|
+
Next session opens with \`kickoff ${planRel}\` (or \`kickoff ${basename(planRel)}\` — kickoff resolves by filename too, so no need to retype the path).
|
|
234
|
+
|
|
221
235
|
## 7. Examples
|
|
222
236
|
${
|
|
223
237
|
specLink
|
|
@@ -511,7 +525,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
511
525
|
process.exit(1);
|
|
512
526
|
}
|
|
513
527
|
const cwd = process.cwd();
|
|
514
|
-
const config = loadConfig(join(cwd,
|
|
528
|
+
const config = loadConfig(join(cwd, CONFIG_FILENAME));
|
|
515
529
|
// Resolve the git worktree root so mem/ledger queries hit the same key
|
|
516
530
|
// state.db uses (git rev-parse --show-toplevel). Running from a subdir
|
|
517
531
|
// would otherwise mismatch: stats return empty, Context (fapony) always
|
|
@@ -555,7 +569,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
555
569
|
);
|
|
556
570
|
}
|
|
557
571
|
|
|
558
|
-
const planDirAbs = join(cwd, planDir(
|
|
572
|
+
const planDirAbs = join(cwd, planDir());
|
|
559
573
|
const planPath = join(planDirAbs, `PLAN-${name}.md`);
|
|
560
574
|
if (existsSync(planPath)) {
|
|
561
575
|
console.error(
|
|
@@ -569,7 +583,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
569
583
|
|
|
570
584
|
let specLink: string | null = null;
|
|
571
585
|
if (withSpec) {
|
|
572
|
-
const specDirAbs = join(cwd, specDir(
|
|
586
|
+
const specDirAbs = join(cwd, specDir());
|
|
573
587
|
const specPath = join(specDirAbs, `SPEC-${name}.md`);
|
|
574
588
|
if (existsSync(specPath)) {
|
|
575
589
|
console.error(
|
|
@@ -589,13 +603,19 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
589
603
|
: null,
|
|
590
604
|
),
|
|
591
605
|
);
|
|
592
|
-
specLink = `../${specDir(
|
|
606
|
+
specLink = `../${specDir().split("/").pop()}/SPEC-${name}.md`;
|
|
593
607
|
}
|
|
594
608
|
|
|
595
609
|
mkdirSync(planDirAbs, { recursive: true });
|
|
596
610
|
writeFileSync(
|
|
597
611
|
planPath,
|
|
598
|
-
planTemplate(
|
|
612
|
+
planTemplate(
|
|
613
|
+
name,
|
|
614
|
+
priorArt,
|
|
615
|
+
contextFapony,
|
|
616
|
+
specLink,
|
|
617
|
+
`${planDir()}/PLAN-${name}.md`,
|
|
618
|
+
),
|
|
599
619
|
);
|
|
600
620
|
console.log(`wrote ${planPath}${specLink ? ` + SPEC-${name}.md` : ""}`);
|
|
601
621
|
}
|
package/src/review-seed.ts
CHANGED
|
@@ -270,6 +270,10 @@ function expandFilesScope(
|
|
|
270
270
|
const notFound: string[] = [];
|
|
271
271
|
const emptyDirs: string[] = [];
|
|
272
272
|
const dirs: string[] = [];
|
|
273
|
+
// Per-dir top-level subdir counts, gathered while walking — cheap because
|
|
274
|
+
// it reuses `rels` already collected below; only printed if the cap cuts
|
|
275
|
+
// something, so a scope that fits never pays for it in the output.
|
|
276
|
+
const breakdowns: string[] = [];
|
|
273
277
|
let dirExpanded = false;
|
|
274
278
|
let cutNamed = 0;
|
|
275
279
|
let cutExpanded = 0;
|
|
@@ -310,6 +314,20 @@ function expandFilesScope(
|
|
|
310
314
|
dirExpanded = true;
|
|
311
315
|
const rels = collectSourceFiles(join(worktree, p));
|
|
312
316
|
if (rels.length === 0) emptyDirs.push(p);
|
|
317
|
+
if (rels.length > MAX_CHANGED_FILES) {
|
|
318
|
+
const counts = new Map<string, number>();
|
|
319
|
+
for (const r of rels) {
|
|
320
|
+
const top = r.includes("/") ? r.slice(0, r.indexOf("/")) : "(root)";
|
|
321
|
+
counts.set(top, (counts.get(top) ?? 0) + 1);
|
|
322
|
+
}
|
|
323
|
+
const list = [...counts.entries()]
|
|
324
|
+
.sort((a, b) => b[1] - a[1])
|
|
325
|
+
.map(
|
|
326
|
+
([name, count]) => `${p === "." ? name : `${p}/${name}`} (${count})`,
|
|
327
|
+
)
|
|
328
|
+
.join(" · ");
|
|
329
|
+
breakdowns.push(`${p} (${rels.length} files) → ${list}`);
|
|
330
|
+
}
|
|
313
331
|
for (const r of rels) add(p === "." ? r : `${p}/${r}`, true);
|
|
314
332
|
}
|
|
315
333
|
if (notFound.length > 0) {
|
|
@@ -329,6 +347,7 @@ function expandFilesScope(
|
|
|
329
347
|
notes.push(
|
|
330
348
|
`… +${cutExpanded} more file(s) under the expanded dirs — capped at ${MAX_CHANGED_FILES}, narrow the scope`,
|
|
331
349
|
);
|
|
350
|
+
for (const b of breakdowns) notes.push(` ${b}`);
|
|
332
351
|
}
|
|
333
352
|
return { files, notes, dirExpanded };
|
|
334
353
|
}
|
package/src/setup.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, statSync, writeFileSync } from "node:fs";
|
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { seedConventionsFile } from "./conventions-seed.js";
|
|
9
|
-
import {
|
|
9
|
+
import { CONFIG_FILENAME, FAPONY_DIR } from "./db/index.js";
|
|
10
10
|
import { initProject } from "./init.js";
|
|
11
11
|
import { isAffirmative } from "./util.js";
|
|
12
12
|
|
|
@@ -78,12 +78,11 @@ export function buildSetupConfig(a: SetupAnswers): Record<string, unknown> {
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
if (a.enableMemory) {
|
|
81
|
-
const memEntry = DEFAULT_MEMORY_ENTRY;
|
|
82
81
|
config.memory = {
|
|
83
|
-
claim: ["
|
|
84
|
-
close: ["
|
|
85
|
-
add: ["
|
|
86
|
-
kickoff: ["
|
|
82
|
+
claim: ["fapony", "mem", "claim", "{id}"],
|
|
83
|
+
close: ["fapony", "mem", "close", "{id}", "{msg}"],
|
|
84
|
+
add: ["fapony", "mem", "add", "{kind}", "{text}"],
|
|
85
|
+
kickoff: ["fapony", "mem", "kickoff"],
|
|
87
86
|
};
|
|
88
87
|
}
|
|
89
88
|
|
|
@@ -183,7 +182,7 @@ export async function cmdSetup(deps: SetupDeps = {}): Promise<void> {
|
|
|
183
182
|
enableMemory,
|
|
184
183
|
});
|
|
185
184
|
|
|
186
|
-
const configPath = join(process.cwd(),
|
|
185
|
+
const configPath = join(process.cwd(), CONFIG_FILENAME);
|
|
187
186
|
if (existsSync(configPath)) {
|
|
188
187
|
const overwrite = await askFn(
|
|
189
188
|
"⚠ fapony.config.json already exists. Overwrite? (y/n)",
|
|
@@ -199,7 +198,7 @@ export async function cmdSetup(deps: SetupDeps = {}): Promise<void> {
|
|
|
199
198
|
console.log(`\n ✓ Wrote ${configPath}`);
|
|
200
199
|
|
|
201
200
|
// --- scaffold worktree ---
|
|
202
|
-
const faponyDir = join(worktreePath,
|
|
201
|
+
const faponyDir = join(worktreePath, FAPONY_DIR);
|
|
203
202
|
if (!existsSync(faponyDir)) {
|
|
204
203
|
try {
|
|
205
204
|
initProject(worktreePath);
|
package/src/stats/data.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// src/stats/data.ts — StatsData shape + getStatsData()
|
|
2
2
|
|
|
3
|
-
import { readdirSync
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { type Event, openDb, type Run } from "../db/index.js";
|
|
5
|
+
import { type Event, openDb, PLAN_DIR, type Run } from "../db/index.js";
|
|
7
6
|
import { loadConfig } from "../db/load.js";
|
|
8
7
|
import { enrichGateWindows } from "../gates.js";
|
|
9
8
|
import { avg, minutesBetween } from "../math.js";
|
|
@@ -89,9 +88,7 @@ function enrichGates(
|
|
|
89
88
|
/**
|
|
90
89
|
* Count un-shipped plan files in a worktree's planDir.
|
|
91
90
|
*
|
|
92
|
-
*
|
|
93
|
-
* central config's worktrees map is optional and usually absent, and each repo
|
|
94
|
-
* picks its own plan dir (vela uses apps/vela/plan, not .fapony/plan).
|
|
91
|
+
* planDir is hardcoded to .fapony/plan — not configurable (gitignored = private).
|
|
95
92
|
*
|
|
96
93
|
* Returns null — never 0 — when the path isn't a readable directory, so a
|
|
97
94
|
* sentinel row like "mcp-external" renders as "—" instead of claiming
|
|
@@ -99,19 +96,10 @@ function enrichGates(
|
|
|
99
96
|
*/
|
|
100
97
|
export function countPendingPlans(worktree: string): number | null {
|
|
101
98
|
if (!worktree.startsWith("/")) return null;
|
|
102
|
-
let planDir = ".fapony/plan";
|
|
103
99
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
107
|
-
if (typeof cfg.paths?.planDir === "string" && cfg.paths.planDir)
|
|
108
|
-
planDir = cfg.paths.planDir;
|
|
109
|
-
} catch {
|
|
110
|
-
// no config (or unreadable/malformed) — fall back to the scaffold default
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
return readdirSync(join(worktree, planDir)).filter((f) => f.endsWith(".md"))
|
|
114
|
-
.length;
|
|
100
|
+
return readdirSync(join(worktree, PLAN_DIR)).filter((f) =>
|
|
101
|
+
f.endsWith(".md"),
|
|
102
|
+
).length;
|
|
115
103
|
} catch {
|
|
116
104
|
return null;
|
|
117
105
|
}
|
|
@@ -397,72 +385,6 @@ export function getPlanBreakdown(
|
|
|
397
385
|
.sort((a, b) => b.runs - a.runs);
|
|
398
386
|
}
|
|
399
387
|
|
|
400
|
-
export interface PlanLastVerdict {
|
|
401
|
-
plan: string;
|
|
402
|
-
runs: number;
|
|
403
|
-
lastVerdict: string;
|
|
404
|
-
/** null for a pass-family last verdict (gateReason only flags non-pass). */
|
|
405
|
-
lastReasonCode: string | null;
|
|
406
|
-
escalated: boolean;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Most recent gate verdict per plan string, for `plan_list` (mcp/tools/plans.ts)
|
|
411
|
-
* to join filesystem plan files against real run history — "2 runs, last:
|
|
412
|
-
* fail(spec_gap)" instead of a bare directory listing.
|
|
413
|
-
*/
|
|
414
|
-
export function getLastVerdictByPlan(
|
|
415
|
-
runs: Run[],
|
|
416
|
-
events: Event[],
|
|
417
|
-
maxRounds: number,
|
|
418
|
-
): PlanLastVerdict[] {
|
|
419
|
-
const runById = new Map(runs.map((r) => [r.id, r]));
|
|
420
|
-
const runCounts = new Map<string, number>();
|
|
421
|
-
const escalatedPlans = new Set<string>();
|
|
422
|
-
for (const r of runs) {
|
|
423
|
-
if (!r.plan) continue;
|
|
424
|
-
runCounts.set(r.plan, (runCounts.get(r.plan) ?? 0) + 1);
|
|
425
|
-
if (r.round > maxRounds) escalatedPlans.add(r.plan);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const lastGateByPlan = new Map<
|
|
429
|
-
string,
|
|
430
|
-
{ ts: string; verdict: string; reason: string | null }
|
|
431
|
-
>();
|
|
432
|
-
for (const e of events) {
|
|
433
|
-
if (e.kind !== "gate" || !e.data) continue;
|
|
434
|
-
const plan = runById.get(e.run_id)?.plan;
|
|
435
|
-
if (!plan) continue;
|
|
436
|
-
let verdict: string | null = null;
|
|
437
|
-
try {
|
|
438
|
-
const d = JSON.parse(e.data) as { verdict?: unknown };
|
|
439
|
-
if (typeof d.verdict === "string") verdict = d.verdict;
|
|
440
|
-
} catch {
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
if (!verdict) continue;
|
|
444
|
-
const prev = lastGateByPlan.get(plan);
|
|
445
|
-
if (!prev || e.ts >= prev.ts) {
|
|
446
|
-
lastGateByPlan.set(plan, {
|
|
447
|
-
ts: e.ts,
|
|
448
|
-
verdict,
|
|
449
|
-
reason: gateReason(e.data),
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return [...runCounts.keys()].map((plan) => {
|
|
455
|
-
const last = lastGateByPlan.get(plan);
|
|
456
|
-
return {
|
|
457
|
-
plan,
|
|
458
|
-
runs: runCounts.get(plan) ?? 0,
|
|
459
|
-
lastVerdict: last?.verdict ?? "(no gate yet)",
|
|
460
|
-
lastReasonCode: last?.reason ?? null,
|
|
461
|
-
escalated: escalatedPlans.has(plan),
|
|
462
|
-
};
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
|
|
466
388
|
/** Runs past the round cap — plan-quality signal, not code (CLAUDE.md #2). */
|
|
467
389
|
function getEscalatedRuns(runs: Run[], maxRounds: number): EscalatedRun[] {
|
|
468
390
|
return runs
|
|
@@ -600,25 +522,35 @@ export interface StatsData {
|
|
|
600
522
|
}
|
|
601
523
|
|
|
602
524
|
/**
|
|
603
|
-
* Charge a session's token
|
|
525
|
+
* Charge a gate its share of its session's token total: 1/N, N = that
|
|
526
|
+
* session's gate count.
|
|
604
527
|
*
|
|
605
|
-
* Tokens are a per-session total
|
|
606
|
-
* gates (measured here: 35 sessions behind 58 gates, up to 5
|
|
607
|
-
*
|
|
608
|
-
*
|
|
528
|
+
* Tokens are a per-session total and one session routinely produces several
|
|
529
|
+
* gates (measured here: 35 sessions behind 58 gates, up to 5 in one), so
|
|
530
|
+
* summing the full total per gate would multiply it by the gate count.
|
|
531
|
+
* Charging it once per bucket instead — what this did until 2026-09-19 — is
|
|
532
|
+
* right within one table and wrong across tables: a session spanning several
|
|
533
|
+
* regimes charged its whole total to every regime it touched. Measured then,
|
|
534
|
+
* claude-opus-5 totalled 218.5M input in by-model while its five by-regime
|
|
535
|
+
* rows summed to 847M (3.9x), and the inflation scaled with how many regimes
|
|
536
|
+
* a model was used in — so `--mode verdict` ranked the broadly-used models as
|
|
537
|
+
* the expensive ones. Shares sum back to the session total in every table,
|
|
538
|
+
* which is why by-model is unchanged by this: one session is one model, so
|
|
539
|
+
* its N shares land in a single bucket.
|
|
609
540
|
*/
|
|
610
|
-
function
|
|
611
|
-
bucket: {
|
|
541
|
+
function addGateTokenShare(
|
|
542
|
+
bucket: { tokensInput: number; tokensOutput: number },
|
|
612
543
|
g: {
|
|
613
544
|
sessionId: string | null;
|
|
614
545
|
tokensInput: number | null;
|
|
615
546
|
tokensOutput: number | null;
|
|
616
547
|
},
|
|
548
|
+
gatesPerSession: Map<string, number>,
|
|
617
549
|
): void {
|
|
618
|
-
if (!g.sessionId
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
550
|
+
if (!g.sessionId) return;
|
|
551
|
+
const n = gatesPerSession.get(g.sessionId) ?? 1;
|
|
552
|
+
bucket.tokensInput += (g.tokensInput ?? 0) / n;
|
|
553
|
+
bucket.tokensOutput += (g.tokensOutput ?? 0) / n;
|
|
622
554
|
}
|
|
623
555
|
|
|
624
556
|
/**
|
|
@@ -733,6 +665,16 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
733
665
|
// --- Gate enrichment ---
|
|
734
666
|
const wtByRun = new Map(runs.map((r) => [r.id, r.worktree]));
|
|
735
667
|
const enriched = enrichGates(events, wtByRun);
|
|
668
|
+
// Denominator for the per-gate token share — see addGateTokenShare.
|
|
669
|
+
const gatesPerSession = new Map<string, number>();
|
|
670
|
+
for (const g of enriched) {
|
|
671
|
+
if (g.sessionId) {
|
|
672
|
+
gatesPerSession.set(
|
|
673
|
+
g.sessionId,
|
|
674
|
+
(gatesPerSession.get(g.sessionId) ?? 0) + 1,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
736
678
|
|
|
737
679
|
// Group by worktree+client+provider+model+agent — the same model name on two
|
|
738
680
|
// providers is two different things. Unknown dimension → "—" (never ""
|
|
@@ -749,7 +691,6 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
749
691
|
fails: number;
|
|
750
692
|
passes: number;
|
|
751
693
|
qualities: number[];
|
|
752
|
-
seen: Set<string>;
|
|
753
694
|
tokensInput: number;
|
|
754
695
|
tokensOutput: number;
|
|
755
696
|
}
|
|
@@ -771,7 +712,6 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
771
712
|
fails: 0,
|
|
772
713
|
passes: 0,
|
|
773
714
|
qualities: [],
|
|
774
|
-
seen: new Set(),
|
|
775
715
|
tokensInput: 0,
|
|
776
716
|
tokensOutput: 0,
|
|
777
717
|
});
|
|
@@ -780,7 +720,7 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
780
720
|
if (g.verdict && isPassFamily(g.verdict)) bucket.passes++;
|
|
781
721
|
const grade = g.verdict as VerdictGrade;
|
|
782
722
|
if (VERDICT_GRADES.has(grade)) bucket.qualities.push(qualityScore(grade));
|
|
783
|
-
|
|
723
|
+
addGateTokenShare(bucket, g, gatesPerSession);
|
|
784
724
|
}
|
|
785
725
|
const byModel = Object.values(modelMap)
|
|
786
726
|
.map((b) => ({
|
|
@@ -846,7 +786,6 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
846
786
|
fails: number;
|
|
847
787
|
passes: number;
|
|
848
788
|
qualities: number[];
|
|
849
|
-
seen: Set<string>;
|
|
850
789
|
tokensInput: number;
|
|
851
790
|
tokensOutput: number;
|
|
852
791
|
}
|
|
@@ -864,7 +803,6 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
864
803
|
fails: 0,
|
|
865
804
|
passes: 0,
|
|
866
805
|
qualities: [],
|
|
867
|
-
seen: new Set<string>(),
|
|
868
806
|
tokensInput: 0,
|
|
869
807
|
tokensOutput: 0,
|
|
870
808
|
});
|
|
@@ -873,7 +811,7 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
873
811
|
if (g.verdict && isPassFamily(g.verdict)) bucket.passes++;
|
|
874
812
|
const grade = g.verdict as VerdictGrade;
|
|
875
813
|
if (VERDICT_GRADES.has(grade)) bucket.qualities.push(qualityScore(grade));
|
|
876
|
-
|
|
814
|
+
addGateTokenShare(bucket, g, gatesPerSession);
|
|
877
815
|
}
|
|
878
816
|
const byPlanMode = Object.values(planModeMap)
|
|
879
817
|
.map((b) => ({
|
|
@@ -919,7 +857,6 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
919
857
|
fails: number;
|
|
920
858
|
passes: number;
|
|
921
859
|
qualities: number[];
|
|
922
|
-
seen: Set<string>;
|
|
923
860
|
tokensInput: number;
|
|
924
861
|
tokensOutput: number;
|
|
925
862
|
}
|
|
@@ -937,7 +874,6 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
937
874
|
fails: 0,
|
|
938
875
|
passes: 0,
|
|
939
876
|
qualities: [],
|
|
940
|
-
seen: new Set<string>(),
|
|
941
877
|
tokensInput: 0,
|
|
942
878
|
tokensOutput: 0,
|
|
943
879
|
});
|
|
@@ -946,7 +882,7 @@ export function getStatsData(worktree?: string): StatsData {
|
|
|
946
882
|
if (g.verdict && isPassFamily(g.verdict)) bucket.passes++;
|
|
947
883
|
const grade = g.verdict as VerdictGrade;
|
|
948
884
|
if (VERDICT_GRADES.has(grade)) bucket.qualities.push(qualityScore(grade));
|
|
949
|
-
|
|
885
|
+
addGateTokenShare(bucket, g, gatesPerSession);
|
|
950
886
|
}
|
|
951
887
|
const byRegime = Object.values(regimeMap)
|
|
952
888
|
.map((b) => ({
|
package/src/stats/format.ts
CHANGED
|
@@ -192,28 +192,27 @@ export function formatStatsText(data: StatsData): string {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// No token columns here: cost per regime is what `--mode verdict` ranks on,
|
|
196
|
+
// and printing it twice invited reading this table as the cost answer when
|
|
197
|
+
// it is the coverage one. Quality and fails are what this table adds.
|
|
195
198
|
if (data.byRegime.length > 0) {
|
|
196
199
|
const showWt = !data.scope;
|
|
197
200
|
lines.push("\nby regime:");
|
|
198
201
|
if (showWt) {
|
|
199
202
|
lines.push(
|
|
200
|
-
" project | regime | model | gates | fails | failRate |
|
|
203
|
+
" project | regime | model | gates | fails | failRate | avgQuality",
|
|
201
204
|
);
|
|
202
205
|
lines.push(
|
|
203
|
-
"
|
|
206
|
+
" ---------|--------|-------|-------|-------|----------|------------",
|
|
204
207
|
);
|
|
205
208
|
} else {
|
|
206
|
-
lines.push(
|
|
207
|
-
|
|
208
|
-
);
|
|
209
|
-
lines.push(
|
|
210
|
-
" --------|-------|-------|-------|-------|----------|-------------|------------|----------------",
|
|
211
|
-
);
|
|
209
|
+
lines.push(" regime | model | gates | fails | failRate | avgQuality");
|
|
210
|
+
lines.push(" --------|-------|-------|-------|----------|------------");
|
|
212
211
|
}
|
|
213
212
|
for (const r of data.byRegime) {
|
|
214
213
|
const wt = showWt ? `${shortWt(r.worktree).padEnd(9)} | ` : "";
|
|
215
214
|
lines.push(
|
|
216
|
-
` ${wt}${r.regime.padEnd(7)} | ${r.model.padEnd(5)} | ${String(r.gates).padStart(5)} | ${String(r.fails).padStart(5)} | ${fmtRate(r.failRate).padStart(8)} | ${
|
|
215
|
+
` ${wt}${r.regime.padEnd(7)} | ${r.model.padEnd(5)} | ${String(r.gates).padStart(5)} | ${String(r.fails).padStart(5)} | ${fmtRate(r.failRate).padStart(8)} | ${r.avgQuality.toFixed(1).padStart(10)}`,
|
|
217
216
|
);
|
|
218
217
|
}
|
|
219
218
|
}
|
package/src/stats/index.ts
CHANGED
package/templates/PLAN.md
CHANGED
|
@@ -11,6 +11,7 @@ blocked_by: <plan or sentence> # required when status: blocked
|
|
|
11
11
|
blocks: PLAN-<other>.md # plans that cannot start until this one lands (comma-separated)
|
|
12
12
|
superseded_by: PLAN-<other>.md # required when status: superseded
|
|
13
13
|
spec: SPEC-<feature>.md # if any
|
|
14
|
+
priority: high # optional: high = appears first in kickoff · omit = normal
|
|
14
15
|
---
|
|
15
16
|
|
|
16
17
|
# PLAN-<feature>.md — <short name>
|
package/src/mcp/tools/context.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// src/mcp/tools/context.ts — project_health_context tool
|
|
2
|
-
//
|
|
3
|
-
// Single call, any caller: returns the paste-ready "known patterns" block built
|
|
4
|
-
// from real run history, keyed by files[] (no raw dump, ~15 lines max). Not a
|
|
5
|
-
// pre-edit reflex — most files have no history (see CLAUDE.md rule 8); it earns
|
|
6
|
-
// its call on a file that does. `plan-with-pony` is one caller, not the only one.
|
|
7
|
-
//
|
|
8
|
-
// It also surfaces project decisions from the mem log — that read is the only
|
|
9
|
-
// I/O here; `buildProjectHealthContext` stays pure and just renders.
|
|
10
|
-
|
|
11
|
-
import { blastRadiusForWorktree } from "../../analyze.js";
|
|
12
|
-
import {
|
|
13
|
-
buildProjectHealthContext,
|
|
14
|
-
HUB_DEPENDENTS_MIN,
|
|
15
|
-
type HubEntry,
|
|
16
|
-
} from "../../context/index.js";
|
|
17
|
-
import { readRecentMemDecisions } from "../../memory.js";
|
|
18
|
-
import { getStatsData } from "../../stats/index.js";
|
|
19
|
-
import type { ToolResult } from "../types.js";
|
|
20
|
-
|
|
21
|
-
export function toolProjectHealthContext(
|
|
22
|
-
args: Record<string, unknown>,
|
|
23
|
-
): ToolResult {
|
|
24
|
-
const worktree =
|
|
25
|
-
typeof args.worktree === "string" && args.worktree
|
|
26
|
-
? args.worktree
|
|
27
|
-
: undefined;
|
|
28
|
-
const files =
|
|
29
|
-
Array.isArray(args.files) && args.files.length > 0
|
|
30
|
-
? args.files.filter(
|
|
31
|
-
(f): f is string => typeof f === "string" && f.length > 0,
|
|
32
|
-
)
|
|
33
|
-
: undefined;
|
|
34
|
-
|
|
35
|
-
// No worktree (or no log) → no decisions line, block still returns.
|
|
36
|
-
const memDecisions = worktree
|
|
37
|
-
? readRecentMemDecisions(worktree, 3, files).map((r) => ({
|
|
38
|
-
text: r.text,
|
|
39
|
-
spec: r.spec,
|
|
40
|
-
}))
|
|
41
|
-
: [];
|
|
42
|
-
|
|
43
|
-
// Hub detection needs both worktree (graph root) and files[] (what to map).
|
|
44
|
-
// Same live-graph cost handoff_check already pays; null/unreadable graph →
|
|
45
|
-
// no hub line, never a throw (PLAN-hub-signal §3).
|
|
46
|
-
const hubs: HubEntry[] =
|
|
47
|
-
worktree && files
|
|
48
|
-
? Object.entries(blastRadiusForWorktree(worktree, files) ?? {})
|
|
49
|
-
.filter(([, b]) => b.dependents >= HUB_DEPENDENTS_MIN)
|
|
50
|
-
.sort((a, b) => b[1].dependents - a[1].dependents)
|
|
51
|
-
.map(([file, b]) => ({
|
|
52
|
-
file,
|
|
53
|
-
dependents: b.dependents,
|
|
54
|
-
tested: b.tested,
|
|
55
|
-
transitive: b.transitive,
|
|
56
|
-
}))
|
|
57
|
-
: [];
|
|
58
|
-
|
|
59
|
-
const block = buildProjectHealthContext(getStatsData(), {
|
|
60
|
-
worktree,
|
|
61
|
-
files,
|
|
62
|
-
memDecisions,
|
|
63
|
-
hubs,
|
|
64
|
-
});
|
|
65
|
-
return { content: [{ type: "text", text: block }] };
|
|
66
|
-
}
|