fapony 0.7.0 → 0.7.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 (64) hide show
  1. package/README.md +144 -156
  2. package/package.json +5 -6
  3. package/skill/define-convention/SKILL.md +3 -3
  4. package/skill/lookup-before-edit/SKILL.md +2 -1
  5. package/skill/move-to-done/SKILL.md +18 -22
  6. package/skill/plan-with-pony/SKILL.md +4 -4
  7. package/skill/review-pony/SKILL.md +10 -12
  8. package/src/adapters/cli.ts +11 -52
  9. package/src/adapters/hooks/index.ts +0 -67
  10. package/src/adapters/hooks/mv-guard.ts +2 -2
  11. package/src/analyze/discover.ts +1 -1
  12. package/src/analyze/index.ts +0 -1
  13. package/src/commands.ts +8 -39
  14. package/src/core/config.ts +0 -8
  15. package/src/core/fapony-dir.ts +71 -0
  16. package/src/core/hook-helpers.ts +2 -11
  17. package/src/debt/load.ts +3 -5
  18. package/src/debt/promotion.ts +2 -2
  19. package/src/debt/scan.ts +1 -1
  20. package/src/digest/collect.ts +6 -7
  21. package/src/fael.ts +139 -0
  22. package/src/hook.ts +1 -81
  23. package/src/init.ts +30 -90
  24. package/src/install/antigravity.ts +8 -99
  25. package/src/install/claude.ts +66 -200
  26. package/src/install/codex.ts +6 -170
  27. package/src/install/detect.ts +4 -14
  28. package/src/install/opencode.ts +50 -486
  29. package/src/install/types.ts +1 -39
  30. package/src/install/zcode.ts +5 -91
  31. package/src/install.ts +9 -35
  32. package/src/memory.ts +10 -69
  33. package/src/plan/index.ts +34 -0
  34. package/src/plan/next.ts +181 -0
  35. package/src/plan/store.ts +41 -0
  36. package/src/{mem/commands/plan.ts → plan/sweep.ts} +66 -46
  37. package/src/seed/plan-seed.ts +10 -9
  38. package/src/setup.ts +4 -22
  39. package/src/update.ts +1 -1
  40. package/templates/PLAN.md +1 -1
  41. package/src/adapters/hooks/bug-markers.ts +0 -60
  42. package/src/adapters/hooks/context-data.ts +0 -211
  43. package/src/adapters/hooks/edit-hint.ts +0 -215
  44. package/src/adapters/hooks/read-hint.ts +0 -400
  45. package/src/adapters/hooks/session-start.ts +0 -112
  46. package/src/adapters/hooks/stop.ts +0 -768
  47. package/src/adapters/mcp/tools/index.ts +0 -149
  48. package/src/adapters/mcp/tools/mem.ts +0 -258
  49. package/src/adapters/mcp/transport.ts +0 -147
  50. package/src/analyze/cache.ts +0 -162
  51. package/src/core/mem-log.ts +0 -394
  52. package/src/init-mem.ts +0 -143
  53. package/src/install/cursor.ts +0 -167
  54. package/src/install/utils.ts +0 -29
  55. package/src/mem/commands/read.ts +0 -792
  56. package/src/mem/commands/rotate.ts +0 -59
  57. package/src/mem/commands/where.ts +0 -56
  58. package/src/mem/commands/write.ts +0 -284
  59. package/src/mem/engine.ts +0 -329
  60. package/src/mem/index.ts +0 -201
  61. package/src/mem/key-registry.ts +0 -116
  62. package/src/mem/render.ts +0 -66
  63. package/src/mem/selectors.ts +0 -227
  64. package/src/mem/store.ts +0 -356
@@ -1,59 +0,0 @@
1
- // commands/rotate.ts — compact log.jsonl once it grows past a row-count threshold
2
- // keep open work rows + active claims; the rest (close/release/synced of already-closed refs)
3
- // is git mv'd to a separate archive file (not deleted) — old history remains, searchable via git log/git show
4
-
5
- import { existsSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { rotateKeep } from "../selectors.js";
8
- import { appendRaw, dir, LOG, rows } from "../store.js";
9
-
10
- // ponytail: threshold = total row count, not just open — the fear is a bloated file / slow grep when many people use it at once
11
- // (the view being unreadable is a separate problem the CAP in write.ts already handles)
12
- // 3000 estimated from one solo week = ~2k rows — tune with MEM_ROTATE_THRESHOLD if the pace differs a lot from this
13
- export const THRESHOLD = Number(process.env.MEM_ROTATE_THRESHOLD) || 3000;
14
-
15
- export const cmdRotate = (a: string[]) => {
16
- const apply = a.includes("--apply");
17
- const all = rows();
18
- const over = all.length >= THRESHOLD;
19
-
20
- if (!apply) {
21
- console.log(
22
- `${all.length} rows (threshold ${THRESHOLD})${over ? " — over the threshold, run --apply" : " — under the threshold"}`,
23
- );
24
- return;
25
- }
26
- if (!over && !process.env.MEM_FORCE) {
27
- console.log(
28
- `${all.length}/${THRESHOLD} rows — under the threshold, no rotate needed (MEM_FORCE=1 to do it anyway)`,
29
- );
30
- return;
31
- }
32
-
33
- const keep = rotateKeep(all);
34
- const stamp = new Date().toISOString().slice(0, 10);
35
- const archived = join(dir, `log.${stamp}.jsonl`);
36
- if (existsSync(archived)) {
37
- console.error(
38
- `${archived} already exists (rotated today?) — remove or move it first to run again`,
39
- );
40
- process.exit(1);
41
- }
42
-
43
- // like plan-sweep: stage first to stop git mv failing silently if the file is untracked (exit 128)
44
- Bun.spawnSync(["git", "add", LOG]);
45
- const mv = Bun.spawnSync(["git", "mv", LOG, archived]);
46
- if (mv.exitCode !== 0) {
47
- console.error(
48
- `git mv failed (${mv.stderr.toString().trim()}) — rotate aborted`,
49
- );
50
- process.exit(1);
51
- }
52
-
53
- writeFileSync(LOG, "");
54
- for (const r of keep) appendRaw(LOG, r);
55
-
56
- console.log(
57
- `rotated: ${all.length} rows → archive ${archived.replace(`${dir}/`, "")} (git history intact), ${keep.length} row(s) left in log.jsonl (open + active claims)`,
58
- );
59
- };
@@ -1,56 +0,0 @@
1
- // src/mem/commands/where.ts — `fapony mem where`: show which mem dir is resolved and how
2
-
3
- import { whereMemDir } from "../../memory.js";
4
-
5
- const USAGE = "usage: fapony mem [--mem-dir <path>] where [--from <dir>]";
6
-
7
- export function cmdWhere(args: string[], memDir?: string): void {
8
- let fromDir: string | undefined;
9
-
10
- for (let i = 0; i < args.length; i++) {
11
- if (args[i] === "--from") {
12
- const v = args[i + 1];
13
- if (!v || v.startsWith("--")) {
14
- console.error(`fapony mem where: --from needs a value\n${USAGE}`);
15
- process.exit(1);
16
- }
17
- fromDir = v;
18
- i++;
19
- } else if (args[i] === "-h" || args[i] === "--help") {
20
- console.log(USAGE);
21
- return;
22
- }
23
- }
24
-
25
- const result = whereMemDir(fromDir, memDir);
26
-
27
- if (result.step === "ambiguous") {
28
- console.error(
29
- "fapony mem: two mem dirs at the same level, refusing to guess",
30
- );
31
- for (const c of result.candidates ?? []) console.error(` ${c}`);
32
- console.error("cd into one of them, set paths.memDir, or pass --mem-dir");
33
- process.exit(1);
34
- }
35
-
36
- if (!result.dir) {
37
- console.log(
38
- `fapony mem where — no .fapony/.memory/ found (step: ${result.step})`,
39
- );
40
- console.log(" Create one with: fapony init <path>");
41
- return;
42
- }
43
-
44
- const labels: Record<string, string> = {
45
- flag: "--mem-dir flag",
46
- config: "paths.memDir in fapony.config.json",
47
- "walk-up": "walked up from cwd",
48
- "repo-root": "git repo root fallback",
49
- ambiguous: "ambiguous",
50
- none: "not found",
51
- };
52
-
53
- console.log(`fapony mem where`);
54
- console.log(` dir: ${result.dir}`);
55
- console.log(` step: ${labels[result.step] ?? result.step}`);
56
- }
@@ -1,284 +0,0 @@
1
- // commands/write.ts — mutating commands: add, close, claim, release, synced, hook
2
-
3
- import { CapError, engineAdd, engineClose } from "../engine.js";
4
- import { loadKeyRegistry } from "../key-registry.js";
5
- import { claimsOf, openRows } from "../selectors.js";
6
- import type { WorkKind } from "../store.js";
7
- import { KINDS, memCmd, put, root, rows } from "../store.js";
8
-
9
- export const cmdAdd = async (a: string[]) => {
10
- // mem add <next|bug|decision|note|hold> "<text>" --files f1,f2 [path/to/SPEC.md]
11
- // mem add <kind> --stdin --files f1,f2 [spec.md] ← read text from stdin (avoids shell metachar issues)
12
- // ponytail: a wrong kind = that row silently vanishes from the view — better to die right here
13
- if (!KINDS.includes(a[0] as WorkKind)) {
14
- console.error(
15
- `kind must be one of ${KINDS.join("|")} — got "${a[0] ?? ""}"`,
16
- );
17
- process.exit(1);
18
- }
19
- // hold requires a spec — checked after parse (the spec may come before --files)
20
- // PLAN-convention-debt chunk 3: files[] is required — a measured optional field
21
- // had fill rate 0 (required+enum = 50/50) and recall on old logs without files[] caught only
22
- // 48.1% — fix the write side, not the read side: a row that does not name the file = unfindable when you touch that file
23
-
24
- // Positional reparse (PLAN-comma-x chunk 2): walk argv once, consume every
25
- // known flag occurrence, keep the rest as positional = text + optional
26
- // trailing spec.md. The old filter removed only the first --files value by
27
- // identity, so `--files a --files b` left the stray `b` in the row text
28
- // (live corruption, row muds6zg5) — and a text word equal to the files
29
- // value was eaten instead. Flag strip stays positional, never by value:
30
- // a key like "fix" may legitimately sit inside the text.
31
- const positional: string[] = [];
32
- const rawFiles: string[] = [];
33
- let filesValueMissing = false;
34
- let useStdin = false;
35
- let sawKey = false;
36
- let keyVal: string | undefined;
37
- const rest = a.slice(1);
38
- for (let i = 0; i < rest.length; i++) {
39
- const t = rest[i];
40
- if (t === "--stdin") {
41
- useStdin = true;
42
- continue;
43
- }
44
- if (t === "--files") {
45
- const v = rest[i + 1];
46
- if (v !== undefined && !v.startsWith("--")) {
47
- rawFiles.push(v);
48
- i++;
49
- } else {
50
- filesValueMissing = true; // dies in validation below
51
- }
52
- continue;
53
- }
54
- if (t === "--key" || t.startsWith("--key=")) {
55
- const eq = t.startsWith("--key=");
56
- const v = eq ? t.slice("--key=".length) : rest[i + 1];
57
- if (!sawKey) {
58
- sawKey = true;
59
- keyVal = v; // first occurrence wins — validated below
60
- }
61
- // Consume the value token on every occurrence so a stray repeat never
62
- // leaks into the text (same leak class as a repeated --files).
63
- if (!eq && v !== undefined && !v.startsWith("--")) i++;
64
- continue;
65
- }
66
- positional.push(t);
67
- }
68
-
69
- // Validation order unchanged from the old parser: files → key → hold → text.
70
- if (filesValueMissing || rawFiles.length === 0) {
71
- console.error(
72
- `--files is required — usage: ${memCmd} add ${a[0]} "<text>" --files path/to/file.ts[,more] [spec.md]`,
73
- );
74
- process.exit(1);
75
- }
76
- const files: string[] = [];
77
- for (const v of rawFiles) {
78
- const parts = v
79
- .split(",")
80
- .map((s) => s.trim().replace(/^\.\//, ""))
81
- .filter(Boolean);
82
- if (parts.length === 0) {
83
- console.error(
84
- `--files needs at least one path — usage: ${memCmd} add ${a[0]} "<text>" --files path/to/file.ts[,more]`,
85
- );
86
- process.exit(1);
87
- }
88
- files.push(...parts);
89
- }
90
- // --key is optional; pattern validation lives in engineAdd (one checker,
91
- // both surfaces — PLAN-mem-keys chunk 1). argv layer only extracts it.
92
- // Accepts `--key value` and `--key=value` alike (same as cmdFind) — an
93
- // exact-match lookup for "--key" would leave a `--key=x` token inside the
94
- // text and write a keyless row with exit 0 (silent drop, never allowed).
95
- if (sawKey && (!keyVal || keyVal.startsWith("--"))) {
96
- console.error(
97
- `--key needs a value — usage: ${memCmd} add ${a[0]} "<text>" --files f1,f2 --key fix-stop-dedupe`,
98
- );
99
- process.exit(1);
100
- }
101
- const spec = positional.at(-1)?.endsWith(".md")
102
- ? positional.pop()
103
- : undefined;
104
- if (a[0] === "hold" && !spec) {
105
- console.error(
106
- `hold requires a spec — usage: ${memCmd} add hold "..." --files f1,f2 <spec.md>`,
107
- );
108
- process.exit(1);
109
- }
110
- let text: string;
111
- if (useStdin) {
112
- text = (await Bun.stdin.text()).trim();
113
- if (!text) {
114
- console.error("stdin was empty — text is required");
115
- process.exit(1);
116
- }
117
- } else {
118
- // text is required
119
- if (positional.length === 0) {
120
- console.error(
121
- `text is required — usage: ${memCmd} add <kind> "<text>" --files f1,f2 [spec.md]`,
122
- );
123
- process.exit(1);
124
- }
125
- text = positional.join(" ");
126
- }
127
- // Domain rules (caps, id) live in the shared engine — this wrapper owns
128
- // only argv surface and the MEM_FORCE hint wording (PLAN-unify-mem-engine).
129
- // The registry lives beside the mem log; absent = free-form keys (never
130
- // reject). root is set by initStore at dispatch; cwd covers direct calls.
131
- try {
132
- const reg = loadKeyRegistry(root || process.cwd());
133
- const { id } = engineAdd({
134
- kind: a[0],
135
- text,
136
- spec,
137
- files,
138
- key: keyVal,
139
- knownDomains: reg.path ? reg.domains : null,
140
- });
141
- console.log(id);
142
- } catch (e) {
143
- if (e instanceof CapError) {
144
- const hint =
145
- e.cap === "next"
146
- ? " (or MEM_FORCE=1 if you really must)"
147
- : " (or MEM_FORCE=1)";
148
- console.error(`${e.message}${hint}`);
149
- } else {
150
- console.error(e instanceof Error ? e.message : String(e));
151
- }
152
- process.exit(1);
153
- }
154
- };
155
-
156
- export const cmdClose = async (a: string[]) => {
157
- // mem close <id> "<what was done / commit>" — the tombstone voids the claim by itself
158
- // mem close <id> --stdin ← read text from stdin
159
- if (!a[0]) {
160
- console.error(`id is required — usage: ${memCmd} close <id> "<text>"`);
161
- process.exit(1);
162
- }
163
- const rest = a.slice(1);
164
- const useStdin = rest.includes("--stdin");
165
- let text: string;
166
- if (useStdin) {
167
- text = (await Bun.stdin.text()).trim();
168
- } else {
169
- text = rest.join(" ");
170
- }
171
- // Tombstone rules (id must exist, text required) live in the shared engine —
172
- // note this tightens CLI: an empty-text close now errors instead of writing
173
- // a textless tombstone (intentional, PLAN-unify-mem-engine §4).
174
- try {
175
- engineClose({ id: a[0], text });
176
- } catch (e) {
177
- console.error(e instanceof Error ? e.message : String(e));
178
- process.exit(1);
179
- }
180
- };
181
-
182
- export const cmdClaim = (a: string[]) => {
183
- // mem claim <id> — id must be real, open, kind=next|bug, with no active claim pending
184
- if (!a[0]) {
185
- console.error(`id is required — usage: ${memCmd} claim <id>`);
186
- process.exit(1);
187
- }
188
- const all = rows();
189
- const open = openRows(all);
190
- const target = open.find((r) => r.id === a[0]);
191
- if (!target) {
192
- const exists = all.find((r) => "id" in r && r.id === a[0]);
193
- console.error(
194
- exists
195
- ? `id "${a[0]}" exists but is already closed (close tombstone)`
196
- : `no id "${a[0]}" in the log`,
197
- );
198
- process.exit(1);
199
- }
200
- if (target.kind !== "next" && target.kind !== "bug") {
201
- console.error(
202
- `only next/bug can be claimed — id "${a[0]}" is ${target.kind}`,
203
- );
204
- process.exit(1);
205
- }
206
- const claims = claimsOf(all);
207
- if (claims.has(a[0])) {
208
- const c = claims.get(a[0])!;
209
- console.error(
210
- `id "${a[0]}" is already claimed by ${c.agent} — release it first`,
211
- );
212
- process.exit(1);
213
- }
214
- put({ kind: "claim", ref: a[0] });
215
- console.log(`claimed ${a[0]}`);
216
- };
217
-
218
- export const cmdRelease = async (a: string[]) => {
219
- // mem release <id> "<reason>?"
220
- // mem release <id> --stdin ← read text from stdin
221
- if (!a[0]) {
222
- console.error(`id is required — usage: ${memCmd} release <id> [reason]`);
223
- process.exit(1);
224
- }
225
- const all = rows();
226
- const claims = claimsOf(all);
227
- if (!claims.has(a[0])) {
228
- console.error(`id "${a[0]}" has no active claim`);
229
- process.exit(1);
230
- }
231
- const rest = a.slice(1);
232
- const useStdin = rest.includes("--stdin");
233
- let text: string | undefined;
234
- if (useStdin) {
235
- const t = (await Bun.stdin.text()).trim();
236
- text = t || undefined;
237
- } else {
238
- text = rest.join(" ") || undefined;
239
- }
240
- put({ kind: "release", ref: a[0], text });
241
- console.log(`released ${a[0]}`);
242
- };
243
-
244
- export const cmdSynced = (a: string[]) => {
245
- // mem synced [path.md ...] — declare the spec now matches the log (none given = every spec the log mentions)
246
- const specs = a.length
247
- ? a
248
- : [
249
- ...new Set(
250
- rows()
251
- .filter((r) => r.kind === "synced" || ("spec" in r && r.spec))
252
- .map((r) => ("spec" in r ? r.spec : undefined))
253
- .filter(Boolean) as string[],
254
- ),
255
- ];
256
- for (const spec of specs) put({ kind: "synced", spec });
257
- console.log(`synced ${specs.length} spec`);
258
- };
259
-
260
- export const cmdHook = async () => {
261
- // PostToolUse: stdin = {tool_input:{file_path}} → editing this app's spec is done = mark synced automatically
262
- const j = (await Bun.stdin.json().catch(() => null)) as Record<
263
- string,
264
- unknown
265
- > | null;
266
- const f =
267
- j && typeof j === "object" && "tool_input" in j
268
- ? ((j as { tool_input?: { file_path?: string } }).tool_input?.file_path ??
269
- "")
270
- : "";
271
- const { planDir, doneDir } = await import("../store.js");
272
- const relF = f.startsWith(root) ? f.slice(root.length + 1) : f;
273
- // match PLAN*.md under this project's plan/ or done/
274
- const planRe = new RegExp(
275
- `^${planDir.replace(`${root}/`, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/.*PLAN.*\\.md$`,
276
- );
277
- const doneRe = new RegExp(
278
- `^${doneDir.replace(`${root}/`, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/.*PLAN.*\\.md$`,
279
- );
280
- if (planRe.test(relF) || doneRe.test(relF)) {
281
- put({ kind: "synced", spec: relF });
282
- console.log(`synced ${relF}`);
283
- }
284
- };