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.
Files changed (51) hide show
  1. package/README.md +111 -46
  2. package/fapony.ts +35 -2
  3. package/package.json +3 -2
  4. package/skill/move-to-done/SKILL.md +18 -28
  5. package/skill/plan-with-pony/SKILL.md +8 -3
  6. package/src/analyze.ts +175 -5
  7. package/src/conventions-seed.ts +3 -3
  8. package/src/db/defaults.ts +13 -5
  9. package/src/db/getters.ts +11 -9
  10. package/src/db/load.ts +2 -2
  11. package/src/db/store.ts +0 -75
  12. package/src/db/types.ts +2 -4
  13. package/src/debt.ts +176 -36
  14. package/src/digest/collect.ts +19 -2
  15. package/src/digest/text.ts +25 -0
  16. package/src/hook.ts +665 -24
  17. package/src/init-mem.ts +123 -37
  18. package/src/init.ts +57 -39
  19. package/src/install/claude.ts +44 -8
  20. package/src/install/codex.ts +105 -13
  21. package/src/install/opencode.ts +169 -5
  22. package/src/install.ts +2 -1
  23. package/src/lint-baseline.ts +3 -8
  24. package/src/mcp/evidence.ts +15 -3
  25. package/src/mcp/tools/index.ts +97 -185
  26. package/src/mcp/tools/mem.ts +153 -11
  27. package/src/mcp/transport.ts +5 -13
  28. package/src/mem/commands/read.ts +448 -0
  29. package/src/mem/commands/where.ts +56 -0
  30. package/{templates → src}/mem/commands/write.ts +12 -5
  31. package/src/mem/index.ts +144 -0
  32. package/src/mem/store.ts +350 -0
  33. package/src/memory.ts +238 -40
  34. package/src/plan-seed.ts +26 -6
  35. package/src/review-seed.ts +19 -0
  36. package/src/setup.ts +7 -8
  37. package/src/stats/data.ts +40 -104
  38. package/src/stats/format.ts +8 -9
  39. package/src/stats/index.ts +0 -1
  40. package/templates/PLAN.md +1 -0
  41. package/src/mcp/tools/context.ts +0 -66
  42. package/src/mcp/tools/plans.ts +0 -255
  43. package/src/mcp/tools/stats.ts +0 -96
  44. package/templates/mem/commands/read.ts +0 -194
  45. package/templates/mem/commands/selftest.ts +0 -450
  46. package/templates/mem/mem.ts +0 -68
  47. package/templates/mem/store.ts +0 -285
  48. /package/{templates → src}/mem/commands/plan.ts +0 -0
  49. /package/{templates → src}/mem/commands/rotate.ts +0 -0
  50. /package/{templates → src}/mem/render.ts +0 -0
  51. /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
- };
@@ -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
- }