fapony 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +473 -0
- package/fapony.ts +78 -0
- package/package.json +42 -0
- package/skill/git-commit-conventional/SKILL.md +68 -0
- package/skill/git-ship/SKILL.md +144 -0
- package/skill/move-to-done/SKILL.md +126 -0
- package/skill/plan-with-pony/SKILL.md +263 -0
- package/skill/review-pony/SKILL.md +254 -0
- package/src/analyze.ts +517 -0
- package/src/context/index.ts +11 -0
- package/src/context/projectHealth.ts +359 -0
- package/src/conventions-seed.ts +420 -0
- package/src/db/defaults.ts +26 -0
- package/src/db/getters.ts +33 -0
- package/src/db/index.ts +7 -0
- package/src/db/load.ts +57 -0
- package/src/db/store.ts +286 -0
- package/src/db/types.ts +79 -0
- package/src/debt.ts +667 -0
- package/src/digest/cli.ts +75 -0
- package/src/digest/collect.ts +625 -0
- package/src/digest/html.ts +208 -0
- package/src/digest/text.ts +191 -0
- package/src/gate.ts +153 -0
- package/src/gates.ts +194 -0
- package/src/hook.ts +436 -0
- package/src/init-mem.ts +71 -0
- package/src/init.ts +237 -0
- package/src/install/claude.ts +361 -0
- package/src/install/codex.ts +61 -0
- package/src/install/cursor.ts +167 -0
- package/src/install/detect.ts +78 -0
- package/src/install/opencode.ts +234 -0
- package/src/install/skills.ts +106 -0
- package/src/install/types.ts +69 -0
- package/src/install/utils.ts +29 -0
- package/src/install/zcode.ts +120 -0
- package/src/install.ts +176 -0
- package/src/lint-baseline.ts +260 -0
- package/src/map.ts +320 -0
- package/src/math.ts +13 -0
- package/src/mcp/evidence.ts +332 -0
- package/src/mcp/primitives.ts +316 -0
- package/src/mcp/tools/check.ts +243 -0
- package/src/mcp/tools/collect.ts +157 -0
- package/src/mcp/tools/context.ts +66 -0
- package/src/mcp/tools/index.ts +309 -0
- package/src/mcp/tools/mem.ts +95 -0
- package/src/mcp/tools/plans.ts +255 -0
- package/src/mcp/tools/report.ts +285 -0
- package/src/mcp/tools/stats.ts +96 -0
- package/src/mcp/tools/usage.ts +211 -0
- package/src/mcp/tools/verdict.ts +148 -0
- package/src/mcp/transport.ts +241 -0
- package/src/mcp/types.ts +54 -0
- package/src/mcp/worktree.ts +27 -0
- package/src/memory.ts +264 -0
- package/src/parse.ts +71 -0
- package/src/plan-seed.ts +599 -0
- package/src/price/fetch.ts +146 -0
- package/src/price/index.ts +8 -0
- package/src/price/resolve.ts +213 -0
- package/src/report/cli.ts +92 -0
- package/src/report/format.ts +37 -0
- package/src/report/index.ts +4 -0
- package/src/report/render.ts +206 -0
- package/src/review-seed.ts +932 -0
- package/src/safety.ts +18 -0
- package/src/session/activeSession.ts +153 -0
- package/src/session/claude-code.ts +412 -0
- package/src/session/codex.ts +347 -0
- package/src/session/findModel.ts +376 -0
- package/src/session/helpers.ts +640 -0
- package/src/session/index.ts +31 -0
- package/src/session/opencode.ts +167 -0
- package/src/session/registry.ts +45 -0
- package/src/session/types.ts +128 -0
- package/src/session/zcode.ts +151 -0
- package/src/setup.ts +242 -0
- package/src/stats/cli.ts +44 -0
- package/src/stats/data.ts +1019 -0
- package/src/stats/format.ts +584 -0
- package/src/stats/index.ts +19 -0
- package/src/telemetry.ts +364 -0
- package/src/test.ts +2 -0
- package/src/update.ts +212 -0
- package/src/usage/cache.ts +125 -0
- package/src/usage/cli.ts +120 -0
- package/src/usage/format.ts +29 -0
- package/src/usage/index.ts +4 -0
- package/src/usage/render.ts +523 -0
- package/src/usage/scan.ts +161 -0
- package/src/util.ts +32 -0
- package/src/web/html.ts +33 -0
- package/templates/PLAN.md +90 -0
- package/templates/SPEC.md +30 -0
- package/templates/mem/commands/plan.ts +360 -0
- package/templates/mem/commands/read.ts +194 -0
- package/templates/mem/commands/rotate.ts +59 -0
- package/templates/mem/commands/selftest.ts +450 -0
- package/templates/mem/commands/write.ts +214 -0
- package/templates/mem/mem.ts +68 -0
- package/templates/mem/render.ts +63 -0
- package/templates/mem/selectors.ts +144 -0
- package/templates/mem/store.ts +285 -0
package/src/db/store.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { faponyDir } from "./load.js";
|
|
4
|
+
import type { Config, Event, Run, RunStatus } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Schema version for state.db — tracked via `PRAGMA user_version`.
|
|
8
|
+
*
|
|
9
|
+
* v1 = the original runs/events tables (no version stamp; legacy DBs that
|
|
10
|
+
* already have tables but user_version 0 are treated as v1).
|
|
11
|
+
*
|
|
12
|
+
* To change the schema: bump SCHEMA_VERSION and append the ALTER/CREATE
|
|
13
|
+
* statements as a new entry in MIGRATIONS (index = version - 1). openDb()
|
|
14
|
+
* applies every entry newer than the stored version, in order.
|
|
15
|
+
*/
|
|
16
|
+
export const SCHEMA_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
const MIGRATIONS: string[][] = [
|
|
19
|
+
// v1 — base schema
|
|
20
|
+
[
|
|
21
|
+
`CREATE TABLE IF NOT EXISTS runs(
|
|
22
|
+
id INTEGER PRIMARY KEY,
|
|
23
|
+
worktree TEXT NOT NULL,
|
|
24
|
+
plan TEXT,
|
|
25
|
+
mem_id TEXT,
|
|
26
|
+
status TEXT NOT NULL,
|
|
27
|
+
base_sha TEXT NOT NULL DEFAULT '',
|
|
28
|
+
round INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
30
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
31
|
+
)`,
|
|
32
|
+
`CREATE TABLE IF NOT EXISTS events(
|
|
33
|
+
id INTEGER PRIMARY KEY,
|
|
34
|
+
run_id INTEGER NOT NULL,
|
|
35
|
+
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
|
36
|
+
kind TEXT NOT NULL,
|
|
37
|
+
data TEXT
|
|
38
|
+
)`,
|
|
39
|
+
],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function getUserVersion(db: Database): number {
|
|
43
|
+
const row = db.prepare("PRAGMA user_version").get() as {
|
|
44
|
+
user_version: number;
|
|
45
|
+
};
|
|
46
|
+
return row.user_version ?? 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tableExists(db: Database, name: string): boolean {
|
|
50
|
+
const row = db
|
|
51
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
52
|
+
.get(name) as {
|
|
53
|
+
name: string;
|
|
54
|
+
} | null;
|
|
55
|
+
return row !== null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Apply pending migrations; exported for tests. */
|
|
59
|
+
export function migrateDb(db: Database): void {
|
|
60
|
+
const current = getUserVersion(db);
|
|
61
|
+
if (current > SCHEMA_VERSION) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`state.db schema version ${current} is newer than supported ${SCHEMA_VERSION} — upgrade fapony first`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (current === SCHEMA_VERSION) return;
|
|
67
|
+
if (current === 0 && tableExists(db, "runs")) {
|
|
68
|
+
// Legacy DB from before versioning — schema matches v1, just stamp it.
|
|
69
|
+
db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (let v = current; v < SCHEMA_VERSION; v++) {
|
|
73
|
+
for (const stmt of MIGRATIONS[v] ?? []) db.run(stmt);
|
|
74
|
+
db.run(`PRAGMA user_version = ${v + 1}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function openDb(config?: Config): Database {
|
|
79
|
+
const dir = faponyDir(config);
|
|
80
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
const db = new Database(`${dir}/state.db`);
|
|
83
|
+
db.run("PRAGMA journal_mode=WAL");
|
|
84
|
+
|
|
85
|
+
migrateDb(db);
|
|
86
|
+
|
|
87
|
+
return db;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function newRun(
|
|
91
|
+
db: Database,
|
|
92
|
+
worktree: string,
|
|
93
|
+
plan: string | null,
|
|
94
|
+
memId: string | null,
|
|
95
|
+
baseSha: string,
|
|
96
|
+
): number {
|
|
97
|
+
const stmt = db.prepare(
|
|
98
|
+
`INSERT INTO runs (worktree, plan, mem_id, status, base_sha, round)
|
|
99
|
+
VALUES (?, ?, ?, 'running', ?, 0)`,
|
|
100
|
+
);
|
|
101
|
+
const result = stmt.run(worktree, plan, memId, baseSha);
|
|
102
|
+
return Number(result.lastInsertRowid);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function setStatus(
|
|
106
|
+
db: Database,
|
|
107
|
+
runId: number,
|
|
108
|
+
status: RunStatus,
|
|
109
|
+
): void {
|
|
110
|
+
db.prepare(
|
|
111
|
+
`UPDATE runs SET status = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
112
|
+
).run(status, runId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function incrementRound(db: Database, runId: number): void {
|
|
116
|
+
db.prepare(
|
|
117
|
+
`UPDATE runs SET round = round + 1, updated_at = datetime('now') WHERE id = ?`,
|
|
118
|
+
).run(runId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function addEvent(
|
|
122
|
+
db: Database,
|
|
123
|
+
runId: number,
|
|
124
|
+
kind: string,
|
|
125
|
+
data: unknown,
|
|
126
|
+
): number {
|
|
127
|
+
const stmt = db.prepare(
|
|
128
|
+
`INSERT INTO events (run_id, kind, data) VALUES (?, ?, ?)`,
|
|
129
|
+
);
|
|
130
|
+
const result = stmt.run(runId, kind, JSON.stringify(data));
|
|
131
|
+
return Number(result.lastInsertRowid);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Merge `patch` into an existing event's JSON data (keeps keys already set).
|
|
136
|
+
* Used to complete a spawn row with bytes_out/usd after the agent finishes —
|
|
137
|
+
* one row per spawn, timing (ts) stays at spawn start. No-op when the row
|
|
138
|
+
* is missing or its data isn't a JSON object.
|
|
139
|
+
*/
|
|
140
|
+
export function updateEventData(
|
|
141
|
+
db: Database,
|
|
142
|
+
eventId: number,
|
|
143
|
+
patch: Record<string, unknown>,
|
|
144
|
+
): void {
|
|
145
|
+
const row = db
|
|
146
|
+
.prepare("SELECT data FROM events WHERE id = ?")
|
|
147
|
+
.get(eventId) as { data: string | null } | null;
|
|
148
|
+
if (!row) return;
|
|
149
|
+
let base: Record<string, unknown> = {};
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(row.data ?? "null") as unknown;
|
|
152
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
153
|
+
base = parsed as Record<string, unknown>;
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
db.prepare("UPDATE events SET data = ? WHERE id = ?").run(
|
|
159
|
+
JSON.stringify({ ...base, ...patch }),
|
|
160
|
+
eventId,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function getRun(db: Database, runId: number): Run | null {
|
|
165
|
+
return db.prepare("SELECT * FROM runs WHERE id = ?").get(runId) as Run | null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function getActiveRuns(db: Database): Run[] {
|
|
169
|
+
return db
|
|
170
|
+
.prepare(
|
|
171
|
+
"SELECT * FROM runs WHERE status NOT IN ('passed', 'stopped') ORDER BY id",
|
|
172
|
+
)
|
|
173
|
+
.all() as Run[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Latest still-open run for a worktree+plan pair, or null.
|
|
178
|
+
* Used by MCP verdict_submit to bind a round-2+ verdict to the original run
|
|
179
|
+
* instead of opening one row per review (which kept every run at round 0 and
|
|
180
|
+
* made review.maxRounds untriggerable). No time window: a passed verdict
|
|
181
|
+
* already closes the run to terminal, so any open match is a failed-not-yet-
|
|
182
|
+
* fixed piece of work that the new verdict belongs to. plan=null never
|
|
183
|
+
* matches — bare-diff reviews always open a fresh run.
|
|
184
|
+
*/
|
|
185
|
+
export function findOpenRun(
|
|
186
|
+
db: Database,
|
|
187
|
+
worktree: string,
|
|
188
|
+
plan: string,
|
|
189
|
+
): Run | null {
|
|
190
|
+
return db
|
|
191
|
+
.prepare(
|
|
192
|
+
`SELECT * FROM runs WHERE worktree = ? AND plan = ? AND status NOT IN ('passed', 'stopped', 'stalled') ORDER BY id DESC LIMIT 1`,
|
|
193
|
+
)
|
|
194
|
+
.get(worktree, plan) as Run | null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Latest still-open run with plan=null for a worktree, or null.
|
|
199
|
+
* Used by MCP verdict_submit as a fallback when no exact plan match is found —
|
|
200
|
+
* lets a verdict with free-text intent bind to a run that was created without
|
|
201
|
+
* a plan (the common "no PLAN file" flow).
|
|
202
|
+
*/
|
|
203
|
+
export function findOpenRunWithNullPlan(
|
|
204
|
+
db: Database,
|
|
205
|
+
worktree: string,
|
|
206
|
+
): Run | null {
|
|
207
|
+
return db
|
|
208
|
+
.prepare(
|
|
209
|
+
`SELECT * FROM runs WHERE worktree = ? AND plan IS NULL AND status NOT IN ('passed', 'stopped', 'stalled') ORDER BY id DESC LIMIT 1`,
|
|
210
|
+
)
|
|
211
|
+
.get(worktree) as Run | null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function getEvents(db: Database, runId: number): Event[] {
|
|
215
|
+
return db
|
|
216
|
+
.prepare("SELECT * FROM events WHERE run_id = ? ORDER BY id")
|
|
217
|
+
.all(runId) as Event[];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Pulls the most recent unresolved "gate fail" note for a worktree+mem_id pair —
|
|
222
|
+
* i.e. the review feedback the next `fapony run` should hand back to the executor.
|
|
223
|
+
* Only looks at the latest run for that pair; if it already passed, returns null
|
|
224
|
+
* (nothing to carry forward).
|
|
225
|
+
*/
|
|
226
|
+
export function getPendingFeedback(
|
|
227
|
+
db: Database,
|
|
228
|
+
worktree: string,
|
|
229
|
+
memId: string,
|
|
230
|
+
excludeRunId?: number,
|
|
231
|
+
): string | null {
|
|
232
|
+
const run = db
|
|
233
|
+
.prepare(
|
|
234
|
+
`SELECT * FROM runs WHERE worktree = ? AND mem_id = ? AND id != ? ORDER BY id DESC LIMIT 1`,
|
|
235
|
+
)
|
|
236
|
+
.get(worktree, memId, excludeRunId ?? -1) as Run | null;
|
|
237
|
+
if (run?.status !== "fixing") return null;
|
|
238
|
+
|
|
239
|
+
const event = db
|
|
240
|
+
.prepare(
|
|
241
|
+
`SELECT * FROM events WHERE run_id = ? AND kind = 'gate' ORDER BY id DESC LIMIT 1`,
|
|
242
|
+
)
|
|
243
|
+
.get(run.id) as Event | null;
|
|
244
|
+
if (!event?.data) return null;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(event.data) as {
|
|
248
|
+
verdict?: string;
|
|
249
|
+
note?: string;
|
|
250
|
+
};
|
|
251
|
+
return parsed.verdict === "fail" && parsed.note ? parsed.note : null;
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Merge extra fields into the most recent gate event's data JSON.
|
|
259
|
+
* Used by MCP verdict_submit to add reason_code / source after gateOnce
|
|
260
|
+
* has already written the gate event with {verdict, note, round}.
|
|
261
|
+
*/
|
|
262
|
+
export function patchLastGateEvent(
|
|
263
|
+
db: Database,
|
|
264
|
+
runId: number,
|
|
265
|
+
extra: Record<string, unknown>,
|
|
266
|
+
): void {
|
|
267
|
+
const event = db
|
|
268
|
+
.prepare(
|
|
269
|
+
`SELECT id, data FROM events WHERE run_id = ? AND kind = 'gate' ORDER BY id DESC LIMIT 1`,
|
|
270
|
+
)
|
|
271
|
+
.get(runId) as { id: number; data: string | null } | null;
|
|
272
|
+
if (!event) return;
|
|
273
|
+
|
|
274
|
+
let existing: Record<string, unknown> = {};
|
|
275
|
+
if (event.data) {
|
|
276
|
+
try {
|
|
277
|
+
existing = JSON.parse(event.data);
|
|
278
|
+
} catch {
|
|
279
|
+
existing = {};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
db.prepare(`UPDATE events SET data = ? WHERE id = ?`).run(
|
|
283
|
+
JSON.stringify({ ...existing, ...extra }),
|
|
284
|
+
event.id,
|
|
285
|
+
);
|
|
286
|
+
}
|
package/src/db/types.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type RunStatus =
|
|
2
|
+
| "running"
|
|
3
|
+
| "awaiting_review"
|
|
4
|
+
| "fixing"
|
|
5
|
+
| "passed"
|
|
6
|
+
| "stopped"
|
|
7
|
+
| "stalled";
|
|
8
|
+
|
|
9
|
+
export interface Run {
|
|
10
|
+
id: number;
|
|
11
|
+
worktree: string;
|
|
12
|
+
plan: string | null;
|
|
13
|
+
mem_id: string | null;
|
|
14
|
+
status: RunStatus;
|
|
15
|
+
base_sha: string;
|
|
16
|
+
round: number;
|
|
17
|
+
created_at: string;
|
|
18
|
+
updated_at: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Event {
|
|
22
|
+
id: number;
|
|
23
|
+
run_id: number;
|
|
24
|
+
ts: string;
|
|
25
|
+
kind: string;
|
|
26
|
+
data: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Config {
|
|
30
|
+
worktrees: Record<string, string>;
|
|
31
|
+
review: {
|
|
32
|
+
maxRounds: number;
|
|
33
|
+
};
|
|
34
|
+
memory: {
|
|
35
|
+
claim: string[];
|
|
36
|
+
close: string[];
|
|
37
|
+
add: string[];
|
|
38
|
+
kickoff?: string[];
|
|
39
|
+
} | null;
|
|
40
|
+
// opt-in only — omit or leave null to keep everything local. See TELEMETRY.md
|
|
41
|
+
// for the exact payload shape (KPI numbers + event kind/timestamp, no
|
|
42
|
+
// plan/commit/gate-note content, ever).
|
|
43
|
+
telemetry?: {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
endpoint: string;
|
|
46
|
+
/** Self-reported metadata — advisory, not machine-observed. */
|
|
47
|
+
metadata?: {
|
|
48
|
+
task_category?: string;
|
|
49
|
+
stack?: string;
|
|
50
|
+
notes?: string;
|
|
51
|
+
};
|
|
52
|
+
} | null;
|
|
53
|
+
// --- Flexible paths / limits (all optional, defaults = old hardcodes) ---
|
|
54
|
+
paths?: {
|
|
55
|
+
// state dir override (default: $XDG_CONFIG_HOME/fapony or ~/.config/fapony).
|
|
56
|
+
// $FAPONY_STATE_DIR env wins over this when set.
|
|
57
|
+
stateDir?: string;
|
|
58
|
+
// plan/spec/memory layout inside each worktree (relative to worktree root).
|
|
59
|
+
planDir?: string;
|
|
60
|
+
specDir?: string;
|
|
61
|
+
doneDir?: string;
|
|
62
|
+
memoryEntry?: string;
|
|
63
|
+
evidenceFile?: string;
|
|
64
|
+
} | null;
|
|
65
|
+
safety?: {
|
|
66
|
+
// regex sources tested against the joined argv; default = the 4 git patterns.
|
|
67
|
+
deny?: string[];
|
|
68
|
+
} | null;
|
|
69
|
+
usageWeb?: {
|
|
70
|
+
port?: number;
|
|
71
|
+
hostname?: string;
|
|
72
|
+
ownerName?: string;
|
|
73
|
+
} | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RolePricing {
|
|
77
|
+
inputPer1k: number;
|
|
78
|
+
outputPer1k: number;
|
|
79
|
+
}
|