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.
- package/README.md +22 -36
- 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 +10 -8
- package/skill/plan-with-pony/SKILL.md +1 -1
- package/src/analyze.ts +9 -8
- package/src/conventions-seed.ts +10 -10
- package/src/db/index.ts +1 -1
- package/src/db/store.ts +4 -0
- package/src/debt.ts +26 -22
- package/src/digest/collect.ts +7 -7
- package/src/digest/html.ts +2 -2
- package/src/digest/text.ts +1 -1
- package/src/gate.ts +3 -3
- package/src/hook.ts +119 -21
- package/src/init-mem.ts +3 -3
- package/src/install/opencode.ts +95 -8
- package/src/install/types.ts +1 -1
- package/src/install.ts +5 -1
- package/src/lint-baseline.ts +9 -9
- package/src/mcp/tools/mem.ts +1 -1
- package/src/mcp/tools/stats.ts +2 -2
- package/src/mcp/tools/usage.ts +3 -3
- package/src/mcp/tools/verdict.ts +29 -16
- package/src/mcp/transport.ts +1 -1
- package/src/plan-seed.ts +23 -21
- package/src/price/fetch.ts +19 -19
- package/src/price/resolve.ts +24 -24
- package/src/review-seed.ts +1 -1
- package/src/stats/data.ts +8 -7
- package/src/stats/format.ts +6 -5
- package/src/usage/render.ts +6 -5
- package/templates/PLAN.md +1 -1
- package/templates/mem/commands/plan.ts +44 -44
- package/templates/mem/commands/read.ts +5 -5
- package/templates/mem/commands/rotate.ts +6 -6
- package/templates/mem/commands/selftest.ts +14 -14
- package/templates/mem/commands/write.ts +13 -13
- package/templates/mem/mem.ts +5 -5
- package/templates/mem/selectors.ts +18 -18
- package/templates/mem/store.ts +50 -50
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
# fapony
|
|
6
6
|
|
|
7
|
+
[](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
|
|
45
|
-
|
|
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.
|
|
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
|
|
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.
|
|
85
|
+
after.
|
|
85
86
|
|
|
86
87
|
## Quick start (MCP)
|
|
87
88
|
|
|
88
89
|
```bash
|
|
89
90
|
# 1. Install (needs Bun — https://bun.sh)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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`
|
|
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):**
|
package/images/logo.png
ADDED
|
Binary file
|
package/images/logo.webp
ADDED
|
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.
|
|
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
|
|
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: "
|
|
379
|
+
detail: "circular imports — refactoring 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:
|
|
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}
|
|
409
|
-
evidence:
|
|
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:
|
|
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
|
-
?
|
|
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) —
|
|
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
|
}
|
package/src/conventions-seed.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// src/conventions-seed.ts — `fapony init` fill-signal (PLAN-convention-debt chunk 2).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// pair-mining
|
|
6
|
-
//
|
|
7
|
-
// eslint
|
|
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
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// (rule 5c) · fapony
|
|
12
|
-
//
|
|
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
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// (
|
|
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
|
-
//
|
|
8
|
-
// resolver
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// fapony
|
|
15
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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) — "
|
|
357
|
+
// --- Promotion signal (chunk 5) — "this recurred N times, time for a checker?" ---
|
|
356
358
|
//
|
|
357
|
-
// "
|
|
358
|
-
//
|
|
359
|
-
//
|
|
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
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
// toLocaleString/useMutation — "throw"/"Error"
|
|
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:
|
|
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
|
);
|
package/src/digest/collect.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// src/digest/collect.ts —
|
|
1
|
+
// src/digest/collect.ts — merge data from 4 sources into a single DigestData
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Renders nothing — just 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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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]),
|
package/src/digest/html.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/digest/html.ts — render digest as a single HTML page
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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 {
|
package/src/digest/text.ts
CHANGED
package/src/gate.ts
CHANGED
|
@@ -53,9 +53,9 @@ export function gateOnce(
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const config = loadConfig();
|
|
56
|
-
// runs.worktree
|
|
57
|
-
//
|
|
58
|
-
// MCP server
|
|
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) ---
|