fapony 0.3.0 → 0.3.4
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 +87 -65
- package/fapony.ts +3 -116
- package/package.json +6 -5
- package/skill/move-to-done/SKILL.md +22 -32
- package/skill/review-pony/SKILL.md +27 -58
- package/src/adapters/cli.ts +123 -0
- package/src/adapters/hooks/compute-hint-impact.ts +107 -0
- package/src/adapters/hooks/context-data.ts +102 -0
- package/src/adapters/hooks/edit-hint.ts +195 -0
- package/src/adapters/hooks/git-autonomy.ts +178 -0
- package/src/adapters/hooks/index.ts +79 -0
- package/src/adapters/hooks/mv-guard.ts +52 -0
- package/src/adapters/hooks/read-hint.ts +383 -0
- package/src/adapters/hooks/session-start.ts +101 -0
- package/src/adapters/hooks/stop.ts +302 -0
- package/src/{mcp → adapters/mcp}/evidence.ts +2 -2
- package/src/{mcp → adapters/mcp}/primitives.ts +3 -3
- package/src/{mcp → adapters/mcp}/tools/check.ts +1 -1
- package/src/{mcp → adapters/mcp}/tools/collect.ts +1 -1
- package/src/{mcp → adapters/mcp}/tools/index.ts +0 -74
- package/src/{mcp → adapters/mcp}/tools/mem.ts +3 -3
- package/src/{mcp → adapters/mcp}/tools/report.ts +4 -4
- package/src/{mcp → adapters/mcp}/transport.ts +4 -39
- package/src/adapters/mcp/types.ts +18 -0
- package/src/{mcp → adapters/mcp}/worktree.ts +2 -2
- package/src/analyze.ts +2 -2
- package/src/conventions-seed.ts +1 -1
- package/src/core/config.ts +199 -0
- package/src/core/debt-format.ts +107 -0
- package/src/core/debt-types.ts +79 -0
- package/src/core/defaults.ts +8 -0
- package/src/core/enums.ts +34 -0
- package/src/core/format.ts +33 -0
- package/src/core/hint-log.ts +68 -0
- package/src/core/hook-helpers.ts +31 -0
- package/src/core/mem-log.ts +357 -0
- package/src/core/parse.ts +71 -0
- package/src/core/pricing.ts +217 -0
- package/src/core/safety.ts +18 -0
- package/src/core/types.ts +142 -0
- package/src/core/util.ts +105 -0
- package/src/db/store.ts +2 -2
- package/src/debt/cli.ts +1 -1
- package/src/debt/format.ts +2 -107
- package/src/debt/load.ts +1 -1
- package/src/debt/promotion.ts +1 -1
- package/src/debt/types.ts +14 -79
- package/src/digest/collect.ts +4 -3
- package/src/gate.ts +5 -5
- package/src/gates.ts +1 -1
- package/src/hook.ts +69 -1337
- package/src/init-mem.ts +58 -72
- package/src/init.ts +13 -17
- package/src/install/antigravity.ts +112 -0
- package/src/install/claude.ts +19 -123
- package/src/install/detect.ts +17 -7
- package/src/install/opencode.ts +167 -26
- package/src/install.ts +23 -7
- package/src/lint-baseline.ts +1 -2
- package/src/map.ts +29 -8
- package/src/mem/commands/plan.ts +70 -42
- package/src/mem/commands/read.ts +219 -146
- package/src/mem/index.ts +4 -13
- package/src/mem/store.ts +5 -1
- package/src/memory.ts +24 -387
- package/src/parse.ts +9 -71
- package/src/price/fetch.ts +4 -16
- package/src/price/resolve.ts +12 -213
- package/src/report/cli.ts +3 -3
- package/src/safety.ts +2 -18
- package/src/{plan-seed.ts → seed/plan-seed.ts} +18 -32
- package/src/seed/primitives.ts +60 -0
- package/src/{review-seed.ts → seed/review-seed.ts} +7 -54
- package/src/session/types.ts +14 -128
- package/src/setup.ts +1 -1
- package/src/stats/data.ts +4 -4
- package/src/telemetry.ts +3 -3
- package/src/usage/cache.ts +1 -2
- package/src/usage/cli.ts +1 -1
- package/src/usage/scan.ts +2 -1
- package/src/util.ts +10 -32
- package/src/web/html.ts +2 -33
- package/templates/SPEC.md +8 -1
- package/images/logo.png +0 -0
- package/images/logo.webp +0 -0
- package/images/logo@400.webp +0 -0
- package/images/sample.webp +0 -0
- package/images/summary.webp +0 -0
- package/src/db/defaults.ts +0 -34
- package/src/db/getters.ts +0 -35
- package/src/db/index.ts +0 -7
- package/src/db/load.ts +0 -57
- package/src/db/types.ts +0 -77
- package/src/math.ts +0 -13
- package/src/mcp/tools/verdict.ts +0 -161
- package/src/mcp/types.ts +0 -54
package/README.md
CHANGED
|
@@ -36,15 +36,16 @@ quietly counted as free.
|
|
|
36
36
|
|
|
37
37
|
</details>
|
|
38
38
|
|
|
39
|
-
That is day one. Past that, fapony
|
|
40
|
-
|
|
41
|
-
the
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
That is day one. Past that, fapony keeps what coding agents actually did — the frozen
|
|
40
|
+
ledger of graded runs (rounds, pass/fail, cost per grade, readable via CLI, no new grades)
|
|
41
|
+
plus the live mem log — through 3 MCP tools any agent can call. If you juggle more than
|
|
42
|
+
one agent, this is the point: the numbers come from the same yardstick everywhere, so
|
|
43
|
+
"which model earns its keep on which kind of task" becomes a data question instead of a
|
|
44
|
+
vibe. On top of history it checks claims against git facts: handoff conformance and
|
|
45
|
+
allowlisted evidence — with everything the agent claimed but couldn't prove marked as such.
|
|
45
46
|
|
|
46
|
-
**What that question looks like answered, from one project's own
|
|
47
|
-
frontier (`fapony stats --mode verdict --regime code`):**
|
|
47
|
+
**What that question looks like answered, from one project's own (frozen — reads history,
|
|
48
|
+
no new grades) ledger — the top of the `n≥5` frontier (`fapony stats --mode verdict --regime code`):**
|
|
48
49
|
|
|
49
50
|
| model | tokens/pass | quality | n |
|
|
50
51
|
|---|---|---|---|
|
|
@@ -63,14 +64,15 @@ accumulates is pain.** An agent has no memory of pain across sessions: it writes
|
|
|
63
64
|
hand-rolled `try/catch` as cheerfully as the first, because every session starts new. Wrappers and
|
|
64
65
|
shared libraries get built by *people* who were hurt by the same thing often enough to remember.
|
|
65
66
|
That is why a codebase written with agents from day one tends not to grow a shared layer — nobody
|
|
66
|
-
in the room remembers. fapony is the part that remembers:
|
|
67
|
-
`files[]`, so the zones that keep coming back in
|
|
67
|
+
in the room remembers. fapony is the part that remembers: mem rows carry
|
|
68
|
+
`files[]`, so the zones that keep coming back in re-done work are a query, not a hunch
|
|
69
|
+
(the frozen ledger's old graded rows carry them too).
|
|
68
70
|
Paired with `fapony debt`, which tracks how far the codebase has actually moved to a convention you
|
|
69
71
|
already decided on, that is the loop: notice the repeated cost, name the shared thing, watch the
|
|
70
72
|
migration finish. Finding dead code and duplication is *not* part of it — knip and friends already
|
|
71
73
|
do that better, and a convention with a `checker` is deliberately left to the checker.
|
|
72
74
|
|
|
73
|
-
**The measurement layer underneath it:** 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
|
|
75
|
+
**The measurement layer underneath it:** 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. The frozen ledger still answers that from history — every old 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" stays a table, not an argument. New accumulation goes to the mem log instead: decisions, bugs and notes with `files[]`, written by the agents doing the work.
|
|
74
76
|
|
|
75
77
|
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.
|
|
76
78
|
|
|
@@ -82,15 +84,16 @@ Stated up front, because the gap between these two things is where most tooling
|
|
|
82
84
|
|
|
83
85
|
- **It does not run your test suite.** The evidence collector runs an allowlist *you* write in
|
|
84
86
|
`.fapony/evidence.json`, and never a command an agent proposes. No allowlist, no evidence.
|
|
85
|
-
- **It does not judge your code.**
|
|
86
|
-
agent supplies
|
|
87
|
+
- **It does not judge your code.** Mem rows *record* decisions, bugs and notes; a human or
|
|
88
|
+
a working agent supplies them. fapony is the memory, not the judge. (The frozen
|
|
89
|
+
ledger's old grades work the same way — *stored*, never computed.)
|
|
87
90
|
- **It checks conformance, not correctness.** What it can verify is that a claim lines up with git
|
|
88
91
|
facts and that uncertainty was declared — not that the code works. Those are different
|
|
89
92
|
guarantees and fapony only offers the first.
|
|
90
93
|
- **Almost nothing blocks.** No CI failure, no gate on your own commands. The one exception is the
|
|
91
|
-
Stop hook, once per turn when a commit
|
|
94
|
+
Stop hook, once per turn when a commit lands with no new mem row; the read/edit/commit hints only annotate.
|
|
92
95
|
Skip the install of all of them and you are back to exactly the workflow you had.
|
|
93
|
-
- **Model attribution is inferred, not declared.** A gate is attributed to whichever client
|
|
96
|
+
- **Model attribution is inferred, not declared.** A gate in the frozen ledger is attributed to whichever client
|
|
94
97
|
session was live in that worktree at that moment. When one model writes the code and another
|
|
95
98
|
reviews and files the verdict, the grade lands on the reviewer. Reports label it `inferred`;
|
|
96
99
|
read it as such.
|
|
@@ -123,21 +126,23 @@ fapony price-scan # fetch the OpenRouter price table →
|
|
|
123
126
|
fapony usage-web # dashboard; re-run the scans to refresh
|
|
124
127
|
# both scans are manual by design — nothing fetches or re-reads session logs behind your back
|
|
125
128
|
|
|
126
|
-
# 4.
|
|
129
|
+
# 4. Turn on the knowledge layer (per project you want it in)
|
|
127
130
|
fapony init /path/to/your-worktree
|
|
128
|
-
#
|
|
131
|
+
# creates .fapony/ — .memory/ (the mem log the 3 MCP tools read and write),
|
|
132
|
+
# conventions.json for `fapony debt`, plan/spec/done, and evidence.json
|
|
133
|
+
# conventions.json + evidence.json are shared rules: commit them
|
|
129
134
|
```
|
|
130
135
|
|
|
131
|
-
With `.fapony/evidence.json` in place, any graded run
|
|
132
|
-
a CLI command, not an MCP tool — the schemas cost every session of
|
|
133
|
-
called them (see [The
|
|
134
|
-
|
|
136
|
+
With `.fapony/evidence.json` in place, any graded run from the frozen ledger can be replayed
|
|
137
|
+
as a report. This one is a CLI command, not an MCP tool — the schemas cost every session of
|
|
138
|
+
every client and no skill called them (see [The 3 tools](#the-3-tools) below). No new runs
|
|
139
|
+
can be created; run ids come from `fapony stats` reading history:
|
|
135
140
|
|
|
136
141
|
```bash
|
|
137
142
|
fapony report <run-id> # run ids come from `fapony stats`
|
|
138
143
|
```
|
|
139
144
|
|
|
140
|
-
You get one report: git facts (files, commits, branch), handoff conformance (claims vs. reality), evidence from the allowlisted commands (pass/fail/timeout/unverified),
|
|
145
|
+
You get one report: git facts (files, commits, branch), handoff conformance (claims vs. reality), evidence from the allowlisted commands (pass/fail/timeout/unverified), the frozen 6-grade verdict, and cost — with anything the agent claimed but couldn't prove marked as such.
|
|
141
146
|
|
|
142
147
|
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.
|
|
143
148
|
|
|
@@ -170,9 +175,9 @@ losing a single number.
|
|
|
170
175
|
|
|
171
176
|
| | The ledger | The work side |
|
|
172
177
|
|---|---|---|
|
|
173
|
-
| What it is |
|
|
178
|
+
| What it is | 3 MCP tools (mem) + a frozen SQLite ledger (reads history) | plans, skills, read-only seed commands |
|
|
174
179
|
| Needs | an MCP client | nothing — or your own tooling instead |
|
|
175
|
-
| Writes | one
|
|
180
|
+
| Writes | one mem row per unit of work, into the project's log | nothing |
|
|
176
181
|
| Skip it and | there is no fapony | fapony still answers every question |
|
|
177
182
|
|
|
178
183
|
### What runs where
|
|
@@ -184,12 +189,12 @@ is `tool.execute.after`. Nothing here is required: skip the hooks and every MCP
|
|
|
184
189
|
|
|
185
190
|
| | Claude Code | OpenCode | Cursor | ZCode | Codex |
|
|
186
191
|
|---|---|---|---|---|---|
|
|
187
|
-
| MCP tools — `mem_find` `mem_add` `mem_close`
|
|
188
|
-
| Stop hook — refuse to end a turn with
|
|
192
|
+
| MCP tools — `mem_find` `mem_add` `mem_close` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
193
|
+
| Stop hook — refuse to end a turn with commits but no new mem row | ✅ | — | ✅ | — | ✅ after trust |
|
|
189
194
|
| Read hint — big-file pointer + debt/mem lines | ✅ before | ✅ after | — | — | — |
|
|
190
195
|
| Re-read hint — unchanged repeat read | ✅ before | ✅ after | — | — | — |
|
|
191
196
|
| Edit hint — importer count before a shape change | ✅ before | ✅ after | — | — | — |
|
|
192
|
-
| Commit hint — `git commit` →
|
|
197
|
+
| Commit hint — `git commit` → record-a-mem-row nudge | — | ✅ after | — | — | — |
|
|
193
198
|
| Skills symlinked into `~/.claude/skills` | ✅ | ✅ | — | — | — |
|
|
194
199
|
| Skills symlinked into `~/.agents/skills` | — | — | — | ✅ | ✅ |
|
|
195
200
|
| `usage-scan` reads this client's session log | ✅ | ✅ | — | ✅ | ✅ |
|
|
@@ -202,10 +207,10 @@ without the agent deciding to call anything ([why](#when-to-call-what)).
|
|
|
202
207
|
|
|
203
208
|
## The ledger — this is the product
|
|
204
209
|
|
|
205
|
-
One habit feeds it:
|
|
206
|
-
optional around that. `
|
|
207
|
-
|
|
208
|
-
session
|
|
210
|
+
One habit feeds it: record a mem row when a unit of work ends. Everything else on this page is
|
|
211
|
+
optional around that. `mem_add` needs no plan file and no skill — any agent that speaks MCP
|
|
212
|
+
can call it, and calling it is what turns a pile of session logs into an answer the next
|
|
213
|
+
session can find.
|
|
209
214
|
|
|
210
215
|
### One turn, end to end
|
|
211
216
|
|
|
@@ -214,32 +219,32 @@ sequenceDiagram
|
|
|
214
219
|
autonumber
|
|
215
220
|
participant A as Any MCP client
|
|
216
221
|
participant F as fapony MCP
|
|
217
|
-
participant
|
|
222
|
+
participant M as project mem log (.fapony/.memory)
|
|
218
223
|
|
|
219
|
-
Note over A,F: end a turn with a commit and no
|
|
220
|
-
A->>F:
|
|
221
|
-
F->>
|
|
222
|
-
opt proof, not just a claim — CLI,
|
|
224
|
+
Note over A,F: end a turn with a commit and no new mem row → the Stop hook blocks it once
|
|
225
|
+
A->>F: mem_add (kind + files + text)
|
|
226
|
+
F->>M: one mem row in the project's log, stamped with the model that did it
|
|
227
|
+
opt proof, not just a claim — CLI, for runs from the frozen ledger
|
|
223
228
|
A->>F: fapony report <run-id>
|
|
224
229
|
F-->>A: git facts + evidence from .fapony/evidence.json, stamped with server_sha
|
|
225
230
|
end
|
|
226
|
-
Note over A,
|
|
231
|
+
Note over A,M: `fapony stats` reads the frozen ledger back — CLI, because you ask it, not the agent
|
|
227
232
|
```
|
|
228
233
|
|
|
229
|
-
The Stop hook is the only thing fapony *blocks* — once per turn, when a commit
|
|
230
|
-
It never
|
|
234
|
+
The Stop hook is the only thing fapony *blocks* — once per turn, when a commit lands with
|
|
235
|
+
no new mem row. It never judges what deserves recording; it cannot see whether the work
|
|
236
|
+
held up. The hints only annotate and never
|
|
231
237
|
block: the **Read** hook adds one factual line when a read is large enough to be cheaper as
|
|
232
238
|
`review-seed`, or when the same file is read again in a session and its mtime has not moved
|
|
233
239
|
(`FAPONY_NO_REREAD_HINT=1` turns the re-read line off); the **Edit** hook names a file's importer
|
|
234
240
|
count, once per session, before you change its shape; OpenCode's **commit** hook nudges after a
|
|
235
|
-
`git commit` that left
|
|
241
|
+
`git commit` that left no new mem row. Claude Code receives read/edit *before* the call, OpenCode
|
|
236
242
|
*after* it — [What runs where](#what-runs-where) has the full client matrix.
|
|
237
243
|
|
|
238
|
-
### The
|
|
244
|
+
### The 3 tools
|
|
239
245
|
|
|
240
246
|
| Tool | Tier | Purpose |
|
|
241
247
|
|------|------|---------|
|
|
242
|
-
| `verdict_submit` | verify | Store a 6-grade verdict (pass-excellent → uncertain) with a required `regime` — the task shape the grade applies to |
|
|
243
248
|
| `mem_find` | recall | Search the project's mem log read-only — decisions/bugs/notes matched on the row's `files[]` (text substring for rows written without it), `text`, `kind` (no default filter), `since`. "What was ever decided about this file?" in one call before editing |
|
|
244
249
|
| `mem_add` | recall | Append a mem row (decision/bug/note/next/hold) with `files[]` required and rejected when empty — the write half of `mem_find`, so the row is findable when you next touch that file |
|
|
245
250
|
| `mem_close` | recall | Close a mem row by id with a tombstone message — a separate tool (not `kind:"close"`) because a close row carries no `files[]`, so sharing `mem_add`'s schema would make required fields depend on another field's value |
|
|
@@ -250,14 +255,16 @@ every client whether or not it is used, while a CLI command costs nothing until
|
|
|
250
255
|
why the handoff/report family is CLI-only, and why `fapony_stats`, `project_health_context`,
|
|
251
256
|
`plan_list` and `fapony_usage` left the MCP surface in 2026-09 (`fapony stats` answers the first, `fapony mem
|
|
252
257
|
kickoff` the third, `fapony usage-web` the fourth; the second had no caller).
|
|
253
|
-
Cutting is not the goal — spending where it pays back is:
|
|
254
|
-
their schemas because nobody is going to type them at the right moment. `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.
|
|
258
|
+
Cutting is not the goal — spending where it pays back is: the three mem tools keep
|
|
259
|
+
their schemas because nobody is going to type them at the right moment. `fapony report <run-id>` prints the full report for a frozen-ledger 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.
|
|
255
260
|
|
|
256
261
|
Full protocol, adapter examples (bash, Python), and safety rules: [docs/mcp-handcheck.md](https://github.com/kire21b/fapony/blob/main/docs/mcp-handcheck.md).
|
|
257
262
|
|
|
258
|
-
### Verdict grades
|
|
263
|
+
### Verdict grades (frozen ledger)
|
|
259
264
|
|
|
260
|
-
|
|
265
|
+
No new grades are recorded — the tool that filed them left the MCP surface in 2026-09.
|
|
266
|
+
The old rows stay readable via `fapony stats` and `fapony report`, and this is the scale
|
|
267
|
+
they were filed on. Verification produced a quality grade, not just pass/fail:
|
|
261
268
|
|
|
262
269
|
| Grade | Meaning |
|
|
263
270
|
|-------|---------|
|
|
@@ -271,8 +278,8 @@ Verification produces a quality grade, not just pass/fail:
|
|
|
271
278
|
### Why measure from the outside
|
|
272
279
|
|
|
273
280
|
- **Raw facts are hard to argue with.** Cost, rounds, diff sizes, pass rates — collected from git and session logs, not self-reported. A vendor can dispute a verdict as unfair; they can't dispute their own token count.
|
|
274
|
-
- **Agent platforms grading their own homework is a conflict of interest.** fapony is a separate layer that
|
|
275
|
-
- **Verification stays honest about its limits.** The collector runs only commands listed in `.fapony/evidence.json`; commands proposed by the agent outside the allowlist are reported as *proposed — not executed*, never run. And because fapony doesn't control your agent's flow, verdicts are labeled as one signal — not promised as truth.
|
|
281
|
+
- **Agent platforms grading their own homework is a conflict of interest.** fapony is a separate layer that measured any agent the same way, which is what made "model X vs. model Y" or "workflow A vs. workflow B" answerable with real data instead of vibes. That history is still queryable; new accumulation is mem rows, not grades.
|
|
282
|
+
- **Verification stays honest about its limits.** The collector runs only commands listed in `.fapony/evidence.json`; commands proposed by the agent outside the allowlist are reported as *proposed — not executed*, never run. And because fapony doesn't control your agent's flow, old verdicts are labeled as one signal — not promised as truth.
|
|
276
283
|
|
|
277
284
|
## The work side — conveniences, not the contract
|
|
278
285
|
|
|
@@ -309,7 +316,7 @@ Code expects, so a client can symlink the directory rather than copy the file:
|
|
|
309
316
|
| Skill | Purpose | Trigger |
|
|
310
317
|
|-------|---------|---------|
|
|
311
318
|
| `skill/plan-with-pony/` | Draft plan + spec from "what's in your head" via conversation | `/plan-with-pony` |
|
|
312
|
-
| `skill/review-pony/` | Review as verification, wired to fapony: scope facts before (`review-seed`),
|
|
319
|
+
| `skill/review-pony/` | Review as verification, wired to fapony: scope facts before (`review-seed`), a mem row after when findings survive | `/review-pony` |
|
|
313
320
|
| `skill/lookup-before-edit/` | Look up unfamiliar files (`review-seed --files` + mem + debt) before reading/editing them | `/lookup-before-edit` |
|
|
314
321
|
| `skill/define-convention/` | Turn a not-yet-migrated pattern into a tracked convention (interview + dry-run `debt`) | `/define-convention` |
|
|
315
322
|
| `skill/move-to-done/` | Archive a shipped PLAN into .fapony/done/ | `/move-to-done` |
|
|
@@ -330,10 +337,10 @@ flowchart TD
|
|
|
330
337
|
R -->|findings| W
|
|
331
338
|
R -->|clean| S["/git-ship"]
|
|
332
339
|
S -->|"there was a PLAN.md"| D["/move-to-done"]
|
|
333
|
-
D -.-> H[(
|
|
340
|
+
D -.-> H[(mem log + frozen ledger)]
|
|
334
341
|
R -.-> H
|
|
335
|
-
C -.->|"Stop hook: a commit needs a
|
|
336
|
-
H -.->|"
|
|
342
|
+
C -.->|"Stop hook: a commit needs a mem row"| H
|
|
343
|
+
H -.->|"pain zones + past model×shape"| Q
|
|
337
344
|
|
|
338
345
|
style H fill:#2d333b,stroke:#768390,color:#adbac7
|
|
339
346
|
```
|
|
@@ -343,19 +350,20 @@ to pick up. Wiring, refactors and UI passes finish in one sitting and the PLAN.m
|
|
|
343
350
|
unread — so `/plan-with-pony` declines those itself and hands over the two seed commands instead.
|
|
344
351
|
Both arms meet at the same review and the same ledger.
|
|
345
352
|
|
|
346
|
-
**The dotted edges are the whole point.**
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
353
|
+
**The dotted edges are the whole point.** Mem rows carry `files[]` and standalone text, so
|
|
354
|
+
the zones that keep hurting are a query, not a hunch — and the frozen ledger still carries
|
|
355
|
+
`regime` on its old rows, so *in this project, which model was worth paying for this shape
|
|
356
|
+
of work* stays answerable from history. That is what flows back to the fork — not "this file
|
|
357
|
+
broke once", which fapony measured at a 1–9% base rate and demoted.
|
|
350
358
|
|
|
351
359
|
| Moment | Call | What fapony gets out of it |
|
|
352
360
|
|---|---|---|
|
|
353
361
|
| Starting anything | `/plan-with-pony` | decides plan-vs-seed, then reads back how this shape has gone |
|
|
354
362
|
| Before editing an unfamiliar file | `fapony review-seed --files` | nothing; it saves you reading the file |
|
|
355
363
|
| Before committing | `/git-commit` | nothing; it just keeps commits reviewable |
|
|
356
|
-
| Before merging | `/review-pony` | writes a
|
|
364
|
+
| Before merging | `/review-pony` | writes a mem row when findings survive (bug/decision + files) |
|
|
357
365
|
| Merging | `/git-ship` (`pr` / `land` on a team) | nothing; pure git plumbing |
|
|
358
|
-
| After it ships | `/move-to-done` | writes the ship
|
|
366
|
+
| After it ships | `/move-to-done` | writes a mem note when the ship taught something, closes the loop |
|
|
359
367
|
| Proving a finished run | `fapony report <run-id>` (CLI, not MCP) | git facts + allowlisted evidence, one page |
|
|
360
368
|
|
|
361
369
|
**Team flow.** `/git-ship pr` stops once the PR is open and hands you the URL; the reviewer does
|
|
@@ -448,7 +456,7 @@ archived one: [examples/](https://github.com/kire21b/fapony/tree/main/examples).
|
|
|
448
456
|
|
|
449
457
|
```bash
|
|
450
458
|
# Verification & reporting
|
|
451
|
-
fapony mcp # MCP server (stdio JSON-RPC —
|
|
459
|
+
fapony mcp # MCP server (stdio JSON-RPC — 3 tools)
|
|
452
460
|
fapony report <run-id> # verification report for a run
|
|
453
461
|
fapony report-web [file] # static HTML report page
|
|
454
462
|
fapony usage-scan # scan session logs → cache (incremental, progress bar)
|
|
@@ -465,10 +473,24 @@ fapony mem close <id> "<msg>" # close a bug
|
|
|
465
473
|
fapony mem find "<text>" # substring-search every row
|
|
466
474
|
fapony mem kickoff [<plan.md>] # open a session + a next-up list
|
|
467
475
|
fapony mem where # show the resolved mem dir and which step won
|
|
468
|
-
fapony mem
|
|
476
|
+
fapony mem done | stale # views
|
|
469
477
|
fapony debt [--id <convention>] [--where <path>] # ไฟล์ไหนยังไม่ย้ายไป convention ที่ประกาศไว้ (live, read-only)
|
|
470
478
|
fapony lint-baseline [--cmd ...] [--diff] # separate "already red" from "I made it red"
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
*When* to call `mem add` is your project's call, not fapony's — write it in your own
|
|
482
|
+
`AGENTS.md`/`CLAUDE.md`, not here. A starting point:
|
|
471
483
|
|
|
484
|
+
```markdown
|
|
485
|
+
## Memory
|
|
486
|
+
- Found a bug while working (not just user-reported)? Log it before fixing:
|
|
487
|
+
`mem_add { kind: "bug", worktree: "<absolute app dir>", files: [...], text: "..." }`
|
|
488
|
+
- `text` must stand alone — read months later with no chat context: what/where/repro/status.
|
|
489
|
+
- Report the row id back in chat.
|
|
490
|
+
- Don't fold the fix into the same chunk — log first, fix as its own next/chunk if you do.
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
```bash
|
|
472
494
|
# Setup & maintenance
|
|
473
495
|
fapony init <path> # scaffold .fapony/ (plan/spec/memory/evidence)
|
|
474
496
|
fapony init-mem # delete .memory/ + warn call sites still referencing it
|
|
@@ -497,11 +519,11 @@ Env overrides: `FAPONY_CONFIG` (config file), `FAPONY_STATE_DIR` (state DB locat
|
|
|
497
519
|
## Scope
|
|
498
520
|
|
|
499
521
|
**Supported:**
|
|
500
|
-
- MCP server —
|
|
501
|
-
- Measurement: cross-run KPIs by model/grade/value, per-file
|
|
502
|
-
- Model attribution across clients — resolved from the session log that was live when the verdict landed, so a
|
|
503
|
-
- Zero setup beyond install: the
|
|
504
|
-
- Verification (
|
|
522
|
+
- MCP server — 3 mem tools via stdio JSON-RPC, works with any MCP client
|
|
523
|
+
- Measurement: cross-run KPIs by model/grade/value from the frozen ledger, per-file pain zones from mem rows (`files[]`) + passive usage (tokens, cost)
|
|
524
|
+
- Model attribution across clients — resolved from the session log that was live when the old verdict landed, so a frozen row carries a model without the caller having declared one
|
|
525
|
+
- Zero setup beyond install: the mem habit ships in the MCP `initialize` response, not in your rules file
|
|
526
|
+
- Verification reports (frozen): handoff conformance, 6-grade verdicts, allowlisted evidence collector (`.fapony/evidence.json`); reports stamped with the producing build's `server_sha` — replayable, no new graded runs
|
|
505
527
|
- Vendor-neutral executor/reviewer roles — anything that reads stdin
|
|
506
528
|
- Memory integration via shell adapter, per project (configurable or default-wired)
|
|
507
529
|
- Opt-in telemetry, off by default ([TELEMETRY.md](https://github.com/kire21b/fapony/blob/main/TELEMETRY.md) lists exactly what leaves the machine)
|
package/fapony.ts
CHANGED
|
@@ -1,121 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
// fapony — measure/verify MCP server for coding agents
|
|
4
|
-
// CLI dispatch
|
|
4
|
+
// CLI dispatch lives in src/adapters/cli.ts
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { cmdAnalyze } from "./src/analyze.js";
|
|
8
|
-
import { cmdDebt } from "./src/debt/cli.js";
|
|
9
|
-
import { cmdDigest } from "./src/digest/cli.js";
|
|
10
|
-
import {
|
|
11
|
-
cmdHookEditHint,
|
|
12
|
-
cmdHookReadHint,
|
|
13
|
-
cmdHookSessionStart,
|
|
14
|
-
cmdHookStop,
|
|
15
|
-
} from "./src/hook.js";
|
|
16
|
-
import { cmdInit } from "./src/init.js";
|
|
17
|
-
import { cmdInitMem } from "./src/init-mem.js";
|
|
18
|
-
import { cmdInstall } from "./src/install.js";
|
|
19
|
-
import { cmdLintBaseline } from "./src/lint-baseline.js";
|
|
20
|
-
import { cmdMcp } from "./src/mcp/transport.js";
|
|
21
|
-
import { cmdMem } from "./src/mem/index.js";
|
|
22
|
-
import { initStore } from "./src/mem/store.js";
|
|
23
|
-
import { cmdPlanSeed } from "./src/plan-seed.js";
|
|
24
|
-
import { cmdPriceScan } from "./src/price/index.js";
|
|
25
|
-
import { cmdReport, cmdReportWeb } from "./src/report/index.js";
|
|
26
|
-
import { cmdReviewSeed } from "./src/review-seed.js";
|
|
27
|
-
import { cmdSetup } from "./src/setup.js";
|
|
28
|
-
import { cmdStats } from "./src/stats/index.js";
|
|
29
|
-
import { cmdTelemetry } from "./src/telemetry.js";
|
|
30
|
-
import { cmdUpdate } from "./src/update.js";
|
|
31
|
-
import { cmdUsageScan, cmdUsageWeb } from "./src/usage/index.js";
|
|
6
|
+
import { cliMain } from "./src/adapters/cli.js";
|
|
32
7
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (cmd === "analyze") {
|
|
36
|
-
cmdAnalyze(a);
|
|
37
|
-
} else if (cmd === "debt") {
|
|
38
|
-
cmdDebt(a);
|
|
39
|
-
} else if (cmd === "lint-baseline") {
|
|
40
|
-
cmdLintBaseline(a);
|
|
41
|
-
} else if (cmd === "plan-seed") {
|
|
42
|
-
cmdPlanSeed(a);
|
|
43
|
-
} else if (cmd === "review-seed") {
|
|
44
|
-
cmdReviewSeed(a);
|
|
45
|
-
} else if (cmd === "digest") {
|
|
46
|
-
await cmdDigest(a);
|
|
47
|
-
} else if (cmd === "stats") {
|
|
48
|
-
cmdStats(a);
|
|
49
|
-
} else if (cmd === "telemetry") {
|
|
50
|
-
await cmdTelemetry(a);
|
|
51
|
-
} else if (cmd === "init-mem") {
|
|
52
|
-
cmdInitMem(a);
|
|
53
|
-
} else if (cmd === "mem") {
|
|
54
|
-
// `--mem-dir <path>` is global to `mem` and must reach both the writer
|
|
55
|
-
// (initStore) and the resolver behind `mem where` — parse it once and thread
|
|
56
|
-
// it through, never strip it and forget.
|
|
57
|
-
const memDirIdx = a.indexOf("--mem-dir");
|
|
58
|
-
let overrideMemDir: string | undefined;
|
|
59
|
-
let rest = a;
|
|
60
|
-
if (memDirIdx !== -1) {
|
|
61
|
-
overrideMemDir = a[memDirIdx + 1];
|
|
62
|
-
if (!overrideMemDir || overrideMemDir.startsWith("--")) {
|
|
63
|
-
console.error("fapony mem: --mem-dir needs a value");
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
if (!existsSync(overrideMemDir)) {
|
|
67
|
-
console.error(
|
|
68
|
-
`fapony mem: --mem-dir path does not exist: ${overrideMemDir}`,
|
|
69
|
-
);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
rest = a.filter((_, i) => i !== memDirIdx && i !== memDirIdx + 1);
|
|
73
|
-
}
|
|
74
|
-
initStore(process.cwd(), overrideMemDir);
|
|
75
|
-
try {
|
|
76
|
-
await cmdMem(rest, overrideMemDir);
|
|
77
|
-
} catch (e) {
|
|
78
|
-
console.error(`fapony mem: ${e instanceof Error ? e.message : String(e)}`);
|
|
79
|
-
process.exit(1);
|
|
80
|
-
}
|
|
81
|
-
} else if (cmd === "init") {
|
|
82
|
-
await cmdInit(a);
|
|
83
|
-
} else if (cmd === "install") {
|
|
84
|
-
await cmdInstall(a);
|
|
85
|
-
} else if (cmd === "setup") {
|
|
86
|
-
await cmdSetup();
|
|
87
|
-
} else if (cmd === "update") {
|
|
88
|
-
await cmdUpdate();
|
|
89
|
-
} else if (cmd === "hook-stop") {
|
|
90
|
-
await cmdHookStop();
|
|
91
|
-
} else if (cmd === "hook-read-hint") {
|
|
92
|
-
await cmdHookReadHint();
|
|
93
|
-
} else if (cmd === "hook-edit-hint") {
|
|
94
|
-
await cmdHookEditHint();
|
|
95
|
-
} else if (cmd === "hook-session-start") {
|
|
96
|
-
await cmdHookSessionStart();
|
|
97
|
-
} else if (cmd === "mcp") {
|
|
98
|
-
cmdMcp();
|
|
99
|
-
} else if (cmd === "report") {
|
|
100
|
-
cmdReport(a);
|
|
101
|
-
} else if (cmd === "report-web") {
|
|
102
|
-
cmdReportWeb(a);
|
|
103
|
-
} else if (cmd === "usage-scan") {
|
|
104
|
-
cmdUsageScan(a);
|
|
105
|
-
} else if (cmd === "price-scan") {
|
|
106
|
-
await cmdPriceScan(a);
|
|
107
|
-
} else if (cmd === "usage-web") {
|
|
108
|
-
cmdUsageWeb(a);
|
|
109
|
-
} else if (cmd === "test") {
|
|
110
|
-
// dynamic: src/test.js re-exports test/index.js, which the npm package
|
|
111
|
-
// doesn't ship (repo self-check only, not a published command) — a static
|
|
112
|
-
// import here would fail module load for every command, not just this one
|
|
113
|
-
const { cmdTest } = await import("./src/test.js");
|
|
114
|
-
await cmdTest();
|
|
115
|
-
} else {
|
|
116
|
-
console.error(`fapony: unknown command "${cmd ?? ""}"`);
|
|
117
|
-
console.error(
|
|
118
|
-
"usage: fapony <setup|update|stats|telemetry|init|init-mem|mem|install|report|report-web|usage-scan|usage-web|price-scan|analyze|debt|lint-baseline|plan-seed|review-seed|digest|mcp|hook-stop|hook-read-hint|hook-edit-hint|hook-session-start|test> [args]",
|
|
119
|
-
);
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
8
|
+
cliMain();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fapony",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "Token usage across Claude Code, OpenCode, Codex & ZCode on one yardstick — plus a project mem log and convention-debt tracker agents query via 3 MCP tools. No server, your data stays local",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "delamind (https://github.com/kire21b)",
|
|
7
7
|
"homepage": "https://github.com/kire21b/fapony#readme",
|
|
@@ -18,14 +18,13 @@
|
|
|
18
18
|
"coding-agent"
|
|
19
19
|
],
|
|
20
20
|
"bin": {
|
|
21
|
-
"fapony": "
|
|
21
|
+
"fapony": "fapony.ts"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"fapony.ts",
|
|
25
25
|
"src/",
|
|
26
26
|
"templates/",
|
|
27
|
-
"skill/"
|
|
28
|
-
"images/"
|
|
27
|
+
"skill/"
|
|
29
28
|
],
|
|
30
29
|
"scripts": {
|
|
31
30
|
"lint": "biome check .",
|
|
@@ -33,8 +32,10 @@
|
|
|
33
32
|
"test": "bun fapony.ts test",
|
|
34
33
|
"test:fast": "SKIP_SLOW=1 bun fapony.ts test",
|
|
35
34
|
"test:one": "bun scripts/test-one.ts",
|
|
35
|
+
"knip": "bunx knip@6 --exclude types,nsTypes || true",
|
|
36
36
|
"check": "bun run lint && bun run typecheck && bun fapony.ts test",
|
|
37
37
|
"prepublishOnly": "bash scripts/smoke-publish.sh",
|
|
38
|
+
"release": "git checkout main && git pull --ff-only && npm version patch -m 'release v%s' && git push origin main --follow-tags",
|
|
38
39
|
"overview": "bun fapony.ts report-web /tmp/fapony-overview.html && open /tmp/fapony-overview.html"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
@@ -31,7 +31,7 @@ You are about to move a PLAN that has been shipped to the archive.
|
|
|
31
31
|
status: superseded
|
|
32
32
|
superseded_by: PLAN-bar.md
|
|
33
33
|
```
|
|
34
|
-
Then skip step 5 — no work shipped, so there is
|
|
34
|
+
Then skip step 5 — no work shipped, so there is nothing to record. Never archive a plan as
|
|
35
35
|
superseded on your own reading; the user says which plan replaced it.
|
|
36
36
|
|
|
37
37
|
A plan that is merely *waiting* (on a person, a customer, a decision) is **not** dead and does
|
|
@@ -40,8 +40,9 @@ You are about to move a PLAN that has been shipped to the archive.
|
|
|
40
40
|
of `done/`, which is what `plan-sweep` and `kickoff` go by.
|
|
41
41
|
|
|
42
42
|
2. **Run `plan-sweep --apply`** — this does the `git mv`, rewrites markdown links inside the
|
|
43
|
-
file and inbound links from
|
|
44
|
-
|
|
43
|
+
file and inbound links from every `.md` under `.fapony/` (`plan/`, `done/`, `spec/`),
|
|
44
|
+
warns about plain-text mentions and about tracked files outside `.fapony/` that still
|
|
45
|
+
name the file (both detect-only), and logs a decision row — all in one call:
|
|
45
46
|
```bash
|
|
46
47
|
fapony mem plan-sweep <PLAN-foo.md> --apply
|
|
47
48
|
```
|
|
@@ -60,29 +61,19 @@ You are about to move a PLAN that has been shipped to the archive.
|
|
|
60
61
|
chore(plan): archive PLAN-foo.md (shipped <hash>)
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
5. **
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
- `
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- `
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
reader could not get from the diff: what the symptom looked like, where the cause actually
|
|
77
|
-
was, and the rule that follows. Standalone prose — it is read months later with no access
|
|
78
|
-
to this conversation.
|
|
79
|
-
- `worktree`: **absolute path** to this repo/worktree (`git rev-parse --show-toplevel`) —
|
|
80
|
-
every other fapony tool and query scopes by
|
|
81
|
-
absolute path too; a bare repo name won't match them
|
|
82
|
-
- `plan`: the archived plan's path (post-move, e.g. `.fapony/done/PLAN-foo.md`)
|
|
83
|
-
- `files`: repo-relative paths this plan touched (`git diff --name-only <base>..HEAD`) —
|
|
84
|
-
the only input to per-file risk history; without it the verdict says something happened
|
|
85
|
-
but not where
|
|
64
|
+
5. **Leave a note when the ship taught something** — `plan-sweep --apply` (step 2)
|
|
65
|
+
already logged the ship itself as a decision row, so a clean ship needs nothing
|
|
66
|
+
more. When the plan hit something a reader could not get from the diff, call the
|
|
67
|
+
`mem_add` MCP tool (fapony) once:
|
|
68
|
+
- `kind`: `note`
|
|
69
|
+
- `text`: what the symptom looked like, where the cause actually was, and the
|
|
70
|
+
rule that follows. Standalone prose — it is read months later with no access
|
|
71
|
+
to this conversation. Write one only then — "clean ship" files nothing, and a
|
|
72
|
+
note that repeats the diff teaches the next session nothing
|
|
73
|
+
- `files`: repo-relative paths this plan touched (`git diff --name-only <base>..HEAD`)
|
|
74
|
+
- `spec`: the archived plan's path (post-move, e.g. `.fapony/done/PLAN-foo.md`)
|
|
75
|
+
- `worktree`: **absolute path** (`git rev-parse --show-toplevel`) — every other
|
|
76
|
+
fapony tool scopes by absolute path too; a bare repo name won't match them
|
|
86
77
|
Skip only if fapony's MCP tools aren't available in this session — don't block the archive on it.
|
|
87
78
|
|
|
88
79
|
## Example
|
|
@@ -95,17 +86,16 @@ Steps:
|
|
|
95
86
|
→ moved, links rewritten, decision logged
|
|
96
87
|
3. spec: untouched, stays in .fapony/spec/
|
|
97
88
|
4. commit
|
|
98
|
-
5.
|
|
99
|
-
— clean ship, so no note
|
|
89
|
+
5. (clean ship — plan-sweep's decision row already recorded it, nothing more to file)
|
|
100
90
|
```
|
|
101
91
|
|
|
102
92
|
A ship worth a note looks like this instead:
|
|
103
93
|
|
|
104
94
|
```
|
|
105
|
-
5.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
95
|
+
5. mem_add(kind="note",
|
|
96
|
+
text="sheet scroll reset on open, not close — the restore hook was on the wrong side; the router's own scrollRestoration resets on every navigate(). Check the router option before writing a restore hook.",
|
|
97
|
+
files=["src/routes/expenses/index.tsx"], spec=".fapony/done/PLAN-quick-nav.md",
|
|
98
|
+
worktree="/Users/you/Project/vela")
|
|
109
99
|
```
|
|
110
100
|
|
|
111
101
|
## If fail
|