fapony 0.1.1 → 0.1.3

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.
Files changed (44) hide show
  1. package/README.md +22 -36
  2. package/images/logo.png +0 -0
  3. package/images/logo.webp +0 -0
  4. package/images/logo@400.webp +0 -0
  5. package/images/sample.webp +0 -0
  6. package/images/summary.webp +0 -0
  7. package/package.json +10 -8
  8. package/skill/plan-with-pony/SKILL.md +1 -1
  9. package/src/analyze.ts +9 -8
  10. package/src/conventions-seed.ts +10 -10
  11. package/src/db/index.ts +1 -1
  12. package/src/db/store.ts +4 -0
  13. package/src/debt.ts +26 -22
  14. package/src/digest/collect.ts +7 -7
  15. package/src/digest/html.ts +2 -2
  16. package/src/digest/text.ts +1 -1
  17. package/src/gate.ts +3 -3
  18. package/src/hook.ts +119 -21
  19. package/src/init-mem.ts +3 -3
  20. package/src/install/opencode.ts +95 -8
  21. package/src/install/types.ts +1 -1
  22. package/src/install.ts +5 -1
  23. package/src/lint-baseline.ts +9 -9
  24. package/src/mcp/tools/mem.ts +1 -1
  25. package/src/mcp/tools/stats.ts +2 -2
  26. package/src/mcp/tools/usage.ts +3 -3
  27. package/src/mcp/tools/verdict.ts +29 -16
  28. package/src/mcp/transport.ts +1 -1
  29. package/src/plan-seed.ts +23 -21
  30. package/src/price/fetch.ts +19 -19
  31. package/src/price/resolve.ts +24 -24
  32. package/src/review-seed.ts +1 -1
  33. package/src/stats/data.ts +8 -7
  34. package/src/stats/format.ts +6 -5
  35. package/src/usage/render.ts +6 -5
  36. package/templates/PLAN.md +1 -1
  37. package/templates/mem/commands/plan.ts +44 -44
  38. package/templates/mem/commands/read.ts +5 -5
  39. package/templates/mem/commands/rotate.ts +6 -6
  40. package/templates/mem/commands/selftest.ts +14 -14
  41. package/templates/mem/commands/write.ts +13 -13
  42. package/templates/mem/mem.ts +5 -5
  43. package/templates/mem/selectors.ts +18 -18
  44. package/templates/mem/store.ts +50 -50
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  # fapony
6
6
 
7
+ [![npm](https://img.shields.io/npm/v/fapony.svg)](https://www.npmjs.com/package/fapony)
8
+
7
9
  **Where did your tokens go?** fapony reads the session logs Claude Code, Codex, OpenCode and ZCode
8
10
  already write, and puts them all on one yardstick — tokens, cost and time per model, per client,
9
11
  per workflow. Nothing to instrument, no per-project setup, no waiting for data to accumulate: it
@@ -41,8 +43,8 @@ which kind of task" becomes a data question instead of a vibe. On top of measure
41
43
  claims against git facts: handoff conformance, allowlisted evidence, a 6-grade verdict — with
42
44
  everything the agent claimed but couldn't prove marked as such.
43
45
 
44
- **What that question looks like answered, from one project's own ledger (52 graded `code`-regime
45
- runs, `fapony stats --mode verdict --regime code`):**
46
+ **What that question looks like answered, from one project's own ledger the top of the `n≥5`
47
+ frontier (`fapony stats --mode verdict --regime code`):**
46
48
 
47
49
  | model | tokens/pass | quality | n |
48
50
  |---|---|---|---|
@@ -56,7 +58,7 @@ on you to hold: work isn't randomly assigned to models, so a gap this size is a
56
58
  controlled trial — you likely route easy tasks to the cheap model already. `n≥5` is fapony's own
57
59
  floor before a model counts toward the frontier at all; below that it's a data point, not a pick.
58
60
 
59
- **The reason to keep it running is the third layer: knowledge accumulation.** Any single client already logs its own session — timing, tokens, tool calls. What none of them see is *across* runs, clients and task shapes: which model earns its keep on which kind of work **in this project**, at what token cost, graded by whoever reviewed it. Every verdict carries a `regime` (`code` / `fix` / `review` / `plan` / `inquiry` / `test`), and runs split by whether there was a plan at all — so "does planning beat diving in, and for which model" is a table, not an argument. Session logs have the tokens but no grades; benchmarks have grades but not your codebase. fapony is the one layer that holds both, because it's the one every client reports into.
61
+ **The reason to keep it running is the third layer: knowledge accumulation.** Any single client already logs its own session — timing, tokens, tool calls. What none of them see is *across* runs, clients and task shapes: which model earns its keep on which kind of work **in this project**, at what token cost, graded by whoever reviewed it. Every verdict carries a `regime` (`code` / `fix` / `review` / `plan` / `inquiry` / `test`), and runs split by whether there was a plan at all — so "does planning beat diving in, and for which model" is a table, not an argument.
60
62
 
61
63
  Three tiers, deliberately: **measurement ships today** and needs no per-project setup — raw facts nobody can call unfair. **Verification is the sharper edge** but stays beta until its evidence layer is hardened; fapony doesn't control your agent's flow, so it never promises "verified" as a headline. **Knowledge accumulation is the compounding one** — it's worthless on run 1 and gets more useful every run after, which is exactly why it's the layer competitors can't clone by copying a feature list.
62
64
 
@@ -67,8 +69,7 @@ Adopting it doesn't change your workflow. There is no loop to join and no framew
67
69
  Stated up front, because the gap between these two things is where most tooling oversells:
68
70
 
69
71
  - **It does not run your test suite.** The evidence collector runs an allowlist *you* write in
70
- `.fapony/evidence.json`, and never a command an agent proposes. No allowlist, no evidence — and
71
- the report says `not_run` rather than staying quiet.
72
+ `.fapony/evidence.json`, and never a command an agent proposes. No allowlist, no evidence.
72
73
  - **It does not judge your code.** `verdict_submit` *stores* a verdict; a human or a reviewing
73
74
  agent supplies it. fapony is the ledger, not the judge.
74
75
  - **`handoff_check` checks conformance, not correctness.** It verifies that what the agent claimed
@@ -81,15 +82,15 @@ Stated up front, because the gap between these two things is where most tooling
81
82
  reviews and files the verdict, the grade lands on the reviewer. Reports label it `inferred`;
82
83
  read it as such.
83
84
  - **The knowledge layer is empty on run 1.** It is worth something around run 5 and more every run
84
- after. That is the trade for it being the layer nobody can clone from a feature list.
85
+ after.
85
86
 
86
87
  ## Quick start (MCP)
87
88
 
88
89
  ```bash
89
90
  # 1. Install (needs Bun — https://bun.sh)
90
- git clone https://github.com/kire21b/fapony.git && cd fapony
91
- bun install
92
- bun link # puts `fapony` on your PATH; or run via `bun fapony.ts`
91
+ npm install -g fapony # or: bun add -g fapony
92
+ # from source instead:
93
+ # git clone https://github.com/kire21b/fapony.git && cd fapony && bun install && bun link
93
94
  # note: `bun link` claims the global `fapony` bin by package name, not path — running it
94
95
  # from a second checkout silently repoints the command there. Re-run it in the one you want.
95
96
 
@@ -112,14 +113,7 @@ fapony usage-web # dashboard; re-run the scans to refre
112
113
 
113
114
  # 4. Verify (optional, per project) — scaffold the evidence allowlist
114
115
  fapony init /path/to/your-worktree
115
- # .fapony/evidence.json lists the commands the evidence collector may run —
116
- # edit the placeholder cmds to your real test/typecheck commands
117
- # commit it: the allowlist is a security boundary your whole team shares.
118
- # If your .gitignore ignores .fapony/ wholesale, re-include it (dir before file):
119
- # **/.fapony/*
120
- # !**/.fapony/evidence.json
121
- # Monorepo: give an app its own apps/<app>/.fapony/evidence.json and reports whose
122
- # changed files all sit under that app use it; anything else uses the root one.
116
+ # edit .fapony/evidence.json to your real test/typecheck commands, then commit it
123
117
  ```
124
118
 
125
119
  With `.fapony/evidence.json` in place, any graded run can be replayed as a report. This one is
@@ -135,6 +129,8 @@ You get one report: git facts (files, commits, branch), handoff conformance (cla
135
129
 
136
130
  Sections that have nothing to report say so (`not_run`, `unavailable`) rather than disappearing — a report with no evidence must not read like a report that passed.
137
131
 
132
+ Two details for the allowlist once it is under version control. If your `.gitignore` ignores `.fapony/` wholesale, re-include the file (dir before file): `**/.fapony/*`, then `!**/.fapony/evidence.json`. In a monorepo, give an app its own `apps/<app>/.fapony/evidence.json` — reports whose changed files all sit under that app use it; anything else uses the root one.
133
+
138
134
  Two things worth knowing about the report header and budget:
139
135
 
140
136
  - **`server_sha`** — every report is stamped with the git SHA of the fapony code that produced it, read once at server start. MCP servers are long-lived: after you edit fapony and don't restart the client, reports keep coming from the old build. Compare the stamp against `git log -1` in the fapony repo; if they differ, reconnect the server before trusting the result.
@@ -199,14 +195,6 @@ ungraded. It never picks the grade; it cannot see whether the work held up.
199
195
 
200
196
  ### The 6 tools
201
197
 
202
- ```
203
- discover: plan_list (plans grouped by state, joined with their run history)
204
- measure: fapony_stats ── fapony_usage
205
- verify: verdict_submit
206
- recall: project_health_context (what failed in these files before — optional, never required)
207
- mem_find (what was ever decided about these files — reads the project's mem log)
208
- ```
209
-
210
198
  | Tool | Tier | Purpose |
211
199
  |------|------|---------|
212
200
  | `plan_list` | discover | Plan files grouped by state — active / blocked / untouched / superseded / trackers — with a progress tally and each one's run history. Not a raw `ls`; see [Plans your agent can answer questions about](#plans-your-agent-can-answer-questions-about) |
@@ -218,7 +206,7 @@ recall: project_health_context (what failed in these files before — optional
218
206
 
219
207
  The handoff/report family is CLI-only — the schemas cost every session of every client and no skill called them. `fapony report <run-id>` prints the full report for a run (facts + handoff conformance + evidence + verdict); `fapony report-web [file]` renders it as a static HTML page (overwrites `file` on every call — safe to reuse the same path). Run `bun run overview` for a one-shot shortcut that writes it to `/tmp/fapony-overview.html` and opens it. `fapony usage-scan` scans session logs and writes a cache file; `fapony usage-web [port]` serves a static HTML dashboard from that cache (no live scanning). Run `fapony usage-scan` periodically to keep data fresh.
220
208
 
221
- Full protocol, adapter examples (bash, Python), and safety rules: [docs/mcp-handcheck.md](docs/mcp-handcheck.md).
209
+ Full protocol, adapter examples (bash, Python), and safety rules: [docs/mcp-handcheck.md](https://github.com/kire21b/fapony/blob/main/docs/mcp-handcheck.md).
222
210
 
223
211
  ### Verdict grades
224
212
 
@@ -304,9 +292,7 @@ flowchart TD
304
292
  **The fork at the top is load-bearing.** A plan file is an artifact for work the next session has
305
293
  to pick up. Wiring, refactors and UI passes finish in one sitting and the PLAN.md gets archived
306
294
  unread — so `/plan-with-pony` declines those itself and hands over the two seed commands instead.
307
- `fapony review-seed --files` takes a directory as well as file names, and answers "what is in
308
- here, who imports it, what is untested" for about a thirtieth of the tokens reading those files
309
- costs. Both arms meet at the same review and the same ledger.
295
+ Both arms meet at the same review and the same ledger.
310
296
 
311
297
  **The dotted edges are the whole point.** Verdicts carry `regime` and `reason_code`, so the
312
298
  ledger can answer the one question no single client can: *in this project, which model is worth
@@ -345,7 +331,7 @@ cat skill/plan-with-pony/SKILL.md | opencode run # OpenCode
345
331
  cat skill/plan-with-pony/SKILL.md | <your-agent> # anything that reads stdin
346
332
  ```
347
333
 
348
- Example plans produced by it live in [examples/](examples/).
334
+ Example plans produced by it live in [examples/](https://github.com/kire21b/fapony/tree/main/examples).
349
335
 
350
336
  ### Plans your agent can answer questions about
351
337
 
@@ -404,7 +390,7 @@ The layout, and why archiving is a plain `git mv`:
404
390
  Ship dates live in the plan's own header (`> ✅ **shipped 2026-09-13** (a1b2c3)`), not in the
405
391
  filename — `grep -h shipped .fapony/done/*.md | sort` answers "what landed when" without paying
406
392
  to rewrite every inbound link on every ship. Example plans, including an un-annotated one and an
407
- archived one: [examples/](examples/).
393
+ archived one: [examples/](https://github.com/kire21b/fapony/tree/main/examples).
408
394
 
409
395
  ## CLI
410
396
 
@@ -430,13 +416,13 @@ fapony install --platform <name> # force a specific client (bypasses de
430
416
  fapony install --dry-run # show what would happen without writing files
431
417
  fapony setup # interactive wizard: config + scaffold in one step
432
418
  fapony update # self-update via git pull
433
- fapony telemetry show|send # opt-in only, default off — see TELEMETRY.md
419
+ fapony telemetry show|send # opt-in only, default off — see https://github.com/kire21b/fapony/blob/main/TELEMETRY.md
434
420
  fapony test # self-check
435
421
  ```
436
422
 
437
423
  ## Config
438
424
 
439
- `fapony.config.json` lives in the fapony checkout and is gitignored (it's per-machine). Copy [fapony.config.example.json](fapony.config.example.json) for a complete working reference; every section is optional with sane defaults. Key fields:
425
+ `fapony.config.json` lives in the fapony checkout and is gitignored (it's per-machine). Copy [fapony.config.example.json](https://github.com/kire21b/fapony/blob/main/fapony.config.example.json) for a complete working reference; every section is optional with sane defaults. Key fields:
440
426
 
441
427
  - `worktrees` — name → absolute path mapping
442
428
  - `review.maxRounds` — round cap enforced by the gate
@@ -444,7 +430,7 @@ fapony test # self-check
444
430
  - `paths` (`planDir`/`doneDir`/`specDir`/`memoryEntry`/`stateDir`) / `safety` — directory layout and the dangerous-command deny-list
445
431
  - `usageWeb` — optional `{ port, hostname }` for `fapony usage-web` server defaults. Run `fapony usage-scan` first to populate the cache.
446
432
 
447
- Env overrides: `FAPONY_CONFIG` (config file), `FAPONY_STATE_DIR` (state DB location; default `~/.config/fapony/`). Full schema, design decisions, and edge cases are documented in [CLAUDE.md](CLAUDE.md) — this README intentionally doesn't duplicate them.
433
+ Env overrides: `FAPONY_CONFIG` (config file), `FAPONY_STATE_DIR` (state DB location; default `~/.config/fapony/`). Full schema, design decisions, and edge cases live with the code in the repo — this README intentionally doesn't duplicate them.
448
434
 
449
435
  ## Scope
450
436
 
@@ -453,10 +439,10 @@ Env overrides: `FAPONY_CONFIG` (config file), `FAPONY_STATE_DIR` (state DB locat
453
439
  - Measurement: cross-run KPIs by model/grade/value, per-file risk (graded touches vs. fails) + passive usage (tokens, cost)
454
440
  - Model attribution across clients — resolved from the session log that was live when the verdict landed, so a verdict carries a model without the caller declaring one
455
441
  - Zero setup beyond install: the two habits fapony depends on ship in the MCP `initialize` response, not in your rules file
456
- - Verification (beta): handoff conformance, 6-grade verdicts, allowlisted evidence collector (`.fapony/evidence.json` — agent-proposed commands are never executed); reports stamped with the producing build's `server_sha`
442
+ - Verification (beta): handoff conformance, 6-grade verdicts, allowlisted evidence collector (`.fapony/evidence.json`); reports stamped with the producing build's `server_sha`
457
443
  - Vendor-neutral executor/reviewer roles — anything that reads stdin
458
444
  - Memory integration via shell adapter, per project (configurable or default-wired)
459
- - Opt-in telemetry, off by default ([TELEMETRY.md](TELEMETRY.md) lists exactly what leaves the machine)
445
+ - Opt-in telemetry, off by default ([TELEMETRY.md](https://github.com/kire21b/fapony/blob/main/TELEMETRY.md) lists exactly what leaves the machine)
460
446
  - Bun-only; run state in SQLite via `bun:sqlite` (WAL mode)
461
447
 
462
448
  **Not supported (yet):**
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fapony",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Measurement layer for coding agents — measure what agents do, verify what they claim. 6 MCP tools, any agent, no loop required",
5
5
  "license": "MIT",
6
6
  "author": "delamind (https://github.com/kire21b)",
@@ -24,19 +24,21 @@
24
24
  "fapony.ts",
25
25
  "src/",
26
26
  "templates/",
27
- "skill/"
27
+ "skill/",
28
+ "images/"
28
29
  ],
29
- "devDependencies": {
30
- "@biomejs/biome": "^2.5.12",
31
- "bun-types": "^1.4.0",
32
- "typescript": "^5.9.3"
33
- },
34
30
  "scripts": {
35
31
  "lint": "biome check .",
36
32
  "typecheck": "tsc --noEmit",
37
33
  "test": "bun fapony.ts test",
38
34
  "test:fast": "SKIP_SLOW=1 bun fapony.ts test",
39
35
  "check": "bun run lint && bun run typecheck && bun fapony.ts test",
36
+ "prepublishOnly": "bash scripts/smoke-publish.sh",
40
37
  "overview": "bun fapony.ts report-web /tmp/fapony-overview.html && open /tmp/fapony-overview.html"
38
+ },
39
+ "devDependencies": {
40
+ "@biomejs/biome": "^2.5.12",
41
+ "bun-types": "^1.4.0",
42
+ "typescript": "^5.9.3"
41
43
  }
42
- }
44
+ }
@@ -102,7 +102,7 @@ the real scope instead.
102
102
  So the seed buys you structure; the draft budget goes on judgment:
103
103
 
104
104
  - **Read the Context block, then skip to filling** — everything else is the empty template.
105
- - **Fill every section yourself** — §1–§6 and the TL;DR start as `_agent เติม_` slots.
105
+ - **Fill every section yourself** — §1–§6 and the TL;DR start as `_agent fills in_` slots.
106
106
  - **Run the Phase −1 commands for facts** when the idea needs them, and put the numbers in the
107
107
  section they answer — a number you measured beats a number the seed guessed at.
108
108
  - **Signatures live in the SPEC chunks only.** Never paste them into plan §7 — link to the spec.
package/src/analyze.ts CHANGED
@@ -376,7 +376,7 @@ export function diagnose(
376
376
  findings.push({
377
377
  kind: "cycle",
378
378
  file: cycle.join(" ↔ "),
379
- detail: "import วนกลับหากันrefactor ฝั่งไหนก่อนก็พังอีกฝั่ง",
379
+ detail: "circular importsrefactoring either side breaks the other",
380
380
  evidence: [...cycle, cycle[0]].join(" → "),
381
381
  });
382
382
  }
@@ -389,7 +389,8 @@ export function diagnose(
389
389
  findings.push({
390
390
  kind: "orphan",
391
391
  file: f,
392
- detail: "ไม่มีใคร import และไม่ใช่ entry point — dead code candidate",
392
+ detail:
393
+ "no one imports it and it is not an entry point — dead code candidate",
393
394
  evidence: "0 dependents",
394
395
  });
395
396
  }
@@ -405,8 +406,8 @@ export function diagnose(
405
406
  findings.push({
406
407
  kind: "hub-untested",
407
408
  file,
408
- detail: `${n} ไฟล์พึ่งอยู่ ไม่มีเทสไหน import มันเลยแก้ตรงนี้ไม่มีอะไรจับตอนพัง`,
409
- evidence: `พึ่งอยู่: ${shown}${rest}`,
409
+ detail: `${n} files depend on it; no test imports it edit here and nothing catches the break`,
410
+ evidence: `dependents: ${shown}${rest}`,
410
411
  });
411
412
  }
412
413
 
@@ -417,10 +418,10 @@ export function diagnose(
417
418
  findings.push({
418
419
  kind: "changed-untested",
419
420
  file: c,
420
- detail: `เพิ่งแก้แต่ไม่มีเทสไหนพึ่งอยู่ (${deps.size} dependent) — พังแล้วไม่มีอะไรจับ`,
421
+ detail: `recently changed but no test depends on it (${deps.size} dependent) — nothing catches it if it breaks`,
421
422
  evidence:
422
423
  deps.size > 0
423
- ? `พึ่งอยู่: ${[...deps].sort().join(", ")}`
424
+ ? `dependents: ${[...deps].sort().join(", ")}`
424
425
  : "0 dependents",
425
426
  });
426
427
  }
@@ -475,7 +476,7 @@ export function formatAnalyze(graph: ImportGraph, findings: Finding[]): string {
475
476
  lines.push("");
476
477
 
477
478
  if (findings.length === 0) {
478
- lines.push("no findings — โครงสร้างไม่มีอะไรน่าห่วง");
479
+ lines.push("no findings — structure looks healthy");
479
480
  } else {
480
481
  for (const f of findings.slice(0, 5)) {
481
482
  const icon = f.kind === "orphan" ? "·" : "⚠";
@@ -488,7 +489,7 @@ export function formatAnalyze(graph: ImportGraph, findings: Finding[]): string {
488
489
  if (rest > 0) lines.push(`… and ${rest} more`);
489
490
  lines.push(
490
491
  graph.unresolved > 0
491
- ? `${findings.length} findings. ${graph.unresolved} unresolved imports (path alias / package name) — ตัวเลข dependent อาจต่ำกว่าจริง`
492
+ ? `${findings.length} findings. ${graph.unresolved} unresolved imports (path alias / package name) — dependent counts may be lower than reality`
492
493
  : `${findings.length} findings.`,
493
494
  );
494
495
  }
@@ -1,15 +1,15 @@
1
1
  // src/conventions-seed.ts — `fapony init` fill-signal (PLAN-convention-debt chunk 2).
2
2
  //
3
- // คำถาม: "การ fill signal ตอน init ทำได้แค่ไหน" (SPEC-convention-debt §2.2) —
4
- // วัดสองรอบ คำตอบรอบแรกผิด: eslint คือของที่ทีมส่วนใหญ่ปล่อย default ตอนติดตั้ง,
5
- // pair-mining พังกับ commit ก้อนใหญ่และ cap ผ่อนไม่ได้ (วัดแล้ว) — ของที่**ทุกรีโปมี**
6
- // คือโค้ดตัวเอง ฐานจึงเป็น wrapper detector (อ่าน snapshot อย่างเดียว ไม่แตะ history),
7
- // eslint เป็น bonus เมื่อมี, pair-mining ยังไม่ทำ (chunk 7 วัด precision ก่อน)
3
+ // Question: "how far can signal-filling at init go" (SPEC-convention-debt §2.2) —
4
+ // Measured twice; the first answer was wrong: eslint is what most teams leave at default on install,
5
+ // pair-mining breaks on large commits and the cap cannot be relaxed (measured) — what **every repo has**
6
+ // is its own code, so the base is a wrapper detector (reads the snapshot only, never touches history),
7
+ // eslint is a bonus when present, pair-mining is not done yet (chunk 7 measures precision first)
8
8
  //
9
- // เขียนได้ที่เดียว: <target>/.fapony/conventions.json — นิยาม convention อยู่ในรีโปที่
10
- // ถูกวัด (SPEC §2.1), commit ได้เพราะทุก field เป็น repo-relative · มีอยู่แล้ว = ไม่แตะ
11
- // (rule 5c) · fapony ไม่เดา convention ที่ไม่มีร่องรอยอันที่ยังไม่เคย migrate และ
12
- // ไม่มี wrapper ไม่มีร่องรอยทั้งใน snapshot และ history ช่องนั้นเหลือให้คนเติม (stale)
9
+ // Written to one place: <target>/.fapony/conventions.json — the convention definition lives in the repo being
10
+ // measured (SPEC §2.1), committable because every field is repo-relative · already exists = leave untouched
11
+ // (rule 5c) · fapony does not guess a convention with no trace one never migrated and
12
+ // with no wrapper has no trace in either snapshot or history, that slot is left for a human to fill (stale)
13
13
 
14
14
  import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
15
15
  import { dirname, join, relative } from "node:path";
@@ -108,7 +108,7 @@ function rowsFromBlock(block: Block, whereBase: string): SeedRow[] {
108
108
  whereGlob && whereGlob !== "." ? `${dir}${whereGlob}` : whereBase || ".";
109
109
  const push = (message: unknown, stale: string | null, ruleId: string) => {
110
110
  const text = flatText(message);
111
- if (!text) return; // checker โดยไม่มีข้อความ = ไม่รู้ว่ากฎคืออะไร
111
+ if (!text) return; // a checker without a message = the rule is unknown
112
112
  out.push({ id: "", rule: text, where, stale, checker: ruleId });
113
113
  };
114
114
  for (const [ruleId, value] of Object.entries(block.rules ?? {})) {
package/src/db/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- // barrel — re-export ทุกอย่างจาก db/* ให้ import เดิมจาก "./db/index.js" ใช้ได้
1
+ // barrel — re-export everything from db/* so existing imports from "./db/index.js" keep working
2
2
 
3
3
  export * from "./defaults.js";
4
4
  export * from "./getters.js";
package/src/db/store.ts CHANGED
@@ -80,7 +80,11 @@ export function openDb(config?: Config): Database {
80
80
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
81
81
 
82
82
  const db = new Database(`${dir}/state.db`);
83
+ // bun default busy_timeout=0 = no retry on SQLITE_BUSY; multiple agent
84
+ // processes share this db, so concurrent writers throw immediately without this.
85
+ db.run("PRAGMA busy_timeout=5000");
83
86
  db.run("PRAGMA journal_mode=WAL");
87
+ db.run("PRAGMA synchronous=NORMAL");
84
88
 
85
89
  migrateDb(db);
86
90
 
package/src/debt.ts CHANGED
@@ -1,18 +1,20 @@
1
1
  // src/debt.ts — `fapony debt`: which files have not moved to a shipped convention yet.
2
2
  //
3
- // คำถามที่ไม่มีใครตอบได้: "ไฟล์ไหนยังไม่ย้าย" — rules files (CLAUDE.md, Cursor rules)
4
- // บอกได้แค่ว่า "กฎคืออะไร" (ชั้น 2) และ "ก๊อปไฟล์ไหน" (ชั้น 1) — ตำแหน่งของหนี้
5
- // (ชั้น 3) อยู่ในหัวเจ้าของ และหายเมื่อลืม (SPEC-convention-debt §1)
3
+ // The question nobody can answer: "which files have not moved" — rules files
4
+ // (CLAUDE.md, Cursor rules) can only say "what the rule is" (layer 2) and
5
+ // "which files were copied" (layer 1) where the debt is (layer 3) lives in
6
+ // the owner's head and vanishes when forgotten (SPEC-convention-debt §1)
6
7
  //
7
- // นิยามของ convention อยู่ในรีโปที่ถูกวัด (<repo>/.fapony/conventions.json — ผ่าน
8
- // resolver เดียวกับ mem log, SPEC §2.1) — fapony ไม่รู้จัก React หรือ Hono และ
9
- // ต้องไม่รู้จัก · หนึ่ง convention = pattern ที่ควรใช้ (ok) + pattern ที่แปลว่า
10
- // ยังไม่ย้าย (stale) + ขอบเขต (where) + เงื่อนไขของไฟล์ (guard, เช่น extends Base)
8
+ // The convention definition lives in the measured repo (<repo>/.fapony/conventions.json
9
+ // — via the same resolver as the mem log, SPEC §2.1) — fapony does not know React
10
+ // or Hono and must not · one convention = pattern to use (ok) + pattern meaning
11
+ // not-yet-migrated (stale) + scope (where) + file condition (guard, e.g. extends Base)
11
12
  //
12
- // หนี้ถูกคำนวณสดทุกครั้ง ไม่เขียนลงที่ใดเลย (แบบเดียวกับ analyze: cache คือหนี้ล้วน —
13
- // ลิสต์ที่ freeze ไว้ตกรุ่นเงียบ เหมือน MASTER.md) · กฎเหล็ก: checker ไม่ null =
14
- // fapony ไม่รายงานหนี้ข้อนั้น การรายงานซ้ำกับ eslint คือ abstraction ที่มี
15
- // implementation เดียว (กฎ 1) และสอนให้ agent ข้ามทั้งคู่ (SPEC §2)
13
+ // Debt is computed live every time, never written anywhere (same as analyze:
14
+ // a cache is pure debt — a frozen list goes stale silently like MASTER.md) ·
15
+ // Iron rule: checker not null = fapony does not report that debt item — reporting
16
+ // twice with eslint is an abstraction with one implementation (rule 1) and
17
+ // teaches the agent to skip both (SPEC §2)
16
18
  //
17
19
  // Read-only stdout: no file writes, no state.db, no cache (rule 5b).
18
20
 
@@ -242,12 +244,12 @@ export function debtScan(
242
244
  const compiled: Compiled[] = [];
243
245
  for (const conv of loaded.convs) {
244
246
  if (conv.checker) {
245
- // กฎเหล็ก — fapony เงียบ ปล่อยให้ checker ทำงาน (SPEC §2)
247
+ // Iron rule — fapony stays silent, leave it to the checker (SPEC §2)
246
248
  checkedCount++;
247
249
  continue;
248
250
  }
249
251
  if (!conv.stale) {
250
- // ช่องเดียวที่คนเติม (SPEC §2.2) — โชว์ว่าค้าง ไม่เดาแทน
252
+ // The one slot a human fills (SPEC §2.2) — show it as pending, don't guess
251
253
  declared.push(conv);
252
254
  continue;
253
255
  }
@@ -352,16 +354,18 @@ export function debtForFile(
352
354
  return out;
353
355
  }
354
356
 
355
- // --- Promotion signal (chunk 5) — "เรื่องนี้ซ้ำครั้งที่ N แล้ว ทำ checker ไหม" ---
357
+ // --- Promotion signal (chunk 5) — "this recurred N times, time for a checker?" ---
356
358
  //
357
- // "ผมจะทำ eslint ตอนที่คิดได้" — จังหวะ "คิดได้" คือสิ่งที่หายไป (SPEC §3) fapony
358
- // เห็นประวัติข้าม session (mem + verdicts) จึงนับได้ว่าเรื่องเดียวกันถูกแก้ซ้ำกี่ครั้ง
359
- // แล้วยื่นคำถามให้คนตัดสิน ไม่ตัดสินเอง ไม่เขียน eslint rule เอง (SPEC §6 fail list)
359
+ // "I'll write eslint when I think of it" — the "think of it" moment is what goes
360
+ // missing (SPEC §3) · fapony sees history across sessions (mem + verdicts), so it
361
+ // can count how often the same thing was fixed, then put the question to a human —
362
+ // it does not decide, does not write the eslint rule itself (SPEC §6 fail list)
360
363
  //
361
- // การ match "เรื่องเดียวกัน"แม่นยำเท่าที่ข้อมูลให้ (SPEC §7: แถวเก่าไม่มี files[]
362
- // ยังไม่ตัดสิน): แถวที่มี files[] ต้อง intersect กับ debt list · ข้อความต้องเอ่ยถึง
363
- // สัญลักษณ์ของ convention (ok เช่น fmtMoney, หรือ identifier ≥ 6 ตัวจาก stale เช่น
364
- // toLocaleString/useMutation — "throw"/"Error" สั้นเกินจึงไม่นับ กัน over-match)
364
+ // Matching "the same thing" — only as precise as the data allows (SPEC §7: old rows
365
+ // lack files[], still undecided): a row with files[] must intersect the debt list ·
366
+ // the text must mention a convention symbol (ok such as fmtMoney, or an identifier
367
+ // ≥ 6 chars from stale such as toLocaleString/useMutation — "throw"/"Error" are too
368
+ // short and don't count, to avoid over-matching)
365
369
 
366
370
  export const PROMOTION_THRESHOLD = 3;
367
371
  const PROMOTION_MAX = 3;
@@ -642,7 +646,7 @@ export function cmdDebt(args: string[]): void {
642
646
  }
643
647
 
644
648
  if (loaded.path === null) {
645
- // SPEC §6: ไม่มี conventions.json = เงียบสนิท ไม่ error ไม่ชวนสร้าง
649
+ // SPEC §6: no conventions.json = completely silent, no error, no prompt to create one
646
650
  console.log(
647
651
  `fapony debt — no conventions.json in ${worktree} (nothing tracked yet)`,
648
652
  );
@@ -1,6 +1,6 @@
1
- // src/digest/collect.ts — รวมข้อมูลจาก 4 แหล่งเป็น DigestData ก้อนเดียว
1
+ // src/digest/collect.ts — merge data from 4 sources into a single DigestData
2
2
  //
3
- // ไม่ render อะไรเลย แค่อ่าน + จัดรูป struct
3
+ // Renders nothingjust reads + shapes into structs
4
4
 
5
5
  import { execSync } from "node:child_process";
6
6
  import { existsSync, readdirSync, readFileSync } from "node:fs";
@@ -144,7 +144,7 @@ function parseFrontmatter(text: string): { status?: string; kind?: string } {
144
144
  }
145
145
 
146
146
  function countCheckboxes(text: string): { done: number; total: number } {
147
- // นับ checkbox ใน section ## แรกเท่านั้น (same logic as plans.ts)
147
+ // Count checkboxes in the first ## section only (same logic as plans.ts)
148
148
  const body = text.replace(/^---\r?\n[\s\S]*?\r?\n---/, "");
149
149
  const start = body.search(/^##\s+/m);
150
150
  if (start < 0) return { done: 0, total: 0 };
@@ -216,7 +216,7 @@ function readUsageAndCost(_worktree: string): {
216
216
  detail: string;
217
217
  } {
218
218
  const cache = readCache();
219
- // หา global entry (worktree=null) หรือรวมทุก entry
219
+ // Find a global entry (worktree=null), otherwise merge every entry
220
220
  const globalEntries = cache.filter((e) => !e.worktree);
221
221
  if (globalEntries.length === 0 && cache.length === 0) {
222
222
  return {
@@ -232,11 +232,11 @@ function readUsageAndCost(_worktree: string): {
232
232
  };
233
233
  }
234
234
 
235
- // ใช้ global entries ถ้ามี ไม่งั้นรวมทุก entry
235
+ // Use global entries if present, otherwise merge every entry
236
236
  const entries = globalEntries.length > 0 ? globalEntries : cache;
237
237
  const usage = entriesToUsage(entries);
238
238
 
239
- // คำนวณราคา
239
+ // Compute the price
240
240
  const prices = loadPrices();
241
241
  if (!prices) {
242
242
  return {
@@ -271,7 +271,7 @@ function readUsageAndCost(_worktree: string): {
271
271
  imputed: m.imputed_cost,
272
272
  }));
273
273
 
274
- // เพิ่ม cost จริงจาก cache
274
+ // Attach the real cost from the cache
275
275
  const cacheByKey = new Map(
276
276
  entries.flatMap((e) =>
277
277
  e.by_model.map((m) => [`${m.provider}\0${m.model}`, m]),
@@ -1,7 +1,7 @@
1
1
  // src/digest/html.ts — render digest as a single HTML page
2
2
  //
3
- // คอลัมน์เดียว max-width: 68rem · ไม่มี tab ไม่มีปุ่มพับ ไม่มี JS
4
- // ใช้ DARK_THEME_CSS + TABLE_CSS จาก src/web/html.ts
3
+ // single column max-width: 68rem · no tabs, no collapse buttons, no JS
4
+ // uses DARK_THEME_CSS + TABLE_CSS from src/web/html.ts
5
5
 
6
6
  import { DARK_THEME_CSS, esc, TABLE_CSS } from "../web/html.js";
7
7
  import type {
@@ -1,6 +1,6 @@
1
1
  // src/digest/text.ts — render digest as terminal text
2
2
  //
3
- // ลำดับ section ตาม SPEC §3 (เล่าเรื่อง ไม่ใช่ dashboard)
3
+ // Section order per SPEC §3 (tells a story, not a dashboard)
4
4
 
5
5
  import type { CostRow, DigestData, MemRow, PlanRow } from "./collect.js";
6
6
 
package/src/gate.ts CHANGED
@@ -53,9 +53,9 @@ export function gateOnce(
53
53
  }
54
54
 
55
55
  const config = loadConfig();
56
- // runs.worktree เก็บ absolute path (SERVER_INSTRUCTIONS บังคับ) — ไม่ใช่ key ของ config.worktrees
57
- // การ lookup จึงพลาดเสมอ และ fallback เดิม "." ทำให้คำสั่ง memory ของโปรเจกต์ไปรันใน cwd ของ
58
- // MCP server แทนที่จะเป็นโปรเจกต์นั้น · lookup ยังอยู่เผื่อ row เก่าที่เคยเก็บเป็น key
56
+ // runs.worktree stores an absolute path (SERVER_INSTRUCTIONS mandates it) — not a key in config.worktrees
57
+ // so the lookup always misses, and the old "." fallback ran the project's memory commands in the cwd of
58
+ // the MCP server instead of that project · the lookup stays in case an old row was stored as a key
59
59
  const worktree = config.worktrees[run.worktree] ?? run.worktree;
60
60
 
61
61
  // --- pass family (4 grades) ---