fapony 0.3.3 → 0.3.5
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 +7 -1
- package/fapony.ts +3 -116
- package/package.json +5 -6
- package/skill/move-to-done/SKILL.md +3 -2
- package/src/adapters/cli.ts +128 -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 +80 -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 +434 -0
- package/src/{mcp → adapters/mcp}/evidence.ts +2 -2
- package/src/{mcp → adapters/mcp}/primitives.ts +2 -2
- 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/mem.ts +3 -3
- package/src/{mcp → adapters/mcp}/tools/report.ts +4 -4
- package/src/adapters/mcp/types.ts +18 -0
- package/src/{mcp → adapters/mcp}/worktree.ts +1 -1
- package/src/analyze.ts +1 -1
- package/src/commands.ts +180 -0
- 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 +2 -2
- package/src/gates.ts +1 -1
- package/src/hook.ts +70 -1424
- package/src/init-mem.ts +1 -1
- package/src/init.ts +1 -1
- package/src/install/claude.ts +17 -0
- package/src/install/opencode.ts +114 -0
- package/src/install.ts +12 -5
- package/src/lint-baseline.ts +1 -2
- package/src/map.ts +29 -8
- package/src/mem/commands/plan.ts +197 -42
- package/src/mem/commands/read.ts +53 -12
- package/src/mem/index.ts +60 -1
- package/src/mem/store.ts +5 -1
- package/src/memory.ts +26 -388
- 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/seed/plan-seed.ts +126 -32
- package/src/seed/primitives.ts +1 -7
- package/src/session/types.ts +14 -128
- package/src/setup.ts +1 -1
- package/src/stats/data.ts +3 -3
- package/src/telemetry.ts +2 -2
- package/src/update.ts +1 -1
- 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 -93
- package/src/web/html.ts +2 -33
- 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/mcp/types.ts +0 -54
- package/src/test.ts +0 -2
- /package/src/{mcp → adapters/mcp}/tools/index.ts +0 -0
- /package/src/{mcp → adapters/mcp}/transport.ts +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// src/adapters/hooks/stop.ts — Stop hook: refuse to end a turn that produced
|
|
2
|
+
// commits but no mem row.
|
|
3
|
+
//
|
|
4
|
+
// Split from src/hook.ts (PLAN-lib-layer chunk 3). Pure helpers (utcStamp,
|
|
5
|
+
// hookTsMs, sessionKey) live in core/hook-helpers.ts.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
appendFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
statSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { hookTsMs, sessionKey, utcStamp } from "../../core/hook-helpers.js";
|
|
17
|
+
import { readMemLog, whereMemDir } from "../../memory.js";
|
|
18
|
+
|
|
19
|
+
// --- Types ---
|
|
20
|
+
|
|
21
|
+
export interface RawStopPayload {
|
|
22
|
+
// Claude Code
|
|
23
|
+
cwd?: string;
|
|
24
|
+
transcript_path?: string | null;
|
|
25
|
+
stop_hook_active?: boolean;
|
|
26
|
+
// Cursor — common schema (https://cursor.com/docs/agent/hooks)
|
|
27
|
+
workspace_roots?: string[];
|
|
28
|
+
conversation_id?: string;
|
|
29
|
+
loop_count?: number;
|
|
30
|
+
status?: string;
|
|
31
|
+
// Codex — hooks contract (https://learn.chatgpt.com/docs/hooks)
|
|
32
|
+
session_id?: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
permission_mode?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type StopClient = "claude" | "cursor" | "codex";
|
|
38
|
+
|
|
39
|
+
export interface NormalizedStopInput {
|
|
40
|
+
client: StopClient;
|
|
41
|
+
cwd: string;
|
|
42
|
+
transcriptPath: string | null;
|
|
43
|
+
stopHookActive: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Pure helpers ---
|
|
47
|
+
|
|
48
|
+
/** Cursor transcript location derived from the conversation id. */
|
|
49
|
+
export function cursorTranscriptPath(
|
|
50
|
+
home: string,
|
|
51
|
+
cwd: string,
|
|
52
|
+
conversationId: string,
|
|
53
|
+
): string {
|
|
54
|
+
const slug = cwd.replace(/^\//, "").replace(/\//g, "-");
|
|
55
|
+
return join(
|
|
56
|
+
home,
|
|
57
|
+
".cursor",
|
|
58
|
+
"projects",
|
|
59
|
+
slug,
|
|
60
|
+
"agent-transcripts",
|
|
61
|
+
conversationId,
|
|
62
|
+
`${conversationId}.jsonl`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isCursorPayload(raw: RawStopPayload): boolean {
|
|
67
|
+
return (
|
|
68
|
+
Array.isArray(raw.workspace_roots) ||
|
|
69
|
+
typeof raw.conversation_id === "string"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Codex sends permission_mode and/or model — fields neither Claude nor Cursor include in Stop. */
|
|
74
|
+
export function isCodexPayload(raw: RawStopPayload): boolean {
|
|
75
|
+
return (
|
|
76
|
+
typeof raw.permission_mode === "string" ||
|
|
77
|
+
(typeof raw.model === "string" && !isCursorPayload(raw))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Field-mapping only — all three clients feed the same decideStop below. */
|
|
82
|
+
export function normalizeStopInput(
|
|
83
|
+
raw: RawStopPayload,
|
|
84
|
+
home: string,
|
|
85
|
+
): NormalizedStopInput {
|
|
86
|
+
if (isCodexPayload(raw)) {
|
|
87
|
+
return {
|
|
88
|
+
client: "codex",
|
|
89
|
+
cwd: raw.cwd ?? process.cwd(),
|
|
90
|
+
transcriptPath:
|
|
91
|
+
typeof raw.transcript_path === "string" && raw.transcript_path
|
|
92
|
+
? raw.transcript_path
|
|
93
|
+
: null,
|
|
94
|
+
stopHookActive: raw.stop_hook_active === true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (isCursorPayload(raw)) {
|
|
98
|
+
const cwd = raw.workspace_roots?.[0] ?? raw.cwd ?? process.cwd();
|
|
99
|
+
let transcriptPath =
|
|
100
|
+
typeof raw.transcript_path === "string" && raw.transcript_path
|
|
101
|
+
? raw.transcript_path
|
|
102
|
+
: null;
|
|
103
|
+
if (!transcriptPath && raw.conversation_id) {
|
|
104
|
+
transcriptPath = cursorTranscriptPath(home, cwd, raw.conversation_id);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
client: "cursor",
|
|
108
|
+
cwd,
|
|
109
|
+
transcriptPath,
|
|
110
|
+
stopHookActive: (raw.loop_count ?? 0) > 0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
client: "claude",
|
|
115
|
+
cwd: raw.cwd ?? process.cwd(),
|
|
116
|
+
transcriptPath: raw.transcript_path ?? null,
|
|
117
|
+
stopHookActive: raw.stop_hook_active === true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Pure decision: block when this session produced commits but no mem row
|
|
123
|
+
* newer than session start exists, or when the agent announced a bug but
|
|
124
|
+
* filed no kind:bug row.
|
|
125
|
+
*/
|
|
126
|
+
export function decideStop(opts: {
|
|
127
|
+
stopHookActive: boolean;
|
|
128
|
+
worktree: string | null;
|
|
129
|
+
commits: number;
|
|
130
|
+
since?: string | null;
|
|
131
|
+
commitList?: string[];
|
|
132
|
+
memLastTs?: string | null;
|
|
133
|
+
memCandidates?: string[];
|
|
134
|
+
bugSignal?: string | null;
|
|
135
|
+
bugRowSinceStart?: boolean;
|
|
136
|
+
}): string | null {
|
|
137
|
+
if (opts.stopHookActive) return null;
|
|
138
|
+
if (!opts.worktree) return null;
|
|
139
|
+
|
|
140
|
+
// --- Bug-signal block (independent of commits) ---
|
|
141
|
+
if (opts.bugSignal && !opts.bugRowSinceStart) {
|
|
142
|
+
return [
|
|
143
|
+
`This turn reported a bug ("${opts.bugSignal}") but no mem row with kind:bug exists for this session.`,
|
|
144
|
+
`A bug described in chat is lost when the room closes — a decision row does not surface it.`,
|
|
145
|
+
` fapony mem add bug "<what is broken>" --files <files>`,
|
|
146
|
+
`Already filed elsewhere, or not a bug? End the turn again — this fires once per session.`,
|
|
147
|
+
].join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- Commit block (original) ---
|
|
151
|
+
if (opts.commits < 1) return null;
|
|
152
|
+
if (!opts.memLastTs && !opts.memCandidates?.length) return null;
|
|
153
|
+
if (opts.since && opts.memLastTs) {
|
|
154
|
+
const sinceMs = hookTsMs(opts.since);
|
|
155
|
+
const memMs = hookTsMs(opts.memLastTs);
|
|
156
|
+
if (Number.isNaN(sinceMs) || Number.isNaN(memMs)) return null;
|
|
157
|
+
if (memMs >= sinceMs) return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const lines: string[] = [
|
|
161
|
+
`${opts.commits} commit(s) landed in ${opts.worktree} this session — no mem row recorded for this work.`,
|
|
162
|
+
];
|
|
163
|
+
const list = opts.commitList ?? [];
|
|
164
|
+
for (const c of list.slice(0, 5)) lines.push(` ${c}`);
|
|
165
|
+
if (list.length > 5) lines.push(` … +${list.length - 5} more`);
|
|
166
|
+
if (opts.memLastTs) {
|
|
167
|
+
lines.push(
|
|
168
|
+
`mem: last row ${opts.memLastTs.slice(0, 10)} — nothing newer this session`,
|
|
169
|
+
);
|
|
170
|
+
} else if (opts.memCandidates?.length) {
|
|
171
|
+
lines.push(
|
|
172
|
+
`mem: no log in scope from ${opts.worktree} — found ` +
|
|
173
|
+
`${opts.memCandidates.join(", ")} (run mem commands from there, or --mem-dir)`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
lines.push(
|
|
178
|
+
`Record a mem row before ending: fapony mem add <decision|bug|note> "what happened" --files <files> ` +
|
|
179
|
+
`${opts.worktree}/.fapony/plan/PLAN.md (or the relevant plan). ` +
|
|
180
|
+
`files[] is required — a row without it is unfindable when you touch that file next session.`,
|
|
181
|
+
);
|
|
182
|
+
if (opts.bugSignal) {
|
|
183
|
+
lines.push(
|
|
184
|
+
`This turn announced a bug ("${opts.bugSignal}") — use kind:bug, not decision.`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Claude blocks with decision:block; Cursor auto-submits as followup_message;
|
|
191
|
+
* Codex continues with decision:block + reason. */
|
|
192
|
+
export function stopOutput(client: StopClient, reason: string): string {
|
|
193
|
+
if (client === "cursor") return JSON.stringify({ followup_message: reason });
|
|
194
|
+
if (client === "codex") return JSON.stringify({ decision: "block", reason });
|
|
195
|
+
return JSON.stringify({ decision: "block", reason });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Block dedupe ---
|
|
199
|
+
|
|
200
|
+
const STOP_BLOCK_DIR = "stop-block";
|
|
201
|
+
|
|
202
|
+
interface StopBlockRow {
|
|
203
|
+
ts: string;
|
|
204
|
+
worktree: string;
|
|
205
|
+
kind?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Absolute path of a session's block log — may not exist. */
|
|
209
|
+
export function stopBlockPath(session: string): string {
|
|
210
|
+
const base =
|
|
211
|
+
process.env.FAPONY_STATE_DIR || join(homedir(), ".config", "fapony");
|
|
212
|
+
return join(base, STOP_BLOCK_DIR, `${sessionKey(session)}.jsonl`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* True when this session already blocked for this worktree + kind — the caller
|
|
217
|
+
* then lets the turn end. Records the block when it has not.
|
|
218
|
+
*
|
|
219
|
+
* `kind` defaults to `"commit"` for backwards compatibility. Bug blocks use
|
|
220
|
+
* `"bug"` so they don't consume the commit block's quota.
|
|
221
|
+
*/
|
|
222
|
+
export function stopBlockedBefore(
|
|
223
|
+
session: string | null,
|
|
224
|
+
worktree: string,
|
|
225
|
+
kind: string = "commit",
|
|
226
|
+
): boolean {
|
|
227
|
+
if (!session) return false;
|
|
228
|
+
const path = stopBlockPath(session);
|
|
229
|
+
try {
|
|
230
|
+
if (existsSync(path)) {
|
|
231
|
+
for (const line of readFileSync(path, "utf-8").split("\n")) {
|
|
232
|
+
if (!line) continue;
|
|
233
|
+
try {
|
|
234
|
+
const row = JSON.parse(line) as StopBlockRow;
|
|
235
|
+
if (row.worktree === worktree && (row.kind ?? "commit") === kind) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// a torn line must not lose the rest of the log
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const dir = join(path, "..");
|
|
244
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
245
|
+
const row: StopBlockRow = { ts: new Date().toISOString(), worktree, kind };
|
|
246
|
+
appendFileSync(path, `${JSON.stringify(row)}\n`, "utf-8");
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- Bug-signal detection ---
|
|
254
|
+
|
|
255
|
+
/** Announcement words that indicate the agent found a bug — not symptom words. */
|
|
256
|
+
const BUG_MARKERS: RegExp[] = [/เจอบั๊ก/];
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Scan assistant text from a Claude transcript for bug markers. Reads only
|
|
260
|
+
* the tail of the file (last 200KB) to avoid parsing the full transcript.
|
|
261
|
+
* Returns the first matched marker word, or null.
|
|
262
|
+
*/
|
|
263
|
+
export function bugSignalFromTranscript(
|
|
264
|
+
transcriptPath: string,
|
|
265
|
+
sinceMs: number,
|
|
266
|
+
): string | null {
|
|
267
|
+
try {
|
|
268
|
+
const stat = statSync(transcriptPath);
|
|
269
|
+
// Cap at 10MB — hook must not stall turn-end
|
|
270
|
+
if (stat.size > 10 * 1024 * 1024) return null;
|
|
271
|
+
|
|
272
|
+
// Read tail to avoid parsing the full transcript
|
|
273
|
+
const tailBytes = Math.min(stat.size, 200 * 1024);
|
|
274
|
+
const fd = require("node:fs").openSync(transcriptPath, "r");
|
|
275
|
+
const buf = Buffer.alloc(tailBytes);
|
|
276
|
+
require("node:fs").readSync(
|
|
277
|
+
fd,
|
|
278
|
+
buf,
|
|
279
|
+
0,
|
|
280
|
+
tailBytes,
|
|
281
|
+
Math.max(0, stat.size - tailBytes),
|
|
282
|
+
);
|
|
283
|
+
require("node:fs").closeSync(fd);
|
|
284
|
+
|
|
285
|
+
const tail = buf.toString("utf-8");
|
|
286
|
+
// When reading from the middle of a large file, the first line is partial
|
|
287
|
+
const readingMidFile = tailBytes < stat.size;
|
|
288
|
+
const startIdx = readingMidFile ? tail.indexOf("\n") : -1;
|
|
289
|
+
const lines =
|
|
290
|
+
startIdx >= 0 ? tail.slice(startIdx + 1).split("\n") : tail.split("\n");
|
|
291
|
+
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
if (!line) continue;
|
|
294
|
+
// Transcript tail: untyped JSONL — the structural type below names only
|
|
295
|
+
// the fields this scan reads (biome noExplicitAny: no `any` annotation).
|
|
296
|
+
let o: {
|
|
297
|
+
message?: {
|
|
298
|
+
role?: string;
|
|
299
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
300
|
+
created_at?: string;
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
try {
|
|
304
|
+
o = JSON.parse(line);
|
|
305
|
+
} catch {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const m = o.message;
|
|
309
|
+
if (m?.role !== "assistant" || !Array.isArray(m.content)) continue;
|
|
310
|
+
// Skip messages older than session start
|
|
311
|
+
if (m.created_at) {
|
|
312
|
+
const msgMs = new Date(m.created_at).getTime();
|
|
313
|
+
if (!Number.isNaN(msgMs) && msgMs < sinceMs) continue;
|
|
314
|
+
}
|
|
315
|
+
for (const b of m.content) {
|
|
316
|
+
if (b.type !== "text" || typeof b.text !== "string") continue;
|
|
317
|
+
for (const re of BUG_MARKERS) {
|
|
318
|
+
if (re.test(b.text)) {
|
|
319
|
+
const match = b.text.match(re);
|
|
320
|
+
return match?.[0] ?? null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* True when the mem log has at least one kind:bug row with ts >= since.
|
|
333
|
+
*/
|
|
334
|
+
function hasBugRowSince(worktree: string, since: string): boolean {
|
|
335
|
+
try {
|
|
336
|
+
const { rows } = readMemLog(worktree, since);
|
|
337
|
+
return rows.some((r) => r.kind === "bug");
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function git(args: string[], cwd: string): string | null {
|
|
344
|
+
try {
|
|
345
|
+
const p = Bun.spawnSync(["git", ...args], {
|
|
346
|
+
cwd,
|
|
347
|
+
stdout: "pipe",
|
|
348
|
+
stderr: "pipe",
|
|
349
|
+
});
|
|
350
|
+
return p.exitCode === 0 ? p.stdout.toString().trim() : null;
|
|
351
|
+
} catch {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Reads the Stop-hook JSON on stdin, prints a block decision or nothing. */
|
|
357
|
+
export async function cmdHookStop(): Promise<void> {
|
|
358
|
+
let reason: string | null = null;
|
|
359
|
+
let client: StopClient = "claude";
|
|
360
|
+
try {
|
|
361
|
+
const raw = JSON.parse(await Bun.stdin.text()) as RawStopPayload;
|
|
362
|
+
const norm = normalizeStopInput(raw, homedir());
|
|
363
|
+
client = norm.client;
|
|
364
|
+
if (client === "cursor" && raw.status !== "completed") return;
|
|
365
|
+
|
|
366
|
+
const worktree = git(["rev-parse", "--show-toplevel"], norm.cwd);
|
|
367
|
+
|
|
368
|
+
let since: string | null = null;
|
|
369
|
+
if (norm.transcriptPath) {
|
|
370
|
+
try {
|
|
371
|
+
since = utcStamp(statSync(norm.transcriptPath).birthtime);
|
|
372
|
+
} catch {
|
|
373
|
+
since = null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let commits = 0;
|
|
378
|
+
let commitList: string[] = [];
|
|
379
|
+
let memLastTs: string | null = null;
|
|
380
|
+
let memCandidates: string[] = [];
|
|
381
|
+
let bugSignal: string | null = null;
|
|
382
|
+
let bugRowSinceStart = false;
|
|
383
|
+
if (worktree && since) {
|
|
384
|
+
const log = git(
|
|
385
|
+
["log", "--since", `${since} +0000`, "--format=%h %s"],
|
|
386
|
+
norm.cwd,
|
|
387
|
+
);
|
|
388
|
+
commitList = log ? log.split("\n").filter(Boolean) : [];
|
|
389
|
+
commits = commitList.length;
|
|
390
|
+
try {
|
|
391
|
+
const mem = readMemLog(worktree);
|
|
392
|
+
memLastTs = mem.rows[0]?.ts ?? null;
|
|
393
|
+
if (!memLastTs) memCandidates = whereMemDir(worktree).candidates ?? [];
|
|
394
|
+
} catch {
|
|
395
|
+
memLastTs = null;
|
|
396
|
+
}
|
|
397
|
+
// Bug-signal detection (independent of commits)
|
|
398
|
+
if (norm.transcriptPath && process.env.FAPONY_NO_BUG_BLOCK !== "1") {
|
|
399
|
+
const sinceMs = hookTsMs(since);
|
|
400
|
+
if (!Number.isNaN(sinceMs)) {
|
|
401
|
+
bugSignal = bugSignalFromTranscript(norm.transcriptPath, sinceMs);
|
|
402
|
+
if (bugSignal) bugRowSinceStart = hasBugRowSince(worktree, since);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
reason = decideStop({
|
|
408
|
+
stopHookActive: norm.stopHookActive,
|
|
409
|
+
worktree: since ? worktree : null,
|
|
410
|
+
commits,
|
|
411
|
+
since,
|
|
412
|
+
commitList,
|
|
413
|
+
memLastTs,
|
|
414
|
+
memCandidates,
|
|
415
|
+
bugSignal,
|
|
416
|
+
bugRowSinceStart,
|
|
417
|
+
});
|
|
418
|
+
if (
|
|
419
|
+
reason &&
|
|
420
|
+
worktree &&
|
|
421
|
+
stopBlockedBefore(
|
|
422
|
+
norm.transcriptPath,
|
|
423
|
+
worktree,
|
|
424
|
+
bugSignal ? "bug" : "commit",
|
|
425
|
+
)
|
|
426
|
+
) {
|
|
427
|
+
reason = null;
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
reason = null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (reason) console.log(stopOutput(client, reason));
|
|
434
|
+
}
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
evidenceFile,
|
|
24
24
|
FAPONY_DIR,
|
|
25
25
|
safetyDeny,
|
|
26
|
-
} from "
|
|
27
|
-
import { assertSafe } from "
|
|
26
|
+
} from "../../core/config.js";
|
|
27
|
+
import { assertSafe } from "../../safety.js";
|
|
28
28
|
import type { EvidenceItem, EvidenceStatus } from "./primitives.js";
|
|
29
29
|
|
|
30
30
|
// --- Config shape (.fapony/evidence.json by default, paths.evidenceFile to move it) ---
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// §0 rule: add-only — never remove or rename exported symbols.
|
|
8
8
|
|
|
9
9
|
import { execSync } from "node:child_process";
|
|
10
|
-
import type { BlastEntry } from "
|
|
11
|
-
import { ROOT } from "
|
|
10
|
+
import type { BlastEntry } from "../../analyze.js";
|
|
11
|
+
import { ROOT } from "../../update.js";
|
|
12
12
|
|
|
13
13
|
// ─── Server build identity ─────────────────────────────────────────────
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/mcp/tools/check.ts — handoff_check tool
|
|
2
2
|
|
|
3
|
-
import { blastRadiusForWorktree } from "
|
|
3
|
+
import { blastRadiusForWorktree } from "../../../analyze.js";
|
|
4
4
|
import type { CheckResult } from "../primitives.js";
|
|
5
5
|
import { errorResult, jsonResult, type ToolResult } from "../types.js";
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/mcp/tools/collect.ts — handoff_collect tool
|
|
2
2
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
-
import { isTestFile } from "
|
|
4
|
+
import { isTestFile } from "../../../analyze.js";
|
|
5
5
|
import { errorResult, jsonResult, type ToolResult } from "../types.js";
|
|
6
6
|
|
|
7
7
|
// --- Git helper ---
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// file is unfindable when you touch that file (fill rate 26% CLI-flag vs 88%
|
|
7
7
|
// MCP-required — schema wins over prose every time, see CLAUDE.md rule 9).
|
|
8
8
|
|
|
9
|
-
import { openRows } from "
|
|
9
|
+
import { openRows } from "../../../mem/selectors.js";
|
|
10
10
|
import {
|
|
11
11
|
initStore,
|
|
12
12
|
KINDS,
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
put,
|
|
15
15
|
rows,
|
|
16
16
|
type WorkKind,
|
|
17
|
-
} from "
|
|
18
|
-
import { type MemRow, readMemLog, resolveMemDir } from "
|
|
17
|
+
} from "../../../mem/store.js";
|
|
18
|
+
import { type MemRow, readMemLog, resolveMemDir } from "../../../memory.js";
|
|
19
19
|
import { errorResult, jsonResult, type ToolResult } from "../types.js";
|
|
20
20
|
|
|
21
21
|
export interface MemFindResult {
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// run metrics + verdict into a single VerificationReport.
|
|
5
5
|
// Calls existing primitives — no duplicate parser/conformance logic.
|
|
6
6
|
|
|
7
|
-
import { blastRadiusForWorktree } from "
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { parseGateEventData } from "
|
|
7
|
+
import { blastRadiusForWorktree } from "../../../analyze.js";
|
|
8
|
+
import { loadConfig } from "../../../core/config.js";
|
|
9
|
+
import { getEvents, getRun, openDb } from "../../../db/store.js";
|
|
10
|
+
import { parseGateEventData } from "../../../parse.js";
|
|
11
11
|
import { collectEvidence } from "../evidence.js";
|
|
12
12
|
import type { CheckResult, VerificationReport } from "../primitives.js";
|
|
13
13
|
import {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/mcp/types.ts — ToolResult, helpers
|
|
2
|
+
// ReasonCode + RegimeCode enums now live in core/enums.ts (PLAN-lib-layer chunk 2c).
|
|
3
|
+
|
|
4
|
+
// Re-export enums for existing callers.
|
|
5
|
+
export {
|
|
6
|
+
REASON_CODES,
|
|
7
|
+
REGIME_CODES,
|
|
8
|
+
type ReasonCode,
|
|
9
|
+
type RegimeCode,
|
|
10
|
+
} from "../../core/enums.js";
|
|
11
|
+
|
|
12
|
+
// Re-export ToolResult types for existing callers.
|
|
13
|
+
export {
|
|
14
|
+
errorResult,
|
|
15
|
+
jsonResult,
|
|
16
|
+
parseToolResult,
|
|
17
|
+
type ToolResult,
|
|
18
|
+
} from "../../core/types.js";
|
package/src/analyze.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
normalize as posixNormalize,
|
|
27
27
|
} from "node:path/posix";
|
|
28
28
|
|
|
29
|
-
import { faponyDir } from "./
|
|
29
|
+
import { faponyDir } from "./core/config.js";
|
|
30
30
|
import { extractExports } from "./map.js";
|
|
31
31
|
|
|
32
32
|
// --- Types (mirror SPEC-analyze-checkup.md) ---
|