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/analyze.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
// src/analyze.ts — `fapony analyze`: structural health diagnosis for a TS/JS project.
|
|
2
2
|
//
|
|
3
3
|
// One file on purpose (plan cap: ≤1 new file in src/). Computes a file-level
|
|
4
|
-
// import graph live with Bun.Transpiler.scan() —
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// import graph live with Bun.Transpiler.scan() — no new table. Read-only: never
|
|
5
|
+
// writes into the analyzed directory. buildGraphCached additionally mirrors the
|
|
6
|
+
// derived graph to the state dir (outside the worktree) as a best-effort cache
|
|
7
|
+
// for callers that run as repeated short-lived processes (hooks) — see below.
|
|
7
8
|
//
|
|
8
9
|
// Same module also serves handoff_check / verification_report: blastRadius()
|
|
9
10
|
// turns facts.files[] into per-file { dependents, tested } facts.
|
|
10
11
|
|
|
11
12
|
import type { Dirent } from "node:fs";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
renameSync,
|
|
19
|
+
statSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
} from "node:fs";
|
|
13
22
|
import { join, relative, resolve, sep } from "node:path";
|
|
14
23
|
import {
|
|
15
24
|
dirname as posixDirname,
|
|
@@ -17,6 +26,7 @@ import {
|
|
|
17
26
|
normalize as posixNormalize,
|
|
18
27
|
} from "node:path/posix";
|
|
19
28
|
|
|
29
|
+
import { faponyDir } from "./db/load.js";
|
|
20
30
|
import { extractExports } from "./map.js";
|
|
21
31
|
|
|
22
32
|
// --- Types (mirror SPEC-analyze-checkup.md) ---
|
|
@@ -132,7 +142,19 @@ export function isTestedThroughBarrels(
|
|
|
132
142
|
export const SCAN_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
133
143
|
|
|
134
144
|
// Always skipped, hardcoded — no config (per plan: no .faponyignore in v1).
|
|
135
|
-
|
|
145
|
+
// "templates" for the same reason knip.json ignores templates/**: those files
|
|
146
|
+
// ship as a template copied into other repos by `fapony init-mem` and never
|
|
147
|
+
// have real importers here — scanning them produces false wrapper/orphan
|
|
148
|
+
// signals (measured: conventions-seed flagged 8 "wrappers" that were all
|
|
149
|
+
// src/mem/commands/*.ts helpers matched against unrelated identically-
|
|
150
|
+
// named calls elsewhere in the repo, e.g. "cmdNow() instead of now(").
|
|
151
|
+
const SKIP_DIRS = new Set([
|
|
152
|
+
"node_modules",
|
|
153
|
+
"dist",
|
|
154
|
+
"build",
|
|
155
|
+
".git",
|
|
156
|
+
"templates",
|
|
157
|
+
]);
|
|
136
158
|
|
|
137
159
|
// A nested checkout (clone or `git worktree add`) is a different project that
|
|
138
160
|
// happens to live inside this one — walking it doubles the graph and makes every
|
|
@@ -322,6 +344,154 @@ export function buildGraph(dir: string): ImportGraph {
|
|
|
322
344
|
return { files, deps, dependents, unresolved, external, barrels };
|
|
323
345
|
}
|
|
324
346
|
|
|
347
|
+
// --- Session-scoped graph cache ---
|
|
348
|
+
//
|
|
349
|
+
// buildGraph is cheap on this repo (~50ms/150 files) but the Edit hint calls it
|
|
350
|
+
// on every Edit — and a Claude Code PreToolUse hook is a *fresh process per
|
|
351
|
+
// tool call*, so an in-process cache alone never survives to the next edit. The
|
|
352
|
+
// graph is therefore mirrored to <faponyDir>/graph-cache/<key>.json:
|
|
353
|
+
// - auto-build: every build is written through, best-effort (never blocks)
|
|
354
|
+
// - auto-invalidate: a fingerprint over the source-file set (rel path + size
|
|
355
|
+
// + mtimeMs) is stored beside the graph; a mismatch means rebuild
|
|
356
|
+
// - cost: the cache file is only stat-ed when there is one to validate, so a
|
|
357
|
+
// first-ever call pays build + write; later calls pay a walk + stat + parse,
|
|
358
|
+
// well under a rebuild on any repo large enough for this to matter
|
|
359
|
+
// Everything here is derived state — an unreadable, corrupt, stale or
|
|
360
|
+
// unwritable cache falls back to a live build and no code path trusts it.
|
|
361
|
+
|
|
362
|
+
const GRAPH_CACHE_VERSION = 1;
|
|
363
|
+
const GRAPH_CACHE_DIR = "graph-cache";
|
|
364
|
+
|
|
365
|
+
interface SerializedGraph {
|
|
366
|
+
v: number;
|
|
367
|
+
fp: string;
|
|
368
|
+
files: string[];
|
|
369
|
+
deps: Record<string, string[]>;
|
|
370
|
+
dependents: Record<string, string[]>;
|
|
371
|
+
unresolved: number;
|
|
372
|
+
external: number;
|
|
373
|
+
barrels: string[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let _graphCache: { dir: string; fp: string; graph: ImportGraph } | null = null;
|
|
377
|
+
|
|
378
|
+
/** Drop the in-process graph cache (tests simulate a fresh hook process). */
|
|
379
|
+
export function resetGraphCache(): void {
|
|
380
|
+
_graphCache = null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Absolute path of a worktree's graph-cache file — may not exist. */
|
|
384
|
+
export function graphCachePath(dir: string): string {
|
|
385
|
+
const abs = resolve(dir);
|
|
386
|
+
const slug = abs.replace(/[^A-Za-z0-9]+/g, "-").slice(0, 60);
|
|
387
|
+
return join(
|
|
388
|
+
faponyDir(),
|
|
389
|
+
GRAPH_CACHE_DIR,
|
|
390
|
+
`${slug}-${Bun.hash(abs).toString(36)}.json`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// The graph changes only when the set of source files or their bytes change —
|
|
395
|
+
// size/mtime/ctime catch that without reading any file. ctime rides the same
|
|
396
|
+
// stat call for free and cannot be forged like mtime can (only the system
|
|
397
|
+
// moves it), so an mtime-preserving rewrite still invalidates.
|
|
398
|
+
function graphFingerprint(absDir: string): string {
|
|
399
|
+
const parts: string[] = [];
|
|
400
|
+
for (const rel of collectSourceFiles(absDir)) {
|
|
401
|
+
try {
|
|
402
|
+
const st = statSync(join(absDir, rel));
|
|
403
|
+
parts.push(
|
|
404
|
+
`${rel}\u0000${st.size}\u0000${st.mtimeMs}\u0000${st.ctimeMs}`,
|
|
405
|
+
);
|
|
406
|
+
} catch {
|
|
407
|
+
parts.push(`${rel}\u0000?\u0000?\u0000?`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return Bun.hash(parts.join("\n")).toString(36);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function serializeGraph(graph: ImportGraph, fp: string): SerializedGraph {
|
|
414
|
+
const rec = (m: Map<string, Set<string>>): Record<string, string[]> => {
|
|
415
|
+
const out: Record<string, string[]> = {};
|
|
416
|
+
for (const [k, v] of m) out[k] = [...v];
|
|
417
|
+
return out;
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
v: GRAPH_CACHE_VERSION,
|
|
421
|
+
fp,
|
|
422
|
+
files: graph.files,
|
|
423
|
+
deps: rec(graph.deps),
|
|
424
|
+
dependents: rec(graph.dependents),
|
|
425
|
+
unresolved: graph.unresolved,
|
|
426
|
+
external: graph.external,
|
|
427
|
+
barrels: [...graph.barrels],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function hydrateGraph(c: SerializedGraph): ImportGraph {
|
|
432
|
+
const toMap = (r: Record<string, string[]>): Map<string, Set<string>> => {
|
|
433
|
+
const m = new Map<string, Set<string>>();
|
|
434
|
+
for (const [k, v] of Object.entries(r)) m.set(k, new Set(v));
|
|
435
|
+
return m;
|
|
436
|
+
};
|
|
437
|
+
return {
|
|
438
|
+
files: c.files,
|
|
439
|
+
deps: toMap(c.deps),
|
|
440
|
+
dependents: toMap(c.dependents),
|
|
441
|
+
unresolved: c.unresolved,
|
|
442
|
+
external: c.external,
|
|
443
|
+
barrels: new Set(c.barrels),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function readCachedGraph(path: string, fp: string): ImportGraph | null {
|
|
448
|
+
try {
|
|
449
|
+
const cached = JSON.parse(readFileSync(path, "utf-8")) as SerializedGraph;
|
|
450
|
+
if (cached.v !== GRAPH_CACHE_VERSION || cached.fp !== fp) return null;
|
|
451
|
+
return hydrateGraph(cached);
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function writeCachedGraph(path: string, graph: ImportGraph, fp: string): void {
|
|
458
|
+
try {
|
|
459
|
+
if (!existsSync(join(faponyDir(), GRAPH_CACHE_DIR))) {
|
|
460
|
+
mkdirSync(join(faponyDir(), GRAPH_CACHE_DIR), { recursive: true });
|
|
461
|
+
}
|
|
462
|
+
// pid-suffixed temp + rename: a reader never sees a half-written file even
|
|
463
|
+
// when two hook processes race.
|
|
464
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
465
|
+
writeFileSync(tmp, JSON.stringify(serializeGraph(graph, fp)), "utf-8");
|
|
466
|
+
renameSync(tmp, path);
|
|
467
|
+
} catch {
|
|
468
|
+
// best-effort — a cache that cannot be written must not break the caller
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function buildGraphCached(dir: string): ImportGraph {
|
|
473
|
+
const abs = resolve(dir);
|
|
474
|
+
// One walk per call: the fingerprint doubles as the in-process validity
|
|
475
|
+
// check, so a same-process second call after an edit rebuilds instead of
|
|
476
|
+
// serving the stale graph. A drift between this fp and the built graph
|
|
477
|
+
// self-heals — the next call recomputes and rebuilds again.
|
|
478
|
+
const fp = graphFingerprint(abs);
|
|
479
|
+
if (_graphCache?.dir === abs && _graphCache.fp === fp)
|
|
480
|
+
return _graphCache.graph;
|
|
481
|
+
const path = graphCachePath(abs);
|
|
482
|
+
if (existsSync(path)) {
|
|
483
|
+
const cached = readCachedGraph(path, fp);
|
|
484
|
+
if (cached) {
|
|
485
|
+
_graphCache = { dir: abs, fp, graph: cached };
|
|
486
|
+
return cached;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const graph = buildGraph(abs);
|
|
490
|
+
_graphCache = { dir: abs, fp, graph };
|
|
491
|
+
writeCachedGraph(path, graph, fp);
|
|
492
|
+
return graph;
|
|
493
|
+
}
|
|
494
|
+
|
|
325
495
|
// --- Diagnosis ---
|
|
326
496
|
|
|
327
497
|
function findCycles(graph: ImportGraph): string[][] {
|
package/src/conventions-seed.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
15
15
|
import { dirname, join, relative } from "node:path";
|
|
16
16
|
import { pathToFileURL } from "node:url";
|
|
17
17
|
import { collectSourceFiles, isSkippedDir, isTestFile } from "./analyze.js";
|
|
18
|
+
import { CONVENTIONS_FILE, FAPONY_DIR } from "./db/index.js";
|
|
18
19
|
import { extractBody, extractExports } from "./map.js";
|
|
19
20
|
|
|
20
21
|
const RESTRICTED_RULES = new Set([
|
|
@@ -190,7 +191,6 @@ async function eslintRows(
|
|
|
190
191
|
const rows: SeedRow[] = [];
|
|
191
192
|
const configs: string[] = [];
|
|
192
193
|
const skipped: string[] = [];
|
|
193
|
-
const seen = new Set<string>();
|
|
194
194
|
for (const abs of findConfigFiles(root)) {
|
|
195
195
|
let raw: string;
|
|
196
196
|
try {
|
|
@@ -377,7 +377,7 @@ function detectWrappers(root: string): SeedRow[] {
|
|
|
377
377
|
// --- entry ---
|
|
378
378
|
|
|
379
379
|
export async function seedConventionsFile(target: string): Promise<SeedResult> {
|
|
380
|
-
const file = join(target,
|
|
380
|
+
const file = join(target, CONVENTIONS_FILE);
|
|
381
381
|
const base: SeedResult = {
|
|
382
382
|
file,
|
|
383
383
|
eslintRows: 0,
|
|
@@ -408,7 +408,7 @@ export async function seedConventionsFile(target: string): Promise<SeedResult> {
|
|
|
408
408
|
// a wrapper scan failure is not an init failure
|
|
409
409
|
}
|
|
410
410
|
const payload = `${JSON.stringify({ conventions: rows }, null, 2)}\n`;
|
|
411
|
-
mkdirSync(join(target,
|
|
411
|
+
mkdirSync(join(target, FAPONY_DIR), { recursive: true });
|
|
412
412
|
writeFileSync(file, payload);
|
|
413
413
|
return {
|
|
414
414
|
file,
|
package/src/db/defaults.ts
CHANGED
|
@@ -6,13 +6,21 @@ export const DEFAULT_SAFETY_DENY = [
|
|
|
6
6
|
"checkout\\s+--\\s",
|
|
7
7
|
"git\\s+stash",
|
|
8
8
|
];
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// --- .fapony/ layout (single source of truth — do not hardcode ".fapony" elsewhere) ---
|
|
10
|
+
// plan/spec live in .fapony/ — not configurable (gitignored = private).
|
|
11
|
+
export const FAPONY_DIR = ".fapony";
|
|
12
|
+
export const CONFIG_FILENAME = "fapony.config.json";
|
|
13
|
+
export const CONVENTIONS_FILENAME = "conventions.json";
|
|
14
|
+
export const EVIDENCE_FILENAME = "evidence.json";
|
|
15
|
+
export const CONVENTIONS_FILE = `${FAPONY_DIR}/${CONVENTIONS_FILENAME}`;
|
|
16
|
+
// plan/spec live in .fapony/ — not configurable (gitignored = private).
|
|
17
|
+
export const PLAN_DIR = `${FAPONY_DIR}/plan`;
|
|
18
|
+
export const SPEC_DIR = `${FAPONY_DIR}/spec`;
|
|
11
19
|
// Archive sits beside plan/, not inside it, so archiving never changes a file's
|
|
12
20
|
// depth and its relative links survive the move untouched.
|
|
13
|
-
export const DEFAULT_DONE_DIR =
|
|
14
|
-
export const
|
|
15
|
-
export const DEFAULT_EVIDENCE_FILE =
|
|
21
|
+
export const DEFAULT_DONE_DIR = `${FAPONY_DIR}/done`;
|
|
22
|
+
export const DEFAULT_MEM_DIR = `${FAPONY_DIR}/.memory`;
|
|
23
|
+
export const DEFAULT_EVIDENCE_FILE = `${FAPONY_DIR}/${EVIDENCE_FILENAME}`;
|
|
16
24
|
|
|
17
25
|
export const DEFAULT_CONFIG: Config = {
|
|
18
26
|
worktrees: {},
|
package/src/db/getters.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_DONE_DIR,
|
|
3
3
|
DEFAULT_EVIDENCE_FILE,
|
|
4
|
-
|
|
5
|
-
DEFAULT_PLAN_DIR,
|
|
4
|
+
DEFAULT_MEM_DIR,
|
|
6
5
|
DEFAULT_SAFETY_DENY,
|
|
7
|
-
|
|
6
|
+
PLAN_DIR,
|
|
7
|
+
SPEC_DIR,
|
|
8
8
|
} from "./defaults.js";
|
|
9
9
|
import type { Config } from "./types.js";
|
|
10
10
|
|
|
@@ -12,20 +12,22 @@ export function safetyDeny(config?: Config): string[] {
|
|
|
12
12
|
return config?.safety?.deny ?? DEFAULT_SAFETY_DENY;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/** Hardcoded — plan/spec live in .fapony/ (gitignored = private). */
|
|
16
|
+
export function planDir(): string {
|
|
17
|
+
return PLAN_DIR;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/** Hardcoded — plan/spec live in .fapony/ (gitignored = private). */
|
|
21
|
+
export function specDir(): string {
|
|
22
|
+
return SPEC_DIR;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function doneDir(config?: Config): string {
|
|
24
26
|
return config?.paths?.doneDir ?? DEFAULT_DONE_DIR;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export function
|
|
28
|
-
return config?.paths?.
|
|
29
|
+
export function memoryDir(config?: Config): string {
|
|
30
|
+
return config?.paths?.memDir ?? DEFAULT_MEM_DIR;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function evidenceFile(config?: Config): string {
|
package/src/db/load.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { DEFAULT_CONFIG } from "./defaults.js";
|
|
4
|
+
import { CONFIG_FILENAME, DEFAULT_CONFIG } from "./defaults.js";
|
|
5
5
|
import type { Config } from "./types.js";
|
|
6
6
|
|
|
7
7
|
// XDG Base Directory convention (macOS ignores Apple's ~/Library/Application Support
|
|
@@ -20,7 +20,7 @@ function _dbPath(config?: Config): string {
|
|
|
20
20
|
|
|
21
21
|
export function configFilePath(): string {
|
|
22
22
|
if (process.env.FAPONY_CONFIG) return process.env.FAPONY_CONFIG;
|
|
23
|
-
return join(process.cwd(),
|
|
23
|
+
return join(process.cwd(), CONFIG_FILENAME);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function freshDefaultConfig(): Config {
|
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
|
@@ -55,11 +55,9 @@ export interface Config {
|
|
|
55
55
|
// state dir override (default: $XDG_CONFIG_HOME/fapony or ~/.config/fapony).
|
|
56
56
|
// $FAPONY_STATE_DIR env wins over this when set.
|
|
57
57
|
stateDir?: string;
|
|
58
|
-
// plan/spec/
|
|
59
|
-
planDir?: string;
|
|
60
|
-
specDir?: string;
|
|
58
|
+
// plan/spec live in .fapony/{plan,spec} — not configurable (gitignored = private).
|
|
61
59
|
doneDir?: string;
|
|
62
|
-
|
|
60
|
+
memDir?: string;
|
|
63
61
|
evidenceFile?: string;
|
|
64
62
|
} | null;
|
|
65
63
|
safety?: {
|