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/memory.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// src/memory.ts — memory domain: config.memory.* shell adapter helpers +
|
|
2
|
+
// the shared mem-log reader (digest and project_health_context both need it).
|
|
3
|
+
// ponytail: dedupe close-command logic that was copy-pasted in run.ts + stop.ts
|
|
4
|
+
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
type Config,
|
|
10
|
+
DEFAULT_MEMORY_ENTRY,
|
|
11
|
+
memoryEntry,
|
|
12
|
+
safetyDeny,
|
|
13
|
+
} from "./db/index.js";
|
|
14
|
+
import { assertSafe } from "./safety.js";
|
|
15
|
+
import { templateArgs } from "./util.js";
|
|
16
|
+
|
|
17
|
+
/** Default memory commands — matches templates/mem/mem.ts CLI. */
|
|
18
|
+
export const DEFAULT_MEMORY: Config["memory"] = {
|
|
19
|
+
claim: ["bun", DEFAULT_MEMORY_ENTRY, "claim", "{id}"],
|
|
20
|
+
close: ["bun", DEFAULT_MEMORY_ENTRY, "close", "{id}", "{msg}"],
|
|
21
|
+
add: ["bun", DEFAULT_MEMORY_ENTRY, "add", "{kind}", "{text}"],
|
|
22
|
+
kickoff: ["bun", DEFAULT_MEMORY_ENTRY, "kickoff"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the effective memory config:
|
|
27
|
+
* - explicit config.memory wins if set
|
|
28
|
+
* - fallback: config.memory === null + <memoryEntry> exists → DEFAULT_MEMORY
|
|
29
|
+
* - otherwise null (no memory)
|
|
30
|
+
*/
|
|
31
|
+
export function resolveMemoryConfig(
|
|
32
|
+
config: Config,
|
|
33
|
+
worktree: string,
|
|
34
|
+
): Config["memory"] {
|
|
35
|
+
if (config.memory) return config.memory;
|
|
36
|
+
if (existsSync(join(worktree, memoryEntry(config)))) return DEFAULT_MEMORY;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function closeMemory(
|
|
41
|
+
config: Config,
|
|
42
|
+
worktree: string,
|
|
43
|
+
memId: string,
|
|
44
|
+
msg: string,
|
|
45
|
+
): void {
|
|
46
|
+
const mem = resolveMemoryConfig(config, worktree);
|
|
47
|
+
if (!mem) return;
|
|
48
|
+
try {
|
|
49
|
+
const cmd = templateArgs(mem.close, { id: memId, msg });
|
|
50
|
+
assertSafe(cmd, safetyDeny(config));
|
|
51
|
+
execSync(cmd.join(" "), {
|
|
52
|
+
cwd: worktree,
|
|
53
|
+
stdio: "ignore",
|
|
54
|
+
timeout: 15_000,
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
// non-fatal, same as existing call sites
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function claimMemory(
|
|
62
|
+
config: Config,
|
|
63
|
+
worktree: string,
|
|
64
|
+
memId: string,
|
|
65
|
+
): boolean {
|
|
66
|
+
const mem = resolveMemoryConfig(config, worktree);
|
|
67
|
+
if (!mem) return false;
|
|
68
|
+
try {
|
|
69
|
+
const cmd = templateArgs(mem.claim, { id: memId });
|
|
70
|
+
assertSafe(cmd, safetyDeny(config));
|
|
71
|
+
execSync(cmd.join(" "), {
|
|
72
|
+
cwd: worktree,
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
74
|
+
timeout: 15_000,
|
|
75
|
+
});
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Runs memory.kickoff (if configured or default-wired) and returns its stdout, or null if unset/failed. */
|
|
83
|
+
export function kickoffMemory(config: Config, worktree: string): string | null {
|
|
84
|
+
const mem = resolveMemoryConfig(config, worktree);
|
|
85
|
+
if (!mem?.kickoff) return null;
|
|
86
|
+
try {
|
|
87
|
+
assertSafe(mem.kickoff, safetyDeny(config));
|
|
88
|
+
return execSync(mem.kickoff.join(" "), {
|
|
89
|
+
cwd: worktree,
|
|
90
|
+
encoding: "utf-8",
|
|
91
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
92
|
+
}).trim();
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- mem log reading ---
|
|
99
|
+
//
|
|
100
|
+
// Shared by `fapony digest` and `project_health_context`: both need the same
|
|
101
|
+
// directory discovery (new `.fapony/.memory`, legacy `.memory/` fallback) and
|
|
102
|
+
// the same rotated-file handling. Kept here so the two callers cannot drift.
|
|
103
|
+
|
|
104
|
+
export interface MemRow {
|
|
105
|
+
ts: string;
|
|
106
|
+
agent: string;
|
|
107
|
+
kind: string;
|
|
108
|
+
text: string;
|
|
109
|
+
spec?: string;
|
|
110
|
+
id?: string;
|
|
111
|
+
ref?: string;
|
|
112
|
+
/** Files the row is about — written by `mem add --files` (PLAN-convention-debt chunk 3). */
|
|
113
|
+
files?: string[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface RawMemRow {
|
|
117
|
+
ts?: string;
|
|
118
|
+
agent?: string;
|
|
119
|
+
kind?: string;
|
|
120
|
+
text?: string;
|
|
121
|
+
spec?: string;
|
|
122
|
+
id?: string;
|
|
123
|
+
ref?: string;
|
|
124
|
+
files?: unknown;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The app's own .fapony/ dir for a worktree root — the same monorepo guess
|
|
129
|
+
* resolveMemDir makes (templates/mem/store.ts is the mirror). Conventions
|
|
130
|
+
* live beside the mem log (SPEC-convention-debt §2.1), so both resolvers must
|
|
131
|
+
* guess identically; the guess is shared here so they cannot drift.
|
|
132
|
+
*/
|
|
133
|
+
export function resolveAppFaponyDir(worktree: string): string {
|
|
134
|
+
const app = process.env.MEM_APP ?? basename(worktree).replace(/^wt-/, "");
|
|
135
|
+
const appBase = ["apps", "packages", "services"]
|
|
136
|
+
.map((d) => join(worktree, d, app))
|
|
137
|
+
.find((p) => existsSync(p));
|
|
138
|
+
return appBase ? join(appBase, ".fapony") : join(worktree, ".fapony");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Locate the memory dir for a worktree root, mirroring templates/mem/store.ts:
|
|
143
|
+
* in a monorepo the log lives under `<apps|packages|services>/<app>/.fapony/.memory`
|
|
144
|
+
* (app guessed from the dir name, `wt-` prefix stripped; MEM_APP overrides), not
|
|
145
|
+
* at the git root. Single repos fall back to the root-relative layout.
|
|
146
|
+
*/
|
|
147
|
+
export function resolveMemDir(worktree: string): string | null {
|
|
148
|
+
const base = resolveAppFaponyDir(worktree);
|
|
149
|
+
|
|
150
|
+
const legacyDir = join(base, "..", ".memory");
|
|
151
|
+
const newDir = join(base, ".memory");
|
|
152
|
+
|
|
153
|
+
if (existsSync(join(legacyDir, "log.jsonl"))) return legacyDir;
|
|
154
|
+
if (existsSync(newDir)) return newDir;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Read every `log*.jsonl` row under the worktree's memory dir, newest first.
|
|
160
|
+
* `sinceIso` (exclusive) drops older rows; omit it to read the whole log.
|
|
161
|
+
* Malformed/unreadable rows are counted, never thrown — a corrupt line must
|
|
162
|
+
* not take down the caller.
|
|
163
|
+
*/
|
|
164
|
+
export function readMemLog(
|
|
165
|
+
worktree: string,
|
|
166
|
+
sinceIso?: string,
|
|
167
|
+
): { rows: MemRow[]; skipped: number; filesFound: number } {
|
|
168
|
+
const dir = resolveMemDir(worktree);
|
|
169
|
+
if (!dir) return { rows: [], skipped: 0, filesFound: 0 };
|
|
170
|
+
|
|
171
|
+
// read every log*.jsonl (excluding rotated files log.YYYY-MM-DD.jsonl)
|
|
172
|
+
const isLogFile = (f: string): boolean =>
|
|
173
|
+
f === "log.jsonl" ||
|
|
174
|
+
(/^log\.[A-Za-z0-9._-]+\.jsonl$/.test(f) &&
|
|
175
|
+
!/^log\.\d{4}-\d{2}-\d{2}\.jsonl$/.test(f));
|
|
176
|
+
|
|
177
|
+
let files: string[];
|
|
178
|
+
try {
|
|
179
|
+
files = readdirSync(dir)
|
|
180
|
+
.filter(isLogFile)
|
|
181
|
+
.sort()
|
|
182
|
+
.map((f) => join(dir, f));
|
|
183
|
+
} catch {
|
|
184
|
+
return { rows: [], skipped: 0, filesFound: 0 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (files.length === 0) return { rows: [], skipped: 0, filesFound: 0 };
|
|
188
|
+
|
|
189
|
+
let skipped = 0;
|
|
190
|
+
const all: MemRow[] = [];
|
|
191
|
+
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
let raw: string;
|
|
194
|
+
try {
|
|
195
|
+
raw = readFileSync(file, "utf-8");
|
|
196
|
+
} catch {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
let parsed: RawMemRow;
|
|
202
|
+
try {
|
|
203
|
+
parsed = JSON.parse(line) as RawMemRow;
|
|
204
|
+
} catch {
|
|
205
|
+
skipped++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (!parsed.ts || !parsed.kind) {
|
|
209
|
+
skipped++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (sinceIso && parsed.ts < sinceIso) continue;
|
|
213
|
+
all.push({
|
|
214
|
+
ts: parsed.ts,
|
|
215
|
+
agent: parsed.agent ?? "unknown",
|
|
216
|
+
kind: parsed.kind,
|
|
217
|
+
text: parsed.text ?? "",
|
|
218
|
+
spec: parsed.spec,
|
|
219
|
+
id: parsed.id,
|
|
220
|
+
ref: parsed.ref,
|
|
221
|
+
...(Array.isArray(parsed.files)
|
|
222
|
+
? { files: parsed.files.filter((f) => typeof f === "string") }
|
|
223
|
+
: {}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
all.sort((a, b) => b.ts.localeCompare(a.ts)); // newest first
|
|
229
|
+
return { rows: all, skipped, filesFound: files.length };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Decisions for the pre-edit context summary — newest first, capped at `limit`.
|
|
234
|
+
*
|
|
235
|
+
* `keywords` (usual suspects: the files about to be touched) are a
|
|
236
|
+
* *preference*, not a filter: decisions mentioning one surface first, but when
|
|
237
|
+
* nothing matches the newest decisions still come back. A summary that goes
|
|
238
|
+
* silent because a path never appeared in a decision is worse than a recent one.
|
|
239
|
+
*/
|
|
240
|
+
export function readRecentMemDecisions(
|
|
241
|
+
worktree: string,
|
|
242
|
+
limit: number,
|
|
243
|
+
keywords?: string[],
|
|
244
|
+
): MemRow[] {
|
|
245
|
+
let decisions: MemRow[];
|
|
246
|
+
try {
|
|
247
|
+
decisions = readMemLog(worktree).rows.filter((r) => r.kind === "decision");
|
|
248
|
+
} catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
if (decisions.length === 0) return [];
|
|
252
|
+
|
|
253
|
+
const kws = (keywords ?? [])
|
|
254
|
+
.map((k) => k.toLowerCase())
|
|
255
|
+
.filter((k) => k.length > 0);
|
|
256
|
+
if (kws.length > 0) {
|
|
257
|
+
const hits = decisions.filter((r) => {
|
|
258
|
+
const hay = `${r.text}\n${r.spec ?? ""}`.toLowerCase();
|
|
259
|
+
return kws.some((k) => hay.includes(k));
|
|
260
|
+
});
|
|
261
|
+
if (hits.length > 0) return hits.slice(0, limit);
|
|
262
|
+
}
|
|
263
|
+
return decisions.slice(0, limit);
|
|
264
|
+
}
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/parse.ts — grades, scores, and gate-event JSON parsing.
|
|
2
|
+
// Marker-text parsers (parseGateVerdict/parsePlanUpdate) were removed with the
|
|
3
|
+
// CLI loop: nothing produces VERDICT:/NEXT-PROMPT/FILE_DONE stdout anymore —
|
|
4
|
+
// gate events are JSON, read via parseGateEventData.
|
|
5
|
+
|
|
6
|
+
/** Locked grade vocabulary — additive-only (append, never rename/remove). */
|
|
7
|
+
export type VerdictGrade =
|
|
8
|
+
| "pass-excellent"
|
|
9
|
+
| "pass-good"
|
|
10
|
+
| "pass-adequate"
|
|
11
|
+
| "pass"
|
|
12
|
+
| "fail"
|
|
13
|
+
| "uncertain";
|
|
14
|
+
|
|
15
|
+
export const VERDICT_GRADES: ReadonlySet<string> = new Set<VerdictGrade>([
|
|
16
|
+
"pass-excellent",
|
|
17
|
+
"pass-good",
|
|
18
|
+
"pass-adequate",
|
|
19
|
+
"pass",
|
|
20
|
+
"fail",
|
|
21
|
+
"uncertain",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/** True when verdict belongs to the pass family (any pass-* variant). */
|
|
25
|
+
export function isPassFamily(v: string): v is VerdictGrade {
|
|
26
|
+
return v.startsWith("pass");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Locked qualityScore mapping — read-time only (never written to events).
|
|
31
|
+
* Values from SPEC-verdict-protocol §qualityScore (additive-only, never change).
|
|
32
|
+
*/
|
|
33
|
+
const SCORE_MAP: Record<VerdictGrade, number> = {
|
|
34
|
+
"pass-excellent": 5,
|
|
35
|
+
"pass-good": 4,
|
|
36
|
+
"pass-adequate": 3,
|
|
37
|
+
pass: 3,
|
|
38
|
+
fail: 0,
|
|
39
|
+
uncertain: 1,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function qualityScore(grade: VerdictGrade): number {
|
|
43
|
+
return SCORE_MAP[grade] ?? 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GateVerdict {
|
|
47
|
+
verdict: VerdictGrade;
|
|
48
|
+
note: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read a verdict back from a stored gate event (`kind='gate'`).
|
|
53
|
+
* Gate events are JSON (`{verdict, note, round}` from gateOnce) — NOT
|
|
54
|
+
* reviewer stdout. Returns null for missing/unparseable data or unknown grades.
|
|
55
|
+
*/
|
|
56
|
+
export function parseGateEventData(data: string | null): GateVerdict | null {
|
|
57
|
+
if (!data) return null;
|
|
58
|
+
let parsed: unknown;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(data);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
65
|
+
const { verdict, note } = parsed as { verdict?: unknown; note?: unknown };
|
|
66
|
+
if (typeof verdict !== "string" || !VERDICT_GRADES.has(verdict)) return null;
|
|
67
|
+
return {
|
|
68
|
+
verdict: verdict as VerdictGrade,
|
|
69
|
+
note: typeof note === "string" ? note : "",
|
|
70
|
+
};
|
|
71
|
+
}
|