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.
- package/README.md +144 -156
- package/package.json +5 -6
- package/skill/define-convention/SKILL.md +3 -3
- package/skill/lookup-before-edit/SKILL.md +2 -1
- package/skill/move-to-done/SKILL.md +18 -22
- package/skill/plan-with-pony/SKILL.md +4 -4
- package/skill/review-pony/SKILL.md +10 -12
- package/src/adapters/cli.ts +11 -52
- package/src/adapters/hooks/index.ts +0 -67
- package/src/adapters/hooks/mv-guard.ts +2 -2
- package/src/analyze/discover.ts +1 -1
- package/src/analyze/index.ts +0 -1
- package/src/commands.ts +8 -39
- package/src/core/config.ts +0 -8
- package/src/core/fapony-dir.ts +71 -0
- package/src/core/hook-helpers.ts +2 -11
- package/src/debt/load.ts +3 -5
- package/src/debt/promotion.ts +2 -2
- package/src/debt/scan.ts +1 -1
- package/src/digest/collect.ts +6 -7
- package/src/fael.ts +139 -0
- package/src/hook.ts +1 -81
- package/src/init.ts +30 -90
- package/src/install/antigravity.ts +8 -99
- package/src/install/claude.ts +66 -200
- package/src/install/codex.ts +6 -170
- package/src/install/detect.ts +4 -14
- package/src/install/opencode.ts +50 -486
- package/src/install/types.ts +1 -39
- package/src/install/zcode.ts +5 -91
- package/src/install.ts +9 -35
- package/src/memory.ts +10 -69
- package/src/plan/index.ts +34 -0
- package/src/plan/next.ts +181 -0
- package/src/plan/store.ts +41 -0
- package/src/{mem/commands/plan.ts → plan/sweep.ts} +66 -46
- package/src/seed/plan-seed.ts +10 -9
- package/src/setup.ts +4 -22
- package/src/update.ts +1 -1
- package/templates/PLAN.md +1 -1
- package/src/adapters/hooks/bug-markers.ts +0 -60
- package/src/adapters/hooks/context-data.ts +0 -211
- package/src/adapters/hooks/edit-hint.ts +0 -215
- package/src/adapters/hooks/read-hint.ts +0 -400
- package/src/adapters/hooks/session-start.ts +0 -112
- package/src/adapters/hooks/stop.ts +0 -768
- package/src/adapters/mcp/tools/index.ts +0 -149
- package/src/adapters/mcp/tools/mem.ts +0 -258
- package/src/adapters/mcp/transport.ts +0 -147
- package/src/analyze/cache.ts +0 -162
- package/src/core/mem-log.ts +0 -394
- package/src/init-mem.ts +0 -143
- package/src/install/cursor.ts +0 -167
- package/src/install/utils.ts +0 -29
- package/src/mem/commands/read.ts +0 -792
- package/src/mem/commands/rotate.ts +0 -59
- package/src/mem/commands/where.ts +0 -56
- package/src/mem/commands/write.ts +0 -284
- package/src/mem/engine.ts +0 -329
- package/src/mem/index.ts +0 -201
- package/src/mem/key-registry.ts +0 -116
- package/src/mem/render.ts +0 -66
- package/src/mem/selectors.ts +0 -227
- 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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
77
|
+
1. Tick its checkbox + stamp the TL;DR, citing the commit sha
|
|
107
78
|
2. Commit — separate from other chunks
|
|
108
|
-
3. \`
|
|
109
|
-
—
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
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/
|
|
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
|
-
`\
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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("
|
|
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
|
|
307
|
-
: `Append the
|
|
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
|
-
//
|
|
4
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
`
|
|
27
|
+
`Antigravity not found — open Antigravity at least once to create ~/.gemini (or put the agy CLI on PATH)`,
|
|
91
28
|
);
|
|
92
|
-
|
|
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
|
-
|
|
124
|
-
reportSkills(results, skillsDir, dryRun);
|
|
33
|
+
reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
|
|
125
34
|
}
|