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/mcp/tools/index.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// src/mcp/tools/index.ts — barrel + TOOLS array
|
|
2
|
+
//
|
|
3
|
+
// Every schema here is paid as input tokens in *every* session of *every*
|
|
4
|
+
// client that connects, whether or not the tool is called. Adding one is
|
|
5
|
+
// buying attention with a standing charge; earning it back means the tool
|
|
6
|
+
// saves more than it costs (see CLAUDE.md "จ่าย token อย่างฉลาด"). Keep
|
|
7
|
+
// descriptions imperative — say what to send, not why it matters.
|
|
2
8
|
|
|
3
9
|
import { VERDICT_GRADES } from "../../parse.js";
|
|
4
10
|
import { REASON_CODES, REGIME_CODES } from "../types.js";
|
|
5
11
|
|
|
6
|
-
export {
|
|
7
|
-
export { toolMemFind } from "./mem.js";
|
|
8
|
-
export { toolPlanList } from "./plans.js";
|
|
9
|
-
export { toolFaponyStats } from "./stats.js";
|
|
12
|
+
export { toolMemAdd, toolMemFind } from "./mem.js";
|
|
10
13
|
export { toolPassiveUsage } from "./usage.js";
|
|
11
14
|
export { toolVerdictSubmit } from "./verdict.js";
|
|
12
15
|
|
|
@@ -16,24 +19,18 @@ export const TOOLS = [
|
|
|
16
19
|
{
|
|
17
20
|
name: "verdict_submit",
|
|
18
21
|
description:
|
|
19
|
-
"Grade a finished unit of work
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"Never leave a run open — one stuck at running/fixing " +
|
|
26
|
-
"absorbs later unrelated verdicts for that worktree. " +
|
|
27
|
-
"Without run_id, binds to the latest still-open run for the same " +
|
|
28
|
-
"worktree+plan (round keeps counting toward review.maxRounds); " +
|
|
29
|
-
"creates a new run entry only when none is open.",
|
|
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.",
|
|
30
28
|
inputSchema: {
|
|
31
29
|
type: "object" as const,
|
|
32
30
|
properties: {
|
|
33
31
|
run_id: {
|
|
34
32
|
type: "number",
|
|
35
|
-
description:
|
|
36
|
-
"Optional run ID from fapony. If omitted, a new run is created automatically.",
|
|
33
|
+
description: "Existing run to attach to. Omitted = bind or create.",
|
|
37
34
|
},
|
|
38
35
|
verdict: {
|
|
39
36
|
type: "string",
|
|
@@ -44,114 +41,141 @@ export const TOOLS = [
|
|
|
44
41
|
type: "string",
|
|
45
42
|
enum: [...REASON_CODES],
|
|
46
43
|
description:
|
|
47
|
-
"
|
|
44
|
+
"Failure reason. Clean passes take 'none', never 'other'.",
|
|
48
45
|
},
|
|
49
46
|
regime: {
|
|
50
47
|
type: "string",
|
|
51
48
|
enum: [...REGIME_CODES],
|
|
52
49
|
description:
|
|
53
|
-
"Task shape: code=
|
|
54
|
-
"review=reviewing
|
|
55
|
-
"inquiry=
|
|
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",
|
|
56
53
|
},
|
|
57
54
|
note: {
|
|
58
55
|
type: "string",
|
|
59
|
-
description:
|
|
56
|
+
description:
|
|
57
|
+
"Standalone note — read months later with no access to this " +
|
|
58
|
+
"conversation. Required when reason_code = 'other'.",
|
|
60
59
|
},
|
|
61
60
|
worktree: {
|
|
62
61
|
type: "string",
|
|
63
62
|
description:
|
|
64
|
-
"Absolute path
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
"Neither case errors.",
|
|
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.",
|
|
68
66
|
},
|
|
69
67
|
plan: {
|
|
70
68
|
type: "string",
|
|
71
|
-
description:
|
|
72
|
-
"Optional plan file path for a new run (used only when run_id is omitted)",
|
|
69
|
+
description: "Plan file path, for a new run",
|
|
73
70
|
},
|
|
74
71
|
session_id: {
|
|
75
72
|
type: "string",
|
|
76
73
|
description:
|
|
77
|
-
"
|
|
78
|
-
"OpenCode/ZCode: the session id
|
|
79
|
-
"
|
|
80
|
-
"guess. Send it whenever the client exposes it. Cannot find it → omit, never invent one.",
|
|
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.",
|
|
81
77
|
},
|
|
82
78
|
files: {
|
|
83
79
|
type: "array",
|
|
84
80
|
items: { type: "string" },
|
|
85
81
|
description:
|
|
86
|
-
"Repo-relative paths
|
|
87
|
-
"
|
|
88
|
-
"risk history — a verdict with no files[] teaches the next session " +
|
|
89
|
-
"nothing about where the risk was.",
|
|
82
|
+
"Repo-relative paths this unit touched. Optional, but always " +
|
|
83
|
+
"send them: nothing else records where the work landed.",
|
|
90
84
|
},
|
|
91
85
|
},
|
|
92
86
|
required: ["verdict", "reason_code", "regime"],
|
|
93
87
|
},
|
|
94
88
|
},
|
|
95
89
|
{
|
|
96
|
-
name: "
|
|
90
|
+
name: "mem_find",
|
|
97
91
|
description:
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
92
|
+
"Search the project's mem log (.fapony/.memory/log*.jsonl — decisions, " +
|
|
93
|
+
"bugs, notes, and bookkeeping kinds alike; NO default kind filter). " +
|
|
94
|
+
"Read-only. Answer 'what was ever decided about this file?' in one call " +
|
|
95
|
+
"BEFORE editing: pass files[] (repo-relative). Matches the row's stored " +
|
|
96
|
+
"files[], falling back to a substring of text/spec/ref for rows written " +
|
|
97
|
+
"without it — a row that names the file nowhere cannot be found. " +
|
|
98
|
+
"memDir shows which log dir was resolved (walked up from the given " +
|
|
99
|
+
"worktree — in a monorepo pass the app directory to read its log). " +
|
|
100
|
+
"Returns {rows, total, filesFound, " +
|
|
101
|
+
"skipped, memDir}: memDir:null = no mem at all, not 'nothing matched'.",
|
|
107
102
|
inputSchema: {
|
|
108
103
|
type: "object" as const,
|
|
109
104
|
properties: {
|
|
110
|
-
|
|
105
|
+
worktree: {
|
|
111
106
|
type: "string",
|
|
112
|
-
enum: ["verdict"],
|
|
113
107
|
description:
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"n=5 are shown separately and never picked as the answer; a model " +
|
|
117
|
-
"tried once at top quality does not get to define the frontier.",
|
|
108
|
+
"Absolute path (git rev-parse --show-toplevel) — required; " +
|
|
109
|
+
"scope of the mem log to read",
|
|
118
110
|
},
|
|
119
|
-
|
|
120
|
-
type: "
|
|
121
|
-
|
|
111
|
+
files: {
|
|
112
|
+
type: "array",
|
|
113
|
+
items: { type: "string" },
|
|
122
114
|
description:
|
|
123
|
-
"
|
|
124
|
-
"(dominated models + candidates too). Omitted = one summary line " +
|
|
125
|
-
"per regime.",
|
|
115
|
+
"Repo-relative paths — matched against the row's files[], else its text",
|
|
126
116
|
},
|
|
127
|
-
|
|
128
|
-
type: "
|
|
117
|
+
text: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "Substring, case-insensitive",
|
|
120
|
+
},
|
|
121
|
+
kind: {
|
|
122
|
+
type: "array",
|
|
123
|
+
items: { type: "string" },
|
|
129
124
|
description:
|
|
130
|
-
"
|
|
125
|
+
"Filter by kind (decision/note/bug/close/…). Omit = every kind — " +
|
|
126
|
+
"no default filter",
|
|
131
127
|
},
|
|
132
|
-
|
|
128
|
+
since: {
|
|
133
129
|
type: "string",
|
|
134
|
-
|
|
135
|
-
description:
|
|
136
|
-
"Optional grouping: top-N reason_code counts, per-plan totals, or " +
|
|
137
|
-
"per-file risk (graded touches vs fails) from real gate events.",
|
|
130
|
+
description: "ISO date — only rows at or after this time",
|
|
138
131
|
},
|
|
139
|
-
|
|
132
|
+
limit: {
|
|
140
133
|
type: "number",
|
|
141
|
-
description:
|
|
134
|
+
description:
|
|
135
|
+
"Max rows returned (default 20) — total still counts all matches",
|
|
142
136
|
},
|
|
137
|
+
},
|
|
138
|
+
required: ["worktree"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "mem_add",
|
|
143
|
+
description:
|
|
144
|
+
"Append a mem row (decision/bug/note/next/hold) with files[]. files is " +
|
|
145
|
+
"required — a row that does not name the file is unfindable when you " +
|
|
146
|
+
"touch that file. Returns the row's id, kind, files, and timestamp.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object" as const,
|
|
149
|
+
properties: {
|
|
143
150
|
worktree: {
|
|
144
151
|
type: "string",
|
|
145
152
|
description:
|
|
146
|
-
"
|
|
153
|
+
"Absolute path (git rev-parse --show-toplevel) — required",
|
|
147
154
|
},
|
|
148
|
-
|
|
149
|
-
type: "
|
|
155
|
+
kind: {
|
|
156
|
+
type: "string",
|
|
157
|
+
enum: ["next", "bug", "decision", "note", "hold"],
|
|
158
|
+
description:
|
|
159
|
+
"Row kind: decision=locked choice, bug=broken thing, note=state " +
|
|
160
|
+
"next session needs, next=pending work, hold=blocked on spec",
|
|
161
|
+
},
|
|
162
|
+
text: {
|
|
163
|
+
type: "string",
|
|
150
164
|
description:
|
|
151
|
-
"
|
|
165
|
+
"Standalone text — read months later, no conversation context",
|
|
166
|
+
},
|
|
167
|
+
files: {
|
|
168
|
+
type: "array",
|
|
169
|
+
items: { type: "string" },
|
|
170
|
+
description:
|
|
171
|
+
"Repo-relative paths this row is about — required, non-empty",
|
|
172
|
+
},
|
|
173
|
+
spec: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "Optional spec/plan .md path",
|
|
152
176
|
},
|
|
153
177
|
},
|
|
154
|
-
required: [],
|
|
178
|
+
required: ["worktree", "kind", "text", "files"],
|
|
155
179
|
},
|
|
156
180
|
},
|
|
157
181
|
{
|
|
@@ -194,116 +218,4 @@ export const TOOLS = [
|
|
|
194
218
|
required: [],
|
|
195
219
|
},
|
|
196
220
|
},
|
|
197
|
-
{
|
|
198
|
-
name: "project_health_context",
|
|
199
|
-
description:
|
|
200
|
-
"Known-patterns block for files[]: recurring fail reasons, escalated runs, " +
|
|
201
|
-
"and round-1-pass shapes from real run history. Worth a call when you are " +
|
|
202
|
-
"about to touch a file that has history — a long-lived file, one you have " +
|
|
203
|
-
"not seen before, or one a previous attempt already failed on. The unit is " +
|
|
204
|
-
"touched files, not a plan; a bug fix with no plan file still qualifies. " +
|
|
205
|
-
"Returns nothing when there is no history (most files); short plain-text " +
|
|
206
|
-
"block, framed as watch-fors, not constraints.",
|
|
207
|
-
inputSchema: {
|
|
208
|
-
type: "object" as const,
|
|
209
|
-
properties: {
|
|
210
|
-
worktree: {
|
|
211
|
-
type: "string",
|
|
212
|
-
description:
|
|
213
|
-
"Scope to one worktree path (absolute). Global across worktrees when omitted.",
|
|
214
|
-
},
|
|
215
|
-
files: {
|
|
216
|
-
type: "array",
|
|
217
|
-
items: { type: "string" },
|
|
218
|
-
description:
|
|
219
|
-
"Filter findings to only those related to these files. " +
|
|
220
|
-
"Matches against stored gate event files and note text.",
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
required: [],
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
name: "mem_find",
|
|
228
|
-
description:
|
|
229
|
-
"Search the project's mem log (.fapony/.memory/log*.jsonl — decisions, " +
|
|
230
|
-
"bugs, notes, and bookkeeping kinds alike; NO default kind filter). " +
|
|
231
|
-
"Read-only. Answer 'what was ever decided about this file?' in one call " +
|
|
232
|
-
"BEFORE editing: pass files[] (repo-relative) to match rows mentioning " +
|
|
233
|
-
"them. mem never stored files[], so match is substring over text/spec/ref " +
|
|
234
|
-
"— a row that never names the file cannot be found (limit of the data, " +
|
|
235
|
-
"not the query). In a monorepo only the log of the app guessed from the " +
|
|
236
|
-
"worktree name is read; memDir in the result shows which one. Returns " +
|
|
237
|
-
"{rows, total, filesFound, skipped, memDir}: total is the match count " +
|
|
238
|
-
"before limit, memDir:null means no mem at all (not 'nothing matched').",
|
|
239
|
-
inputSchema: {
|
|
240
|
-
type: "object" as const,
|
|
241
|
-
properties: {
|
|
242
|
-
worktree: {
|
|
243
|
-
type: "string",
|
|
244
|
-
description:
|
|
245
|
-
"Absolute path (git rev-parse --show-toplevel) — required; " +
|
|
246
|
-
"scope of the mem log to read",
|
|
247
|
-
},
|
|
248
|
-
files: {
|
|
249
|
-
type: "array",
|
|
250
|
-
items: { type: "string" },
|
|
251
|
-
description:
|
|
252
|
-
"Repo-relative paths — match rows whose text/spec/ref mentions them (substring)",
|
|
253
|
-
},
|
|
254
|
-
text: {
|
|
255
|
-
type: "string",
|
|
256
|
-
description: "Substring, case-insensitive",
|
|
257
|
-
},
|
|
258
|
-
kind: {
|
|
259
|
-
type: "array",
|
|
260
|
-
items: { type: "string" },
|
|
261
|
-
description:
|
|
262
|
-
"Filter by kind (decision/note/bug/close/…). Omit = every kind — " +
|
|
263
|
-
"no default filter",
|
|
264
|
-
},
|
|
265
|
-
since: {
|
|
266
|
-
type: "string",
|
|
267
|
-
description: "ISO date — only rows at or after this time",
|
|
268
|
-
},
|
|
269
|
-
limit: {
|
|
270
|
-
type: "number",
|
|
271
|
-
description:
|
|
272
|
-
"Max rows returned (default 20) — total still counts all matches",
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
required: ["worktree"],
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
name: "plan_list",
|
|
280
|
-
description:
|
|
281
|
-
"List pending plan files grouped by state — active / blocked / " +
|
|
282
|
-
"untouched / superseded / trackers — each with title and last-run " +
|
|
283
|
-
"status joined from fapony run history, plus a count of archived " +
|
|
284
|
-
"ones. Not a raw directory listing: answers 'what is left, what is " +
|
|
285
|
-
"waiting on what, what should come next'. State comes from optional " +
|
|
286
|
-
"plan frontmatter (kind: tracker | status: active|blocked|superseded " +
|
|
287
|
-
"| blocked_by: | blocks: | superseded_by:); a plan without " +
|
|
288
|
-
"frontmatter is grouped by run history alone (attempted = active, " +
|
|
289
|
-
"never attempted = untouched). Within active, whatever unblocks the " +
|
|
290
|
-
"most other plans is listed first.",
|
|
291
|
-
inputSchema: {
|
|
292
|
-
type: "object" as const,
|
|
293
|
-
properties: {
|
|
294
|
-
worktree: {
|
|
295
|
-
type: "string",
|
|
296
|
-
description: "Absolute path to the project's git worktree",
|
|
297
|
-
},
|
|
298
|
-
format: {
|
|
299
|
-
type: "string",
|
|
300
|
-
enum: ["json", "markdown"],
|
|
301
|
-
description:
|
|
302
|
-
"'markdown' renders the groups as a checklist to paste or read; " +
|
|
303
|
-
"default 'json'.",
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
required: ["worktree"],
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
221
|
];
|
package/src/mcp/tools/mem.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
// src/mcp/tools/mem.ts — mem_find
|
|
1
|
+
// src/mcp/tools/mem.ts — mem_find (read) + mem_add (write)
|
|
2
2
|
//
|
|
3
|
-
// "writable ≠ readable back" — the write side
|
|
4
|
-
//
|
|
5
|
-
// files
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// filtering `synced`/`next`/`claim` by default would be enforcing prose that
|
|
9
|
-
// was already retracted.
|
|
3
|
+
// "writable ≠ readable back" — the write side was CLI-only until now; what was
|
|
4
|
+
// missing is letting an agent append a mem row mid-session without spawning a
|
|
5
|
+
// shell. files[] is required on the write side: a row that does not name the
|
|
6
|
+
// file is unfindable when you touch that file (fill rate 26% CLI-flag vs 88%
|
|
7
|
+
// MCP-required — schema wins over prose every time, see CLAUDE.md rule 9).
|
|
10
8
|
|
|
9
|
+
import { openRows } from "../../mem/selectors.js";
|
|
10
|
+
import {
|
|
11
|
+
initStore,
|
|
12
|
+
KINDS,
|
|
13
|
+
nextId,
|
|
14
|
+
put,
|
|
15
|
+
rows,
|
|
16
|
+
type WorkKind,
|
|
17
|
+
} from "../../mem/store.js";
|
|
11
18
|
import { type MemRow, readMemLog, resolveMemDir } from "../../memory.js";
|
|
12
19
|
import { errorResult, jsonResult, type ToolResult } from "../types.js";
|
|
13
20
|
|
|
@@ -39,11 +46,15 @@ export function memFind(args: {
|
|
|
39
46
|
rows = rows.filter((r) => r.text.toLowerCase().includes(needle));
|
|
40
47
|
}
|
|
41
48
|
if (args.files && args.files.length > 0) {
|
|
42
|
-
// mem
|
|
43
|
-
//
|
|
44
|
-
//
|
|
49
|
+
// Rows written by `mem add --files` carry files[] — match that first.
|
|
50
|
+
// Older rows (and any row whose author skipped --files) have none, so the
|
|
51
|
+
// text/spec/ref substring stays as the fallback: low recall by nature,
|
|
52
|
+
// a limit of the data rather than of the query (spec §5.4).
|
|
45
53
|
const paths = args.files.map((f) => f.toLowerCase());
|
|
46
54
|
rows = rows.filter((r) => {
|
|
55
|
+
const stored = (r.files ?? []).map((f) => f.toLowerCase());
|
|
56
|
+
if (stored.some((f) => paths.some((p) => f === p || f.endsWith(`/${p}`))))
|
|
57
|
+
return true;
|
|
47
58
|
const hay = `${r.text}\n${r.spec ?? ""}\n${r.ref ?? ""}`.toLowerCase();
|
|
48
59
|
return paths.some((p) => hay.includes(p));
|
|
49
60
|
});
|
|
@@ -93,3 +104,134 @@ export function toolMemFind(args: Record<string, unknown>): ToolResult {
|
|
|
93
104
|
|
|
94
105
|
return jsonResult(memFind({ worktree, files, text, kind, since, limit }));
|
|
95
106
|
}
|
|
107
|
+
|
|
108
|
+
// --- mem_add ---
|
|
109
|
+
|
|
110
|
+
export interface MemAddResult {
|
|
111
|
+
id: string;
|
|
112
|
+
kind: string;
|
|
113
|
+
text: string;
|
|
114
|
+
files: string[];
|
|
115
|
+
spec?: string;
|
|
116
|
+
ts: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const CAP_NEXT = 15;
|
|
120
|
+
const CAP_HOLD = 10;
|
|
121
|
+
|
|
122
|
+
export function memAdd(args: {
|
|
123
|
+
worktree: string;
|
|
124
|
+
kind: string;
|
|
125
|
+
text: string;
|
|
126
|
+
files: string[];
|
|
127
|
+
spec?: string;
|
|
128
|
+
}): MemAddResult {
|
|
129
|
+
if (!KINDS.includes(args.kind as WorkKind)) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`kind must be one of ${KINDS.join("|")} — got "${args.kind}"`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (args.files.length === 0) {
|
|
135
|
+
throw new Error("files must contain at least one path");
|
|
136
|
+
}
|
|
137
|
+
if (!args.text.trim()) {
|
|
138
|
+
throw new Error("text is required and must not be empty");
|
|
139
|
+
}
|
|
140
|
+
// CLI cmdAdd rejects hold without a spec (write.ts) — keep the two writers
|
|
141
|
+
// in lockstep: without a spec a hold can never be resolved by rotate.
|
|
142
|
+
if (args.kind === "hold" && !args.spec) {
|
|
143
|
+
throw new Error("hold requires a spec — pass spec: <path/to/SPEC.md>");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
initStore(args.worktree);
|
|
147
|
+
const all = rows();
|
|
148
|
+
|
|
149
|
+
// Cap check — matches CLI cmdAdd behaviour
|
|
150
|
+
if (args.kind === "next" && !process.env.MEM_FORCE) {
|
|
151
|
+
const openNext = openRows(all).filter((r) => r.kind === "next").length;
|
|
152
|
+
if (openNext >= CAP_NEXT) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`open next ${openNext}/${CAP_NEXT} is full — close an old one first`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (args.kind === "hold" && !process.env.MEM_FORCE) {
|
|
159
|
+
const openHold = openRows(all).filter((r) => r.kind === "hold").length;
|
|
160
|
+
if (openHold >= CAP_HOLD) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`open hold ${openHold}/${CAP_HOLD} is full — close/release an old one first`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const id = nextId(all);
|
|
168
|
+
const ts = new Date().toISOString();
|
|
169
|
+
put({
|
|
170
|
+
id,
|
|
171
|
+
kind: args.kind as WorkKind,
|
|
172
|
+
text: args.text,
|
|
173
|
+
spec: args.spec,
|
|
174
|
+
files: args.files,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
id,
|
|
179
|
+
kind: args.kind,
|
|
180
|
+
text: args.text,
|
|
181
|
+
files: args.files,
|
|
182
|
+
spec: args.spec,
|
|
183
|
+
ts,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function toolMemAdd(args: Record<string, unknown>): ToolResult {
|
|
188
|
+
const worktree =
|
|
189
|
+
typeof args.worktree === "string" ? args.worktree.trim() : "";
|
|
190
|
+
if (!worktree) {
|
|
191
|
+
return errorResult(
|
|
192
|
+
"worktree is required and must be an absolute path " +
|
|
193
|
+
"(git rev-parse --show-toplevel)",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (!worktree.startsWith("/")) {
|
|
197
|
+
return errorResult(`worktree must be an absolute path, got: ${worktree}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const kind = typeof args.kind === "string" ? args.kind.trim() : "";
|
|
201
|
+
if (!kind) {
|
|
202
|
+
return errorResult(`kind is required — one of ${KINDS.join("|")}`);
|
|
203
|
+
}
|
|
204
|
+
if (!KINDS.includes(kind as WorkKind)) {
|
|
205
|
+
return errorResult(
|
|
206
|
+
`kind must be one of ${KINDS.join("|")} — got "${kind}"`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
211
|
+
if (!text) {
|
|
212
|
+
return errorResult("text is required and must not be empty");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const files = Array.isArray(args.files)
|
|
216
|
+
? args.files
|
|
217
|
+
.filter((f): f is string => typeof f === "string" && f.length > 0)
|
|
218
|
+
.map((f) => f.trim().replace(/^\.\//, ""))
|
|
219
|
+
: [];
|
|
220
|
+
if (files.length === 0) {
|
|
221
|
+
return errorResult(
|
|
222
|
+
"files is required and must contain at least one repo-relative path",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const spec =
|
|
227
|
+
typeof args.spec === "string" && args.spec.trim().endsWith(".md")
|
|
228
|
+
? args.spec.trim()
|
|
229
|
+
: undefined;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const result = memAdd({ worktree, kind, text, files, spec });
|
|
233
|
+
return jsonResult(result);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return errorResult(e instanceof Error ? e.message : String(e));
|
|
236
|
+
}
|
|
237
|
+
}
|
package/src/mcp/transport.ts
CHANGED
|
@@ -8,11 +8,9 @@ import { mergeBytesByTool, type UsageDetail } from "../session/index.js";
|
|
|
8
8
|
import { getServerSha } from "./primitives.js";
|
|
9
9
|
import {
|
|
10
10
|
TOOLS,
|
|
11
|
-
|
|
11
|
+
toolMemAdd,
|
|
12
12
|
toolMemFind,
|
|
13
13
|
toolPassiveUsage,
|
|
14
|
-
toolPlanList,
|
|
15
|
-
toolProjectHealthContext,
|
|
16
14
|
toolVerdictSubmit,
|
|
17
15
|
} from "./tools/index.js";
|
|
18
16
|
import { errorResult, type ToolResult } from "./types.js";
|
|
@@ -112,7 +110,7 @@ function writeStatuslineCache(toolResult: ToolResult): void {
|
|
|
112
110
|
|
|
113
111
|
const MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
114
112
|
const SERVER_NAME = "fapony-handcheck";
|
|
115
|
-
const SERVER_VERSION = "0.1
|
|
113
|
+
const SERVER_VERSION = "0.2.1";
|
|
116
114
|
|
|
117
115
|
// --- JSON-RPC dispatch ---
|
|
118
116
|
|
|
@@ -154,21 +152,15 @@ function dispatchToolCall(params: {
|
|
|
154
152
|
case "verdict_submit":
|
|
155
153
|
result = toolVerdictSubmit(args);
|
|
156
154
|
break;
|
|
157
|
-
case "fapony_stats":
|
|
158
|
-
result = toolFaponyStats(args);
|
|
159
|
-
break;
|
|
160
155
|
case "fapony_usage":
|
|
161
156
|
result = toolPassiveUsage(args);
|
|
162
157
|
break;
|
|
163
|
-
case "project_health_context":
|
|
164
|
-
result = toolProjectHealthContext(args);
|
|
165
|
-
break;
|
|
166
|
-
case "plan_list":
|
|
167
|
-
result = toolPlanList(args);
|
|
168
|
-
break;
|
|
169
158
|
case "mem_find":
|
|
170
159
|
result = toolMemFind(args);
|
|
171
160
|
break;
|
|
161
|
+
case "mem_add":
|
|
162
|
+
result = toolMemAdd(args);
|
|
163
|
+
break;
|
|
172
164
|
default:
|
|
173
165
|
return errorResult(`unknown tool: ${params.name}`);
|
|
174
166
|
}
|