fapony 0.1.2 → 0.2.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/README.md +94 -77
- package/fapony.ts +32 -1
- package/images/logo.png +0 -0
- package/images/logo.webp +0 -0
- package/images/logo@400.webp +0 -0
- package/images/sample.webp +0 -0
- package/images/summary.webp +0 -0
- package/package.json +5 -3
- package/skill/move-to-done/SKILL.md +18 -28
- package/skill/plan-with-pony/SKILL.md +8 -3
- package/src/analyze.ts +13 -1
- package/src/conventions-seed.ts +0 -1
- package/src/db/defaults.ts +1 -1
- package/src/db/getters.ts +3 -3
- package/src/db/store.ts +0 -75
- package/src/db/types.ts +1 -1
- package/src/debt.ts +169 -34
- package/src/digest/collect.ts +16 -0
- package/src/digest/text.ts +25 -0
- package/src/hook.ts +424 -16
- package/src/init-mem.ts +123 -37
- package/src/init.ts +51 -34
- package/src/install/claude.ts +7 -5
- package/src/install/opencode.ts +59 -5
- package/src/lint-baseline.ts +1 -7
- package/src/mcp/evidence.ts +1 -1
- 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 +348 -0
- package/src/memory.ts +237 -40
- package/src/plan-seed.ts +20 -1
- package/src/review-seed.ts +19 -0
- package/src/setup.ts +4 -6
- package/src/stats/data.ts +34 -86
- 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/db/getters.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_DONE_DIR,
|
|
3
3
|
DEFAULT_EVIDENCE_FILE,
|
|
4
|
-
|
|
4
|
+
DEFAULT_MEM_DIR,
|
|
5
5
|
DEFAULT_PLAN_DIR,
|
|
6
6
|
DEFAULT_SAFETY_DENY,
|
|
7
7
|
DEFAULT_SPEC_DIR,
|
|
@@ -24,8 +24,8 @@ export function doneDir(config?: Config): string {
|
|
|
24
24
|
return config?.paths?.doneDir ?? DEFAULT_DONE_DIR;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function
|
|
28
|
-
return config?.paths?.
|
|
27
|
+
export function memoryDir(config?: Config): string {
|
|
28
|
+
return config?.paths?.memDir ?? DEFAULT_MEM_DIR;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function evidenceFile(config?: Config): string {
|
package/src/db/store.ts
CHANGED
|
@@ -135,48 +135,10 @@ export function addEvent(
|
|
|
135
135
|
return Number(result.lastInsertRowid);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
/**
|
|
139
|
-
* Merge `patch` into an existing event's JSON data (keeps keys already set).
|
|
140
|
-
* Used to complete a spawn row with bytes_out/usd after the agent finishes —
|
|
141
|
-
* one row per spawn, timing (ts) stays at spawn start. No-op when the row
|
|
142
|
-
* is missing or its data isn't a JSON object.
|
|
143
|
-
*/
|
|
144
|
-
export function updateEventData(
|
|
145
|
-
db: Database,
|
|
146
|
-
eventId: number,
|
|
147
|
-
patch: Record<string, unknown>,
|
|
148
|
-
): void {
|
|
149
|
-
const row = db
|
|
150
|
-
.prepare("SELECT data FROM events WHERE id = ?")
|
|
151
|
-
.get(eventId) as { data: string | null } | null;
|
|
152
|
-
if (!row) return;
|
|
153
|
-
let base: Record<string, unknown> = {};
|
|
154
|
-
try {
|
|
155
|
-
const parsed = JSON.parse(row.data ?? "null") as unknown;
|
|
156
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
157
|
-
base = parsed as Record<string, unknown>;
|
|
158
|
-
}
|
|
159
|
-
} catch {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
db.prepare("UPDATE events SET data = ? WHERE id = ?").run(
|
|
163
|
-
JSON.stringify({ ...base, ...patch }),
|
|
164
|
-
eventId,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
138
|
export function getRun(db: Database, runId: number): Run | null {
|
|
169
139
|
return db.prepare("SELECT * FROM runs WHERE id = ?").get(runId) as Run | null;
|
|
170
140
|
}
|
|
171
141
|
|
|
172
|
-
export function getActiveRuns(db: Database): Run[] {
|
|
173
|
-
return db
|
|
174
|
-
.prepare(
|
|
175
|
-
"SELECT * FROM runs WHERE status NOT IN ('passed', 'stopped') ORDER BY id",
|
|
176
|
-
)
|
|
177
|
-
.all() as Run[];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
142
|
/**
|
|
181
143
|
* Latest still-open run for a worktree+plan pair, or null.
|
|
182
144
|
* Used by MCP verdict_submit to bind a round-2+ verdict to the original run
|
|
@@ -221,43 +183,6 @@ export function getEvents(db: Database, runId: number): Event[] {
|
|
|
221
183
|
.all(runId) as Event[];
|
|
222
184
|
}
|
|
223
185
|
|
|
224
|
-
/**
|
|
225
|
-
* Pulls the most recent unresolved "gate fail" note for a worktree+mem_id pair —
|
|
226
|
-
* i.e. the review feedback the next `fapony run` should hand back to the executor.
|
|
227
|
-
* Only looks at the latest run for that pair; if it already passed, returns null
|
|
228
|
-
* (nothing to carry forward).
|
|
229
|
-
*/
|
|
230
|
-
export function getPendingFeedback(
|
|
231
|
-
db: Database,
|
|
232
|
-
worktree: string,
|
|
233
|
-
memId: string,
|
|
234
|
-
excludeRunId?: number,
|
|
235
|
-
): string | null {
|
|
236
|
-
const run = db
|
|
237
|
-
.prepare(
|
|
238
|
-
`SELECT * FROM runs WHERE worktree = ? AND mem_id = ? AND id != ? ORDER BY id DESC LIMIT 1`,
|
|
239
|
-
)
|
|
240
|
-
.get(worktree, memId, excludeRunId ?? -1) as Run | null;
|
|
241
|
-
if (run?.status !== "fixing") return null;
|
|
242
|
-
|
|
243
|
-
const event = db
|
|
244
|
-
.prepare(
|
|
245
|
-
`SELECT * FROM events WHERE run_id = ? AND kind = 'gate' ORDER BY id DESC LIMIT 1`,
|
|
246
|
-
)
|
|
247
|
-
.get(run.id) as Event | null;
|
|
248
|
-
if (!event?.data) return null;
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
const parsed = JSON.parse(event.data) as {
|
|
252
|
-
verdict?: string;
|
|
253
|
-
note?: string;
|
|
254
|
-
};
|
|
255
|
-
return parsed.verdict === "fail" && parsed.note ? parsed.note : null;
|
|
256
|
-
} catch {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
186
|
/**
|
|
262
187
|
* Merge extra fields into the most recent gate event's data JSON.
|
|
263
188
|
* Used by MCP verdict_submit to add reason_code / source after gateOnce
|
package/src/db/types.ts
CHANGED
package/src/debt.ts
CHANGED
|
@@ -18,18 +18,16 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Read-only stdout: no file writes, no state.db, no cache (rule 5b).
|
|
20
20
|
|
|
21
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
22
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
21
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
22
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
23
23
|
import { collectSourceFiles } from "./analyze.js";
|
|
24
24
|
import { openDb } from "./db/index.js";
|
|
25
|
-
import {
|
|
25
|
+
import { readMemLog, resolveMemDir } from "./memory.js";
|
|
26
26
|
|
|
27
27
|
// A stale regex matching more than this many files is not a convention — it is
|
|
28
28
|
// a broken/wide regex (stale="e" would flag the repo). SPEC §6: drop the entry
|
|
29
29
|
// and say so, never report 600 files.
|
|
30
30
|
const DEBT_FILE_CAP = 250;
|
|
31
|
-
// Same cap for the file list printed per convention — a wall is not an answer.
|
|
32
|
-
const LIST_SHOWN = 40;
|
|
33
31
|
|
|
34
32
|
export interface Convention {
|
|
35
33
|
id: string;
|
|
@@ -51,7 +49,10 @@ export interface Convention {
|
|
|
51
49
|
// --- conventions.json resolution (same guess as the mem log, SPEC §2.1) ---
|
|
52
50
|
|
|
53
51
|
export function resolveConventionsPath(worktree: string): string | null {
|
|
54
|
-
|
|
52
|
+
// Conventions live in the same .fapony/ dir as the mem log — derive from
|
|
53
|
+
// the resolved mem dir so both resolvers cannot drift apart.
|
|
54
|
+
const memDir = resolveMemDir(worktree);
|
|
55
|
+
const base = memDir ? join(memDir, "..") : join(worktree, ".fapony");
|
|
55
56
|
const app = join(base, "conventions.json");
|
|
56
57
|
if (existsSync(app)) return app;
|
|
57
58
|
// Monorepo where the app has not scaffolded .fapony/ yet, and single repos
|
|
@@ -295,7 +296,7 @@ export function debtScan(
|
|
|
295
296
|
// Stop counting a runaway regex early — the entry will be dropped.
|
|
296
297
|
if (cur.length > DEBT_FILE_CAP) tooBroad.set(c.conv.id, cur.length);
|
|
297
298
|
}
|
|
298
|
-
if (c.okRe
|
|
299
|
+
if (c.okRe?.test(content)) {
|
|
299
300
|
moved.set(c.conv.id, (moved.get(c.conv.id) ?? 0) + 1);
|
|
300
301
|
}
|
|
301
302
|
}
|
|
@@ -509,41 +510,99 @@ export function formatPromotions(promotions: Promotion[]): string[] {
|
|
|
509
510
|
|
|
510
511
|
// --- Formatting ---
|
|
511
512
|
|
|
512
|
-
|
|
513
|
+
// Zone grouping: a zone is a file's *directory*, capped at this many leading
|
|
514
|
+
// segments — never a fixed-depth prefix of the path (which would cut into the
|
|
515
|
+
// filename) and never the filename itself. SPEC §4 shows zones at depth 5
|
|
516
|
+
// (`apps/mdl/src/server/services`) and depth 3 (`packages/cache/src`) in the
|
|
517
|
+
// same report, so the cap must follow the directory, not a constant.
|
|
518
|
+
const ZONE_DEPTH = 5;
|
|
519
|
+
// Default cap on zones shown per convention — more than this is a wall, not an answer.
|
|
520
|
+
const ZONE_CAP = 6;
|
|
521
|
+
|
|
522
|
+
/** The zone of a file: its directory path, capped at `depth` segments. */
|
|
523
|
+
function zoneOf(file: string, depth: number): string {
|
|
524
|
+
const parts = dirname(file)
|
|
525
|
+
.split("/")
|
|
526
|
+
.filter((p) => p && p !== ".");
|
|
527
|
+
return parts.slice(0, depth).join("/") || ".";
|
|
528
|
+
}
|
|
513
529
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
530
|
+
/** Group files by their directory zone (see `zoneOf`). */
|
|
531
|
+
function groupFilesByZone(
|
|
532
|
+
files: string[],
|
|
533
|
+
depth: number,
|
|
534
|
+
): Map<string, string[]> {
|
|
535
|
+
const zones = new Map<string, string[]>();
|
|
517
536
|
for (const f of files) {
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
} else {
|
|
523
|
-
cur = piece;
|
|
524
|
-
}
|
|
537
|
+
const zone = zoneOf(f, depth);
|
|
538
|
+
const cur = zones.get(zone) ?? [];
|
|
539
|
+
cur.push(f);
|
|
540
|
+
zones.set(zone, cur);
|
|
525
541
|
}
|
|
526
|
-
|
|
527
|
-
return
|
|
542
|
+
// Sort zones by file count descending, then alphabetically
|
|
543
|
+
return new Map(
|
|
544
|
+
[...zones.entries()].sort((a, b) => {
|
|
545
|
+
const d = b[1].length - a[1].length;
|
|
546
|
+
return d !== 0 ? d : a[0].localeCompare(b[0]);
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Escape a regex source for use in a shell grep command. */
|
|
552
|
+
function shellEscapeRe(src: string): string {
|
|
553
|
+
return src.replace(/'/g, "'\\''");
|
|
528
554
|
}
|
|
529
555
|
|
|
530
|
-
export function formatDebt(report: DebtReport): string {
|
|
556
|
+
export function formatDebt(report: DebtReport, showAll = false): string {
|
|
531
557
|
const lines: string[] = [];
|
|
532
558
|
lines.push(
|
|
533
559
|
`fapony debt — ${report.entries.length + report.declared.length + report.checkedCount} convention(s), ` +
|
|
534
560
|
`${report.scannedFiles} files scanned, ${report.ms}ms — derived fresh, not stored`,
|
|
535
561
|
);
|
|
536
562
|
for (const e of report.entries) {
|
|
537
|
-
const moved =
|
|
538
|
-
|
|
563
|
+
const moved =
|
|
564
|
+
e.movedCount !== null && e.files.length > 0
|
|
565
|
+
? ` · moved ${e.movedCount} (${Math.round((e.movedCount / (e.files.length + e.movedCount)) * 100)}%)`
|
|
566
|
+
: e.movedCount !== null
|
|
567
|
+
? ` · moved ${e.movedCount}`
|
|
568
|
+
: "";
|
|
569
|
+
lines.push(`\n${e.conv.id} — ${e.conv.rule}`);
|
|
570
|
+
// Show the patterns actually used
|
|
571
|
+
const patterns: string[] = [];
|
|
572
|
+
if (e.conv.stale) patterns.push(`stale: ${e.conv.stale}`);
|
|
573
|
+
if (e.conv.ok) patterns.push(`ok: ${e.conv.ok}`);
|
|
574
|
+
if (e.conv.guard) patterns.push(`guard: ${e.conv.guard}`);
|
|
575
|
+
patterns.push(`where ${e.conv.where}`);
|
|
576
|
+
lines.push(` ${patterns.join(" · ")}`);
|
|
539
577
|
if (e.files.length === 0) {
|
|
540
578
|
lines.push(` debt 0${moved} — clean`);
|
|
541
579
|
continue;
|
|
542
580
|
}
|
|
543
|
-
lines.push(` debt ${e.files.length}${moved}
|
|
544
|
-
|
|
545
|
-
if (e.
|
|
546
|
-
lines.push(`
|
|
581
|
+
lines.push(` debt ${e.files.length}${moved}`);
|
|
582
|
+
// Verify command derived from stale
|
|
583
|
+
if (e.conv.stale) {
|
|
584
|
+
lines.push(` verify: grep -rn '${shellEscapeRe(e.conv.stale)}' <zone>`);
|
|
585
|
+
}
|
|
586
|
+
// Zone grouping
|
|
587
|
+
const zones = groupFilesByZone(e.files, ZONE_DEPTH);
|
|
588
|
+
const zoneEntries = [...zones.entries()];
|
|
589
|
+
const cap = showAll
|
|
590
|
+
? zoneEntries.length
|
|
591
|
+
: Math.min(zoneEntries.length, ZONE_CAP);
|
|
592
|
+
let totalCapped = 0;
|
|
593
|
+
for (let i = 0; i < cap; i++) {
|
|
594
|
+
const [zone, zoneFiles] = zoneEntries[i];
|
|
595
|
+
const pad = " ".repeat(Math.max(0, 42 - zone.length));
|
|
596
|
+
lines.push(`\n ${zone}${pad}${zoneFiles.length} ไฟล์`);
|
|
597
|
+
lines.push(` ${zoneFiles.map((f) => f.split("/").pop()).join(" · ")}`);
|
|
598
|
+
totalCapped += zoneFiles.length;
|
|
599
|
+
}
|
|
600
|
+
if (zoneEntries.length > cap) {
|
|
601
|
+
const remaining = e.files.length - totalCapped;
|
|
602
|
+
const remainingZones = zoneEntries.length - cap;
|
|
603
|
+
lines.push(
|
|
604
|
+
`\n … อีก ${remainingZones} โซน (${remaining} ไฟล์) — fapony debt --id ${e.conv.id} --all`,
|
|
605
|
+
);
|
|
547
606
|
}
|
|
548
607
|
}
|
|
549
608
|
for (const c of report.declared) {
|
|
@@ -565,27 +624,66 @@ export function formatDebt(report: DebtReport): string {
|
|
|
565
624
|
|
|
566
625
|
// --- CLI ---
|
|
567
626
|
|
|
568
|
-
const USAGE =
|
|
569
|
-
|
|
570
|
-
|
|
627
|
+
const USAGE = `usage: fapony debt [path] [options]
|
|
628
|
+
--files f1,f2 check specific files instead of scanning
|
|
629
|
+
--id <conv> show only this convention
|
|
630
|
+
--where <path> narrow scope to files under this path
|
|
631
|
+
--all show all zones (default: cap at ${ZONE_CAP})
|
|
632
|
+
--json output raw JSON
|
|
633
|
+
-h, --help this help`;
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* The dir `debt` measures: the nearest ancestor of `arg` (or cwd) that holds
|
|
637
|
+
* `.fapony/conventions.json`, bounded by the git root.
|
|
638
|
+
*
|
|
639
|
+
* Jumping straight to the git root was the bug: in a monorepo the root has no
|
|
640
|
+
* conventions.json and two apps have one each, so the mem resolver went
|
|
641
|
+
* ambiguous and `debt` said "nothing tracked yet" while
|
|
642
|
+
* apps/<x>/.fapony/conventions.json sat right there — and the positional path
|
|
643
|
+
* argument was silently ignored. Falling back to the git root keeps single
|
|
644
|
+
* repos run from a subdir scanning the whole repo.
|
|
645
|
+
*/
|
|
646
|
+
export function worktreeOf(arg: string | undefined): string {
|
|
571
647
|
const base = resolve(arg ?? ".");
|
|
648
|
+
let gitRoot: string | null = null;
|
|
572
649
|
try {
|
|
573
650
|
const p = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
|
|
574
651
|
cwd: base,
|
|
575
652
|
stdout: "pipe",
|
|
576
653
|
stderr: "pipe",
|
|
577
654
|
});
|
|
578
|
-
if (p.exitCode === 0)
|
|
655
|
+
if (p.exitCode === 0) gitRoot = p.stdout.toString().trim() || null;
|
|
579
656
|
} catch {
|
|
580
|
-
//
|
|
657
|
+
// not a repo — base is all we have
|
|
658
|
+
}
|
|
659
|
+
// `git rev-parse` returns a physical path (/var → /private/var on macOS),
|
|
660
|
+
// so the boundary check compares realpaths, same as the mem resolver.
|
|
661
|
+
const real = (d: string): string => {
|
|
662
|
+
try {
|
|
663
|
+
return realpathSync(d);
|
|
664
|
+
} catch {
|
|
665
|
+
return d;
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
const boundary = gitRoot ? real(gitRoot) : null;
|
|
669
|
+
let dir = base;
|
|
670
|
+
while (true) {
|
|
671
|
+
if (existsSync(join(dir, ".fapony", "conventions.json"))) return dir;
|
|
672
|
+
if (boundary && real(dir) === boundary) break;
|
|
673
|
+
const parent = dirname(dir);
|
|
674
|
+
if (parent === dir) break;
|
|
675
|
+
dir = parent;
|
|
581
676
|
}
|
|
582
|
-
return base;
|
|
677
|
+
return gitRoot ?? base;
|
|
583
678
|
}
|
|
584
679
|
|
|
585
680
|
export function cmdDebt(args: string[]): void {
|
|
586
681
|
let path: string | undefined;
|
|
587
682
|
let filesMode: string[] | null = null;
|
|
588
683
|
let json = false;
|
|
684
|
+
let filterId: string | undefined;
|
|
685
|
+
let wherePath: string | undefined;
|
|
686
|
+
let showAll = false;
|
|
589
687
|
for (let i = 0; i < args.length; i++) {
|
|
590
688
|
const a = args[i];
|
|
591
689
|
if (a === "--files") {
|
|
@@ -603,6 +701,24 @@ export function cmdDebt(args: string[]): void {
|
|
|
603
701
|
console.error(`fapony debt: --files needs at least one path\n${USAGE}`);
|
|
604
702
|
process.exit(1);
|
|
605
703
|
}
|
|
704
|
+
} else if (a === "--id") {
|
|
705
|
+
const v = args[i + 1];
|
|
706
|
+
if (!v || v.startsWith("--")) {
|
|
707
|
+
console.error(`fapony debt: --id needs a convention id\n${USAGE}`);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
i++;
|
|
711
|
+
filterId = v;
|
|
712
|
+
} else if (a === "--where") {
|
|
713
|
+
const v = args[i + 1];
|
|
714
|
+
if (!v || v.startsWith("--")) {
|
|
715
|
+
console.error(`fapony debt: --where needs a path\n${USAGE}`);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
i++;
|
|
719
|
+
wherePath = v;
|
|
720
|
+
} else if (a === "--all") {
|
|
721
|
+
showAll = true;
|
|
606
722
|
} else if (a === "--json") {
|
|
607
723
|
json = true;
|
|
608
724
|
} else if (a === "-h" || a === "--help") {
|
|
@@ -653,6 +769,25 @@ export function cmdDebt(args: string[]): void {
|
|
|
653
769
|
return;
|
|
654
770
|
}
|
|
655
771
|
const report = debtScan(worktree, loaded);
|
|
772
|
+
|
|
773
|
+
// --id filter: keep only the named convention
|
|
774
|
+
if (filterId) {
|
|
775
|
+
report.entries = report.entries.filter((e) => e.conv.id === filterId);
|
|
776
|
+
report.declared = report.declared.filter((c) => c.id === filterId);
|
|
777
|
+
report.checkedCount = 0; // not relevant when filtering
|
|
778
|
+
report.dropped = report.dropped.filter((d) => d.id === filterId);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// --where filter: narrow file lists to paths under the given prefix
|
|
782
|
+
if (wherePath) {
|
|
783
|
+
const prefix = wherePath.replace(/\/+$/, "");
|
|
784
|
+
for (const e of report.entries) {
|
|
785
|
+
e.files = e.files.filter(
|
|
786
|
+
(f) => f === prefix || f.startsWith(`${prefix}/`),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
656
791
|
if (json) {
|
|
657
792
|
console.log(
|
|
658
793
|
JSON.stringify(
|
|
@@ -663,7 +798,7 @@ export function cmdDebt(args: string[]): void {
|
|
|
663
798
|
);
|
|
664
799
|
return;
|
|
665
800
|
}
|
|
666
|
-
console.log(formatDebt(report));
|
|
801
|
+
console.log(formatDebt(report, showAll));
|
|
667
802
|
for (const w of loaded.warnings) console.log(`⚠ ${w}`);
|
|
668
803
|
for (const l of formatPromotions(findPromotions(worktree, report))) {
|
|
669
804
|
console.log(l);
|
package/src/digest/collect.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
planDir,
|
|
14
14
|
type Run,
|
|
15
15
|
} from "../db/index.js";
|
|
16
|
+
import { computeHintImpact, type HintImpact, hintLogPath } from "../hook.js";
|
|
16
17
|
import { type MemRow, readMemLog } from "../memory.js";
|
|
17
18
|
import { isPassFamily, VERDICT_GRADES } from "../parse.js";
|
|
18
19
|
import { imputeResult, loadPrices } from "../price/index.js";
|
|
@@ -83,6 +84,8 @@ export interface DigestData {
|
|
|
83
84
|
/** % of those whose earliest in-period gate passed at round ≤ 1. */
|
|
84
85
|
round1_pct: number;
|
|
85
86
|
};
|
|
87
|
+
/** Hint-fire log counts for this worktree — null when no log exists. */
|
|
88
|
+
impact: HintImpact | null;
|
|
86
89
|
skipped_malformed: number;
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -586,6 +589,18 @@ export async function collectDigest(
|
|
|
586
589
|
detail: verdictResult.detail,
|
|
587
590
|
});
|
|
588
591
|
|
|
592
|
+
// 6. hint-fire log — annotate surface, this worktree only. A log that
|
|
593
|
+
// exists but fired 0 in the window is "0", not "no data" (done criteria 6).
|
|
594
|
+
const hasHintLog = existsSync(hintLogPath(worktree));
|
|
595
|
+
const impact = hasHintLog ? computeHintImpact(sinceIso, worktree) : null;
|
|
596
|
+
sources.push({
|
|
597
|
+
name: "hint-log",
|
|
598
|
+
ok: hasHintLog,
|
|
599
|
+
detail: impact
|
|
600
|
+
? `${impact.fired} fired this window (${impact.debt.shown} debt shown)`
|
|
601
|
+
: "no hints recorded",
|
|
602
|
+
});
|
|
603
|
+
|
|
589
604
|
// classify mem rows
|
|
590
605
|
const decisions = mem.rows.filter((r) => r.kind === "decision");
|
|
591
606
|
const notes = mem.rows.filter((r) => r.kind === "note");
|
|
@@ -620,6 +635,7 @@ export async function collectDigest(
|
|
|
620
635
|
units_graded: verdictResult.units_graded,
|
|
621
636
|
round1_pct: verdictResult.round1_pct,
|
|
622
637
|
},
|
|
638
|
+
impact,
|
|
623
639
|
skipped_malformed: skippedMalformed,
|
|
624
640
|
};
|
|
625
641
|
}
|
package/src/digest/text.ts
CHANGED
|
@@ -34,6 +34,23 @@ function fmtTokens(n: number): string {
|
|
|
34
34
|
return String(Math.round(n));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function impactLines(i: NonNullable<DigestData["impact"]>): string[] {
|
|
38
|
+
const b = i.by_surface;
|
|
39
|
+
const out = [
|
|
40
|
+
`hints fired: ${i.fired} (read ${b.read} · debt ${b.debt} · mem ${b.mem} · commit ${b.commit})`,
|
|
41
|
+
];
|
|
42
|
+
if (i.debt.shown > 0) {
|
|
43
|
+
const pct = Math.round((i.debt.resolved / i.debt.shown) * 100);
|
|
44
|
+
out.push(
|
|
45
|
+
`debt lines shown: ${i.debt.shown} · no longer present at HEAD: ${i.debt.resolved} (${pct}%) · unverifiable: ${i.debt.unknown}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
out.push(
|
|
49
|
+
"note: counts what fapony showed and what the repo looks like now, not proof the agent acted because of the hint",
|
|
50
|
+
);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
function costLine(c: CostRow): string {
|
|
38
55
|
const name = c.provider ? `${c.provider}/${c.model}` : c.model;
|
|
39
56
|
const costStr =
|
|
@@ -70,6 +87,14 @@ export function renderDigestText(d: DigestData): string {
|
|
|
70
87
|
);
|
|
71
88
|
lines.push("");
|
|
72
89
|
|
|
90
|
+
// 2b. fapony impact — what the annotate surfaces showed, not proof of use
|
|
91
|
+
lines.push(
|
|
92
|
+
section("FAPONY IMPACT", [
|
|
93
|
+
...(d.impact ? impactLines(d.impact) : ["(no hints recorded)"]),
|
|
94
|
+
]),
|
|
95
|
+
);
|
|
96
|
+
lines.push("");
|
|
97
|
+
|
|
73
98
|
// 3. Decisions
|
|
74
99
|
lines.push(
|
|
75
100
|
section(`DECISIONS (${d.decisions.length})`, [
|