fapony 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -412
- package/package.json +1 -1
- package/skill/lookup-before-edit/SKILL.md +2 -2
- package/skill/review-pony/SKILL.md +2 -2
- package/src/adapters/hooks/compute-hint-impact.ts +12 -1
- package/src/adapters/hooks/context-data.ts +31 -5
- package/src/adapters/hooks/edit-hint.ts +10 -0
- package/src/adapters/hooks/index.ts +15 -0
- package/src/adapters/hooks/read-hint.ts +10 -0
- package/src/adapters/hooks/stop.ts +347 -7
- package/src/adapters/mcp/tools/index.ts +13 -1
- package/src/adapters/mcp/tools/mem.ts +30 -5
- package/src/adapters/mcp/transport.ts +1 -1
- package/src/core/hint-log.ts +16 -1
- package/src/core/mem-log.ts +18 -0
- package/src/debt/cli.ts +25 -10
- package/src/hook.ts +15 -0
- package/src/install/antigravity.ts +18 -7
- package/src/install/detect.ts +8 -5
- package/src/install/opencode.ts +6 -0
- package/src/install.ts +6 -4
- package/src/mem/commands/plan.ts +70 -0
- package/src/mem/commands/read.ts +73 -18
- package/src/mem/commands/write.ts +75 -18
- package/src/mem/engine.ts +68 -2
- package/src/mem/index.ts +7 -5
- package/src/mem/render.ts +4 -1
- package/src/mem/selectors.ts +13 -0
- package/src/mem/store.ts +2 -0
- package/src/seed/plan-seed.ts +139 -10
- package/src/seed/review-seed.ts +141 -54
|
@@ -23,6 +23,8 @@ export interface MemFindResult {
|
|
|
23
23
|
filesFound: number;
|
|
24
24
|
skipped: number;
|
|
25
25
|
memDir: string | null;
|
|
26
|
+
/** Present only when a key query missed — every distinct key in the log. */
|
|
27
|
+
knownKeys?: string[];
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function memFind(args: {
|
|
@@ -33,6 +35,7 @@ export function memFind(args: {
|
|
|
33
35
|
since?: string;
|
|
34
36
|
limit?: number;
|
|
35
37
|
open?: boolean;
|
|
38
|
+
key?: string;
|
|
36
39
|
}): MemFindResult {
|
|
37
40
|
// Query logic lives in the shared engine (src/mem/engine.ts) — this wrapper
|
|
38
41
|
// owns only the read (readMemLog sees live + rotated archives via its loose
|
|
@@ -40,22 +43,24 @@ export function memFind(args: {
|
|
|
40
43
|
// omitting kind returns every kind (locked by test). CLI cmdFind calls the
|
|
41
44
|
// same engine with its own bookkeeping exclude.
|
|
42
45
|
const read = readMemLog(args.worktree);
|
|
43
|
-
const
|
|
46
|
+
const found = engineFind(read.rows, {
|
|
44
47
|
text: args.text,
|
|
45
48
|
files: args.files,
|
|
46
49
|
kind: args.kind,
|
|
47
50
|
sinceIso: args.since,
|
|
51
|
+
key: args.key,
|
|
48
52
|
limit: args.limit,
|
|
49
53
|
open: args.open,
|
|
50
54
|
});
|
|
51
55
|
return {
|
|
52
|
-
rows:
|
|
53
|
-
total,
|
|
56
|
+
rows: found.rows,
|
|
57
|
+
total: found.total,
|
|
54
58
|
filesFound: read.filesFound,
|
|
55
59
|
skipped: read.skipped,
|
|
56
60
|
// memDir separates "no mem at all" from "nothing matched" (spec §3) and
|
|
57
61
|
// makes the monorepo single-log limit visible (spec §5.1).
|
|
58
62
|
memDir: resolveMemDir(args.worktree),
|
|
63
|
+
...(found.knownKeys ? { knownKeys: found.knownKeys } : {}),
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -93,9 +98,18 @@ export function toolMemFind(args: Record<string, unknown>): ToolResult {
|
|
|
93
98
|
}
|
|
94
99
|
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
95
100
|
const open = typeof args.open === "boolean" ? args.open : undefined;
|
|
101
|
+
// Shape gate like mem_add — a non-string key must not coerce to undefined
|
|
102
|
+
// (silent drop = reject-without-saying). Pattern is NOT checked here: find
|
|
103
|
+
// answers a wrong-pattern key with knownKeys, not a reject (SPEC fail example).
|
|
104
|
+
if (args.key !== undefined && typeof args.key !== "string") {
|
|
105
|
+
return errorResult(
|
|
106
|
+
'key must be a string matching [a-z0-9-]{3,40} — e.g. "fix-stop-dedupe"',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const key = typeof args.key === "string" ? args.key : undefined;
|
|
96
110
|
|
|
97
111
|
return jsonResult(
|
|
98
|
-
memFind({ worktree, files, text, kind, since, limit, open }),
|
|
112
|
+
memFind({ worktree, files, text, kind, since, limit, open, key }),
|
|
99
113
|
);
|
|
100
114
|
}
|
|
101
115
|
|
|
@@ -112,6 +126,7 @@ export function memAdd(args: {
|
|
|
112
126
|
text: string;
|
|
113
127
|
files: string[];
|
|
114
128
|
spec?: string;
|
|
129
|
+
key?: string;
|
|
115
130
|
}): MemAddResult {
|
|
116
131
|
initStore(args.worktree);
|
|
117
132
|
return engineAdd({
|
|
@@ -119,6 +134,7 @@ export function memAdd(args: {
|
|
|
119
134
|
text: args.text,
|
|
120
135
|
files: args.files,
|
|
121
136
|
spec: args.spec,
|
|
137
|
+
key: args.key,
|
|
122
138
|
});
|
|
123
139
|
}
|
|
124
140
|
|
|
@@ -166,8 +182,17 @@ export function toolMemAdd(args: Record<string, unknown>): ToolResult {
|
|
|
166
182
|
? args.spec.trim()
|
|
167
183
|
: undefined;
|
|
168
184
|
|
|
185
|
+
// Shape gate here, pattern check in engineAdd — a non-string key must not
|
|
186
|
+
// slip through as undefined (silent drop = reject-without-saying, SPEC §Validation).
|
|
187
|
+
if (args.key !== undefined && typeof args.key !== "string") {
|
|
188
|
+
return errorResult(
|
|
189
|
+
'key must be a string matching [a-z0-9-]{3,40} — e.g. "fix-stop-dedupe"',
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const key = typeof args.key === "string" ? args.key : undefined;
|
|
193
|
+
|
|
169
194
|
try {
|
|
170
|
-
const result = memAdd({ worktree, kind, text, files, spec });
|
|
195
|
+
const result = memAdd({ worktree, kind, text, files, spec, key });
|
|
171
196
|
return jsonResult(result);
|
|
172
197
|
} catch (e) {
|
|
173
198
|
return errorResult(e instanceof Error ? e.message : String(e));
|
|
@@ -19,7 +19,7 @@ import { errorResult, type ToolResult } from "./types.js";
|
|
|
19
19
|
|
|
20
20
|
const SERVER_INSTRUCTIONS = `fapony records decisions, bugs, and notes about this project so the next session (or the next agent) knows what happened and what to watch out for.
|
|
21
21
|
|
|
22
|
-
When you finish a unit of work, record a mem row: fapony mem add <decision|bug|note> "what happened" --files <files> <path/to/PLAN.md>. files[] is required — a row without it is unfindable when you touch that file next session.
|
|
22
|
+
When you finish a unit of work, record a mem row: fapony mem add <decision|bug|note> "what happened" --files <files> <path/to/PLAN.md>. files[] is required — a row without it is unfindable when you touch that file next session. Name a known problem with --key <a-z0-9-id> and recall every row for it via mem_find key.
|
|
23
23
|
|
|
24
24
|
worktree must be the absolute path (git rev-parse --show-toplevel): every query scopes by it, so a bare name or none files the row where nothing reads it, and nothing errors to say so. Write the note standalone — it is read months later with no access to this conversation.
|
|
25
25
|
|
package/src/core/hint-log.ts
CHANGED
|
@@ -30,7 +30,17 @@ export function hintLogPath(worktree: string): string {
|
|
|
30
30
|
export interface HintFireRow {
|
|
31
31
|
ts: string;
|
|
32
32
|
worktree: string;
|
|
33
|
-
surface:
|
|
33
|
+
surface:
|
|
34
|
+
| "read"
|
|
35
|
+
| "debt"
|
|
36
|
+
| "mem"
|
|
37
|
+
| "commit"
|
|
38
|
+
| "edit"
|
|
39
|
+
| "open-bug"
|
|
40
|
+
| "handoff-would-block"
|
|
41
|
+
| "handoff-pass"
|
|
42
|
+
| "commit-block"
|
|
43
|
+
| "bug-block";
|
|
34
44
|
file: string | null;
|
|
35
45
|
count: number;
|
|
36
46
|
ids?: string[];
|
|
@@ -62,6 +72,11 @@ export interface HintImpact {
|
|
|
62
72
|
mem: number;
|
|
63
73
|
commit: number;
|
|
64
74
|
edit: number;
|
|
75
|
+
"open-bug": number;
|
|
76
|
+
"handoff-would-block": number;
|
|
77
|
+
"handoff-pass": number;
|
|
78
|
+
"commit-block": number;
|
|
79
|
+
"bug-block": number;
|
|
65
80
|
};
|
|
66
81
|
debt: { shown: number; resolved: number; unknown: number };
|
|
67
82
|
window: string | null;
|
package/src/core/mem-log.ts
CHANGED
|
@@ -22,8 +22,22 @@ export interface MemRow {
|
|
|
22
22
|
ref?: string;
|
|
23
23
|
/** Files the row is about — written by `mem add --files` (PLAN-convention-debt chunk 3). */
|
|
24
24
|
files?: string[];
|
|
25
|
+
/** Problem identity, distinct from kind (action) and files[] (place) — written by `mem add --key` (PLAN-mem-keys chunk 1). */
|
|
26
|
+
key?: string;
|
|
27
|
+
/** Row schema version: 2 = may carry `key`; absent = v:1 legacy row, read as-is. */
|
|
28
|
+
v?: number;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
/** One mem-row text budget for any hint/seed surface that shows a row (was context-data's private const). */
|
|
32
|
+
export const MEM_TEXT_MAX = 120;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The only accepted shape of a mem row `key` — problem identity, not action
|
|
36
|
+
* (kind) and not place (files[]). Lives here in core so CLI and MCP validate
|
|
37
|
+
* through one regex (PLAN-unify-mem-engine: one engine, both surfaces).
|
|
38
|
+
*/
|
|
39
|
+
export const KEY_RE = /^[a-z0-9-]{3,40}$/;
|
|
40
|
+
|
|
27
41
|
interface RawMemRow {
|
|
28
42
|
ts?: string;
|
|
29
43
|
agent?: string;
|
|
@@ -33,6 +47,8 @@ interface RawMemRow {
|
|
|
33
47
|
id?: string;
|
|
34
48
|
ref?: string;
|
|
35
49
|
files?: unknown;
|
|
50
|
+
key?: unknown;
|
|
51
|
+
v?: unknown;
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
/**
|
|
@@ -319,6 +335,8 @@ export function readMemLog(
|
|
|
319
335
|
...(Array.isArray(parsed.files)
|
|
320
336
|
? { files: parsed.files.filter((f) => typeof f === "string") }
|
|
321
337
|
: {}),
|
|
338
|
+
...(typeof parsed.key === "string" ? { key: parsed.key } : {}),
|
|
339
|
+
...(typeof parsed.v === "number" ? { v: parsed.v } : {}),
|
|
322
340
|
});
|
|
323
341
|
}
|
|
324
342
|
}
|
package/src/debt/cli.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { ZONE_CAP } from "./types.js";
|
|
|
13
13
|
|
|
14
14
|
const USAGE = `usage: fapony debt [path] [options]
|
|
15
15
|
--files f1,f2 check specific files instead of scanning
|
|
16
|
-
--id
|
|
16
|
+
--id a,b show only these conventions
|
|
17
17
|
--where <path> narrow scope to files under this path
|
|
18
18
|
--all show all zones (default: cap at ${ZONE_CAP})
|
|
19
19
|
--json output raw JSON
|
|
@@ -68,7 +68,7 @@ export function cmdDebt(args: string[]): void {
|
|
|
68
68
|
let path: string | undefined;
|
|
69
69
|
let filesMode: string[] | null = null;
|
|
70
70
|
let json = false;
|
|
71
|
-
let
|
|
71
|
+
let filterIds: string[] = [];
|
|
72
72
|
let wherePath: string | undefined;
|
|
73
73
|
let showAll = false;
|
|
74
74
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -80,14 +80,17 @@ export function cmdDebt(args: string[]): void {
|
|
|
80
80
|
process.exit(1);
|
|
81
81
|
}
|
|
82
82
|
i++;
|
|
83
|
-
|
|
83
|
+
// Accumulate, never reassign: a repeated --files grows the set
|
|
84
|
+
// (PLAN-comma-x chunk 2 — last-wins was silent data loss).
|
|
85
|
+
const parts = v
|
|
84
86
|
.split(",")
|
|
85
87
|
.map((s) => s.trim())
|
|
86
88
|
.filter(Boolean);
|
|
87
|
-
if (
|
|
89
|
+
if (parts.length === 0) {
|
|
88
90
|
console.error(`fapony debt: --files needs at least one path\n${USAGE}`);
|
|
89
91
|
process.exit(1);
|
|
90
92
|
}
|
|
93
|
+
filesMode = [...(filesMode ?? []), ...parts];
|
|
91
94
|
} else if (a === "--id") {
|
|
92
95
|
const v = args[i + 1];
|
|
93
96
|
if (!v || v.startsWith("--")) {
|
|
@@ -95,7 +98,17 @@ export function cmdDebt(args: string[]): void {
|
|
|
95
98
|
process.exit(1);
|
|
96
99
|
}
|
|
97
100
|
i++;
|
|
98
|
-
|
|
101
|
+
// Comma list, same shape as --files: ids are slugs, an id can't hold
|
|
102
|
+
// a comma, so any comma splits (PLAN-comma-x). Repeats accumulate too.
|
|
103
|
+
const parts = v
|
|
104
|
+
.split(",")
|
|
105
|
+
.map((s) => s.trim())
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
if (parts.length === 0) {
|
|
108
|
+
console.error(`fapony debt: --id needs a convention id\n${USAGE}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
filterIds = [...filterIds, ...parts];
|
|
99
112
|
} else if (a === "--where") {
|
|
100
113
|
const v = args[i + 1];
|
|
101
114
|
if (!v || v.startsWith("--")) {
|
|
@@ -171,12 +184,14 @@ export function cmdDebt(args: string[]): void {
|
|
|
171
184
|
}
|
|
172
185
|
const report = debtScan(worktree, loaded);
|
|
173
186
|
|
|
174
|
-
// --id filter: keep only the named
|
|
175
|
-
if (
|
|
176
|
-
report.entries = report.entries.filter((e) =>
|
|
177
|
-
|
|
187
|
+
// --id filter: keep only the named conventions (comma list allowed)
|
|
188
|
+
if (filterIds.length > 0) {
|
|
189
|
+
report.entries = report.entries.filter((e) =>
|
|
190
|
+
filterIds.includes(e.conv.id),
|
|
191
|
+
);
|
|
192
|
+
report.declared = report.declared.filter((c) => filterIds.includes(c.id));
|
|
178
193
|
report.checkedCount = 0; // not relevant when filtering
|
|
179
|
-
report.dropped = report.dropped.filter((d) => d.id
|
|
194
|
+
report.dropped = report.dropped.filter((d) => filterIds.includes(d.id));
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
// --where filter: narrow file lists to paths under the given prefix
|
package/src/hook.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
export {
|
|
9
9
|
BUG_MARKERS,
|
|
10
|
+
bugDedupeKey,
|
|
10
11
|
bugSignalFromTranscript,
|
|
11
12
|
COMMIT_HINT_MIN_COMMITS,
|
|
12
13
|
type CommitHintInput,
|
|
@@ -19,12 +20,15 @@ export {
|
|
|
19
20
|
cmdHookStop,
|
|
20
21
|
commitHintFor,
|
|
21
22
|
computeHintImpact,
|
|
23
|
+
countTicks,
|
|
22
24
|
cursorTranscriptPath,
|
|
25
|
+
decideHandoff,
|
|
23
26
|
decideStop,
|
|
24
27
|
type EditHintInput,
|
|
25
28
|
type EditTrackRow,
|
|
26
29
|
editHintFor,
|
|
27
30
|
editTrackPath,
|
|
31
|
+
fileAtRevision,
|
|
28
32
|
GIT_AUTONOMY_COMMIT_POLICY,
|
|
29
33
|
GIT_AUTONOMY_PLUGIN_FILE,
|
|
30
34
|
GIT_AUTONOMY_PLUGIN_NAME,
|
|
@@ -35,15 +39,24 @@ export {
|
|
|
35
39
|
type GitAutonomyStatus,
|
|
36
40
|
type GitAutonomyToolRewrite,
|
|
37
41
|
gitAutonomyStatus,
|
|
42
|
+
type HandoffDecision,
|
|
43
|
+
type HandoffPlanFile,
|
|
38
44
|
type HintFireRow,
|
|
39
45
|
type HintImpact,
|
|
46
|
+
handoffBaseSha,
|
|
47
|
+
handoffBlockMessage,
|
|
48
|
+
handoffDedupeKey,
|
|
40
49
|
hasBugMarker,
|
|
50
|
+
hasHandoffLiteral,
|
|
41
51
|
hintLogDir,
|
|
42
52
|
hintLogPath,
|
|
43
53
|
hookTsMs,
|
|
44
54
|
isBugfixCommit,
|
|
45
55
|
isCodexPayload,
|
|
46
56
|
isCursorPayload,
|
|
57
|
+
isPlanPath,
|
|
58
|
+
memHasHandoffForPlan,
|
|
59
|
+
mergeStopReasons,
|
|
47
60
|
mvGuardDecision,
|
|
48
61
|
type NormalizedStopInput,
|
|
49
62
|
normalizeStopInput,
|
|
@@ -64,9 +77,11 @@ export {
|
|
|
64
77
|
SESSION_START_MAX_CHARS,
|
|
65
78
|
type StopClient,
|
|
66
79
|
sessionKey,
|
|
80
|
+
sessionPlanFiles,
|
|
67
81
|
sessionStartContext,
|
|
68
82
|
stopBlockedBefore,
|
|
69
83
|
stopBlockPath,
|
|
84
|
+
stopBlockSurface,
|
|
70
85
|
stopOutput,
|
|
71
86
|
utcStamp,
|
|
72
87
|
worktreeKey,
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
// Skills path). No hooks in the first phase — Antigravity's hook surface is
|
|
6
6
|
// still evolving.
|
|
7
7
|
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { defaultCheckCmd } from "../setup.js";
|
|
11
12
|
import { agentsSkillsDir, linkSkills, reportSkills } from "./skills.js";
|
|
12
13
|
import {
|
|
13
14
|
CURSOR_MCP_ENTRY,
|
|
@@ -49,14 +50,20 @@ export function cmdInstallAntigravity(
|
|
|
49
50
|
): void {
|
|
50
51
|
const exitFn = deps.exit ?? defaultExit;
|
|
51
52
|
const getHome = deps.homedir ?? homedir;
|
|
52
|
-
const
|
|
53
|
+
const checkCmd = deps.checkCmd ?? defaultCheckCmd;
|
|
54
|
+
let geminiDir = findGeminiDir(getHome);
|
|
53
55
|
|
|
54
56
|
if (!geminiDir) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
// Detect signal #2: the `agy` CLI on PATH — installed but never launched,
|
|
58
|
+
// so ~/.gemini doesn't exist yet; the write path below creates it.
|
|
59
|
+
if (!checkCmd("agy")) {
|
|
60
|
+
console.error(
|
|
61
|
+
`Antigravity not found — open Antigravity at least once to create ~/.gemini (or put the agy CLI on PATH)`,
|
|
62
|
+
);
|
|
63
|
+
exitFn(1);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
geminiDir = join(getHome(), ".gemini");
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
// --- 1. MCP server (mcpServers.fapony in ~/.gemini/config/mcp_config.json) ---
|
|
@@ -99,6 +106,10 @@ export function cmdInstallAntigravity(
|
|
|
99
106
|
`── dry-run: would ${mcpIsNew ? "create" : "write"} ${mcpPath}${mcpIsNew ? "" : ` (mcpServers.${MCP_KEY})`} ──`,
|
|
100
107
|
);
|
|
101
108
|
} else {
|
|
109
|
+
// Escape hatch (plan §5): ~/.gemini/config may not exist yet — either
|
|
110
|
+
// the app never got as far as its config dir, or agy-on-PATH created
|
|
111
|
+
// nothing. Recursive mkdir is idempotent and non-destructive.
|
|
112
|
+
mkdirSync(configDir, { recursive: true });
|
|
102
113
|
writeFileSync(mcpPath, `${JSON.stringify(after, null, 2)}\n`, "utf-8");
|
|
103
114
|
console.error(`✓ added mcpServers.${MCP_KEY} to ${mcpPath}`);
|
|
104
115
|
console.error(` restart Antigravity to load the MCP server`);
|
package/src/install/detect.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/install/detect.ts — detect which MCP clients are installed on this machine.
|
|
2
2
|
//
|
|
3
3
|
// Signal per client:
|
|
4
|
-
// antigravity = ~/.gemini exists (the app creates it on first run)
|
|
4
|
+
// antigravity = ~/.gemini exists (the app creates it on first run) OR `agy` on PATH
|
|
5
5
|
// claude = `command -v claude` (CLI on PATH)
|
|
6
6
|
// cursor = ~/.cursor exists (the app creates it on first run)
|
|
7
7
|
// opencode = ~/.config/opencode/{opencode.json,opencode.jsonc} exists
|
|
@@ -28,14 +28,15 @@ export interface DetectedClient {
|
|
|
28
28
|
* Returns one entry per platform, ordered: antigravity, claude, cursor, opencode, zcode, codex.
|
|
29
29
|
*
|
|
30
30
|
* Uses resolvers from each provider (file/dir-exists check) for
|
|
31
|
-
* antigravity/cursor/opencode/zcode/codex, and `command -v claude
|
|
32
|
-
* through injected deps for testability.
|
|
31
|
+
* antigravity/cursor/opencode/zcode/codex, and `command -v claude`/`command -v agy`
|
|
32
|
+
* for claude/antigravity — all through injected deps for testability.
|
|
33
33
|
*/
|
|
34
34
|
export function detectClients(deps: InstallDeps = {}): DetectedClient[] {
|
|
35
35
|
const getHome = deps.homedir ?? homedir;
|
|
36
36
|
const checkCmd = deps.checkCmd ?? defaultCheckCmd;
|
|
37
37
|
|
|
38
38
|
const claude = checkCmd("claude");
|
|
39
|
+
const agy = checkCmd("agy");
|
|
39
40
|
|
|
40
41
|
const geminiDir = findGeminiDir(getHome);
|
|
41
42
|
const cursorDir = findCursorDir(getHome);
|
|
@@ -46,10 +47,12 @@ export function detectClients(deps: InstallDeps = {}): DetectedClient[] {
|
|
|
46
47
|
return [
|
|
47
48
|
{
|
|
48
49
|
platform: "antigravity",
|
|
49
|
-
installed: geminiDir !== null,
|
|
50
|
+
installed: geminiDir !== null || agy,
|
|
50
51
|
why: geminiDir
|
|
51
52
|
? `dir at ${geminiDir}`
|
|
52
|
-
:
|
|
53
|
+
: agy
|
|
54
|
+
? "agy CLI on PATH"
|
|
55
|
+
: "no ~/.gemini directory (open Antigravity once)",
|
|
53
56
|
},
|
|
54
57
|
{
|
|
55
58
|
platform: "claude",
|
package/src/install/opencode.ts
CHANGED
|
@@ -271,6 +271,9 @@ export const FaponyReadHint = async ({ directory }) => {
|
|
|
271
271
|
if (ctx.memLines.length > 0) {
|
|
272
272
|
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "mem", file: rel, count: ctx.memLines.length });
|
|
273
273
|
}
|
|
274
|
+
if (ctx.openBugIds.length > 0) {
|
|
275
|
+
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "open-bug", file: rel, count: ctx.openBugIds.length, ids: ctx.openBugIds });
|
|
276
|
+
}
|
|
274
277
|
}
|
|
275
278
|
} catch {
|
|
276
279
|
// a hint must never break a read
|
|
@@ -412,6 +415,9 @@ export const FaponyEditHint = async ({ directory }) => {
|
|
|
412
415
|
if (ctx?.memLines?.length) {
|
|
413
416
|
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "mem", file: rel, count: ctx.memLines.length });
|
|
414
417
|
}
|
|
418
|
+
if (ctx?.openBugIds?.length) {
|
|
419
|
+
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "open-bug", file: rel, count: ctx.openBugIds.length, ids: ctx.openBugIds });
|
|
420
|
+
}
|
|
415
421
|
}
|
|
416
422
|
} catch {
|
|
417
423
|
// best-effort
|
package/src/install.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/install.ts — `fapony install --platform opencode|claude|cursor|zcode|codex` command.
|
|
1
|
+
// src/install.ts — `fapony install --platform antigravity|agy|opencode|claude|cursor|zcode|codex` command.
|
|
2
2
|
// opencode: adds mcp.fapony config to ~/.config/opencode/opencode.json or opencode.jsonc.
|
|
3
3
|
// claude: shells out to `claude mcp add` (never parses/writes ~/.claude.json directly).
|
|
4
4
|
// cursor: writes mcpServers.fapony to ~/.cursor/mcp.json directly and merges the
|
|
@@ -59,7 +59,9 @@ export async function cmdInstall(
|
|
|
59
59
|
args: string[],
|
|
60
60
|
deps: InstallDeps = {},
|
|
61
61
|
): Promise<void> {
|
|
62
|
-
const
|
|
62
|
+
const platformArg = args.find((a) => !a.startsWith("--"));
|
|
63
|
+
// `agy` = Antigravity's CLI name — alias, same provider.
|
|
64
|
+
const platform = platformArg === "agy" ? "antigravity" : platformArg;
|
|
63
65
|
const dryRun = args.includes("--dry-run");
|
|
64
66
|
const installAll = args.includes("--all");
|
|
65
67
|
// Opt-in only: the git-autonomy rewrite is an opinion (commit-as-you-go),
|
|
@@ -96,10 +98,10 @@ export async function cmdInstall(
|
|
|
96
98
|
}
|
|
97
99
|
if (platform !== undefined) {
|
|
98
100
|
console.error(
|
|
99
|
-
`usage: fapony install --platform antigravity|opencode|claude|cursor|zcode|codex [--dry-run] [--git-autonomy] [--plugins-only]`,
|
|
101
|
+
`usage: fapony install --platform antigravity|agy|opencode|claude|cursor|zcode|codex [--dry-run] [--git-autonomy] [--plugins-only]`,
|
|
100
102
|
);
|
|
101
103
|
console.error(
|
|
102
|
-
` supported platforms: antigravity, opencode, claude, cursor, zcode, codex`,
|
|
104
|
+
` supported platforms: antigravity (agy), opencode, claude, cursor, zcode, codex`,
|
|
103
105
|
);
|
|
104
106
|
(deps.exit ?? defaultExit)(1);
|
|
105
107
|
return;
|
package/src/mem/commands/plan.ts
CHANGED
|
@@ -190,6 +190,68 @@ export const collectDepIssues = (active: string[]): string[] => {
|
|
|
190
190
|
return issues;
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
+
// W1: status header says not-started but chunks are ticked.
|
|
194
|
+
// W2: all chunks ticked but header never marked shipped.
|
|
195
|
+
// WARN-level only — counted separately from issues, never changes exit code.
|
|
196
|
+
//
|
|
197
|
+
// Vocab measured from .fapony/{plan,done}/*.md (2026-09-23):
|
|
198
|
+
// in-progress 8 · done 4 · shipped/✅ shipped 6 · draft 1
|
|
199
|
+
// frontmatter status: shipped 8 · blocked 2
|
|
200
|
+
const NOT_STARTED =
|
|
201
|
+
/(?:^|\n)>\s*\*?\*?Status:?\*?\*?\s+.*(?:draft|drafted|not[\s-]started)/i;
|
|
202
|
+
const SHIPPED_RE =
|
|
203
|
+
/(?:^|\n)>\s*✅|(?:^|\n)>\s*\*?\*?Status:?\*?\*?\s+.*(?:shipped|done)/i;
|
|
204
|
+
const FM_SHIPPED = /^status:\s*shipped\b/m;
|
|
205
|
+
|
|
206
|
+
export const collectDriftWarns = (active: string[]): string[] => {
|
|
207
|
+
const warns: string[] = [];
|
|
208
|
+
for (const f of active) {
|
|
209
|
+
const fm = parsePlanFrontmatter(f);
|
|
210
|
+
// Skip plans whose frontmatter already says shipped/blocked/superseded or
|
|
211
|
+
// is a tracker — these have their own handling elsewhere.
|
|
212
|
+
if (FM_SHIPPED.test("") && fm.status === "shipped") continue;
|
|
213
|
+
if (fm.status === "blocked" || fm.status === "superseded") continue;
|
|
214
|
+
if (fm.kind === "tracker") continue;
|
|
215
|
+
// frontmatter status: shipped = already done
|
|
216
|
+
if (fm.status === "shipped") continue;
|
|
217
|
+
|
|
218
|
+
const text = readFileSync(f, "utf8");
|
|
219
|
+
const relPath = relative(planBase, f);
|
|
220
|
+
const { checked, unchecked } = countFirstSection(f);
|
|
221
|
+
const total = checked + unchecked;
|
|
222
|
+
if (total === 0) continue;
|
|
223
|
+
|
|
224
|
+
// W1: header says not-started but chunks are ticked
|
|
225
|
+
const head = text.slice(0, 2048);
|
|
226
|
+
if (
|
|
227
|
+
checked > 0 &&
|
|
228
|
+
NOT_STARTED.test(head) &&
|
|
229
|
+
!SHIPPED_RE.test(head) &&
|
|
230
|
+
!FM_SHIPPED.test(text)
|
|
231
|
+
) {
|
|
232
|
+
// Extract the status value for the message
|
|
233
|
+
const statusMatch = />\s*\*?\*?Status:?\*?\*?\s+(.+)/i.exec(head);
|
|
234
|
+
const statusVal = statusMatch?.[1]?.replace(/\*\*/g, "").trim() ?? "?";
|
|
235
|
+
warns.push(
|
|
236
|
+
`${relPath} — status header says "${statusVal}" but ${checked} chunk(s) are ticked\n fix: update the header to 🚧 in-progress or ✅ shipped`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// W2: all chunks ticked but header never marked shipped
|
|
241
|
+
if (
|
|
242
|
+
checked > 0 &&
|
|
243
|
+
unchecked === 0 &&
|
|
244
|
+
!SHIPPED_RE.test(head) &&
|
|
245
|
+
!FM_SHIPPED.test(text)
|
|
246
|
+
) {
|
|
247
|
+
warns.push(
|
|
248
|
+
`${relPath} — all ${checked} chunk(s) ticked but header never marked shipped\n fix: add ✅ shipped to the header or run ${planSweepCmd} ${basename(f)} --apply`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return warns;
|
|
253
|
+
};
|
|
254
|
+
|
|
193
255
|
// status:blocked with every first-section chunk ticked = deferred doc debt:
|
|
194
256
|
// the work reads done but the plan stays in plan/ forever (shippedNotMoved
|
|
195
257
|
// never lists it — HELD excludes it). Trackers never finish, so they are out.
|
|
@@ -735,6 +797,10 @@ export const cmdPlanCheck = (a: string[]) => {
|
|
|
735
797
|
// (HELD excludes it), so without this flag it sits in plan/ silently.
|
|
736
798
|
for (const issue of collectBlockedTickedIssues(active)) issues.push(issue);
|
|
737
799
|
|
|
800
|
+
// 7) Drift warns — W1 (not-started header + ticks) and W2 (all ticked +
|
|
801
|
+
// not shipped). WARN-only: counted separately, never changes exit code.
|
|
802
|
+
const driftWarns = collectDriftWarns(active);
|
|
803
|
+
|
|
738
804
|
if (!quiet) {
|
|
739
805
|
console.log(
|
|
740
806
|
`closed chunks: ${closed} · citing a commit: ${citing} · verified: ${verified}`,
|
|
@@ -759,6 +825,10 @@ export const cmdPlanCheck = (a: string[]) => {
|
|
|
759
825
|
);
|
|
760
826
|
}
|
|
761
827
|
}
|
|
828
|
+
if (driftWarns.length > 0) {
|
|
829
|
+
console.log(`\n⚠ ${driftWarns.length} drift warning(s) (not blocking):`);
|
|
830
|
+
for (const w of driftWarns) console.log(`- ${w}`);
|
|
831
|
+
}
|
|
762
832
|
}
|
|
763
833
|
|
|
764
834
|
if (issues.length === 0) {
|