fapony 0.1.3 → 0.2.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/README.md +75 -44
- package/fapony.ts +32 -1
- 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 +13 -1
- package/src/conventions-seed.ts +0 -1
- package/src/db/defaults.ts +1 -1
- package/src/db/getters.ts +3 -3
- package/src/db/store.ts +0 -75
- package/src/db/types.ts +1 -1
- package/src/debt.ts +169 -34
- package/src/digest/collect.ts +16 -0
- package/src/digest/text.ts +25 -0
- package/src/hook.ts +424 -16
- package/src/init-mem.ts +123 -37
- package/src/init.ts +51 -34
- package/src/install/claude.ts +7 -5
- package/src/install/opencode.ts +59 -5
- package/src/lint-baseline.ts +1 -7
- package/src/mcp/evidence.ts +1 -1
- 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 +348 -0
- package/src/memory.ts +237 -40
- package/src/plan-seed.ts +20 -1
- package/src/review-seed.ts +19 -0
- package/src/setup.ts +4 -6
- package/src/stats/data.ts +34 -86
- 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/init-mem.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
// src/init-mem.ts —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// .fapony/; run state stays in ~/.config/fapony/state.db, never in the worktree).
|
|
7
|
-
// Re-run to re-sync after editing the template — not automatic, on purpose.
|
|
1
|
+
// src/init-mem.ts — cleanup old .memory/ directories and warn about stale call sites.
|
|
2
|
+
//
|
|
3
|
+
// Before PLAN-agent-one-call, this file scaffolded templates/mem/ into every repo.
|
|
4
|
+
// Now fapony owns the mem code (src/mem/) and calls it directly via `fapony mem`.
|
|
5
|
+
// This command's new job: delete legacy .memory/ dirs + warn about package.json refs.
|
|
8
6
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import {
|
|
8
|
+
copyFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { loadConfig } from "./db/index.js";
|
|
12
17
|
|
|
13
18
|
export function copyDir(src: string, dest: string): string[] {
|
|
14
19
|
mkdirSync(dest, { recursive: true });
|
|
@@ -27,13 +32,11 @@ export function copyDir(src: string, dest: string): string[] {
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export function cmdInitMem(args: string[]): void {
|
|
30
|
-
const
|
|
35
|
+
const force = args.includes("--force");
|
|
31
36
|
const worktreeKey = args.find((x) => !x.startsWith("-"));
|
|
32
37
|
const config = loadConfig();
|
|
33
38
|
|
|
34
|
-
// no key given = the repo you are standing in
|
|
35
|
-
// without registering the worktree first (loadConfig already reads the cwd's fapony.config.json,
|
|
36
|
-
// so it picks up that project's paths.memoryEntry by itself)
|
|
39
|
+
// no key given = the repo you are standing in
|
|
37
40
|
const worktree = worktreeKey ? config.worktrees[worktreeKey] : process.cwd();
|
|
38
41
|
if (!worktree) {
|
|
39
42
|
console.error(`unknown worktree key: ${worktreeKey}`);
|
|
@@ -41,31 +44,114 @@ export function cmdInitMem(args: string[]): void {
|
|
|
41
44
|
process.exit(1);
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const gitRoot = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
|
|
48
|
+
cwd: worktree,
|
|
49
|
+
})
|
|
50
|
+
.stdout.toString()
|
|
51
|
+
.trim();
|
|
52
|
+
const root = gitRoot || worktree;
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
// 1) Find and delete .memory/ directories (legacy layout)
|
|
55
|
+
const memoryDirs: string[] = [];
|
|
56
|
+
const walk = (dir: string, depth = 0) => {
|
|
57
|
+
if (depth > 4) return;
|
|
58
|
+
if (existsSync(`${dir}/.memory`)) {
|
|
59
|
+
memoryDirs.push(`${dir}/.memory`);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
63
|
+
if (
|
|
64
|
+
entry.isDirectory() &&
|
|
65
|
+
!entry.name.startsWith(".") &&
|
|
66
|
+
entry.name !== "node_modules"
|
|
67
|
+
) {
|
|
68
|
+
walk(join(dir, entry.name), depth + 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
walk(root);
|
|
76
|
+
|
|
77
|
+
if (memoryDirs.length === 0) {
|
|
78
|
+
console.log("no legacy .memory/ directories found — already clean");
|
|
79
|
+
} else {
|
|
80
|
+
let removed = 0;
|
|
81
|
+
let kept = 0;
|
|
82
|
+
for (const d of memoryDirs) {
|
|
83
|
+
// Any log*.jsonl is history — includes log.<person>.jsonl (the standard
|
|
84
|
+
// filename) and rotated log.YYYY-MM-DD.jsonl, not just log.jsonl.
|
|
85
|
+
let logs: string[] = [];
|
|
86
|
+
try {
|
|
87
|
+
logs = readdirSync(d).filter(
|
|
88
|
+
(f) => f === "log.jsonl" || /^log\..*\.jsonl$/.test(f),
|
|
89
|
+
);
|
|
90
|
+
} catch {
|
|
91
|
+
// unreadable dir — fall through and remove
|
|
92
|
+
}
|
|
93
|
+
if (logs.length > 0 && !force) {
|
|
94
|
+
console.log(
|
|
95
|
+
`keeping ${d} — has ${logs.length} log file(s): ${logs.join(", ")}`,
|
|
96
|
+
);
|
|
97
|
+
console.log(
|
|
98
|
+
` move them under ${join(d, "..", ".fapony", ".memory")}/, or re-run with --force to delete`,
|
|
99
|
+
);
|
|
100
|
+
kept++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
console.log(
|
|
104
|
+
`deleting ${d}${logs.length > 0 ? ` (--force: ${logs.length} log file(s))` : ""}`,
|
|
105
|
+
);
|
|
106
|
+
rmSync(d, { recursive: true, force: true });
|
|
107
|
+
removed++;
|
|
108
|
+
}
|
|
109
|
+
console.log(
|
|
110
|
+
`\nremoved ${removed} legacy .memory/ director${removed === 1 ? "y" : "ies"}${
|
|
111
|
+
kept > 0
|
|
112
|
+
? ` · kept ${kept} with logs — move them under .fapony/.memory/, then re-run`
|
|
113
|
+
: ""
|
|
114
|
+
}`,
|
|
52
115
|
);
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
if (update && !existsSync(destFile)) {
|
|
56
|
-
console.error(`${destFile} not found — run \`fapony init\` first.`);
|
|
57
|
-
process.exit(1);
|
|
58
116
|
}
|
|
59
117
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
118
|
+
// 2) Warn about package.json call sites still referencing .memory/mem.ts
|
|
119
|
+
const warnAboutCallSites = (dir: string, depth = 0) => {
|
|
120
|
+
if (depth > 4) return;
|
|
121
|
+
const pkg = join(dir, "package.json");
|
|
122
|
+
if (existsSync(pkg)) {
|
|
123
|
+
try {
|
|
124
|
+
const raw = readFileSync(pkg, "utf8");
|
|
125
|
+
if (raw.includes(".memory/mem.ts")) {
|
|
126
|
+
console.log(`\n⚠ ${pkg} still references .memory/mem.ts`);
|
|
127
|
+
console.log(
|
|
128
|
+
` update to: "fapony mem <sub>" (e.g. "fapony mem add", "fapony mem close")`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// ignore
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
137
|
+
if (
|
|
138
|
+
entry.isDirectory() &&
|
|
139
|
+
!entry.name.startsWith(".") &&
|
|
140
|
+
entry.name !== "node_modules"
|
|
141
|
+
) {
|
|
142
|
+
warnAboutCallSites(join(dir, entry.name), depth + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
warnAboutCallSites(root);
|
|
150
|
+
|
|
151
|
+
console.log("\nmem commands are now built into fapony:");
|
|
152
|
+
console.log(' fapony mem add <kind> "<text>" --files <files>');
|
|
153
|
+
console.log(' fapony mem close <id> "<text>"');
|
|
154
|
+
console.log(" fapony mem find <word>");
|
|
155
|
+
console.log(" fapony mem kickoff [id|spec.md]");
|
|
156
|
+
console.log(" fapony mem now");
|
|
71
157
|
}
|
package/src/init.ts
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
// state.db stays in ~/.config/fapony/ by design (security boundary — see db.ts),
|
|
4
4
|
// never inside the worktree where agents have full write access.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
appendFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
7
13
|
import { dirname, join, relative } from "node:path";
|
|
8
14
|
import { createInterface } from "node:readline";
|
|
9
15
|
import { seedConventionsFile } from "./conventions-seed.js";
|
|
@@ -11,11 +17,10 @@ import {
|
|
|
11
17
|
type Config,
|
|
12
18
|
doneDir,
|
|
13
19
|
evidenceFile,
|
|
14
|
-
|
|
20
|
+
memoryDir,
|
|
15
21
|
planDir,
|
|
16
22
|
specDir,
|
|
17
23
|
} from "./db/index.js";
|
|
18
|
-
import { copyDir } from "./init-mem.js";
|
|
19
24
|
import { isAffirmative } from "./util.js";
|
|
20
25
|
|
|
21
26
|
const FAPONY_README = `# .fapony/ — fapony project dir (plans, specs, memory)
|
|
@@ -37,7 +42,7 @@ const FAPONY_README = `# .fapony/ — fapony project dir (plans, specs, memory)
|
|
|
37
42
|
# running in this worktree cannot rewrite run state / audit trail.
|
|
38
43
|
#
|
|
39
44
|
# Ask your agent for the plan picture instead of listing these by hand:
|
|
40
|
-
#
|
|
45
|
+
# run "fapony mem kickoff" — priority plans, the first unchecked chunk, open bugs
|
|
41
46
|
`;
|
|
42
47
|
|
|
43
48
|
// Static template — deliberately NOT derived from the repo (reading package.json
|
|
@@ -59,22 +64,22 @@ const EVIDENCE_JSON = `{
|
|
|
59
64
|
// Not in SERVER_INSTRUCTIONS either: that reaches every MCP session of every user,
|
|
60
65
|
// and most of them never ran `fapony init` — it would tell them to run a command
|
|
61
66
|
// that does not exist.
|
|
62
|
-
const RULES_SNIPPET =
|
|
63
|
-
|
|
64
|
-
) => `## Memory: ${dirname(memEntry)}/log.<you>.jsonl (append-only)
|
|
67
|
+
const RULES_SNIPPET =
|
|
68
|
+
() => `## Memory: .fapony/.memory/log.<you>.jsonl (append-only)
|
|
65
69
|
|
|
66
70
|
The log is this project's shared brain — it lives in git, so anyone who clones the
|
|
67
71
|
repo gets every decision, bug and note with it. The filename comes from
|
|
68
|
-
\`git config user.name
|
|
72
|
+
\`git config user.name\` — one file per person, and \`*.jsonl merge=union\` in
|
|
73
|
+
.gitattributes keeps both sides when two people end up sharing a name anyway.
|
|
69
74
|
|
|
70
75
|
Log as you work — do not wait to be asked. Nothing writes it for you:
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
fapony mem kickoff <plan.md> # start a session with this
|
|
78
|
+
fapony mem add decision "what was locked, and why" --files src/x.ts
|
|
79
|
+
fapony mem add bug "what is broken" --files src/x.ts
|
|
80
|
+
fapony mem add note "state the next session needs" --files src/x.ts
|
|
81
|
+
fapony mem close <id> "fixed in <sha>"
|
|
82
|
+
fapony mem find "<text>"
|
|
78
83
|
|
|
79
84
|
Write each entry standalone — it is read months later with no chat to refer to.
|
|
80
85
|
--files is required: rows that name no file cannot be recalled when that file is
|
|
@@ -90,13 +95,13 @@ Finish a chunk, before starting the next:
|
|
|
90
95
|
1. Tick its checkbox + stamp the TL;DR in the plan file
|
|
91
96
|
2. Commit — separate from other chunks
|
|
92
97
|
3. \`verdict_submit\` (fapony MCP), grading what actually happened
|
|
93
|
-
4. \`
|
|
94
|
-
—
|
|
98
|
+
4. \`fapony mem add note "what the next chunk needs" --files f1,f2 <path/to/PLAN-x.md>\`
|
|
99
|
+
— use the same plan path every time
|
|
95
100
|
5. Stop. Do not continue to the next chunk in the same session unless told to.
|
|
96
101
|
|
|
97
|
-
Next chunk, new session — open with \`
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
Next chunk, new session — open with \`fapony mem kickoff <path/to/PLAN-x.md>\` instead
|
|
103
|
+
of carrying the old transcript forward. kickoff already filters to the rows for that plan,
|
|
104
|
+
and takes just the filename (\`kickoff PLAN-x.md\`) when you do not want to type the path.`;
|
|
100
105
|
|
|
101
106
|
export function initProject(targetPath: string, config?: Config): void {
|
|
102
107
|
// Create target root
|
|
@@ -112,6 +117,22 @@ export function initProject(targetPath: string, config?: Config): void {
|
|
|
112
117
|
mkdirSync(faponyDir, { recursive: true });
|
|
113
118
|
writeFileSync(join(faponyDir, "README"), FAPONY_README);
|
|
114
119
|
|
|
120
|
+
// --- .gitattributes: union-merge the append-only logs ---
|
|
121
|
+
//
|
|
122
|
+
// Every row is one person's append, so both sides of a "conflict" are always
|
|
123
|
+
// right. Without the line, two people whose git user.name collides (admin /
|
|
124
|
+
// user / owner — what a fresh OS install offers) resolve a conflict by hand
|
|
125
|
+
// on every pull. Appended, never rewritten: the file is the repo's, not ours.
|
|
126
|
+
const attrs = join(targetPath, ".gitattributes");
|
|
127
|
+
const attrsBody = existsSync(attrs) ? readFileSync(attrs, "utf-8") : "";
|
|
128
|
+
if (!/^\s*\*\.jsonl\s+merge=union\s*$/m.test(attrsBody)) {
|
|
129
|
+
appendFileSync(
|
|
130
|
+
attrs,
|
|
131
|
+
`${attrsBody && !attrsBody.endsWith("\n") ? "\n" : ""}# append-only memory logs: keep both sides, never hand-resolve\n*.jsonl merge=union\n`,
|
|
132
|
+
);
|
|
133
|
+
console.log(` + ${attrs} — *.jsonl merge=union`);
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
// --- evidence.json (verification_report allowlist — see src/mcp/evidence.ts) ---
|
|
116
137
|
const evidencePath = join(targetPath, evidenceFile(config));
|
|
117
138
|
if (existsSync(evidencePath)) {
|
|
@@ -141,19 +162,16 @@ export function initProject(targetPath: string, config?: Config): void {
|
|
|
141
162
|
}
|
|
142
163
|
mkdirSync(specDirAbs, { recursive: true });
|
|
143
164
|
|
|
144
|
-
// --- .memory/ (
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
targetPath,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`${join(targetPath, memEntry)} already exists — delete it first if you want a fresh copy.`,
|
|
165
|
+
// --- .fapony/.memory/ (empty dir — mem commands are built into fapony now) ---
|
|
166
|
+
const memoryDirPath = join(targetPath, memoryDir(config));
|
|
167
|
+
if (existsSync(memoryDirPath)) {
|
|
168
|
+
console.log(` ${relative(targetPath, memoryDirPath)}/ — already exists`);
|
|
169
|
+
} else {
|
|
170
|
+
mkdirSync(memoryDirPath, { recursive: true });
|
|
171
|
+
console.log(
|
|
172
|
+
` ${relative(targetPath, memoryDirPath)}/ — mem commands are built into fapony (fapony mem ...)`,
|
|
153
173
|
);
|
|
154
174
|
}
|
|
155
|
-
const templateDir = join(import.meta.dir, "..", "templates", "mem");
|
|
156
|
-
const files = copyDir(templateDir, memoryDir);
|
|
157
175
|
|
|
158
176
|
console.log(`scaffolded ${targetPath}/`);
|
|
159
177
|
console.log(
|
|
@@ -166,13 +184,13 @@ export function initProject(targetPath: string, config?: Config): void {
|
|
|
166
184
|
` ${evidenceFile(config)} — allowlist for 'fapony report' (edit the cmds!)`,
|
|
167
185
|
);
|
|
168
186
|
console.log(
|
|
169
|
-
` ${relative(targetPath,
|
|
187
|
+
` ${relative(targetPath, memoryDirPath)}/ — mem commands are built into fapony (fapony mem ...)`,
|
|
170
188
|
);
|
|
171
189
|
console.log(`\nNext: add "${targetPath}" to fapony.config.json worktrees`);
|
|
172
190
|
console.log(
|
|
173
191
|
`\nThen paste this into your agent-rules file (CLAUDE.md / AGENTS.md / opencode.json\ninstructions) — the memory log only fills up if the rules your agent already reads\ntell it to write:\n`,
|
|
174
192
|
);
|
|
175
|
-
console.log(RULES_SNIPPET(
|
|
193
|
+
console.log(RULES_SNIPPET());
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
const AGENT_RULE_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
@@ -228,8 +246,7 @@ export async function cmdInit(args: string[]): Promise<void> {
|
|
|
228
246
|
);
|
|
229
247
|
if (!isAffirmative(answer)) return;
|
|
230
248
|
|
|
231
|
-
const
|
|
232
|
-
const snippet = `\n\n${RULES_SNIPPET(memEntry)}\n`;
|
|
249
|
+
const snippet = `\n\n${RULES_SNIPPET()}\n`;
|
|
233
250
|
for (const f of found) {
|
|
234
251
|
appendFileSync(f, snippet);
|
|
235
252
|
console.log(` appended to ${relative(targetPath, f)}`);
|
package/src/install/claude.ts
CHANGED
|
@@ -345,11 +345,13 @@ function installStopHook(dryRun: boolean, deps: InstallDeps): void {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
|
-
* PreToolUse hook on Read: annotates a full-file read
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
348
|
+
* PreToolUse hook on Read: annotates a full-file read with one factual line —
|
|
349
|
+
* the size + the review-seed command when the file is large, and the re-read
|
|
350
|
+
* line when the same path was already read this session and its mtime has not
|
|
351
|
+
* moved (an unchanged file, so the second read buys nothing; a changed one
|
|
352
|
+
* stays silent). Annotate only — no permissionDecision is ever returned, the
|
|
353
|
+
* read always proceeds. The matcher "Read" keeps the spawn off every other
|
|
354
|
+
* tool call.
|
|
353
355
|
*/
|
|
354
356
|
function installReadHintHook(dryRun: boolean, deps: InstallDeps): void {
|
|
355
357
|
ensureClaudeHook(dryRun, deps, {
|
package/src/install/opencode.ts
CHANGED
|
@@ -185,27 +185,64 @@ export function readHintPluginSource(root: string): string {
|
|
|
185
185
|
// factual review-seed pointer, plus any convention debt or mem-log row about
|
|
186
186
|
// the file. Annotate only: never blocks, never dedupes.
|
|
187
187
|
// Generated by \`fapony install\` — edit src/hook.ts in the fapony checkout.
|
|
188
|
-
import { readHintFor,
|
|
188
|
+
import { readHintFor, rereadHintFor, readContextData, recordHintFire } from ${hookModule};
|
|
189
189
|
|
|
190
190
|
export const FaponyReadHint = async ({ directory }) => {
|
|
191
191
|
return {
|
|
192
192
|
"tool.execute.after": async (input, output) => {
|
|
193
193
|
try {
|
|
194
194
|
if (input.tool !== "read") return;
|
|
195
|
+
const filePath = input.args?.filePath;
|
|
195
196
|
const parts = [];
|
|
196
197
|
const hint = readHintFor({
|
|
197
|
-
filePath
|
|
198
|
+
filePath,
|
|
198
199
|
offset: input.args?.offset,
|
|
199
200
|
limit: input.args?.limit,
|
|
200
201
|
cwd: directory,
|
|
201
202
|
});
|
|
202
203
|
if (hint) parts.push(hint);
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
const reread = rereadHintFor({
|
|
205
|
+
filePath,
|
|
206
|
+
offset: input.args?.offset,
|
|
207
|
+
limit: input.args?.limit,
|
|
208
|
+
cwd: directory,
|
|
209
|
+
session: input.sessionID,
|
|
210
|
+
});
|
|
211
|
+
if (reread) parts.push(reread);
|
|
212
|
+
const ctx = readContextData(filePath, directory);
|
|
213
|
+
if (ctx) {
|
|
214
|
+
for (const line of [...ctx.debtLines, ...ctx.memLines]) {
|
|
215
|
+
parts.push(line);
|
|
216
|
+
}
|
|
205
217
|
}
|
|
206
218
|
if (parts.length > 0 && typeof output.output === "string") {
|
|
207
219
|
output.output = output.output + "\\n" + parts.join("\\n");
|
|
208
220
|
}
|
|
221
|
+
|
|
222
|
+
// --- hint-fire log (PLAN-feedback-surface chunk 1) ---
|
|
223
|
+
// After output — best-effort, never block the hint.
|
|
224
|
+
if (ctx?.worktree) {
|
|
225
|
+
const rel = typeof filePath === "string" ? (() => {
|
|
226
|
+
try {
|
|
227
|
+
const { realpathSync } = require("node:fs");
|
|
228
|
+
const { relative } = require("node:path");
|
|
229
|
+
const { join: pjoin } = require("node:path");
|
|
230
|
+
const wt = ctx.worktree;
|
|
231
|
+
const abs = realpathSync(filePath.startsWith("/") ? filePath : pjoin(wt, filePath));
|
|
232
|
+
const r = relative(wt, abs).split("\\\\").join("/");
|
|
233
|
+
return r.startsWith("..") ? null : r;
|
|
234
|
+
} catch { return null; }
|
|
235
|
+
})() : null;
|
|
236
|
+
if (hint) {
|
|
237
|
+
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "read", file: rel, count: 1 });
|
|
238
|
+
}
|
|
239
|
+
if (ctx.debtIds.length > 0) {
|
|
240
|
+
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "debt", file: rel, count: ctx.debtIds.length, ids: ctx.debtIds });
|
|
241
|
+
}
|
|
242
|
+
if (ctx.memLines.length > 0) {
|
|
243
|
+
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "mem", file: rel, count: ctx.memLines.length });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
209
246
|
} catch {
|
|
210
247
|
// a hint must never break a read
|
|
211
248
|
}
|
|
@@ -230,7 +267,7 @@ export function commitHintPluginSource(root: string): string {
|
|
|
230
267
|
// verdict reminder when commits have no verdict filed yet.
|
|
231
268
|
// Annotate only: never blocks, never dedupes.
|
|
232
269
|
// Generated by \`fapony install\` — edit src/hook.ts in the fapony checkout.
|
|
233
|
-
import { commitHintFor } from ${hookModule};
|
|
270
|
+
import { commitHintFor, recordHintFire } from ${hookModule};
|
|
234
271
|
|
|
235
272
|
export const FaponyCommitHint = async ({ directory }) => {
|
|
236
273
|
return {
|
|
@@ -245,6 +282,23 @@ export const FaponyCommitHint = async ({ directory }) => {
|
|
|
245
282
|
if (hint && typeof output.output === "string") {
|
|
246
283
|
output.output = output.output + "\\n" + hint;
|
|
247
284
|
}
|
|
285
|
+
|
|
286
|
+
// --- hint-fire log (PLAN-feedback-surface chunk 1) ---
|
|
287
|
+
// After output — best-effort, never block the hint.
|
|
288
|
+
if (hint) {
|
|
289
|
+
try {
|
|
290
|
+
const { spawnSync } = require("node:child_process");
|
|
291
|
+
const git = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: directory, encoding: "utf-8" });
|
|
292
|
+
if (git.status === 0) {
|
|
293
|
+
const worktree = require("node:fs").realpathSync(git.stdout.trim());
|
|
294
|
+
// Count commits from the hint message (lines starting with "fapony: ").
|
|
295
|
+
const commitLines = hint.split("\\n").filter(l => /^fapony:\\s+\\w{7,}/.test(l));
|
|
296
|
+
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "commit", file: null, count: commitLines.length || 1 });
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
// best-effort
|
|
300
|
+
}
|
|
301
|
+
}
|
|
248
302
|
} catch {
|
|
249
303
|
// a hint must never break a bash command
|
|
250
304
|
}
|
package/src/lint-baseline.ts
CHANGED
|
@@ -12,13 +12,7 @@
|
|
|
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";
|
|
23
17
|
import { faponyDir } from "./db/load.js";
|
|
24
18
|
import { assertSafe } from "./safety.js";
|
package/src/mcp/evidence.ts
CHANGED
|
@@ -41,7 +41,7 @@ const AGENT_PROVENANCE = { verified: false, source: "agent_report" } as const;
|
|
|
41
41
|
|
|
42
42
|
// --- App-scoped resolution ---
|
|
43
43
|
|
|
44
|
-
// Monorepo group dirs — same order as
|
|
44
|
+
// Monorepo group dirs — same order as src/mem/store.ts (first hit wins there;
|
|
45
45
|
// here every file must already agree on one group+app, so order never matters).
|
|
46
46
|
const APP_GROUPS = ["apps", "packages", "services"];
|
|
47
47
|
|