fapony 0.2.1 → 0.3.3
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 +95 -72
- package/fapony.ts +12 -5
- package/package.json +5 -4
- package/skill/define-convention/SKILL.md +77 -0
- package/skill/lookup-before-edit/SKILL.md +48 -0
- package/skill/move-to-done/SKILL.md +19 -30
- package/skill/review-pony/SKILL.md +38 -61
- package/src/analyze.ts +1 -1
- package/src/debt/cli.ts +193 -0
- package/src/debt/format.ts +107 -0
- package/src/debt/index.ts +19 -0
- package/src/debt/load.ts +92 -0
- package/src/debt/promotion.ts +152 -0
- package/src/debt/scan.ts +214 -0
- package/src/debt/types.ts +79 -0
- package/src/detect.ts +92 -0
- package/src/gate.ts +3 -3
- package/src/hook.ts +349 -100
- package/src/init-mem.ts +57 -71
- package/src/init.ts +12 -16
- package/src/install/antigravity.ts +112 -0
- package/src/install/claude.ts +16 -123
- package/src/install/codex.ts +58 -19
- package/src/install/detect.ts +17 -7
- package/src/install/opencode.ts +131 -6
- package/src/install.ts +12 -3
- package/src/lint-baseline.ts +2 -2
- package/src/mcp/primitives.ts +1 -1
- package/src/mcp/tools/index.ts +13 -102
- package/src/mcp/tools/mem.ts +71 -0
- package/src/mcp/transport.ts +8 -94
- package/src/mcp/worktree.ts +1 -1
- package/src/mem/commands/read.ts +231 -141
- package/src/mem/index.ts +4 -13
- package/src/memory.ts +17 -8
- package/src/{plan-seed.ts → seed/plan-seed.ts} +14 -32
- package/src/seed/primitives.ts +66 -0
- package/src/{review-seed.ts → seed/review-seed.ts} +7 -54
- package/src/session/helpers.ts +1 -1
- package/src/session/registry.ts +3 -6
- package/src/setup.ts +1 -1
- package/src/stats/data.ts +1 -1
- package/src/telemetry.ts +1 -1
- package/src/util.ts +61 -0
- package/templates/SPEC.md +8 -1
- package/images/logo.png +0 -0
- package/images/logo.webp +0 -0
- package/images/logo@400.webp +0 -0
- package/images/sample.webp +0 -0
- package/images/summary.webp +0 -0
- package/src/debt.ts +0 -811
- package/src/math.ts +0 -13
- package/src/mcp/tools/usage.ts +0 -211
- package/src/mcp/tools/verdict.ts +0 -161
package/src/install/detect.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// src/install/detect.ts — detect which MCP clients are installed on this machine.
|
|
2
2
|
//
|
|
3
3
|
// Signal per client:
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// antigravity = ~/.gemini exists (the app creates it on first run)
|
|
5
|
+
// claude = `command -v claude` (CLI on PATH)
|
|
6
|
+
// cursor = ~/.cursor exists (the app creates it on first run)
|
|
7
|
+
// opencode = ~/.config/opencode/{opencode.json,opencode.jsonc} exists
|
|
8
|
+
// zcode = ~/.zcode/cli/config.json or ~/.agents/mcp.json exists
|
|
9
|
+
// codex = ~/.codex/config.toml exists
|
|
9
10
|
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { defaultCheckCmd } from "../setup.js";
|
|
13
|
+
import { findGeminiDir } from "./antigravity.js";
|
|
12
14
|
import { findCodexConfig } from "./codex.js";
|
|
13
15
|
import { findCursorDir } from "./cursor.js";
|
|
14
16
|
import { findOpencodeConfig } from "./opencode.js";
|
|
@@ -23,10 +25,10 @@ export interface DetectedClient {
|
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Detect which MCP clients are available on this machine.
|
|
26
|
-
* Returns one entry per platform, ordered: claude, cursor, opencode, zcode, codex.
|
|
28
|
+
* Returns one entry per platform, ordered: antigravity, claude, cursor, opencode, zcode, codex.
|
|
27
29
|
*
|
|
28
30
|
* Uses resolvers from each provider (file/dir-exists check) for
|
|
29
|
-
* cursor/opencode/zcode/codex, and `command -v claude` for claude — all
|
|
31
|
+
* antigravity/cursor/opencode/zcode/codex, and `command -v claude` for claude — all
|
|
30
32
|
* through injected deps for testability.
|
|
31
33
|
*/
|
|
32
34
|
export function detectClients(deps: InstallDeps = {}): DetectedClient[] {
|
|
@@ -35,12 +37,20 @@ export function detectClients(deps: InstallDeps = {}): DetectedClient[] {
|
|
|
35
37
|
|
|
36
38
|
const claude = checkCmd("claude");
|
|
37
39
|
|
|
40
|
+
const geminiDir = findGeminiDir(getHome);
|
|
38
41
|
const cursorDir = findCursorDir(getHome);
|
|
39
42
|
const opencodePath = findOpencodeConfig(getHome);
|
|
40
43
|
const zcodeResult = findZcodeConfig(getHome);
|
|
41
44
|
const codexPath = findCodexConfig(getHome);
|
|
42
45
|
|
|
43
46
|
return [
|
|
47
|
+
{
|
|
48
|
+
platform: "antigravity",
|
|
49
|
+
installed: geminiDir !== null,
|
|
50
|
+
why: geminiDir
|
|
51
|
+
? `dir at ${geminiDir}`
|
|
52
|
+
: "no ~/.gemini directory (open Antigravity once)",
|
|
53
|
+
},
|
|
44
54
|
{
|
|
45
55
|
platform: "claude",
|
|
46
56
|
installed: claude,
|
package/src/install/opencode.ts
CHANGED
|
@@ -114,6 +114,7 @@ export function cmdInstallOpencode(
|
|
|
114
114
|
installReadHintPlugin(dryRun, getHome);
|
|
115
115
|
installCommitHintPlugin(dryRun, getHome);
|
|
116
116
|
installEditHintPlugin(dryRun, getHome);
|
|
117
|
+
installSessionStartPlugin(dryRun, getHome);
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -138,6 +139,7 @@ export function cmdInstallOpencode(
|
|
|
138
139
|
installReadHintPlugin(dryRun, getHome);
|
|
139
140
|
installCommitHintPlugin(dryRun, getHome);
|
|
140
141
|
installEditHintPlugin(dryRun, getHome);
|
|
142
|
+
installSessionStartPlugin(dryRun, getHome);
|
|
141
143
|
return;
|
|
142
144
|
}
|
|
143
145
|
|
|
@@ -160,6 +162,7 @@ export function cmdInstallOpencode(
|
|
|
160
162
|
installReadHintPlugin(dryRun, getHome);
|
|
161
163
|
installCommitHintPlugin(dryRun, getHome);
|
|
162
164
|
installEditHintPlugin(dryRun, getHome);
|
|
165
|
+
installSessionStartPlugin(dryRun, getHome);
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
/**
|
|
@@ -332,10 +335,10 @@ export const FaponyCommitHint = async ({ directory }) => {
|
|
|
332
335
|
export function editHintPluginSource(root: string): string {
|
|
333
336
|
const hookModule = JSON.stringify(join(root, "src", "hook.ts"));
|
|
334
337
|
return `// fapony edit hint — annotates edits to source files that have importers
|
|
335
|
-
// with the importer count plus
|
|
338
|
+
// with the importer count, plus mem/debt context rows about the file.
|
|
336
339
|
// Annotate only: never blocks, dedupe per (session, file) inside editHintFor.
|
|
337
340
|
// Generated by \`fapony install\` — edit src/hook.ts in the fapony checkout.
|
|
338
|
-
import { editHintFor, recordHintFire } from ${hookModule};
|
|
341
|
+
import { editHintFor, readContextData, recordHintFire } from ${hookModule};
|
|
339
342
|
|
|
340
343
|
export const FaponyEditHint = async ({ directory }) => {
|
|
341
344
|
return {
|
|
@@ -347,16 +350,25 @@ export const FaponyEditHint = async ({ directory }) => {
|
|
|
347
350
|
// spends the per-session dedupe row or logs a fire that never showed.
|
|
348
351
|
if (typeof output.output !== "string") return;
|
|
349
352
|
const filePath = input.args?.filePath;
|
|
353
|
+
const parts = [];
|
|
350
354
|
const hint = editHintFor({
|
|
351
355
|
filePath,
|
|
352
356
|
cwd: directory,
|
|
353
357
|
session: input.sessionID,
|
|
354
358
|
});
|
|
359
|
+
if (hint) parts.push(hint);
|
|
360
|
+
// Attach mem/debt context (same as read hint — annotate only, cap 5 lines)
|
|
361
|
+
const ctx = readContextData(filePath, directory);
|
|
362
|
+
if (ctx) {
|
|
363
|
+
for (const line of [...ctx.debtLines, ...ctx.memLines]) {
|
|
364
|
+
parts.push(line);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (parts.length > 0) {
|
|
368
|
+
output.output = output.output + "\\n" + parts.join("\\n");
|
|
355
369
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (hint) {
|
|
359
|
-
output.output = output.output + "\\n" + hint;
|
|
370
|
+
// --- hint-fire log (PLAN-edit-importer-hint, opencode parity) ---
|
|
371
|
+
// After output — best-effort, never block the hint.
|
|
360
372
|
try {
|
|
361
373
|
const { spawnSync } = require("node:child_process");
|
|
362
374
|
const git = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: directory, encoding: "utf-8" });
|
|
@@ -371,6 +383,12 @@ export const FaponyEditHint = async ({ directory }) => {
|
|
|
371
383
|
} catch { return null; }
|
|
372
384
|
})() : null;
|
|
373
385
|
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "edit", file: rel, count: 1 });
|
|
386
|
+
if (ctx?.debtIds?.length) {
|
|
387
|
+
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "debt", file: rel, count: ctx.debtIds.length, ids: ctx.debtIds });
|
|
388
|
+
}
|
|
389
|
+
if (ctx?.memLines?.length) {
|
|
390
|
+
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "mem", file: rel, count: ctx.memLines.length });
|
|
391
|
+
}
|
|
374
392
|
}
|
|
375
393
|
} catch {
|
|
376
394
|
// best-effort
|
|
@@ -483,3 +501,110 @@ function installEditHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
|
483
501
|
` edit hint: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
484
502
|
);
|
|
485
503
|
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* OpenCode plugin — session-start context, the parity of Claude's SessionStart
|
|
507
|
+
* hook.
|
|
508
|
+
*
|
|
509
|
+
* Why `experimental.chat.system.transform` and not the `event` hook on
|
|
510
|
+
* `session.created`: the event hook is observer-only (returns void, no
|
|
511
|
+
* injection channel), while system.transform's `output.system` is the
|
|
512
|
+
* documented injection point (`packages/plugin/src/index.ts`: input
|
|
513
|
+
* `{ sessionID?, model }`, output `{ system: string[] }`). The context lands
|
|
514
|
+
* on the first model dispatch instead of at session creation — one step late,
|
|
515
|
+
* the same shape as every other OpenCode hint (annotate-after), and arguably
|
|
516
|
+
* better: sessions that never dispatch never pay the spawn.
|
|
517
|
+
*
|
|
518
|
+
* The guard (no mem log = silence), the kickoff spawn and the cap all live in
|
|
519
|
+
* `sessionStartContext` (src/hook.ts), imported at runtime like the read /
|
|
520
|
+
* commit / edit hint plugins — so `git pull` updates this client too. It used
|
|
521
|
+
* to bake `mem kickoff` into the generated body and push stdout, which injected
|
|
522
|
+
* kickoff's "# <path> — 0 entries" header into every session of every repo
|
|
523
|
+
* with no mem log (bug mub2ezhi) and could not be fixed by a pull. One import,
|
|
524
|
+
* one implementation, shared with the Claude/Codex hooks.
|
|
525
|
+
*
|
|
526
|
+
* Once per session (closure Set — the factory runs once per server), silent
|
|
527
|
+
* when `sessionStartContext` returns null (no mem log in scope), never throws.
|
|
528
|
+
* `process.execPath` is the bun running opencode itself — no PATH dependency.
|
|
529
|
+
* Verified against OpenCode 1.18.29; `experimental.chat.system.transform` is an
|
|
530
|
+
* experimental hook, so a rename makes this plugin quiet with no error — check
|
|
531
|
+
* on OpenCode upgrades.
|
|
532
|
+
*/
|
|
533
|
+
export function sessionStartPluginSource(root: string): string {
|
|
534
|
+
const hookModule = JSON.stringify(join(root, "src", "hook.ts"));
|
|
535
|
+
const faponyTs = JSON.stringify(join(root, "fapony.ts"));
|
|
536
|
+
return `// fapony session start — injects the \`fapony mem kickoff\` digest as context
|
|
537
|
+
// once per session via sessionStartContext, silent when the repo has no mem log.
|
|
538
|
+
// Annotate only: never blocks.
|
|
539
|
+
// Generated by \`fapony install\` — logic lives in src/hook.ts (sessionStartContext).
|
|
540
|
+
import { sessionStartContext } from ${hookModule};
|
|
541
|
+
|
|
542
|
+
export const FaponySessionStart = async ({ directory }) => {
|
|
543
|
+
const seen = new Set();
|
|
544
|
+
return {
|
|
545
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
546
|
+
try {
|
|
547
|
+
const sessionID = input?.sessionID;
|
|
548
|
+
if (!sessionID || seen.has(sessionID)) return;
|
|
549
|
+
seen.add(sessionID);
|
|
550
|
+
const ctx = sessionStartContext(directory, ${faponyTs});
|
|
551
|
+
if (ctx) output.system.push(ctx);
|
|
552
|
+
} catch {
|
|
553
|
+
// context must never break a session start
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
};
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function installSessionStartPlugin(
|
|
562
|
+
dryRun: boolean,
|
|
563
|
+
getHome: () => string,
|
|
564
|
+
): void {
|
|
565
|
+
const pluginsDir = join(getHome(), ".config", "opencode", "plugins");
|
|
566
|
+
const pluginPath = join(pluginsDir, "fapony-session-start.ts");
|
|
567
|
+
if (existsSync(pluginPath)) {
|
|
568
|
+
let current = "";
|
|
569
|
+
try {
|
|
570
|
+
current = readFileSync(pluginPath, "utf-8");
|
|
571
|
+
} catch {
|
|
572
|
+
current = "";
|
|
573
|
+
}
|
|
574
|
+
if (current.includes("FaponySessionStart")) {
|
|
575
|
+
// Ours — and generated, so never overwritten. A file baked by an older
|
|
576
|
+
// fapony differs from what we generate now: say so instead of "no change",
|
|
577
|
+
// which is how the pre-hook plugin (mub2ezhi) would have been kept.
|
|
578
|
+
if (current !== sessionStartPluginSource(INSTALL_ROOT)) {
|
|
579
|
+
console.error(
|
|
580
|
+
` session start: stale fapony plugin — delete ${pluginPath} and re-run install`,
|
|
581
|
+
);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
console.error(` session start: already installed — no change`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
console.error(
|
|
588
|
+
` session start: ${pluginPath} exists but isn't fapony's — not overwriting.`,
|
|
589
|
+
);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (!dryRun) {
|
|
593
|
+
try {
|
|
594
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
595
|
+
writeFileSync(
|
|
596
|
+
pluginPath,
|
|
597
|
+
sessionStartPluginSource(INSTALL_ROOT),
|
|
598
|
+
"utf-8",
|
|
599
|
+
);
|
|
600
|
+
} catch (e) {
|
|
601
|
+
console.error(
|
|
602
|
+
` session start: failed to write — ${(e as Error).message}`,
|
|
603
|
+
);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
console.error(
|
|
608
|
+
` session start: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
609
|
+
);
|
|
610
|
+
}
|
package/src/install.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// ZCode gets skill/<name>/ symlinked into ~/.agents/skills.
|
|
13
13
|
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
|
+
import { cmdInstallAntigravity } from "./install/antigravity.js";
|
|
15
16
|
import { cmdInstallClaude } from "./install/claude.js";
|
|
16
17
|
import { cmdInstallCodex } from "./install/codex.js";
|
|
17
18
|
import { cmdInstallCursor } from "./install/cursor.js";
|
|
@@ -22,6 +23,7 @@ import { cmdInstallZcode } from "./install/zcode.js";
|
|
|
22
23
|
import { ask } from "./setup.js";
|
|
23
24
|
import { isAffirmative } from "./util.js";
|
|
24
25
|
|
|
26
|
+
export { cmdInstallAntigravity } from "./install/antigravity.js";
|
|
25
27
|
export {
|
|
26
28
|
claudeAddArgs,
|
|
27
29
|
claudeGetArgs,
|
|
@@ -58,6 +60,10 @@ export async function cmdInstall(
|
|
|
58
60
|
const installAll = args.includes("--all");
|
|
59
61
|
|
|
60
62
|
// --- explicit platform: original behavior, unchanged ---
|
|
63
|
+
if (platform === "antigravity") {
|
|
64
|
+
cmdInstallAntigravity(dryRun, deps);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
61
67
|
if (platform === "claude") {
|
|
62
68
|
cmdInstallClaude(dryRun, deps);
|
|
63
69
|
return;
|
|
@@ -80,10 +86,10 @@ export async function cmdInstall(
|
|
|
80
86
|
}
|
|
81
87
|
if (platform !== undefined) {
|
|
82
88
|
console.error(
|
|
83
|
-
`usage: fapony install --platform opencode|claude|cursor|zcode|codex [--dry-run]`,
|
|
89
|
+
`usage: fapony install --platform antigravity|opencode|claude|cursor|zcode|codex [--dry-run]`,
|
|
84
90
|
);
|
|
85
91
|
console.error(
|
|
86
|
-
` supported platforms: opencode, claude, cursor, zcode, codex`,
|
|
92
|
+
` supported platforms: antigravity, opencode, claude, cursor, zcode, codex`,
|
|
87
93
|
);
|
|
88
94
|
(deps.exit ?? defaultExit)(1);
|
|
89
95
|
return;
|
|
@@ -107,7 +113,7 @@ export async function cmdInstall(
|
|
|
107
113
|
if (found.length === 0) {
|
|
108
114
|
console.error();
|
|
109
115
|
console.error(
|
|
110
|
-
` no MCP client found (looked for claude on PATH; config files for cursor, opencode, zcode, codex).`,
|
|
116
|
+
` no MCP client found (looked for claude on PATH; config files for antigravity, cursor, opencode, zcode, codex).`,
|
|
111
117
|
);
|
|
112
118
|
console.error(
|
|
113
119
|
` open the app once, then re-run — or force with: fapony install --platform <name>`,
|
|
@@ -162,6 +168,9 @@ function installPlatform(
|
|
|
162
168
|
deps: InstallDeps,
|
|
163
169
|
): void {
|
|
164
170
|
switch (platform) {
|
|
171
|
+
case "antigravity":
|
|
172
|
+
cmdInstallAntigravity(dryRun, deps);
|
|
173
|
+
break;
|
|
165
174
|
case "claude":
|
|
166
175
|
cmdInstallClaude(dryRun, deps);
|
|
167
176
|
break;
|
package/src/lint-baseline.ts
CHANGED
|
@@ -52,12 +52,12 @@ function gitSha(cwd: string): string {
|
|
|
52
52
|
return p.exitCode === 0 ? p.stdout.toString().trim().slice(0, 12) : "none";
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function baselinePath(worktree: string): string {
|
|
55
|
+
export function baselinePath(worktree: string): string {
|
|
56
56
|
const key = worktree.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
57
57
|
return join(faponyDir(), "lint-baseline", `${key}.json`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function readEvidenceLintCmd(worktree: string): string | null {
|
|
60
|
+
export function readEvidenceLintCmd(worktree: string): string | null {
|
|
61
61
|
const evidence = join(worktree, DEFAULT_EVIDENCE_FILE);
|
|
62
62
|
try {
|
|
63
63
|
const parsed = JSON.parse(readFileSync(evidence, "utf-8")) as {
|
package/src/mcp/primitives.ts
CHANGED
|
@@ -157,7 +157,7 @@ export interface VerificationReport {
|
|
|
157
157
|
evidence: EvidenceItem[];
|
|
158
158
|
/** Evidence summary (computed from evidence array). */
|
|
159
159
|
evidence_summary: EvidenceSummary;
|
|
160
|
-
/** Verdict grade + note (from gate/
|
|
160
|
+
/** Verdict grade + note (from the frozen gate/verdict ledger, or null if never graded). */
|
|
161
161
|
verdict: {
|
|
162
162
|
grade: string;
|
|
163
163
|
note: string;
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -6,86 +6,11 @@
|
|
|
6
6
|
// saves more than it costs (see CLAUDE.md "จ่าย token อย่างฉลาด"). Keep
|
|
7
7
|
// descriptions imperative — say what to send, not why it matters.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
import { REASON_CODES, REGIME_CODES } from "../types.js";
|
|
11
|
-
|
|
12
|
-
export { toolMemAdd, toolMemFind } from "./mem.js";
|
|
13
|
-
export { toolPassiveUsage } from "./usage.js";
|
|
14
|
-
export { toolVerdictSubmit } from "./verdict.js";
|
|
9
|
+
export { toolMemAdd, toolMemClose, toolMemFind } from "./mem.js";
|
|
15
10
|
|
|
16
11
|
// --- Tool definitions ---
|
|
17
12
|
|
|
18
13
|
export const TOOLS = [
|
|
19
|
-
{
|
|
20
|
-
name: "verdict_submit",
|
|
21
|
-
description:
|
|
22
|
-
"Grade a finished unit of work — every unit that ends, including work " +
|
|
23
|
-
"that went right the first time. Submit 'fail' the moment a first " +
|
|
24
|
-
"attempt turns out wrong, then a pass-family verdict once the fix is " +
|
|
25
|
-
"verified. Never leave a run open: one stuck at running/fixing absorbs " +
|
|
26
|
-
"later unrelated verdicts for that worktree. Without run_id, binds to " +
|
|
27
|
-
"the latest open run for the same worktree+plan, else creates one.",
|
|
28
|
-
inputSchema: {
|
|
29
|
-
type: "object" as const,
|
|
30
|
-
properties: {
|
|
31
|
-
run_id: {
|
|
32
|
-
type: "number",
|
|
33
|
-
description: "Existing run to attach to. Omitted = bind or create.",
|
|
34
|
-
},
|
|
35
|
-
verdict: {
|
|
36
|
-
type: "string",
|
|
37
|
-
enum: [...VERDICT_GRADES],
|
|
38
|
-
description: "Verdict grade",
|
|
39
|
-
},
|
|
40
|
-
reason_code: {
|
|
41
|
-
type: "string",
|
|
42
|
-
enum: [...REASON_CODES],
|
|
43
|
-
description:
|
|
44
|
-
"Failure reason. Clean passes take 'none', never 'other'.",
|
|
45
|
-
},
|
|
46
|
-
regime: {
|
|
47
|
-
type: "string",
|
|
48
|
-
enum: [...REGIME_CODES],
|
|
49
|
-
description:
|
|
50
|
-
"Task shape: code=feature/refactor, fix=debugging a defect, " +
|
|
51
|
-
"review=reviewing a diff, plan=producing a plan or spec, " +
|
|
52
|
-
"inquiry=questions without editing files, test=writing tests",
|
|
53
|
-
},
|
|
54
|
-
note: {
|
|
55
|
-
type: "string",
|
|
56
|
-
description:
|
|
57
|
-
"Standalone note — read months later with no access to this " +
|
|
58
|
-
"conversation. Required when reason_code = 'other'.",
|
|
59
|
-
},
|
|
60
|
-
worktree: {
|
|
61
|
-
type: "string",
|
|
62
|
-
description:
|
|
63
|
-
"Absolute repo path (git rev-parse --show-toplevel). Every query " +
|
|
64
|
-
"scopes by it, so a bare name or an omission files the verdict " +
|
|
65
|
-
"where nothing reads it. Neither case errors.",
|
|
66
|
-
},
|
|
67
|
-
plan: {
|
|
68
|
-
type: "string",
|
|
69
|
-
description: "Plan file path, for a new run",
|
|
70
|
-
},
|
|
71
|
-
session_id: {
|
|
72
|
-
type: "string",
|
|
73
|
-
description:
|
|
74
|
-
"Client session id (Claude Code/Codex: the transcript .jsonl " +
|
|
75
|
-
"path; OpenCode/ZCode: the session id) — this is what attributes " +
|
|
76
|
-
"the verdict to a model. Cannot find it → omit, never invent one.",
|
|
77
|
-
},
|
|
78
|
-
files: {
|
|
79
|
-
type: "array",
|
|
80
|
-
items: { type: "string" },
|
|
81
|
-
description:
|
|
82
|
-
"Repo-relative paths this unit touched. Optional, but always " +
|
|
83
|
-
"send them: nothing else records where the work landed.",
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
required: ["verdict", "reason_code", "regime"],
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
14
|
{
|
|
90
15
|
name: "mem_find",
|
|
91
16
|
description:
|
|
@@ -179,43 +104,29 @@ export const TOOLS = [
|
|
|
179
104
|
},
|
|
180
105
|
},
|
|
181
106
|
{
|
|
182
|
-
name: "
|
|
107
|
+
name: "mem_close",
|
|
183
108
|
description:
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"another's, so this is the only way to compare them. Filter by " +
|
|
188
|
-
"worktree and time range.",
|
|
109
|
+
"Close a mem row by id with a tombstone message (what was done). " +
|
|
110
|
+
"The write half of closing what mem_find shows as open — id must exist. " +
|
|
111
|
+
"Returns the ref and timestamp.",
|
|
189
112
|
inputSchema: {
|
|
190
113
|
type: "object" as const,
|
|
191
114
|
properties: {
|
|
192
115
|
worktree: {
|
|
193
116
|
type: "string",
|
|
194
|
-
description: "Filter by worktree path (absolute)",
|
|
195
|
-
},
|
|
196
|
-
since: {
|
|
197
|
-
type: "number",
|
|
198
|
-
description:
|
|
199
|
-
"Unix timestamp — include sessions created at or after this time",
|
|
200
|
-
},
|
|
201
|
-
until: {
|
|
202
|
-
type: "number",
|
|
203
117
|
description:
|
|
204
|
-
"
|
|
118
|
+
"Absolute path (git rev-parse --show-toplevel) — required",
|
|
205
119
|
},
|
|
206
|
-
|
|
207
|
-
type: "
|
|
208
|
-
description:
|
|
209
|
-
"If true, include tool-call breakdown + step counts per session " +
|
|
210
|
-
"(activity signal, not quality). Default false keeps output compact.",
|
|
120
|
+
id: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "The row id to close",
|
|
211
123
|
},
|
|
212
|
-
|
|
213
|
-
type: "
|
|
214
|
-
description:
|
|
215
|
-
"If true, return raw JSON PassiveUsageResult. If false (default), return human-readable text.",
|
|
124
|
+
text: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Tombstone message — what was done (commit sha counts)",
|
|
216
127
|
},
|
|
217
128
|
},
|
|
218
|
-
required: [],
|
|
129
|
+
required: ["worktree", "id", "text"],
|
|
219
130
|
},
|
|
220
131
|
},
|
|
221
132
|
];
|
package/src/mcp/tools/mem.ts
CHANGED
|
@@ -235,3 +235,74 @@ export function toolMemAdd(args: Record<string, unknown>): ToolResult {
|
|
|
235
235
|
return errorResult(e instanceof Error ? e.message : String(e));
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
+
|
|
239
|
+
// --- mem_close ---
|
|
240
|
+
//
|
|
241
|
+
// A separate tool on purpose, not kind:"close" inside mem_add: a close row
|
|
242
|
+
// carries {ref, text} with no files[] and no spec, while mem_add requires a
|
|
243
|
+
// non-empty files[] (and a spec for hold) — folding them into one schema
|
|
244
|
+
// would make required fields depend on the value of another field, the shape
|
|
245
|
+
// models call wrong most often. Mirrors CLI `mem close <id> "<msg>"`
|
|
246
|
+
// (commands/write.ts cmdClose): the id must exist; the tombstone voids the
|
|
247
|
+
// claim by itself.
|
|
248
|
+
|
|
249
|
+
export interface MemCloseResult {
|
|
250
|
+
ref: string;
|
|
251
|
+
text: string;
|
|
252
|
+
ts: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function memClose(args: {
|
|
256
|
+
worktree: string;
|
|
257
|
+
id: string;
|
|
258
|
+
text: string;
|
|
259
|
+
}): MemCloseResult {
|
|
260
|
+
if (!args.id.trim()) {
|
|
261
|
+
throw new Error("id is required");
|
|
262
|
+
}
|
|
263
|
+
if (!args.text.trim()) {
|
|
264
|
+
throw new Error("text is required and must not be empty");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
initStore(args.worktree);
|
|
268
|
+
const all = rows();
|
|
269
|
+
if (!all.some((r) => "id" in r && r.id === args.id)) {
|
|
270
|
+
throw new Error(`no id "${args.id}" in the log`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const ts = new Date().toISOString();
|
|
274
|
+
put({ kind: "close", ref: args.id, text: args.text });
|
|
275
|
+
|
|
276
|
+
return { ref: args.id, text: args.text, ts };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function toolMemClose(args: Record<string, unknown>): ToolResult {
|
|
280
|
+
const worktree =
|
|
281
|
+
typeof args.worktree === "string" ? args.worktree.trim() : "";
|
|
282
|
+
if (!worktree) {
|
|
283
|
+
return errorResult(
|
|
284
|
+
"worktree is required and must be an absolute path " +
|
|
285
|
+
"(git rev-parse --show-toplevel)",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (!worktree.startsWith("/")) {
|
|
289
|
+
return errorResult(`worktree must be an absolute path, got: ${worktree}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
293
|
+
if (!id) {
|
|
294
|
+
return errorResult("id is required — the row id to close");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
298
|
+
if (!text) {
|
|
299
|
+
return errorResult("text is required and must not be empty");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const result = memClose({ worktree, id, text });
|
|
304
|
+
return jsonResult(result);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return errorResult(e instanceof Error ? e.message : String(e));
|
|
307
|
+
}
|
|
308
|
+
}
|