fapony 0.1.3 → 0.2.1
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 +111 -46
- package/fapony.ts +35 -2
- package/package.json +3 -2
- package/skill/move-to-done/SKILL.md +18 -28
- package/skill/plan-with-pony/SKILL.md +8 -3
- package/src/analyze.ts +175 -5
- package/src/conventions-seed.ts +3 -3
- package/src/db/defaults.ts +13 -5
- package/src/db/getters.ts +11 -9
- package/src/db/load.ts +2 -2
- package/src/db/store.ts +0 -75
- package/src/db/types.ts +2 -4
- package/src/debt.ts +176 -36
- package/src/digest/collect.ts +19 -2
- package/src/digest/text.ts +25 -0
- package/src/hook.ts +665 -24
- package/src/init-mem.ts +123 -37
- package/src/init.ts +57 -39
- package/src/install/claude.ts +44 -8
- package/src/install/codex.ts +105 -13
- package/src/install/opencode.ts +169 -5
- package/src/install.ts +2 -1
- package/src/lint-baseline.ts +3 -8
- package/src/mcp/evidence.ts +15 -3
- package/src/mcp/tools/index.ts +97 -185
- package/src/mcp/tools/mem.ts +153 -11
- package/src/mcp/transport.ts +5 -13
- package/src/mem/commands/read.ts +448 -0
- package/src/mem/commands/where.ts +56 -0
- package/{templates → src}/mem/commands/write.ts +12 -5
- package/src/mem/index.ts +144 -0
- package/src/mem/store.ts +350 -0
- package/src/memory.ts +238 -40
- package/src/plan-seed.ts +26 -6
- package/src/review-seed.ts +19 -0
- package/src/setup.ts +7 -8
- package/src/stats/data.ts +40 -104
- package/src/stats/format.ts +8 -9
- package/src/stats/index.ts +0 -1
- package/templates/PLAN.md +1 -0
- package/src/mcp/tools/context.ts +0 -66
- package/src/mcp/tools/plans.ts +0 -255
- package/src/mcp/tools/stats.ts +0 -96
- package/templates/mem/commands/read.ts +0 -194
- package/templates/mem/commands/selftest.ts +0 -450
- package/templates/mem/mem.ts +0 -68
- package/templates/mem/store.ts +0 -285
- /package/{templates → src}/mem/commands/plan.ts +0 -0
- /package/{templates → src}/mem/commands/rotate.ts +0 -0
- /package/{templates → src}/mem/render.ts +0 -0
- /package/{templates → src}/mem/selectors.ts +0 -0
package/src/install/opencode.ts
CHANGED
|
@@ -113,6 +113,7 @@ export function cmdInstallOpencode(
|
|
|
113
113
|
reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
|
|
114
114
|
installReadHintPlugin(dryRun, getHome);
|
|
115
115
|
installCommitHintPlugin(dryRun, getHome);
|
|
116
|
+
installEditHintPlugin(dryRun, getHome);
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -136,6 +137,7 @@ export function cmdInstallOpencode(
|
|
|
136
137
|
console.log(computeDiff(before, after));
|
|
137
138
|
installReadHintPlugin(dryRun, getHome);
|
|
138
139
|
installCommitHintPlugin(dryRun, getHome);
|
|
140
|
+
installEditHintPlugin(dryRun, getHome);
|
|
139
141
|
return;
|
|
140
142
|
}
|
|
141
143
|
|
|
@@ -157,6 +159,7 @@ export function cmdInstallOpencode(
|
|
|
157
159
|
reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
|
|
158
160
|
installReadHintPlugin(dryRun, getHome);
|
|
159
161
|
installCommitHintPlugin(dryRun, getHome);
|
|
162
|
+
installEditHintPlugin(dryRun, getHome);
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
/**
|
|
@@ -185,27 +188,64 @@ export function readHintPluginSource(root: string): string {
|
|
|
185
188
|
// factual review-seed pointer, plus any convention debt or mem-log row about
|
|
186
189
|
// the file. Annotate only: never blocks, never dedupes.
|
|
187
190
|
// Generated by \`fapony install\` — edit src/hook.ts in the fapony checkout.
|
|
188
|
-
import { readHintFor,
|
|
191
|
+
import { readHintFor, rereadHintFor, readContextData, recordHintFire } from ${hookModule};
|
|
189
192
|
|
|
190
193
|
export const FaponyReadHint = async ({ directory }) => {
|
|
191
194
|
return {
|
|
192
195
|
"tool.execute.after": async (input, output) => {
|
|
193
196
|
try {
|
|
194
197
|
if (input.tool !== "read") return;
|
|
198
|
+
const filePath = input.args?.filePath;
|
|
195
199
|
const parts = [];
|
|
196
200
|
const hint = readHintFor({
|
|
197
|
-
filePath
|
|
201
|
+
filePath,
|
|
198
202
|
offset: input.args?.offset,
|
|
199
203
|
limit: input.args?.limit,
|
|
200
204
|
cwd: directory,
|
|
201
205
|
});
|
|
202
206
|
if (hint) parts.push(hint);
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
const reread = rereadHintFor({
|
|
208
|
+
filePath,
|
|
209
|
+
offset: input.args?.offset,
|
|
210
|
+
limit: input.args?.limit,
|
|
211
|
+
cwd: directory,
|
|
212
|
+
session: input.sessionID,
|
|
213
|
+
});
|
|
214
|
+
if (reread) parts.push(reread);
|
|
215
|
+
const ctx = readContextData(filePath, directory);
|
|
216
|
+
if (ctx) {
|
|
217
|
+
for (const line of [...ctx.debtLines, ...ctx.memLines]) {
|
|
218
|
+
parts.push(line);
|
|
219
|
+
}
|
|
205
220
|
}
|
|
206
221
|
if (parts.length > 0 && typeof output.output === "string") {
|
|
207
222
|
output.output = output.output + "\\n" + parts.join("\\n");
|
|
208
223
|
}
|
|
224
|
+
|
|
225
|
+
// --- hint-fire log (PLAN-feedback-surface chunk 1) ---
|
|
226
|
+
// After output — best-effort, never block the hint.
|
|
227
|
+
if (ctx?.worktree) {
|
|
228
|
+
const rel = typeof filePath === "string" ? (() => {
|
|
229
|
+
try {
|
|
230
|
+
const { realpathSync } = require("node:fs");
|
|
231
|
+
const { relative } = require("node:path");
|
|
232
|
+
const { join: pjoin } = require("node:path");
|
|
233
|
+
const wt = ctx.worktree;
|
|
234
|
+
const abs = realpathSync(filePath.startsWith("/") ? filePath : pjoin(directory, filePath));
|
|
235
|
+
const r = relative(wt, abs).split("\\\\").join("/");
|
|
236
|
+
return r.startsWith("..") ? null : r;
|
|
237
|
+
} catch { return null; }
|
|
238
|
+
})() : null;
|
|
239
|
+
if (hint) {
|
|
240
|
+
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "read", file: rel, count: 1 });
|
|
241
|
+
}
|
|
242
|
+
if (ctx.debtIds.length > 0) {
|
|
243
|
+
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "debt", file: rel, count: ctx.debtIds.length, ids: ctx.debtIds });
|
|
244
|
+
}
|
|
245
|
+
if (ctx.memLines.length > 0) {
|
|
246
|
+
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "mem", file: rel, count: ctx.memLines.length });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
209
249
|
} catch {
|
|
210
250
|
// a hint must never break a read
|
|
211
251
|
}
|
|
@@ -230,7 +270,7 @@ export function commitHintPluginSource(root: string): string {
|
|
|
230
270
|
// verdict reminder when commits have no verdict filed yet.
|
|
231
271
|
// Annotate only: never blocks, never dedupes.
|
|
232
272
|
// Generated by \`fapony install\` — edit src/hook.ts in the fapony checkout.
|
|
233
|
-
import { commitHintFor } from ${hookModule};
|
|
273
|
+
import { commitHintFor, recordHintFire } from ${hookModule};
|
|
234
274
|
|
|
235
275
|
export const FaponyCommitHint = async ({ directory }) => {
|
|
236
276
|
return {
|
|
@@ -245,6 +285,23 @@ export const FaponyCommitHint = async ({ directory }) => {
|
|
|
245
285
|
if (hint && typeof output.output === "string") {
|
|
246
286
|
output.output = output.output + "\\n" + hint;
|
|
247
287
|
}
|
|
288
|
+
|
|
289
|
+
// --- hint-fire log (PLAN-feedback-surface chunk 1) ---
|
|
290
|
+
// After output — best-effort, never block the hint.
|
|
291
|
+
if (hint) {
|
|
292
|
+
try {
|
|
293
|
+
const { spawnSync } = require("node:child_process");
|
|
294
|
+
const git = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: directory, encoding: "utf-8" });
|
|
295
|
+
if (git.status === 0) {
|
|
296
|
+
const worktree = require("node:fs").realpathSync(git.stdout.trim());
|
|
297
|
+
// Count commits from the hint message (lines starting with "fapony: ").
|
|
298
|
+
const commitLines = hint.split("\\n").filter(l => /^fapony:\\s+\\w{7,}/.test(l));
|
|
299
|
+
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "commit", file: null, count: commitLines.length || 1 });
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
// best-effort
|
|
303
|
+
}
|
|
304
|
+
}
|
|
248
305
|
} catch {
|
|
249
306
|
// a hint must never break a bash command
|
|
250
307
|
}
|
|
@@ -254,6 +311,80 @@ export const FaponyCommitHint = async ({ directory }) => {
|
|
|
254
311
|
`;
|
|
255
312
|
}
|
|
256
313
|
|
|
314
|
+
/**
|
|
315
|
+
* OpenCode plugin — the edit hint's in-process shape. `tool.execute.after`
|
|
316
|
+
* on the edit and write tools: when the touched file has importers, appends
|
|
317
|
+
* the importer count plus the review-seed command that lists them. Annotate
|
|
318
|
+
* only (no block, no dedupe beyond what editHintFor does itself).
|
|
319
|
+
*
|
|
320
|
+
* This is the OpenCode parity of Claude's Edit PreToolUse hook
|
|
321
|
+
* (PLAN-edit-importer-hint): same shared logic (editHintFor in src/hook.ts),
|
|
322
|
+
* same once-per-(session, file) dedupe, same `"edit"` fire surface. One
|
|
323
|
+
* known difference: OpenCode's only annotate channel is `after`, so the hint
|
|
324
|
+
* lands one step after the edit instead of before it — the `before` hook can
|
|
325
|
+
* only mutate args or throw, and a hint must never block a tool call.
|
|
326
|
+
*
|
|
327
|
+
* `write` is guarded alongside `edit` for free: a write to a new file
|
|
328
|
+
* resolves to null inside editHintFor (nothing imports it yet), so only a
|
|
329
|
+
* write that overwrites an imported file fires. `apply_patch` is out of
|
|
330
|
+
* scope — its paths live inside patchText, not filePath (separate plan).
|
|
331
|
+
*/
|
|
332
|
+
export function editHintPluginSource(root: string): string {
|
|
333
|
+
const hookModule = JSON.stringify(join(root, "src", "hook.ts"));
|
|
334
|
+
return `// fapony edit hint — annotates edits to source files that have importers
|
|
335
|
+
// with the importer count plus the review-seed command that lists them.
|
|
336
|
+
// Annotate only: never blocks, dedupe per (session, file) inside editHintFor.
|
|
337
|
+
// Generated by \`fapony install\` — edit src/hook.ts in the fapony checkout.
|
|
338
|
+
import { editHintFor, recordHintFire } from ${hookModule};
|
|
339
|
+
|
|
340
|
+
export const FaponyEditHint = async ({ directory }) => {
|
|
341
|
+
return {
|
|
342
|
+
"tool.execute.after": async (input, output) => {
|
|
343
|
+
try {
|
|
344
|
+
if (input.tool !== "edit" && input.tool !== "write") return;
|
|
345
|
+
// The only channel that reaches the agent is output.output; if it is
|
|
346
|
+
// not a string the hint cannot surface, so bail before editHintFor
|
|
347
|
+
// spends the per-session dedupe row or logs a fire that never showed.
|
|
348
|
+
if (typeof output.output !== "string") return;
|
|
349
|
+
const filePath = input.args?.filePath;
|
|
350
|
+
const hint = editHintFor({
|
|
351
|
+
filePath,
|
|
352
|
+
cwd: directory,
|
|
353
|
+
session: input.sessionID,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// --- hint-fire log (PLAN-edit-importer-hint, opencode parity) ---
|
|
357
|
+
// After output — best-effort, never block the hint.
|
|
358
|
+
if (hint) {
|
|
359
|
+
output.output = output.output + "\\n" + hint;
|
|
360
|
+
try {
|
|
361
|
+
const { spawnSync } = require("node:child_process");
|
|
362
|
+
const git = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: directory, encoding: "utf-8" });
|
|
363
|
+
if (git.status === 0) {
|
|
364
|
+
const worktree = require("node:fs").realpathSync(git.stdout.trim());
|
|
365
|
+
const rel = typeof filePath === "string" ? (() => {
|
|
366
|
+
try {
|
|
367
|
+
const { relative, join: pjoin } = require("node:path");
|
|
368
|
+
const abs = require("node:fs").realpathSync(filePath.startsWith("/") ? filePath : pjoin(directory, filePath));
|
|
369
|
+
const r = relative(worktree, abs).split("\\\\").join("/");
|
|
370
|
+
return r.startsWith("..") ? null : r;
|
|
371
|
+
} catch { return null; }
|
|
372
|
+
})() : null;
|
|
373
|
+
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "edit", file: rel, count: 1 });
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
// best-effort
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
// a hint must never break an edit
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
|
|
257
388
|
function installReadHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
258
389
|
const pluginsDir = join(getHome(), ".config", "opencode", "plugins");
|
|
259
390
|
const pluginPath = join(pluginsDir, "fapony-read-hint.ts");
|
|
@@ -319,3 +450,36 @@ function installCommitHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
|
319
450
|
` commit hint: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
320
451
|
);
|
|
321
452
|
}
|
|
453
|
+
|
|
454
|
+
function installEditHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
455
|
+
const pluginsDir = join(getHome(), ".config", "opencode", "plugins");
|
|
456
|
+
const pluginPath = join(pluginsDir, "fapony-edit-hint.ts");
|
|
457
|
+
if (existsSync(pluginPath)) {
|
|
458
|
+
let current = "";
|
|
459
|
+
try {
|
|
460
|
+
current = readFileSync(pluginPath, "utf-8");
|
|
461
|
+
} catch {
|
|
462
|
+
current = "";
|
|
463
|
+
}
|
|
464
|
+
if (current.includes("editHintFor")) {
|
|
465
|
+
console.error(` edit hint: already installed — no change`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
console.error(
|
|
469
|
+
` edit hint: ${pluginPath} exists but isn't fapony's — not overwriting.`,
|
|
470
|
+
);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (!dryRun) {
|
|
474
|
+
try {
|
|
475
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
476
|
+
writeFileSync(pluginPath, editHintPluginSource(INSTALL_ROOT), "utf-8");
|
|
477
|
+
} catch (e) {
|
|
478
|
+
console.error(` edit hint: failed to write — ${(e as Error).message}`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
console.error(
|
|
483
|
+
` edit hint: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
484
|
+
);
|
|
485
|
+
}
|
package/src/install.ts
CHANGED
|
@@ -28,12 +28,13 @@ export {
|
|
|
28
28
|
claudeGetPointsToFapony,
|
|
29
29
|
cmdInstallClaude,
|
|
30
30
|
} from "./install/claude.js";
|
|
31
|
-
export { cmdInstallCodex } from "./install/codex.js";
|
|
31
|
+
export { cmdInstallCodex, findCodexHooksJson } from "./install/codex.js";
|
|
32
32
|
export { cmdInstallCursor } from "./install/cursor.js";
|
|
33
33
|
export { detectClients } from "./install/detect.js";
|
|
34
34
|
export {
|
|
35
35
|
cmdInstallOpencode,
|
|
36
36
|
commitHintPluginSource,
|
|
37
|
+
editHintPluginSource,
|
|
37
38
|
readHintPluginSource,
|
|
38
39
|
} from "./install/opencode.js";
|
|
39
40
|
export {
|
package/src/lint-baseline.ts
CHANGED
|
@@ -12,14 +12,9 @@
|
|
|
12
12
|
// never blocks — the agent decides whether to fix the old issues too
|
|
13
13
|
|
|
14
14
|
import { execSync } from "node:child_process";
|
|
15
|
-
import {
|
|
16
|
-
existsSync,
|
|
17
|
-
mkdirSync,
|
|
18
|
-
readFileSync,
|
|
19
|
-
rmSync,
|
|
20
|
-
writeFileSync,
|
|
21
|
-
} from "node:fs";
|
|
15
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
22
16
|
import { join, relative } from "node:path";
|
|
17
|
+
import { DEFAULT_EVIDENCE_FILE } from "./db/index.js";
|
|
23
18
|
import { faponyDir } from "./db/load.js";
|
|
24
19
|
import { assertSafe } from "./safety.js";
|
|
25
20
|
|
|
@@ -63,7 +58,7 @@ function baselinePath(worktree: string): string {
|
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
function readEvidenceLintCmd(worktree: string): string | null {
|
|
66
|
-
const evidence = join(worktree,
|
|
61
|
+
const evidence = join(worktree, DEFAULT_EVIDENCE_FILE);
|
|
67
62
|
try {
|
|
68
63
|
const parsed = JSON.parse(readFileSync(evidence, "utf-8")) as {
|
|
69
64
|
commands?: { name?: string; cmd?: string }[];
|
package/src/mcp/evidence.ts
CHANGED
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
import { execSync } from "node:child_process";
|
|
18
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
19
19
|
import { join, relative } from "node:path";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type Config,
|
|
22
|
+
EVIDENCE_FILENAME,
|
|
23
|
+
evidenceFile,
|
|
24
|
+
FAPONY_DIR,
|
|
25
|
+
safetyDeny,
|
|
26
|
+
} from "../db/index.js";
|
|
21
27
|
import { assertSafe } from "../safety.js";
|
|
22
28
|
import type { EvidenceItem, EvidenceStatus } from "./primitives.js";
|
|
23
29
|
|
|
@@ -41,7 +47,7 @@ const AGENT_PROVENANCE = { verified: false, source: "agent_report" } as const;
|
|
|
41
47
|
|
|
42
48
|
// --- App-scoped resolution ---
|
|
43
49
|
|
|
44
|
-
// Monorepo group dirs — same order as
|
|
50
|
+
// Monorepo group dirs — same order as src/mem/store.ts (first hit wins there;
|
|
45
51
|
// here every file must already agree on one group+app, so order never matters).
|
|
46
52
|
const APP_GROUPS = ["apps", "packages", "services"];
|
|
47
53
|
|
|
@@ -80,7 +86,13 @@ export function resolveEvidencePath(
|
|
|
80
86
|
APP_GROUPS.includes(group) &&
|
|
81
87
|
heads.every(([g, a]) => g === group && a === app)
|
|
82
88
|
) {
|
|
83
|
-
const candidate = join(
|
|
89
|
+
const candidate = join(
|
|
90
|
+
worktree,
|
|
91
|
+
group,
|
|
92
|
+
app,
|
|
93
|
+
FAPONY_DIR,
|
|
94
|
+
EVIDENCE_FILENAME,
|
|
95
|
+
);
|
|
84
96
|
if (existsSync(candidate)) return candidate;
|
|
85
97
|
}
|
|
86
98
|
}
|