fapony 0.2.0 → 0.3.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 +50 -15
- package/fapony.ts +12 -3
- package/package.json +1 -1
- package/skill/define-convention/SKILL.md +77 -0
- package/skill/lookup-before-edit/SKILL.md +48 -0
- package/skill/review-pony/SKILL.md +11 -3
- package/src/analyze.ts +162 -4
- package/src/conventions-seed.ts +3 -2
- package/src/db/defaults.ts +13 -5
- package/src/db/getters.ts +8 -6
- package/src/db/load.ts +2 -2
- package/src/db/types.ts +1 -3
- package/src/debt/cli.ts +193 -0
- package/src/debt/format.ts +107 -0
- package/src/debt/index.ts +19 -0
- package/src/debt/load.ts +92 -0
- package/src/debt/promotion.ts +152 -0
- package/src/debt/scan.ts +214 -0
- package/src/debt/types.ts +79 -0
- package/src/detect.ts +92 -0
- package/src/digest/collect.ts +3 -2
- package/src/hook.ts +412 -17
- package/src/init-mem.ts +5 -5
- package/src/init.ts +6 -5
- package/src/install/claude.ts +51 -3
- package/src/install/codex.ts +144 -13
- package/src/install/opencode.ts +209 -1
- package/src/install.ts +2 -1
- package/src/lint-baseline.ts +4 -3
- package/src/mcp/evidence.ts +14 -2
- package/src/mcp/tools/index.ts +13 -28
- package/src/mcp/tools/mem.ts +71 -0
- package/src/mcp/transport.ts +12 -63
- package/src/mem/commands/read.ts +18 -1
- package/src/mem/store.ts +6 -4
- package/src/memory.ts +23 -13
- package/src/plan-seed.ts +7 -6
- package/src/session/helpers.ts +1 -1
- package/src/session/registry.ts +3 -6
- package/src/setup.ts +4 -3
- package/src/stats/data.ts +6 -18
- package/src/debt.ts +0 -806
- package/src/mcp/tools/usage.ts +0 -211
package/src/mcp/tools/mem.ts
CHANGED
|
@@ -235,3 +235,74 @@ export function toolMemAdd(args: Record<string, unknown>): ToolResult {
|
|
|
235
235
|
return errorResult(e instanceof Error ? e.message : String(e));
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
+
|
|
239
|
+
// --- mem_close ---
|
|
240
|
+
//
|
|
241
|
+
// A separate tool on purpose, not kind:"close" inside mem_add: a close row
|
|
242
|
+
// carries {ref, text} with no files[] and no spec, while mem_add requires a
|
|
243
|
+
// non-empty files[] (and a spec for hold) — folding them into one schema
|
|
244
|
+
// would make required fields depend on the value of another field, the shape
|
|
245
|
+
// models call wrong most often. Mirrors CLI `mem close <id> "<msg>"`
|
|
246
|
+
// (commands/write.ts cmdClose): the id must exist; the tombstone voids the
|
|
247
|
+
// claim by itself.
|
|
248
|
+
|
|
249
|
+
export interface MemCloseResult {
|
|
250
|
+
ref: string;
|
|
251
|
+
text: string;
|
|
252
|
+
ts: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function memClose(args: {
|
|
256
|
+
worktree: string;
|
|
257
|
+
id: string;
|
|
258
|
+
text: string;
|
|
259
|
+
}): MemCloseResult {
|
|
260
|
+
if (!args.id.trim()) {
|
|
261
|
+
throw new Error("id is required");
|
|
262
|
+
}
|
|
263
|
+
if (!args.text.trim()) {
|
|
264
|
+
throw new Error("text is required and must not be empty");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
initStore(args.worktree);
|
|
268
|
+
const all = rows();
|
|
269
|
+
if (!all.some((r) => "id" in r && r.id === args.id)) {
|
|
270
|
+
throw new Error(`no id "${args.id}" in the log`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const ts = new Date().toISOString();
|
|
274
|
+
put({ kind: "close", ref: args.id, text: args.text });
|
|
275
|
+
|
|
276
|
+
return { ref: args.id, text: args.text, ts };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function toolMemClose(args: Record<string, unknown>): ToolResult {
|
|
280
|
+
const worktree =
|
|
281
|
+
typeof args.worktree === "string" ? args.worktree.trim() : "";
|
|
282
|
+
if (!worktree) {
|
|
283
|
+
return errorResult(
|
|
284
|
+
"worktree is required and must be an absolute path " +
|
|
285
|
+
"(git rev-parse --show-toplevel)",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (!worktree.startsWith("/")) {
|
|
289
|
+
return errorResult(`worktree must be an absolute path, got: ${worktree}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
293
|
+
if (!id) {
|
|
294
|
+
return errorResult("id is required — the row id to close");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
298
|
+
if (!text) {
|
|
299
|
+
return errorResult("text is required and must not be empty");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const result = memClose({ worktree, id, text });
|
|
304
|
+
return jsonResult(result);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return errorResult(e instanceof Error ? e.message : String(e));
|
|
307
|
+
}
|
|
308
|
+
}
|
package/src/mcp/transport.ts
CHANGED
|
@@ -4,13 +4,12 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
7
|
-
import { mergeBytesByTool, type UsageDetail } from "../session/index.js";
|
|
8
7
|
import { getServerSha } from "./primitives.js";
|
|
9
8
|
import {
|
|
10
9
|
TOOLS,
|
|
11
10
|
toolMemAdd,
|
|
11
|
+
toolMemClose,
|
|
12
12
|
toolMemFind,
|
|
13
|
-
toolPassiveUsage,
|
|
14
13
|
toolVerdictSubmit,
|
|
15
14
|
} from "./tools/index.js";
|
|
16
15
|
import { errorResult, type ToolResult } from "./types.js";
|
|
@@ -39,68 +38,18 @@ Skip it and every tool still answers correctly, on a thinner history.`;
|
|
|
39
38
|
//
|
|
40
39
|
// Written after every MCP tool call. The Claude Code statusline script reads
|
|
41
40
|
// this file (< 1ms, no spawn, no db). Format: single line of text.
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
41
|
+
// No remaining tool produces bytes_by_tool detail, so every call writes the
|
|
42
|
+
// minimal "fapony" marker — cross-client spend lives on the CLI now
|
|
43
|
+
// (`fapony usage-scan` + `fapony usage-web`), which the statusline cannot
|
|
44
|
+
// spawn (it must stay < 1ms).
|
|
45
45
|
|
|
46
46
|
const STATUSLINE_PATH = join(homedir(), ".config", "fapony", "statusline");
|
|
47
47
|
|
|
48
|
-
function writeStatuslineCache(
|
|
48
|
+
function writeStatuslineCache(): void {
|
|
49
49
|
try {
|
|
50
|
-
// Extract bytes_by_tool from fapony_usage detail JSON response.
|
|
51
|
-
let line = "fapony";
|
|
52
|
-
if (
|
|
53
|
-
toolResult &&
|
|
54
|
-
typeof toolResult === "object" &&
|
|
55
|
-
"content" in toolResult &&
|
|
56
|
-
Array.isArray(toolResult.content)
|
|
57
|
-
) {
|
|
58
|
-
for (const c of toolResult.content) {
|
|
59
|
-
if (
|
|
60
|
-
c &&
|
|
61
|
-
typeof c === "object" &&
|
|
62
|
-
c.type === "text" &&
|
|
63
|
-
typeof c.text === "string"
|
|
64
|
-
) {
|
|
65
|
-
// Try to extract bytes_by_tool from JSON text response.
|
|
66
|
-
// Aggregated across all clients: the bytes live on the Claude Code
|
|
67
|
-
// sub-object (claude_code.detail), never on the top-level detail,
|
|
68
|
-
// so reading top-level alone would always miss.
|
|
69
|
-
try {
|
|
70
|
-
const parsed = JSON.parse(c.text) as {
|
|
71
|
-
detail?: UsageDetail | null;
|
|
72
|
-
zcode?: { detail?: UsageDetail | null } | null;
|
|
73
|
-
claude_code?: { detail?: UsageDetail | null } | null;
|
|
74
|
-
codex?: { detail?: UsageDetail | null } | null;
|
|
75
|
-
};
|
|
76
|
-
const bbt = mergeBytesByTool(
|
|
77
|
-
parsed?.detail,
|
|
78
|
-
parsed?.zcode?.detail,
|
|
79
|
-
parsed?.claude_code?.detail,
|
|
80
|
-
parsed?.codex?.detail,
|
|
81
|
-
);
|
|
82
|
-
const entries = Object.entries(bbt).sort((a, b) => b[1] - a[1]);
|
|
83
|
-
const total = entries.reduce((s, e) => s + e[1], 0);
|
|
84
|
-
if (total > 0) {
|
|
85
|
-
// Format: "84.2k" for total, or "Read 42k · Grep 31k" for top tools.
|
|
86
|
-
const fmt = (n: number) =>
|
|
87
|
-
n >= 1024 ? `${(n / 1024).toFixed(1)}k` : `${Math.round(n)}`;
|
|
88
|
-
if (entries.length <= 3) {
|
|
89
|
-
line = `fapony ${entries.map((e) => `${e[0]} ${fmt(e[1])}`).join(" · ")}`;
|
|
90
|
-
} else {
|
|
91
|
-
line = `fapony ${fmt(total)}`;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
} catch {
|
|
95
|
-
// Not JSON — that's fine, use default "fapony" marker.
|
|
96
|
-
}
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
50
|
const dir = join(homedir(), ".config", "fapony");
|
|
102
51
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
103
|
-
writeFileSync(STATUSLINE_PATH,
|
|
52
|
+
writeFileSync(STATUSLINE_PATH, "fapony", "utf-8");
|
|
104
53
|
} catch {
|
|
105
54
|
// Cache write is best-effort — never block MCP on it.
|
|
106
55
|
}
|
|
@@ -110,7 +59,7 @@ function writeStatuslineCache(toolResult: ToolResult): void {
|
|
|
110
59
|
|
|
111
60
|
const MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
112
61
|
const SERVER_NAME = "fapony-handcheck";
|
|
113
|
-
const SERVER_VERSION = "0.
|
|
62
|
+
const SERVER_VERSION = "0.3.0";
|
|
114
63
|
|
|
115
64
|
// --- JSON-RPC dispatch ---
|
|
116
65
|
|
|
@@ -152,20 +101,20 @@ function dispatchToolCall(params: {
|
|
|
152
101
|
case "verdict_submit":
|
|
153
102
|
result = toolVerdictSubmit(args);
|
|
154
103
|
break;
|
|
155
|
-
case "fapony_usage":
|
|
156
|
-
result = toolPassiveUsage(args);
|
|
157
|
-
break;
|
|
158
104
|
case "mem_find":
|
|
159
105
|
result = toolMemFind(args);
|
|
160
106
|
break;
|
|
161
107
|
case "mem_add":
|
|
162
108
|
result = toolMemAdd(args);
|
|
163
109
|
break;
|
|
110
|
+
case "mem_close":
|
|
111
|
+
result = toolMemClose(args);
|
|
112
|
+
break;
|
|
164
113
|
default:
|
|
165
114
|
return errorResult(`unknown tool: ${params.name}`);
|
|
166
115
|
}
|
|
167
116
|
// Write statusline cache after every tool call — best-effort, never blocks.
|
|
168
|
-
writeStatuslineCache(
|
|
117
|
+
writeStatuslineCache();
|
|
169
118
|
return result;
|
|
170
119
|
}
|
|
171
120
|
|
package/src/mem/commands/read.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
|
+
import { baselinePath, readEvidenceLintCmd } from "../../lint-baseline.js";
|
|
5
6
|
import { doneLines, fmtClose, fmtRow, printOpenRows } from "../render.js";
|
|
6
7
|
import { claimsOf, openRows, staleReport } from "../selectors.js";
|
|
7
8
|
import type { CloseRow, WorkRow } from "../store.js";
|
|
8
|
-
import { allRows, app, memCmd, planDir, rows } from "../store.js";
|
|
9
|
+
import { allRows, app, memCmd, planDir, root, rows } from "../store.js";
|
|
9
10
|
import { planSweepCmd, shippedNotMoved } from "./plan.js";
|
|
10
11
|
import { THRESHOLD } from "./rotate.js";
|
|
11
12
|
|
|
@@ -406,6 +407,22 @@ export const cmdKickoff = (a: string[]) => {
|
|
|
406
407
|
});
|
|
407
408
|
}
|
|
408
409
|
|
|
410
|
+
// group 4 — the lint baseline. "Is this red mine or was it already red" is
|
|
411
|
+
// already answered by `lint-baseline`, but only if it was captured before
|
|
412
|
+
// the work started — and nobody remembers at session open, which is what
|
|
413
|
+
// kickoff is. Only offered when the repo declared a lint command; without
|
|
414
|
+
// one the capture would just fail.
|
|
415
|
+
try {
|
|
416
|
+
if (readEvidenceLintCmd(root) && !existsSync(baselinePath(root))) {
|
|
417
|
+
suggestions.push({
|
|
418
|
+
text: "lint baseline ยังไม่ capture — แดงที่มีอยู่ก่อนจะถูกนับเป็นของคุณ",
|
|
419
|
+
run: "fapony lint-baseline --capture",
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// state dir unreadable — the baseline line is a convenience, never a gate
|
|
424
|
+
}
|
|
425
|
+
|
|
409
426
|
// Print and optionally execute. Only a suggestion with a complete command is
|
|
410
427
|
// runnable — a plan item is context, a bug's close is a template needing a message.
|
|
411
428
|
if (suggestions.length) {
|
package/src/mem/store.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "node:fs";
|
|
16
16
|
import { hostname, networkInterfaces } from "node:os";
|
|
17
17
|
import { basename, dirname, join, relative } from "node:path";
|
|
18
|
+
import { CONFIG_FILENAME, DEFAULT_MEM_DIR, FAPONY_DIR } from "../db/index.js";
|
|
18
19
|
import { whereMemDir } from "../memory.js";
|
|
19
20
|
|
|
20
21
|
// --- types ---
|
|
@@ -174,7 +175,7 @@ export function initStore(worktree: string, overrideMemDir?: string): void {
|
|
|
174
175
|
`two or more .fapony/.memory dirs under ${root} — refusing to guess:\n` +
|
|
175
176
|
`${list}\ncd into one, set paths.memDir, or pass --mem-dir`;
|
|
176
177
|
}
|
|
177
|
-
dir = resolved.dir ?? join(root,
|
|
178
|
+
dir = resolved.dir ?? join(root, DEFAULT_MEM_DIR);
|
|
178
179
|
|
|
179
180
|
// planBase: the .fapony dir — plan/done/conventions live here.
|
|
180
181
|
// Derive from dir by going up from .fapony/.memory → .fapony
|
|
@@ -184,10 +185,11 @@ export function initStore(worktree: string, overrideMemDir?: string): void {
|
|
|
184
185
|
app = dirname(planBase);
|
|
185
186
|
|
|
186
187
|
// Read fapony.config.json
|
|
187
|
-
const configDir =
|
|
188
|
+
const configDir =
|
|
189
|
+
planBase === `${root}/${FAPONY_DIR}` ? root : dirname(planBase);
|
|
188
190
|
const configPaths = ((): Record<string, string> => {
|
|
189
191
|
try {
|
|
190
|
-
const raw = readFileSync(`${configDir}
|
|
192
|
+
const raw = readFileSync(`${configDir}/${CONFIG_FILENAME}`, "utf8");
|
|
191
193
|
return (JSON.parse(raw)?.paths ?? {}) as Record<string, string>;
|
|
192
194
|
} catch {
|
|
193
195
|
return {};
|
|
@@ -198,7 +200,7 @@ export function initStore(worktree: string, overrideMemDir?: string): void {
|
|
|
198
200
|
? join(configDir, configPaths[key])
|
|
199
201
|
: null;
|
|
200
202
|
|
|
201
|
-
planDir =
|
|
203
|
+
planDir = `${planBase}/plan`;
|
|
202
204
|
|
|
203
205
|
// done/ sits beside plan/ (same depth, relative links survive)
|
|
204
206
|
doneDir =
|
package/src/memory.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
6
|
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
7
7
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import { CONFIG_FILENAME, DEFAULT_MEM_DIR, FAPONY_DIR } from "./db/defaults.js";
|
|
8
9
|
import { type Config, loadConfig, safetyDeny } from "./db/index.js";
|
|
9
10
|
import { assertSafe } from "./safety.js";
|
|
10
11
|
import { templateArgs } from "./util.js";
|
|
@@ -202,7 +203,7 @@ function walkUpForMemDir(fromDir: string, acceptEmpty: boolean): string | null {
|
|
|
202
203
|
const boundary = repoRootOf(start) ?? "/";
|
|
203
204
|
let dir = start;
|
|
204
205
|
while (true) {
|
|
205
|
-
const candidate = join(dir,
|
|
206
|
+
const candidate = join(dir, DEFAULT_MEM_DIR);
|
|
206
207
|
if (existsSync(candidate) && (acceptEmpty || hasMemLogs(candidate))) {
|
|
207
208
|
return candidate;
|
|
208
209
|
}
|
|
@@ -217,7 +218,13 @@ function walkUpForMemDir(fromDir: string, acceptEmpty: boolean): string | null {
|
|
|
217
218
|
export interface MemDirResult {
|
|
218
219
|
dir: string | null;
|
|
219
220
|
step: "flag" | "config" | "walk-up" | "repo-root" | "ambiguous" | "none";
|
|
220
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* `.fapony/.memory/` dirs under the repo root that hold logs but are not in
|
|
223
|
+
* scope from `fromDir`. Set for "ambiguous" (two or more, the refusal) and
|
|
224
|
+
* also for "repo-root"/"none" (one) — a caller that reports "nothing
|
|
225
|
+
* recorded here" is lying when a single app-scoped log exists one level
|
|
226
|
+
* down, and the honest line needs the path to say where.
|
|
227
|
+
*/
|
|
221
228
|
candidates?: string[];
|
|
222
229
|
}
|
|
223
230
|
|
|
@@ -254,8 +261,8 @@ function findMemDirsUnder(root: string): string[] {
|
|
|
254
261
|
}
|
|
255
262
|
for (const e of entries) {
|
|
256
263
|
if (!e.isDirectory()) continue;
|
|
257
|
-
if (e.name ===
|
|
258
|
-
const candidate = join(dir,
|
|
264
|
+
if (e.name === FAPONY_DIR) {
|
|
265
|
+
const candidate = join(dir, DEFAULT_MEM_DIR);
|
|
259
266
|
if (hasMemLogs(candidate)) found.push(candidate);
|
|
260
267
|
continue;
|
|
261
268
|
}
|
|
@@ -298,7 +305,7 @@ function resolveMemDirFrom(
|
|
|
298
305
|
// path source here — spec §1 pins this to fapony.config.json)
|
|
299
306
|
const configDir = root ?? cwd;
|
|
300
307
|
try {
|
|
301
|
-
const configPath = join(configDir,
|
|
308
|
+
const configPath = join(configDir, CONFIG_FILENAME);
|
|
302
309
|
if (existsSync(configPath)) {
|
|
303
310
|
const config = loadConfig(configPath);
|
|
304
311
|
if (config?.paths?.memDir) {
|
|
@@ -319,20 +326,23 @@ function resolveMemDirFrom(
|
|
|
319
326
|
// Guard (SPEC §1 fail example): nothing at/above cwd holds a log, but the repo
|
|
320
327
|
// has two or more app-scoped ones — refuse instead of silently creating a
|
|
321
328
|
// third log at the root that no app-scoped reader will ever see.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
329
|
+
// One candidate is not ambiguous, so resolution is unchanged — but it still
|
|
330
|
+
// travels back with the result: out-of-scope is not the same as absent.
|
|
331
|
+
const candidates = root ? findMemDirsUnder(root) : [];
|
|
332
|
+
if (candidates.length >= 2) {
|
|
333
|
+
return { dir: null, step: "ambiguous", candidates };
|
|
327
334
|
}
|
|
335
|
+
const outOfScope = candidates.length ? { candidates } : {};
|
|
328
336
|
|
|
329
337
|
// Step 4: <repo root>/.fapony/.memory/ — where a new log is created
|
|
330
338
|
if (root) {
|
|
331
|
-
const rootDir = join(root,
|
|
332
|
-
if (existsSync(rootDir))
|
|
339
|
+
const rootDir = join(root, DEFAULT_MEM_DIR);
|
|
340
|
+
if (existsSync(rootDir)) {
|
|
341
|
+
return { dir: rootDir, step: "repo-root", ...outOfScope };
|
|
342
|
+
}
|
|
333
343
|
}
|
|
334
344
|
|
|
335
|
-
return { dir: null, step: "none" };
|
|
345
|
+
return { dir: null, step: "none", ...outOfScope };
|
|
336
346
|
}
|
|
337
347
|
|
|
338
348
|
export function resolveMemDir(
|
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 {
|
|
@@ -524,7 +525,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
524
525
|
process.exit(1);
|
|
525
526
|
}
|
|
526
527
|
const cwd = process.cwd();
|
|
527
|
-
const config = loadConfig(join(cwd,
|
|
528
|
+
const config = loadConfig(join(cwd, CONFIG_FILENAME));
|
|
528
529
|
// Resolve the git worktree root so mem/ledger queries hit the same key
|
|
529
530
|
// state.db uses (git rev-parse --show-toplevel). Running from a subdir
|
|
530
531
|
// would otherwise mismatch: stats return empty, Context (fapony) always
|
|
@@ -568,7 +569,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
568
569
|
);
|
|
569
570
|
}
|
|
570
571
|
|
|
571
|
-
const planDirAbs = join(cwd, planDir(
|
|
572
|
+
const planDirAbs = join(cwd, planDir());
|
|
572
573
|
const planPath = join(planDirAbs, `PLAN-${name}.md`);
|
|
573
574
|
if (existsSync(planPath)) {
|
|
574
575
|
console.error(
|
|
@@ -582,7 +583,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
582
583
|
|
|
583
584
|
let specLink: string | null = null;
|
|
584
585
|
if (withSpec) {
|
|
585
|
-
const specDirAbs = join(cwd, specDir(
|
|
586
|
+
const specDirAbs = join(cwd, specDir());
|
|
586
587
|
const specPath = join(specDirAbs, `SPEC-${name}.md`);
|
|
587
588
|
if (existsSync(specPath)) {
|
|
588
589
|
console.error(
|
|
@@ -602,7 +603,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
602
603
|
: null,
|
|
603
604
|
),
|
|
604
605
|
);
|
|
605
|
-
specLink = `../${specDir(
|
|
606
|
+
specLink = `../${specDir().split("/").pop()}/SPEC-${name}.md`;
|
|
606
607
|
}
|
|
607
608
|
|
|
608
609
|
mkdirSync(planDirAbs, { recursive: true });
|
|
@@ -613,7 +614,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
613
614
|
priorArt,
|
|
614
615
|
contextFapony,
|
|
615
616
|
specLink,
|
|
616
|
-
`${planDir(
|
|
617
|
+
`${planDir()}/PLAN-${name}.md`,
|
|
617
618
|
),
|
|
618
619
|
);
|
|
619
620
|
console.log(`wrote ${planPath}${specLink ? ` + SPEC-${name}.md` : ""}`);
|
package/src/session/helpers.ts
CHANGED
|
@@ -270,7 +270,7 @@ export function readDetailFromDb(
|
|
|
270
270
|
.all(...filter.params) as DetailPerSessionToolRow[];
|
|
271
271
|
|
|
272
272
|
// Context bytes per tool — summed `state.output` length. Same signal the
|
|
273
|
-
// Claude Code/Codex readers derive from tool_result blocks, so
|
|
273
|
+
// Claude Code/Codex readers derive from tool_result blocks, so usage-scan
|
|
274
274
|
// stops reporting zero bytes for the SQLite clients.
|
|
275
275
|
const bytesRows = db
|
|
276
276
|
.prepare(
|
package/src/session/registry.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Adding a client: write its reader (src/session/<name>.ts, same
|
|
4
4
|
// PassiveUsageResult shape as an existing one — SQLite or JSONL, whatever the
|
|
5
|
-
// client actually stores), then add one entry below. usage-scan
|
|
6
|
-
//
|
|
5
|
+
// client actually stores), then add one entry below. usage-scan iterates
|
|
6
|
+
// this list; nothing else needs any other change.
|
|
7
7
|
//
|
|
8
8
|
// This does not remove the real work of a new client (reverse-engineering
|
|
9
9
|
// its storage format) — it only removes the "wire it into 2 call sites"
|
|
@@ -16,12 +16,10 @@ import type { PassiveUsageReader } from "./types.js";
|
|
|
16
16
|
import { readZcodeUsage } from "./zcode.js";
|
|
17
17
|
|
|
18
18
|
export interface ClientAdapter {
|
|
19
|
-
/** cache `client` field and
|
|
19
|
+
/** usage-cache.jsonl `client` field and usage-scan label default. */
|
|
20
20
|
key: string;
|
|
21
21
|
/** usage-scan progress label — defaults to `key`. */
|
|
22
22
|
scanLabel?: string;
|
|
23
|
-
/** fapony_usage text-report section label — defaults to `key`. */
|
|
24
|
-
reportLabel?: string;
|
|
25
23
|
/** The one client whose totals lead the report as the unlabeled top-level summary. */
|
|
26
24
|
primary?: boolean;
|
|
27
25
|
read: PassiveUsageReader;
|
|
@@ -38,7 +36,6 @@ export const CLIENTS: ClientAdapter[] = [
|
|
|
38
36
|
{
|
|
39
37
|
key: "claude_code",
|
|
40
38
|
scanLabel: "claude-code",
|
|
41
|
-
reportLabel: "claude code",
|
|
42
39
|
read: readClaudeCodeUsage,
|
|
43
40
|
},
|
|
44
41
|
{ key: "codex", read: readCodexUsage },
|
package/src/setup.ts
CHANGED
|
@@ -6,6 +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 { CONFIG_FILENAME, FAPONY_DIR } from "./db/index.js";
|
|
9
10
|
import { initProject } from "./init.js";
|
|
10
11
|
import { isAffirmative } from "./util.js";
|
|
11
12
|
|
|
@@ -181,7 +182,7 @@ export async function cmdSetup(deps: SetupDeps = {}): Promise<void> {
|
|
|
181
182
|
enableMemory,
|
|
182
183
|
});
|
|
183
184
|
|
|
184
|
-
const configPath = join(process.cwd(),
|
|
185
|
+
const configPath = join(process.cwd(), CONFIG_FILENAME);
|
|
185
186
|
if (existsSync(configPath)) {
|
|
186
187
|
const overwrite = await askFn(
|
|
187
188
|
"⚠ fapony.config.json already exists. Overwrite? (y/n)",
|
|
@@ -197,7 +198,7 @@ export async function cmdSetup(deps: SetupDeps = {}): Promise<void> {
|
|
|
197
198
|
console.log(`\n ✓ Wrote ${configPath}`);
|
|
198
199
|
|
|
199
200
|
// --- scaffold worktree ---
|
|
200
|
-
const faponyDir = join(worktreePath,
|
|
201
|
+
const faponyDir = join(worktreePath, FAPONY_DIR);
|
|
201
202
|
if (!existsSync(faponyDir)) {
|
|
202
203
|
try {
|
|
203
204
|
initProject(worktreePath);
|
|
@@ -228,7 +229,7 @@ export async function cmdSetup(deps: SetupDeps = {}): Promise<void> {
|
|
|
228
229
|
│ fapony install --platform claude │
|
|
229
230
|
│ │
|
|
230
231
|
│ 2. Ask your agent: │
|
|
231
|
-
│ "
|
|
232
|
+
│ "Find past decisions with mem_find" │
|
|
232
233
|
│ │
|
|
233
234
|
│ 3. Read a run's report: │
|
|
234
235
|
│ fapony report <run-id> │
|
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
|
-
try {
|
|
104
|
-
const cfg = JSON.parse(
|
|
105
|
-
readFileSync(join(worktree, "fapony.config.json"), "utf8"),
|
|
106
|
-
) as { paths?: { planDir?: unknown } };
|
|
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
99
|
try {
|
|
113
|
-
return readdirSync(join(worktree,
|
|
114
|
-
.
|
|
100
|
+
return readdirSync(join(worktree, PLAN_DIR)).filter((f) =>
|
|
101
|
+
f.endsWith(".md"),
|
|
102
|
+
).length;
|
|
115
103
|
} catch {
|
|
116
104
|
return null;
|
|
117
105
|
}
|