fapony 0.1.2 → 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 +94 -77
- package/fapony.ts +32 -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/package.json +5 -3
- 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
|
@@ -1,450 +0,0 @@
|
|
|
1
|
-
// commands/selftest.ts — fixture tests for selectors + plan-sweep link rewrite functions
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
existsSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
mkdtempSync,
|
|
7
|
-
readdirSync,
|
|
8
|
-
readFileSync,
|
|
9
|
-
rmSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
} from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { dirname, join, resolve } from "node:path";
|
|
14
|
-
import { claimsOf, openRows, rotateKeep } from "../selectors.js";
|
|
15
|
-
import type { LogRow } from "../store.js";
|
|
16
|
-
import {
|
|
17
|
-
countPlainTextMentions,
|
|
18
|
-
hasShippedHeader,
|
|
19
|
-
rewriteMarkdownLinks,
|
|
20
|
-
rewriteMovedFileLinks,
|
|
21
|
-
} from "./plan.js";
|
|
22
|
-
|
|
23
|
-
const assert = (cond: boolean, msg: string) => {
|
|
24
|
-
if (!cond) throw new Error(`ASSERT FAILED: ${msg}`);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// fixture helper — creates files in os.tmpdir(), auto-cleanup in finally
|
|
28
|
-
const makeFixture = (dir: string, name: string, body: string): string => {
|
|
29
|
-
const p = join(dir, name);
|
|
30
|
-
writeFileSync(p, body);
|
|
31
|
-
return p;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const runSelectorTests = () => {
|
|
35
|
-
const t: LogRow[] = [
|
|
36
|
-
{ id: "a1", kind: "next", text: "work A", ts: "", agent: "" },
|
|
37
|
-
{ id: "a2", kind: "next", text: "work B", ts: "", agent: "" },
|
|
38
|
-
{ kind: "close", ref: "a2", text: "done", ts: "", agent: "" },
|
|
39
|
-
{ kind: "claim", ref: "a1", ts: "2026-01-01T00:00:00Z", agent: "agent-1" },
|
|
40
|
-
// (1) single claim → active
|
|
41
|
-
{ id: "a3", kind: "bug", text: "work C", ts: "", agent: "" },
|
|
42
|
-
{ kind: "claim", ref: "a3", ts: "2026-01-01T01:00:00Z", agent: "agent-1" },
|
|
43
|
-
{
|
|
44
|
-
kind: "release",
|
|
45
|
-
ref: "a3",
|
|
46
|
-
text: "paused",
|
|
47
|
-
ts: "2026-01-01T02:00:00Z",
|
|
48
|
-
agent: "agent-1",
|
|
49
|
-
},
|
|
50
|
-
// (2) release → inactive
|
|
51
|
-
{ id: "a4", kind: "next", text: "work D", ts: "", agent: "" },
|
|
52
|
-
{ kind: "claim", ref: "a4", ts: "2026-01-01T03:00:00Z", agent: "agent-2" },
|
|
53
|
-
{ kind: "close", ref: "a4", text: "shipped", ts: "", agent: "" },
|
|
54
|
-
// (3) claim+close → inactive
|
|
55
|
-
{ id: "a5", kind: "next", text: "work E", ts: "", agent: "" },
|
|
56
|
-
{ kind: "claim", ref: "a5", ts: "2026-01-01T04:00:00Z", agent: "agent-1" },
|
|
57
|
-
{ kind: "claim", ref: "a5", ts: "2026-01-01T05:00:00Z", agent: "agent-3" },
|
|
58
|
-
// (4) double claim → the later one wins (agent-3)
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const claims = claimsOf(t);
|
|
62
|
-
const open = openRows(t);
|
|
63
|
-
|
|
64
|
-
// (1) a1 claim → active
|
|
65
|
-
assert(
|
|
66
|
-
claims.has("a1") && claims.get("a1")?.agent === "agent-1",
|
|
67
|
-
"claim active is wrong",
|
|
68
|
-
);
|
|
69
|
-
// (2) a3 release → inactive
|
|
70
|
-
assert(!claims.has("a3"), "release did not void the claim");
|
|
71
|
-
// (3) a4 close → inactive
|
|
72
|
-
assert(!claims.has("a4"), "close void claim");
|
|
73
|
-
// (4) a5 double claim → agent-3 wins
|
|
74
|
-
assert(
|
|
75
|
-
claims.has("a5") && claims.get("a5")?.agent === "agent-3",
|
|
76
|
-
"double claim after the winner",
|
|
77
|
-
);
|
|
78
|
-
// a2 closed → should not appear in open
|
|
79
|
-
assert(!open.some((r) => r.id === "a2"), "tombstone broken");
|
|
80
|
-
// open = a1, a3 (released but still open), a5
|
|
81
|
-
assert(
|
|
82
|
-
open
|
|
83
|
-
.map((r) => r.id)
|
|
84
|
-
.sort()
|
|
85
|
-
.join() === "a1,a3,a5",
|
|
86
|
-
"open rows are wrong",
|
|
87
|
-
);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const runRotateTests = () => {
|
|
91
|
-
const t: LogRow[] = [
|
|
92
|
-
{ id: "r1", kind: "next", text: "keep me", ts: "", agent: "" },
|
|
93
|
-
{ kind: "claim", ref: "r1", ts: "2026-01-01T00:00:00Z", agent: "agent-1" },
|
|
94
|
-
// r1: open + claimed → both the work row and the claim row must be kept
|
|
95
|
-
|
|
96
|
-
{ id: "r2", kind: "bug", text: "closed already", ts: "", agent: "" },
|
|
97
|
-
{ kind: "claim", ref: "r2", ts: "2026-01-01T00:00:00Z", agent: "agent-1" },
|
|
98
|
-
{ kind: "close", ref: "r2", text: "shipped", ts: "", agent: "" },
|
|
99
|
-
// r2: closed → work row, claim row, close tombstone all dropped
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
id: "r3",
|
|
103
|
-
kind: "decision",
|
|
104
|
-
text: "resolved decision",
|
|
105
|
-
ts: "2026-01-01T00:00:00Z",
|
|
106
|
-
agent: "",
|
|
107
|
-
spec: "PLAN-a.md",
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
kind: "synced",
|
|
111
|
-
spec: "PLAN-a.md",
|
|
112
|
-
ts: "2026-01-02T00:00:00Z",
|
|
113
|
-
agent: "",
|
|
114
|
-
},
|
|
115
|
-
// r3: spec synced *after* this decision → resolved, can be archived
|
|
116
|
-
|
|
117
|
-
{
|
|
118
|
-
id: "r4",
|
|
119
|
-
kind: "decision",
|
|
120
|
-
text: "still relevant decision",
|
|
121
|
-
ts: "2026-01-03T00:00:00Z",
|
|
122
|
-
agent: "",
|
|
123
|
-
spec: "PLAN-a.md",
|
|
124
|
-
},
|
|
125
|
-
// r4: decision newer than the latest synced → not resolved yet, keep it
|
|
126
|
-
|
|
127
|
-
{ id: "r5", kind: "note", text: "note no spec", ts: "", agent: "" },
|
|
128
|
-
// r5: a note with no spec → no way to know if resolved, always kept
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
const kept = rotateKeep(t);
|
|
132
|
-
assert(
|
|
133
|
-
kept.some((r) => "id" in r && r.id === "r1" && r.kind === "next"),
|
|
134
|
-
"rotateKeep: must keep an open work row",
|
|
135
|
-
);
|
|
136
|
-
assert(
|
|
137
|
-
kept.some((r) => r.kind === "claim" && "ref" in r && r.ref === "r1"),
|
|
138
|
-
"rotateKeep: must keep an active claim on an open row",
|
|
139
|
-
);
|
|
140
|
-
assert(
|
|
141
|
-
!kept.some((r) => "id" in r && r.id === "r2"),
|
|
142
|
-
"rotateKeep: must not keep a closed work row",
|
|
143
|
-
);
|
|
144
|
-
assert(
|
|
145
|
-
!kept.some((r) => r.kind === "claim" && "ref" in r && r.ref === "r2"),
|
|
146
|
-
"rotateKeep: must not keep a claim on a closed ref",
|
|
147
|
-
);
|
|
148
|
-
assert(
|
|
149
|
-
!kept.some((r) => r.kind === "close"),
|
|
150
|
-
"rotateKeep: must not keep a close tombstone",
|
|
151
|
-
);
|
|
152
|
-
assert(
|
|
153
|
-
!kept.some((r) => "id" in r && r.id === "r3"),
|
|
154
|
-
"rotateKeep: decision whose spec synced afterwards is resolved — drop it",
|
|
155
|
-
);
|
|
156
|
-
assert(
|
|
157
|
-
kept.some((r) => "id" in r && r.id === "r4"),
|
|
158
|
-
"rotateKeep: decision newer than the last sync is still relevant — keep it",
|
|
159
|
-
);
|
|
160
|
-
assert(
|
|
161
|
-
kept.some((r) => "id" in r && r.id === "r5"),
|
|
162
|
-
"rotateKeep: a note with no spec is always kept",
|
|
163
|
-
);
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const runPlanSweepTests = () => {
|
|
167
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "mem-test-"));
|
|
168
|
-
try {
|
|
169
|
-
// === Case A: rewriteMarkdownLinks + countPlainTextMentions ===
|
|
170
|
-
// build fixture: a file that references plan/PLAN-page-style.md several ways
|
|
171
|
-
const planDir = join(tmpDir, "plan");
|
|
172
|
-
const doneDir = join(tmpDir, "plan", "done");
|
|
173
|
-
mkdirSync(planDir, { recursive: true });
|
|
174
|
-
mkdirSync(doneDir, { recursive: true });
|
|
175
|
-
|
|
176
|
-
// "old" file — simulates apps/vela/plan/PLAN-page-style.md (touch only)
|
|
177
|
-
const oldFile = join(planDir, "PLAN-page-style.md");
|
|
178
|
-
writeFileSync(oldFile, "# PLAN-page-style\n");
|
|
179
|
-
|
|
180
|
-
// inbound file — simulates apps/vela/plan/PLAN-people-style.md
|
|
181
|
-
const inbound = [
|
|
182
|
-
"# PLAN-people-style — refs",
|
|
183
|
-
"",
|
|
184
|
-
"[txt](PLAN-page-style.md)", // markdown link → should be rebased
|
|
185
|
-
"look at PLAN-page-style.md for details", // plain text → detect only
|
|
186
|
-
"`PLAN-page-style.md` in backtick", // backtick → detect only
|
|
187
|
-
"```",
|
|
188
|
-
"PLAN-page-style.md", // code fence → detect only
|
|
189
|
-
"```",
|
|
190
|
-
"see PLAN-page-style §1.5 for tone", // bare (no .md) with section ref → detect only
|
|
191
|
-
].join("\n");
|
|
192
|
-
const inboundFile = join(planDir, "PLAN-people-style.md");
|
|
193
|
-
writeFileSync(inboundFile, inbound);
|
|
194
|
-
|
|
195
|
-
// A1: rewriteMarkdownLinks — rebase markdown link [txt](PLAN-page-style.md) → [txt](../done/PLAN-page-style.md)
|
|
196
|
-
const oldAbs = join(planDir, "PLAN-page-style.md");
|
|
197
|
-
const newAbs = join(doneDir, "PLAN-page-style.md");
|
|
198
|
-
const mdCount = rewriteMarkdownLinks(inboundFile, oldAbs, newAbs);
|
|
199
|
-
assert(
|
|
200
|
-
mdCount === 1,
|
|
201
|
-
`rewriteMarkdownLinks should fix 1 markdown link, got ${mdCount}`,
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
// A2: countPlainTextMentions — should detect all plain text variants (not markdown links)
|
|
205
|
-
const ptCount = countPlainTextMentions(inboundFile, "PLAN-page-style.md");
|
|
206
|
-
// after rewriteMarkdownLinks, the markdown link is now [txt](../done/...) — no longer a plain text match
|
|
207
|
-
// remaining: plain text (line 3), backtick (line 4), code fence (line 6), bare with § (line 8) = 4
|
|
208
|
-
assert(ptCount === 4, `countPlainTextMentions should be 4, got ${ptCount}`);
|
|
209
|
-
|
|
210
|
-
// A3: assert file content — markdown link was rebased, plain text untouched
|
|
211
|
-
const afterA = readFileSync(inboundFile, "utf8");
|
|
212
|
-
assert(
|
|
213
|
-
afterA.includes("[txt](done/PLAN-page-style.md)"),
|
|
214
|
-
"markdown link should be rebased to done/",
|
|
215
|
-
);
|
|
216
|
-
assert(
|
|
217
|
-
afterA.includes("look at PLAN-page-style.md for details"),
|
|
218
|
-
"plain text should remain unchanged",
|
|
219
|
-
);
|
|
220
|
-
assert(
|
|
221
|
-
afterA.includes("`PLAN-page-style.md` in backtick"),
|
|
222
|
-
"backtick should remain unchanged",
|
|
223
|
-
);
|
|
224
|
-
assert(
|
|
225
|
-
afterA.includes("see PLAN-page-style §1.5 for tone"),
|
|
226
|
-
"bare name with § should remain unchanged",
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
// === Case B: rewriteMovedFileLinks — file moved, target stayed in old dir ===
|
|
230
|
-
// Create sibling file in planDir (stayed behind when main file moved to doneDir)
|
|
231
|
-
makeFixture(planDir, "PLAN-nav.md", "# PLAN-nav\n");
|
|
232
|
-
const movedFile = join(doneDir, "PLAN-page-style.md");
|
|
233
|
-
writeFileSync(
|
|
234
|
-
movedFile,
|
|
235
|
-
[
|
|
236
|
-
"# PLAN-page-style",
|
|
237
|
-
"[sibling](PLAN-nav.md)", // sibling in old dir → rebase to ../
|
|
238
|
-
"[external](https://example.com)", // external → skip
|
|
239
|
-
].join("\n"),
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const movedCount = rewriteMovedFileLinks(movedFile, planDir, doneDir);
|
|
243
|
-
assert(
|
|
244
|
-
movedCount === 1,
|
|
245
|
-
`rewriteMovedFileLinks should fix 1 link, got ${movedCount}`,
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const afterB = readFileSync(movedFile, "utf8");
|
|
249
|
-
assert(
|
|
250
|
-
afterB.includes("[sibling](../PLAN-nav.md)"),
|
|
251
|
-
"sibling link should rebase to ../PLAN-nav.md (target stayed in plan/)",
|
|
252
|
-
);
|
|
253
|
-
assert(
|
|
254
|
-
afterB.includes("[external](https://example.com)"),
|
|
255
|
-
"external link should be untouched",
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
// === Case B2: rewriteMovedFileLinks — both source and target in same dir after move ===
|
|
259
|
-
// This is the actual bug: when both files move from plan/ to plan/done/, the outbound
|
|
260
|
-
// link should be same-dir relative (just the filename), not ../filename
|
|
261
|
-
makeFixture(doneDir, "PLAN-page-style.md", "# PLAN-page-style\n");
|
|
262
|
-
const movedFile2 = join(doneDir, "PLAN-people-style.md");
|
|
263
|
-
writeFileSync(
|
|
264
|
-
movedFile2,
|
|
265
|
-
[
|
|
266
|
-
"# PLAN-people-style",
|
|
267
|
-
"[page](PLAN-page-style.md)", // target also in done/ → same-dir relative
|
|
268
|
-
"[nav](PLAN-nav.md)", // target stayed in plan/ → ../
|
|
269
|
-
].join("\n"),
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
const movedCount2 = rewriteMovedFileLinks(movedFile2, planDir, doneDir);
|
|
273
|
-
assert(
|
|
274
|
-
movedCount2 === 2,
|
|
275
|
-
`rewriteMovedFileLinks B2 should fix 2 links, got ${movedCount2}`,
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
const afterB2 = readFileSync(movedFile2, "utf8");
|
|
279
|
-
assert(
|
|
280
|
-
afterB2.includes("[page](PLAN-page-style.md)"),
|
|
281
|
-
"B2: same-dir link should stay as filename (both in done/)",
|
|
282
|
-
);
|
|
283
|
-
assert(
|
|
284
|
-
afterB2.includes("[nav](../PLAN-nav.md)"),
|
|
285
|
-
"B2: cross-dir link should rebase to ../ (target stayed in plan/)",
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
// === Case C: countPlainTextMentions edge case — bare name (no .md) ===
|
|
289
|
-
const edgeFile = join(tmpDir, "edge.md");
|
|
290
|
-
writeFileSync(edgeFile, "see PLAN-page-style for details\n");
|
|
291
|
-
const edgeCount = countPlainTextMentions(edgeFile, "PLAN-page-style.md");
|
|
292
|
-
assert(
|
|
293
|
-
edgeCount === 1,
|
|
294
|
-
`bare PLAN-page-style (no .md) should count = 1, got ${edgeCount}`,
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
// edge: no match
|
|
298
|
-
const noMatchFile = join(tmpDir, "nomatch.md");
|
|
299
|
-
writeFileSync(noMatchFile, "nothing here about plans\n");
|
|
300
|
-
const noCount = countPlainTextMentions(noMatchFile, "PLAN-page-style.md");
|
|
301
|
-
assert(noCount === 0, `no match should return 0, got ${noCount}`);
|
|
302
|
-
|
|
303
|
-
// === Case D: regex fix — path-prefixed mentions (§1 root cause fix) ===
|
|
304
|
-
const d1 = join(tmpDir, "d1.md");
|
|
305
|
-
writeFileSync(d1, "`done/PLAN-page-style.md`");
|
|
306
|
-
assert(
|
|
307
|
-
countPlainTextMentions(d1, "PLAN-page-style.md") === 1,
|
|
308
|
-
"D1: backtick with path prefix should detect (was 0 before fix)",
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
const d2 = join(tmpDir, "d2.md");
|
|
312
|
-
writeFileSync(d2, "apps/vela/plan/PLAN-page-style.md");
|
|
313
|
-
assert(
|
|
314
|
-
countPlainTextMentions(d2, "PLAN-page-style.md") === 1,
|
|
315
|
-
"D2: plain text with path prefix should detect (was 0 before fix)",
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const d3 = join(tmpDir, "d3.md");
|
|
319
|
-
writeFileSync(d3, "[x](PLAN-page-style.md)");
|
|
320
|
-
assert(
|
|
321
|
-
countPlainTextMentions(d3, "PLAN-page-style.md") === 0,
|
|
322
|
-
"D3: markdown link should not double-count",
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
const d4 = join(tmpDir, "d4.md");
|
|
326
|
-
writeFileSync(d4, "done/PLAN-page-style.md and `PLAN-page-style.md`");
|
|
327
|
-
assert(
|
|
328
|
-
countPlainTextMentions(d4, "PLAN-page-style.md") === 2,
|
|
329
|
-
"D4: multiple mentions with path prefix should count all",
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// D5: regression guard — markdown link whose display text equals the target filename
|
|
333
|
-
// (actual repo convention, e.g. [PLAN-page-style.md](../done/PLAN-page-style.md)) must NOT
|
|
334
|
-
// be double-counted as a plain-text mention — first attempt at this fix only stripped the
|
|
335
|
-
// `(target)` part and left `[display-text]` unguarded, miscounting every such link
|
|
336
|
-
const d5 = join(tmpDir, "d5.md");
|
|
337
|
-
writeFileSync(
|
|
338
|
-
d5,
|
|
339
|
-
"see [PLAN-page-style.md](../done/PLAN-page-style.md) §1.5.4 for tone",
|
|
340
|
-
);
|
|
341
|
-
assert(
|
|
342
|
-
countPlainTextMentions(d5, "PLAN-page-style.md") === 0,
|
|
343
|
-
"D5: markdown link with matching display text should not double-count",
|
|
344
|
-
);
|
|
345
|
-
} finally {
|
|
346
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const runPlanCheckTests = () => {
|
|
351
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "mem-pc-"));
|
|
352
|
-
try {
|
|
353
|
-
const planDir = join(tmpDir, "plan");
|
|
354
|
-
const doneDir = join(planDir, "done");
|
|
355
|
-
mkdirSync(doneDir, { recursive: true });
|
|
356
|
-
|
|
357
|
-
// E1: shipped-not-moved detection
|
|
358
|
-
writeFileSync(
|
|
359
|
-
join(planDir, "PLAN-shipped.md"),
|
|
360
|
-
"> ✅ **Scope:** test\n# shipped plan\n",
|
|
361
|
-
);
|
|
362
|
-
writeFileSync(join(planDir, "PLAN-active.md"), "# active plan\n");
|
|
363
|
-
|
|
364
|
-
// a plan in the current format: frontmatter + title come before the ✅ shipped header
|
|
365
|
-
writeFileSync(
|
|
366
|
-
join(planDir, "PLAN-frontmatter.md"),
|
|
367
|
-
"---\nkind: unit\n---\n\n# shipped with frontmatter\n\n> ✅ **shipped 2026-09-13** (abc1234)\n",
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
// shipped some chunks and waiting on externals — frontmatter says it is meant to stay, do not count as forgotten
|
|
371
|
-
writeFileSync(
|
|
372
|
-
join(planDir, "PLAN-blocked.md"),
|
|
373
|
-
"---\nstatus: blocked\nblocked_by: VPS#2\n---\n\n# partly shipped\n\n> ✅ **chunk 1 shipped 2026-09-13** (abc1234)\n",
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
const mdFilesLocal = (dir: string): string[] =>
|
|
377
|
-
existsSync(dir)
|
|
378
|
-
? readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
|
|
379
|
-
e.isDirectory()
|
|
380
|
-
? mdFilesLocal(join(dir, e.name))
|
|
381
|
-
: e.name.endsWith(".md")
|
|
382
|
-
? [join(dir, e.name)]
|
|
383
|
-
: [],
|
|
384
|
-
)
|
|
385
|
-
: [];
|
|
386
|
-
|
|
387
|
-
const activeFiles = mdFilesLocal(planDir).filter(
|
|
388
|
-
(f) => !f.includes("/done/"),
|
|
389
|
-
);
|
|
390
|
-
const shipped = activeFiles.filter(hasShippedHeader);
|
|
391
|
-
assert(
|
|
392
|
-
shipped.length === 2,
|
|
393
|
-
`E1: should find 2 shipped-not-moved, got ${shipped.length}`,
|
|
394
|
-
);
|
|
395
|
-
assert(
|
|
396
|
-
shipped.some((f) => f.includes("PLAN-shipped.md")),
|
|
397
|
-
"E1: should detect PLAN-shipped.md",
|
|
398
|
-
);
|
|
399
|
-
assert(
|
|
400
|
-
shipped.some((f) => f.includes("PLAN-frontmatter.md")),
|
|
401
|
-
"E1: header below frontmatter + title must still count as shipped",
|
|
402
|
-
);
|
|
403
|
-
assert(
|
|
404
|
-
!shipped.some((f) => f.includes("PLAN-blocked.md")),
|
|
405
|
-
"E1: status: blocked must beat the ✅ header — it is held on purpose, not forgotten",
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
// E2: broken link detection
|
|
409
|
-
writeFileSync(
|
|
410
|
-
join(planDir, "PLAN-broken.md"),
|
|
411
|
-
"# broken\n[ref](PLAN-notexist.md)\n",
|
|
412
|
-
);
|
|
413
|
-
const allFiles = mdFilesLocal(planDir);
|
|
414
|
-
const linkRe = /\]\(([^)]+)\)/g;
|
|
415
|
-
const broken: string[] = [];
|
|
416
|
-
for (const f of allFiles) {
|
|
417
|
-
const src = readFileSync(f, "utf8");
|
|
418
|
-
let m: RegExpExecArray | null;
|
|
419
|
-
while ((m = linkRe.exec(src)) !== null) {
|
|
420
|
-
const target = m[1];
|
|
421
|
-
if (!target || /^(https?:|mailto:|\/)/.test(target)) continue;
|
|
422
|
-
const [pathPart] = target.split("#");
|
|
423
|
-
if (!pathPart) continue;
|
|
424
|
-
const resolved = resolve(dirname(f), pathPart);
|
|
425
|
-
if (!resolved.startsWith(planDir)) continue;
|
|
426
|
-
if (!existsSync(resolved)) {
|
|
427
|
-
broken.push(`${f} → ${target}`);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
assert(
|
|
432
|
-
broken.length === 1,
|
|
433
|
-
`E2: should find 1 broken link, got ${broken.length}`,
|
|
434
|
-
);
|
|
435
|
-
assert(
|
|
436
|
-
broken[0]?.includes("PLAN-notexist.md") === true,
|
|
437
|
-
"E2: broken link should reference PLAN-notexist.md",
|
|
438
|
-
);
|
|
439
|
-
} finally {
|
|
440
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
export const cmdTest = () => {
|
|
445
|
-
runSelectorTests();
|
|
446
|
-
runRotateTests();
|
|
447
|
-
runPlanSweepTests();
|
|
448
|
-
runPlanCheckTests();
|
|
449
|
-
console.log("ok");
|
|
450
|
-
};
|
package/templates/mem/mem.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
// append-only memory log. Never edit old lines — close a task = close (tombstone), git = history
|
|
4
|
-
// the log always sits beside this file when placed by `fapony init` (<project>/.fapony/.memory/log.jsonl);
|
|
5
|
-
// the central copy in a monorepo (single code copy at the root) still splits logs per app: apps/<app>/.fapony/.memory/log.jsonl
|
|
6
|
-
// (a repo not yet migrated with an old log at apps/<app>/.memory/log.jsonl keeps reading that location)
|
|
7
|
-
//
|
|
8
|
-
// CLI entry point — all logic lives in:
|
|
9
|
-
// store.ts (types + config + rows/put)
|
|
10
|
-
// selectors.ts (openRows / claimsOf / staleReport)
|
|
11
|
-
// render.ts (fmtRow / fmtClose / printOpenRows / doneLines)
|
|
12
|
-
// commands/write.ts (add / close / claim / release / synced / hook)
|
|
13
|
-
// commands/read.ts (now / done / stale / find / kickoff)
|
|
14
|
-
// commands/selftest.ts (test)
|
|
15
|
-
|
|
16
|
-
import { cmdPlanCheck, cmdPlanSweep } from "./commands/plan.js";
|
|
17
|
-
import {
|
|
18
|
-
cmdDone,
|
|
19
|
-
cmdFind,
|
|
20
|
-
cmdKickoff,
|
|
21
|
-
cmdNow,
|
|
22
|
-
cmdStale,
|
|
23
|
-
} from "./commands/read.js";
|
|
24
|
-
import { cmdRotate } from "./commands/rotate.js";
|
|
25
|
-
import { cmdTest } from "./commands/selftest.js";
|
|
26
|
-
import {
|
|
27
|
-
cmdAdd,
|
|
28
|
-
cmdClaim,
|
|
29
|
-
cmdClose,
|
|
30
|
-
cmdHook,
|
|
31
|
-
cmdRelease,
|
|
32
|
-
cmdSynced,
|
|
33
|
-
} from "./commands/write.js";
|
|
34
|
-
|
|
35
|
-
const [cmd, ...a] = process.argv.slice(2);
|
|
36
|
-
|
|
37
|
-
if (cmd === "add") {
|
|
38
|
-
await cmdAdd(a);
|
|
39
|
-
} else if (cmd === "close") {
|
|
40
|
-
await cmdClose(a);
|
|
41
|
-
} else if (cmd === "claim") {
|
|
42
|
-
cmdClaim(a);
|
|
43
|
-
} else if (cmd === "release") {
|
|
44
|
-
await cmdRelease(a);
|
|
45
|
-
} else if (cmd === "synced") {
|
|
46
|
-
cmdSynced(a);
|
|
47
|
-
} else if (cmd === "hook") {
|
|
48
|
-
await cmdHook();
|
|
49
|
-
} else if (cmd === "done") {
|
|
50
|
-
cmdDone();
|
|
51
|
-
} else if (cmd === "stale") {
|
|
52
|
-
cmdStale();
|
|
53
|
-
} else if (cmd === "find") {
|
|
54
|
-
cmdFind(a);
|
|
55
|
-
} else if (cmd === "test") {
|
|
56
|
-
cmdTest();
|
|
57
|
-
} else if (cmd === "kickoff") {
|
|
58
|
-
cmdKickoff(a);
|
|
59
|
-
} else if (cmd === "plan-sweep") {
|
|
60
|
-
cmdPlanSweep(a);
|
|
61
|
-
} else if (cmd === "plan-check") {
|
|
62
|
-
cmdPlanCheck(a);
|
|
63
|
-
} else if (cmd === "rotate") {
|
|
64
|
-
cmdRotate(a);
|
|
65
|
-
} else {
|
|
66
|
-
// mem now (default) — next+bug+hold. decision/note is not pending work → search with find instead
|
|
67
|
-
cmdNow();
|
|
68
|
-
}
|