fapony 0.6.2 → 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 (63) 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 -6
  15. package/src/core/fapony-dir.ts +71 -0
  16. package/src/core/hook-helpers.ts +1 -2
  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 -139
  43. package/src/adapters/hooks/edit-hint.ts +0 -215
  44. package/src/adapters/hooks/read-hint.ts +0 -410
  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 -252
  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 -375
  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 -738
  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 -273
  59. package/src/mem/engine.ts +0 -315
  60. package/src/mem/index.ts +0 -201
  61. package/src/mem/render.ts +0 -66
  62. package/src/mem/selectors.ts +0 -160
  63. package/src/mem/store.ts +0 -356
package/src/fael.ts ADDED
@@ -0,0 +1,139 @@
1
+ // src/fael.ts — read the repo's memory from fael (`fael find --json --all`).
2
+ //
3
+ // Memory moved to fael 2026-09-25; .fapony/.memory is frozen (imported into
4
+ // .fael/). fapony only reads, never writes, and speaks MemRow internally, so
5
+ // the fael row shape is translated here and nowhere else:
6
+ // by → agent · kind "issue" → "bug" · a close — inline `closed` (compacted
7
+ // rows) or a kindless row with `ref` (close file) — → a kind:"close" row
8
+ // with ref = the closed row's id (the old tombstone shape).
9
+ // Close-file rows reach here from fael 0.0.5 on; 0.0.4 printed only inline
10
+ // `closed`, so a native `fael close` read as open.
11
+
12
+ /** fapony's internal row shape — what every reader downstream consumes. */
13
+ export interface MemRow {
14
+ ts: string;
15
+ agent: string;
16
+ kind: string;
17
+ text: string;
18
+ spec?: string;
19
+ id?: string;
20
+ /** On a kind:"close" row: the id of the row it closes. */
21
+ ref?: string;
22
+ files?: string[];
23
+ key?: string;
24
+ }
25
+
26
+ interface FaelRow {
27
+ id: string;
28
+ ts: string;
29
+ by: string;
30
+ kind: string;
31
+ text: string;
32
+ files?: string[];
33
+ spec?: string;
34
+ key?: string;
35
+ ref?: string;
36
+ closed?: { id: string; ts: string; by: string; text: string };
37
+ }
38
+
39
+ export interface FaelRead {
40
+ rows: MemRow[];
41
+ /** false = fael missing or failed — "no memory", not "no match". */
42
+ ok: boolean;
43
+ skipped: number;
44
+ }
45
+
46
+ /** fael `--json` lines → MemRows (+ close rows), newest first, ts ≥ sinceIso. */
47
+ export function faelLinesToMemRows(
48
+ stdout: string,
49
+ sinceIso?: string,
50
+ ): { rows: MemRow[]; skipped: number } {
51
+ const rows: MemRow[] = [];
52
+ let skipped = 0;
53
+ for (const line of stdout.split("\n")) {
54
+ if (!line.trim()) continue;
55
+ let r: FaelRow;
56
+ try {
57
+ r = JSON.parse(line) as FaelRow;
58
+ } catch {
59
+ skipped++;
60
+ continue;
61
+ }
62
+ // Close row (docs/format.md: no kind, `ref` names the closed row).
63
+ if (!r.kind && r.ref) {
64
+ rows.push({
65
+ ts: r.ts,
66
+ agent: r.by,
67
+ kind: "close",
68
+ text: r.text,
69
+ id: r.id,
70
+ ref: r.ref,
71
+ });
72
+ continue;
73
+ }
74
+ rows.push({
75
+ ts: r.ts,
76
+ agent: r.by,
77
+ kind: r.kind === "issue" ? "bug" : r.kind,
78
+ text: r.text,
79
+ id: r.id,
80
+ ...(r.files ? { files: r.files } : {}),
81
+ ...(r.spec ? { spec: r.spec } : {}),
82
+ ...(r.key ? { key: r.key } : {}),
83
+ });
84
+ if (r.closed) {
85
+ rows.push({
86
+ ts: r.closed.ts,
87
+ agent: r.closed.by,
88
+ kind: "close",
89
+ text: r.closed.text,
90
+ id: r.closed.id,
91
+ ref: r.id,
92
+ });
93
+ }
94
+ }
95
+ const kept = sinceIso ? rows.filter((r) => r.ts >= sinceIso) : rows;
96
+ kept.sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0));
97
+ return { rows: kept, skipped };
98
+ }
99
+
100
+ /** Every row fael sees from `worktree` — or, with `openOnly`, only the rows
101
+ * fael itself counts as open (not closed, not superseded). Never throws. */
102
+ export function readFaelLog(
103
+ worktree: string,
104
+ sinceIso?: string,
105
+ openOnly = false,
106
+ ): FaelRead {
107
+ const argv = ["fael", "find", "--json", ...(openOnly ? [] : ["--all"])];
108
+ try {
109
+ const p = Bun.spawnSync(argv, {
110
+ cwd: worktree,
111
+ env: process.env, // resolve `fael` on the live PATH, not the startup one
112
+ stdout: "pipe",
113
+ stderr: "pipe",
114
+ });
115
+ if (p.exitCode !== 0) return { rows: [], ok: false, skipped: 0 };
116
+ return { ...faelLinesToMemRows(p.stdout.toString(), sinceIso), ok: true };
117
+ } catch {
118
+ // fael not on PATH
119
+ return { rows: [], ok: false, skipped: 0 };
120
+ }
121
+ }
122
+
123
+ /** Newest decisions, keyword hits first when any match. */
124
+ export function recentDecisions(
125
+ rows: MemRow[],
126
+ limit: number,
127
+ keywords: string[] = [],
128
+ ): MemRow[] {
129
+ const decisions = rows.filter((r) => r.kind === "decision");
130
+ const kws = keywords.map((k) => k.toLowerCase()).filter(Boolean);
131
+ if (kws.length > 0) {
132
+ const hits = decisions.filter((r) => {
133
+ const hay = `${r.text}\n${r.spec ?? ""}`.toLowerCase();
134
+ return kws.some((k) => hay.includes(k));
135
+ });
136
+ if (hits.length > 0) return hits.slice(0, limit);
137
+ }
138
+ return decisions.slice(0, limit);
139
+ }
package/src/hook.ts CHANGED
@@ -5,84 +5,4 @@
5
5
  // src/digest/collect.ts, and the generated OpenCode plugin all import
6
6
  // from this path.
7
7
 
8
- export {
9
- BUG_MARKERS,
10
- bugDedupeKey,
11
- bugSignalFromTranscript,
12
- COMMIT_HINT_MIN_COMMITS,
13
- type CommitHintInput,
14
- type ContextLineData,
15
- capContext,
16
- cmdHookEditHint,
17
- cmdHookMvGuard,
18
- cmdHookReadHint,
19
- cmdHookSessionStart,
20
- cmdHookStop,
21
- commitHintFor,
22
- computeHintImpact,
23
- countTicks,
24
- cursorTranscriptPath,
25
- decideHandoff,
26
- decideStop,
27
- type EditHintInput,
28
- type EditTrackRow,
29
- editHintFor,
30
- editTrackPath,
31
- fileAtRevision,
32
- GIT_AUTONOMY_COMMIT_POLICY,
33
- GIT_AUTONOMY_PLUGIN_FILE,
34
- GIT_AUTONOMY_PLUGIN_NAME,
35
- GIT_AUTONOMY_SYSTEM_REWRITES,
36
- GIT_AUTONOMY_TOOL_POLICY,
37
- GIT_AUTONOMY_TOOL_REWRITES,
38
- type GitAutonomyRewrite,
39
- type GitAutonomyStatus,
40
- type GitAutonomyToolRewrite,
41
- gitAutonomyStatus,
42
- type HandoffDecision,
43
- type HandoffPlanFile,
44
- type HintFireRow,
45
- type HintImpact,
46
- handoffBaseSha,
47
- handoffBlockMessage,
48
- handoffDedupeKey,
49
- hasBugMarker,
50
- hasHandoffLiteral,
51
- hintLogDir,
52
- hintLogPath,
53
- hookTsMs,
54
- isBugfixCommit,
55
- isCodexPayload,
56
- isCursorPayload,
57
- isPlanPath,
58
- memHasHandoffForPlan,
59
- mergeStopReasons,
60
- mvGuardDecision,
61
- type NormalizedStopInput,
62
- normalizeStopInput,
63
- type RawStopPayload,
64
- READ_HINT_MIN_BYTES,
65
- READ_HINT_MIN_LIMIT,
66
- type ReadHintInput,
67
- type ReadTrackRow,
68
- type RereadHintInput,
69
- readContextData,
70
- readContextLines,
71
- readHintFor,
72
- readTrackPath,
73
- recordHintFire,
74
- rereadHintFor,
75
- rewriteGitAutonomySystem,
76
- rewriteGitAutonomyTool,
77
- SESSION_START_MAX_CHARS,
78
- type StopClient,
79
- sessionKey,
80
- sessionPlanFiles,
81
- sessionStartContext,
82
- stopBlockedBefore,
83
- stopBlockPath,
84
- stopBlockSurface,
85
- stopOutput,
86
- utcStamp,
87
- worktreeKey,
88
- } from "./adapters/hooks/index.js";
8
+ export * from "./adapters/hooks/index.js";
package/src/init.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/init.ts — scaffold fapony project structure at a target path.
2
- // Creates .fapony/plan/, .fapony/done/, .fapony/spec/, .fapony/.memory/ (from template).
2
+ // Creates .fapony/plan/, .fapony/done/, .fapony/spec/ + evidence.json. Memory
3
+ // (decisions, bugs, notes) is fael's — `fael install`, not scaffolded here.
3
4
  // state.db stays in ~/.config/fapony/ by design (security boundary — see db.ts),
4
5
  // never inside the worktree where agents have full write access.
5
6
 
@@ -19,18 +20,18 @@ import {
19
20
  doneDir,
20
21
  evidenceFile,
21
22
  FAPONY_DIR,
22
- memoryDir,
23
23
  planDir,
24
24
  specDir,
25
25
  } from "./core/config.js";
26
26
  import { isAffirmative } from "./util.js";
27
27
 
28
- const FAPONY_README = `# .fapony/ — fapony project dir (plans, specs, memory)
28
+ const FAPONY_README = `# .fapony/ — fapony project dir (plans, specs, conventions)
29
29
  # plan/ holds live plans, done/ the shipped ones, spec/ every spec (specs are a
30
30
  # reference library — they are not archived). done/ sits beside plan/ rather
31
31
  # than inside it so archiving never changes a file's depth, and the relative
32
32
  # links inside it keep working.
33
- # memory lives in .fapony/.memory/, the evidence allowlist in .fapony/evidence.json.
33
+ # memory (decisions, bugs, notes) lives in fael; the evidence allowlist is
34
+ # .fapony/evidence.json.
34
35
  #
35
36
  # evidence.json SHOULD be committed — it is the shared allowlist that decides
36
37
  # which commands 'fapony report' may run, and the team must run the same
@@ -44,7 +45,7 @@ const FAPONY_README = `# .fapony/ — fapony project dir (plans, specs, memory)
44
45
  # running in this worktree cannot rewrite run state / audit trail.
45
46
  #
46
47
  # Ask your agent for the plan picture instead of listing these by hand:
47
- # run "fapony mem kickoff" — priority plans, the first unchecked chunk, open bugs
48
+ # run "fapony plan" — every active plan, its progress and next chunk
48
49
  `;
49
50
 
50
51
  // Static template — deliberately NOT derived from the repo (reading package.json
@@ -59,59 +60,30 @@ const EVIDENCE_JSON = `{
59
60
  }
60
61
  `;
61
62
 
62
- // Rules snippet for the user's own agent-rules file. Nothing writes
63
- // log.<person>.jsonl on its own — an agent does, because the rules file it already
64
- // reads says to. That file is the user's, so init asks before writing it (rule 6c)
65
- // and never writes it twice (RULES_MARKER). Not in SERVER_INSTRUCTIONS either: that
66
- // reaches every MCP session of every user, and most of them never ran `fapony init`
67
- // — it would tell them to run a command that does not exist.
68
- const RULES_MARKER = "## Memory: .fapony/.memory";
69
- const RULES_SNIPPET = () => `${RULES_MARKER}/log.<you>.jsonl (append-only)
63
+ // Rules snippet for the user's own agent-rules file: how to run a plan chunk
64
+ // by chunk. The file is the user's, so init asks before writing it (rule 6c)
65
+ // and never writes it twice (RULES_MARKER). Memory rules are fael's own skill.
66
+ const RULES_MARKER = "## Plans: .fapony/plan (fapony)";
67
+ const RULES_SNIPPET = () => `${RULES_MARKER}
70
68
 
71
- The log is this project's shared brain — it lives in git, so anyone who clones the
72
- repo gets every decision, bug and note with it. The filename comes from
73
- \`git config user.name\` — one file per person, and \`*.jsonl merge=union\` in
74
- .gitattributes keeps both sides when two people end up sharing a name anyway.
75
-
76
- Log as you work — do not wait to be asked. Nothing writes it for you. With the
77
- fapony MCP server connected, call mem_add / mem_find / mem_close directly;
78
- otherwise the CLI:
79
-
80
- fapony mem kickoff <plan.md> # start a session with this
81
- fapony mem add decision "what was locked, and why" --files src/x.ts
82
- fapony mem add bug "what is broken" --files src/x.ts
83
- fapony mem add note "state the next session needs" --files src/x.ts
84
- fapony mem close <id> "fixed in <sha>"
85
- fapony mem find "<text>"
86
-
87
- What goes in — would someone cloning this repo tomorrow need it, and can they not
88
- find it anywhere else?
89
- - bug — something broken, even when found mid-task on something else: add it now,
90
- not at the end. Once fixed, close it — only close closes a bug.
91
- - decision — something agreed or locked that git and the plan do not say, with why.
92
- - note — state the next session needs (where a chunk stopped, what is half-done).
93
- Before ending a turn that committed work: at least one row about it.
94
-
95
- Write each entry standalone — it is read months later with no chat to refer to.
96
- --files is required: rows that name no file cannot be recalled when that file is
97
- touched later (add refuses without it).
98
-
99
- ## Executing a plan chunk-by-chunk
69
+ Plans live in .fapony/plan/ (shipped ones in .fapony/done/). Memory — decisions,
70
+ bugs, notes — lives in fael (\`fael add\` / \`fael find\`), not in .fapony/.
100
71
 
101
72
  A long plan run in one unbroken session accumulates context with nothing to shrink
102
73
  it — token cost and coherence both degrade with session length, not with amount of
103
74
  work done. Cut at chunk boundaries instead:
104
75
 
105
76
  Finish a chunk, before starting the next:
106
- 1. Tick its checkbox + stamp the TL;DR in the plan file
77
+ 1. Tick its checkbox + stamp the TL;DR, citing the commit sha
107
78
  2. Commit — separate from other chunks
108
- 3. \`fapony mem add note "what the next chunk needs" --files f1,f2 <path/to/PLAN-x.md>\`
109
- — use the same plan path every time
79
+ 3. \`fael add note "what the next chunk needs" --files f1,f2,<path/to/PLAN-x.md>\`
80
+ — the plan path in --files is what finds the note again
110
81
  4. Stop. Do not continue to the next chunk in the same session unless told to.
111
82
 
112
- Next chunk, new session — open with \`fapony mem kickoff <path/to/PLAN-x.md>\` instead
113
- of carrying the old transcript forward. kickoff already filters to the rows for that plan,
114
- and takes just the filename (\`kickoff PLAN-x.md\`) when you do not want to type the path.`;
83
+ Next chunk, new session — open with \`fapony plan PLAN-x.md\` (unchecked chunks +
84
+ the notes left for it) instead of carrying the old transcript forward.
85
+ \`fapony plan\` alone lists every active plan; ship one with
86
+ \`fapony plan sweep PLAN-x.md --apply\` (moves it to done/, fixes the links).`;
115
87
 
116
88
  export function initProject(targetPath: string, config?: Config): void {
117
89
  // Create target root
@@ -127,22 +99,6 @@ export function initProject(targetPath: string, config?: Config): void {
127
99
  mkdirSync(faponyDir, { recursive: true });
128
100
  writeFileSync(join(faponyDir, "README"), FAPONY_README);
129
101
 
130
- // --- .gitattributes: union-merge the append-only logs ---
131
- //
132
- // Every row is one person's append, so both sides of a "conflict" are always
133
- // right. Without the line, two people whose git user.name collides (admin /
134
- // user / owner — what a fresh OS install offers) resolve a conflict by hand
135
- // on every pull. Appended, never rewritten: the file is the repo's, not ours.
136
- const attrs = join(targetPath, ".gitattributes");
137
- const attrsBody = existsSync(attrs) ? readFileSync(attrs, "utf-8") : "";
138
- if (!/^\s*\*\.jsonl\s+merge=union\s*$/m.test(attrsBody)) {
139
- appendFileSync(
140
- attrs,
141
- `${attrsBody && !attrsBody.endsWith("\n") ? "\n" : ""}# append-only memory logs: keep both sides, never hand-resolve\n*.jsonl merge=union\n`,
142
- );
143
- console.log(` + ${attrs} — *.jsonl merge=union`);
144
- }
145
-
146
102
  // --- evidence.json (verification_report allowlist — see src/mcp/evidence.ts) ---
147
103
  const evidencePath = join(targetPath, evidenceFile(config));
148
104
  if (existsSync(evidencePath)) {
@@ -151,7 +107,7 @@ export function initProject(targetPath: string, config?: Config): void {
151
107
  mkdirSync(dirname(evidencePath), { recursive: true });
152
108
  writeFileSync(evidencePath, EVIDENCE_JSON);
153
109
 
154
- // --- plan/ spec/ .memory/ — all under .fapony/ ---
110
+ // --- plan/ spec/ — all under .fapony/ ---
155
111
  const planDirAbs = join(targetPath, planDir());
156
112
  if (existsSync(planDirAbs)) {
157
113
  throw new Error(`${planDirAbs} already exists — not overwriting.`);
@@ -172,33 +128,17 @@ export function initProject(targetPath: string, config?: Config): void {
172
128
  }
173
129
  mkdirSync(specDirAbs, { recursive: true });
174
130
 
175
- // --- .fapony/.memory/ (empty dir — mem commands are built into fapony now) ---
176
- const memoryDirPath = join(targetPath, memoryDir(config));
177
- if (existsSync(memoryDirPath)) {
178
- console.log(` ${relative(targetPath, memoryDirPath)}/ — already exists`);
179
- } else {
180
- mkdirSync(memoryDirPath, { recursive: true });
181
- console.log(
182
- ` ${relative(targetPath, memoryDirPath)}/ — mem commands are built into fapony (fapony mem ...)`,
183
- );
184
- }
185
-
186
131
  console.log(`scaffolded ${targetPath}/`);
187
- console.log(
188
- ` .fapony/ — project dir (plans, specs, memory, evidence)`,
189
- );
132
+ console.log(` .fapony/ — project dir (plans, specs, evidence)`);
190
133
  console.log(` ${planDir()}/ — live plan files`);
191
134
  console.log(` ${doneDir(config)}/ — shipped plans (archive)`);
192
135
  console.log(` ${specDir()}/ — spec files`);
193
136
  console.log(
194
137
  ` ${evidenceFile(config)} — allowlist for 'fapony report' (edit the cmds!)`,
195
138
  );
196
- console.log(
197
- ` ${relative(targetPath, memoryDirPath)}/ — mem commands are built into fapony (fapony mem ...)`,
198
- );
199
139
  console.log(`\nNext: add "${targetPath}" to fapony.config.json worktrees`);
200
140
  console.log(
201
- `\nThe memory log only fills up if the rules your agent already reads tell it\nto write — these go into CLAUDE.md / AGENTS.md next:\n`,
141
+ `\nPlans are only run chunk by chunk if the rules your agent already reads say\nso — these go into CLAUDE.md / AGENTS.md next:\n`,
202
142
  );
203
143
  console.log(RULES_SNIPPET());
204
144
  }
@@ -235,7 +175,7 @@ export function rulesTargets(targetPath: string): {
235
175
  }
236
176
 
237
177
  /**
238
- * Write the memory rules into the agent-rules files. No file yet = AGENTS.md
178
+ * Write the plan rules into the agent-rules files. No file yet = AGENTS.md
239
179
  * (read by OpenCode/Codex/Cursor) + a CLAUDE.md that imports it, so the rules
240
180
  * exist once. Files that already carry the rules are left alone.
241
181
  */
@@ -244,7 +184,7 @@ export function writeRules(targetPath: string): void {
244
184
  if (create) {
245
185
  writeFileSync(join(targetPath, "AGENTS.md"), `${RULES_SNIPPET()}\n`);
246
186
  writeFileSync(join(targetPath, "CLAUDE.md"), "@AGENTS.md\n");
247
- console.log(" + AGENTS.md (memory rules) · CLAUDE.md → @AGENTS.md");
187
+ console.log(" + AGENTS.md (plan rules) · CLAUDE.md → @AGENTS.md");
248
188
  return;
249
189
  }
250
190
  for (const f of append) {
@@ -253,7 +193,7 @@ export function writeRules(targetPath: string): void {
253
193
  f,
254
194
  `${body.endsWith("\n") ? "\n" : "\n\n"}${RULES_SNIPPET()}\n`,
255
195
  );
256
- console.log(` appended memory rules to ${relative(targetPath, f)}`);
196
+ console.log(` appended plan rules to ${relative(targetPath, f)}`);
257
197
  }
258
198
  }
259
199
 
@@ -264,7 +204,7 @@ export async function cmdInit(args: string[]): Promise<void> {
264
204
  if (!targetPath) {
265
205
  console.error("usage: fapony init <path> [--rules] [--yes]");
266
206
  console.error(
267
- " --rules only write the memory rules into CLAUDE.md / AGENTS.md (repo already set up)",
207
+ " --rules only write the plan rules into CLAUDE.md / AGENTS.md (repo already set up)",
268
208
  );
269
209
  console.error(" --yes write them without asking");
270
210
  process.exit(1);
@@ -298,13 +238,13 @@ export async function cmdInit(args: string[]): Promise<void> {
298
238
 
299
239
  const { create, append } = rulesTargets(targetPath);
300
240
  if (!create && append.length === 0) {
301
- console.log(" memory rules already in the agent-rules file");
241
+ console.log(" plan rules already in the agent-rules file");
302
242
  return;
303
243
  }
304
244
  if (!yes) {
305
245
  const what = create
306
- ? "Create AGENTS.md with the memory-logging rules (+ CLAUDE.md → @AGENTS.md)"
307
- : `Append the memory-logging rules to ${append.map((f) => relative(targetPath, f)).join(" and ")}`;
246
+ ? "Create AGENTS.md with the plan rules (+ CLAUDE.md → @AGENTS.md)"
247
+ : `Append the plan rules to ${append.map((f) => relative(targetPath, f)).join(" and ")}`;
308
248
  const rl = createInterface({
309
249
  input: process.stdin,
310
250
  output: process.stdout,
@@ -1,21 +1,14 @@
1
1
  // src/install/antigravity.ts — Google Antigravity install provider
2
2
  //
3
- // Writes mcpServers.fapony to ~/.gemini/config/mcp_config.json (Antigravity's
4
- // global MCP config) and symlinks skills into ~/.agents/skills/ (Progressive
5
- // Skills path). No hooks in the first phase — Antigravity's hook surface is
6
- // still evolving.
3
+ // Symlinks skills into ~/.agents/skills/ (Progressive Skills path). Memory
4
+ // moved to fael.
7
5
 
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { existsSync } from "node:fs";
9
7
  import { homedir } from "node:os";
10
8
  import { join } from "node:path";
11
9
  import { defaultCheckCmd } from "../setup.js";
12
10
  import { agentsSkillsDir, linkSkills, reportSkills } from "./skills.js";
13
- import {
14
- CURSOR_MCP_ENTRY,
15
- defaultExit,
16
- type InstallDeps,
17
- MCP_KEY,
18
- } from "./types.js";
11
+ import { defaultExit, type InstallDeps } from "./types.js";
19
12
 
20
13
  /** ~/.gemini — created on first Antigravity launch. */
21
14
  export function findGeminiDir(getHome: () => string): string | null {
@@ -23,103 +16,19 @@ export function findGeminiDir(getHome: () => string): string | null {
23
16
  return existsSync(dir) ? dir : null;
24
17
  }
25
18
 
26
- function isFaponyEntry(entry: unknown): boolean {
27
- if (!entry || typeof entry !== "object") return false;
28
- const cfg = entry as Record<string, unknown>;
29
- return (
30
- cfg.command === CURSOR_MCP_ENTRY.command &&
31
- JSON.stringify(cfg.args) === JSON.stringify(CURSOR_MCP_ENTRY.args)
32
- );
33
- }
34
-
35
- function readJsonObject(path: string): Record<string, unknown> | null {
36
- try {
37
- const raw = readFileSync(path, "utf-8").trim();
38
- if (!raw) return {};
39
- const parsed: unknown = JSON.parse(raw);
40
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
41
- return null;
42
- }
43
- return parsed as Record<string, unknown>;
44
- } catch {
45
- return null;
46
- }
47
- }
48
-
49
19
  export function cmdInstallAntigravity(
50
20
  dryRun: boolean,
51
21
  deps: InstallDeps = {},
52
22
  ): void {
53
- const exitFn = deps.exit ?? defaultExit;
54
23
  const getHome = deps.homedir ?? homedir;
55
24
  const checkCmd = deps.checkCmd ?? defaultCheckCmd;
56
- let geminiDir = findGeminiDir(getHome);
57
-
58
- if (!geminiDir) {
59
- // Detect signal #2: the `agy` CLI on PATH — installed but never launched,
60
- // so ~/.gemini doesn't exist yet; the write path below creates it.
61
- if (!checkCmd("agy")) {
62
- console.error(
63
- `Antigravity not found — open Antigravity at least once to create ~/.gemini (or put the agy CLI on PATH)`,
64
- );
65
- exitFn(1);
66
- return;
67
- }
68
- geminiDir = join(getHome(), ".gemini");
69
- }
70
-
71
- // --- 1. MCP server (mcpServers.fapony in ~/.gemini/config/mcp_config.json) ---
72
- const configDir = join(geminiDir, "config");
73
- const mcpPath = join(configDir, "mcp_config.json");
74
- let mcp: Record<string, unknown> = {};
75
- let mcpIsNew = true;
76
- if (existsSync(mcpPath)) {
77
- const parsed = readJsonObject(mcpPath);
78
- if (!parsed) {
79
- console.error(`failed to parse ${mcpPath} — fix or remove it first`);
80
- exitFn(1);
81
- return;
82
- }
83
- mcp = parsed;
84
- mcpIsNew = false;
85
- }
86
- const servers = (mcp.mcpServers ?? {}) as Record<string, unknown>;
87
- const existing = servers[MCP_KEY];
88
- if (existing !== undefined && !isFaponyEntry(existing)) {
25
+ if (!findGeminiDir(getHome) && !checkCmd("agy")) {
89
26
  console.error(
90
- `an MCP server named "${MCP_KEY}" exists but points elsewhere — not overwriting.`,
27
+ `Antigravity not found — open Antigravity at least once to create ~/.gemini (or put the agy CLI on PATH)`,
91
28
  );
92
- console.error(` inspect ${mcpPath} and remove it first`);
93
- exitFn(1);
29
+ (deps.exit ?? defaultExit)(1);
94
30
  return;
95
31
  }
96
- if (existing !== undefined) {
97
- console.error(
98
- `✓ mcpServers.${MCP_KEY} already configured — no change needed`,
99
- );
100
- console.error(` (${mcpPath})`);
101
- } else {
102
- const after = {
103
- ...mcp,
104
- mcpServers: { ...servers, [MCP_KEY]: CURSOR_MCP_ENTRY },
105
- };
106
- if (dryRun) {
107
- console.error(
108
- `── dry-run: would ${mcpIsNew ? "create" : "write"} ${mcpPath}${mcpIsNew ? "" : ` (mcpServers.${MCP_KEY})`} ──`,
109
- );
110
- } else {
111
- // Escape hatch (plan §5): ~/.gemini/config may not exist yet — either
112
- // the app never got as far as its config dir, or agy-on-PATH created
113
- // nothing. Recursive mkdir is idempotent and non-destructive.
114
- mkdirSync(configDir, { recursive: true });
115
- writeFileSync(mcpPath, `${JSON.stringify(after, null, 2)}\n`, "utf-8");
116
- console.error(`✓ added mcpServers.${MCP_KEY} to ${mcpPath}`);
117
- console.error(` restart Antigravity to load the MCP server`);
118
- }
119
- }
120
-
121
- // --- 2. Skills (~/.agents/skills/) ---
122
32
  const skillsDir = agentsSkillsDir(getHome);
123
- const results = linkSkills(skillsDir, dryRun);
124
- reportSkills(results, skillsDir, dryRun);
33
+ reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
125
34
  }