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.
- 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 -6
- package/src/core/fapony-dir.ts +71 -0
- package/src/core/hook-helpers.ts +1 -2
- 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 -139
- package/src/adapters/hooks/edit-hint.ts +0 -215
- package/src/adapters/hooks/read-hint.ts +0 -410
- 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 -252
- package/src/adapters/mcp/transport.ts +0 -147
- package/src/analyze/cache.ts +0 -162
- package/src/core/mem-log.ts +0 -375
- 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 -738
- package/src/mem/commands/rotate.ts +0 -59
- package/src/mem/commands/where.ts +0 -56
- package/src/mem/commands/write.ts +0 -273
- package/src/mem/engine.ts +0 -315
- package/src/mem/index.ts +0 -201
- package/src/mem/render.ts +0 -66
- package/src/mem/selectors.ts +0 -160
- package/src/mem/store.ts +0 -356
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: review-pony
|
|
3
|
-
description: Review a plan, PR, diff, or design doc as a verification rather than an opinion — scope first, walk the real path, break it on paper, cite everything. Takes optional effort (low|medium|high|max, widens the walk only, never skips a pass) and --fix (apply CONFIRMED blocker/major findings after the report). Records surviving findings to
|
|
3
|
+
description: Review a plan, PR, diff, or design doc as a verification rather than an opinion — scope first, walk the real path, break it on paper, cite everything. Takes optional effort (low|medium|high|max, widens the walk only, never skips a pass) and --fix (apply CONFIRMED blocker/major findings after the report). Records surviving findings to fael after the report. Trigger on /review-pony and proactively whenever the user asks to review, audit, scrutinize, sanity-check, or get a second opinion on a plan, PR, diff, design doc, or proposed code change.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Review Pony
|
|
@@ -84,7 +84,7 @@ Write down every place the walk surprises you. Surprises outrank style; chase th
|
|
|
84
84
|
- `medium` (default) — as written above: full path, callers, tests on the path.
|
|
85
85
|
- `high` — also second-degree callers, and read the tests that exercise them, not just the path.
|
|
86
86
|
- `max` — also grep every changed file for second-degree callers, and re-open
|
|
87
|
-
every `deferred` line from the last review of this scope, if
|
|
87
|
+
every `deferred` line from the last review of this scope, if fael has one (`fael find --files <scope>`).
|
|
88
88
|
|
|
89
89
|
Whatever level stopped you, say so in the one-line coverage note (see Report) — "walked to 1 hop"
|
|
90
90
|
is honest, "walked" alone at `low` is not.
|
|
@@ -164,21 +164,20 @@ Fixing changes what actually shipped, not what the review found — re-run pass
|
|
|
164
164
|
check on the new state before calling it done, but don't re-run the whole review. Record
|
|
165
165
|
what you found, not the post-fix state (the row's text can say the fix was applied).
|
|
166
166
|
|
|
167
|
-
## After: record what the next session needs (
|
|
167
|
+
## After: record what the next session needs (fael)
|
|
168
168
|
|
|
169
169
|
If a blocker/major CONFIRMED finding survived, or the verdict is rework/reject,
|
|
170
|
-
call `
|
|
170
|
+
call fael's `add` tool once (or `fael add` in the shell), after the report is shown. Don't block the report on it,
|
|
171
171
|
and don't let it change the report's content. Clean reviews (ship, nit-only
|
|
172
172
|
findings) record nothing — there is nothing the next session needs to find.
|
|
173
173
|
|
|
174
|
-
kind is `
|
|
174
|
+
kind is `issue` when a finding survived (something is broken), `decision` when
|
|
175
175
|
the verdict turns on scope alone (rework/reject from pass 1 — the review locks
|
|
176
176
|
a direction). text is the report's verdict line, standalone — what broke or was
|
|
177
177
|
decided, not that a review happened. files are the repo-relative paths actually
|
|
178
|
-
walked — required, a row without them is unfindable.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
If `mem_add` errors, say so in one line and move on — never re-run a review
|
|
178
|
+
walked — required, a row without them is unfindable. No fael in this session
|
|
179
|
+
(no tool, no `fael` on PATH) = skip the record, never install it uninvited.
|
|
180
|
+
If `add` errors, say so in one line and move on — never re-run a review
|
|
182
181
|
because storage failed.
|
|
183
182
|
|
|
184
183
|
---
|
|
@@ -201,10 +200,9 @@ The four passes are the rules. These three are what they fail on in practice:
|
|
|
201
200
|
```
|
|
202
201
|
1-4. scope holds; walked the new gate branch; ran the evidence command — it exits 0
|
|
203
202
|
without running the suite (CONFIRMED: `bun test` with no test dir exits 0)
|
|
204
|
-
post.
|
|
203
|
+
post. add(kind="issue",
|
|
205
204
|
text="incremental scan replaces cached history with a delta — cache holds 500, truth 1500",
|
|
206
|
-
files=["cache.ts", "claude-code.ts"]
|
|
207
|
-
worktree="/Users/you/Project/fapony/wt-fapony")
|
|
205
|
+
files=["cache.ts", "claude-code.ts"])
|
|
208
206
|
```
|
|
209
207
|
|
|
210
208
|
A report in budget — same review that, narrated, ran five paragraphs:
|
package/src/adapters/cli.ts
CHANGED
|
@@ -4,17 +4,14 @@
|
|
|
4
4
|
// (fapony.ts) calls cliMain(); this file owns all feature imports and the
|
|
5
5
|
// argv routing.
|
|
6
6
|
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
7
|
import { cmdAnalyze } from "../analyze/index.js";
|
|
9
8
|
import { renderUsage, suggestCommand } from "../commands.js";
|
|
10
9
|
import { cmdDebt } from "../debt/cli.js";
|
|
11
10
|
import { cmdDigest } from "../digest/cli.js";
|
|
12
11
|
import { cmdInit } from "../init.js";
|
|
13
|
-
import { cmdInitMem } from "../init-mem.js";
|
|
14
12
|
import { cmdInstall } from "../install.js";
|
|
15
13
|
import { cmdLintBaseline } from "../lint-baseline.js";
|
|
16
|
-
import {
|
|
17
|
-
import { initStore } from "../mem/store.js";
|
|
14
|
+
import { cmdPlan } from "../plan/index.js";
|
|
18
15
|
import { cmdPriceScan } from "../price/index.js";
|
|
19
16
|
import { cmdReport, cmdReportWeb } from "../report/index.js";
|
|
20
17
|
import { cmdPlanSeed } from "../seed/plan-seed.js";
|
|
@@ -24,14 +21,7 @@ import { cmdStats } from "../stats/index.js";
|
|
|
24
21
|
import { cmdTelemetry } from "../telemetry.js";
|
|
25
22
|
import { cmdUpdate } from "../update.js";
|
|
26
23
|
import { cmdUsageScan, cmdUsageWeb } from "../usage/index.js";
|
|
27
|
-
import {
|
|
28
|
-
cmdHookEditHint,
|
|
29
|
-
cmdHookMvGuard,
|
|
30
|
-
cmdHookReadHint,
|
|
31
|
-
cmdHookSessionStart,
|
|
32
|
-
cmdHookStop,
|
|
33
|
-
} from "./hooks/index.js";
|
|
34
|
-
import { cmdMcp } from "./mcp/transport.js";
|
|
24
|
+
import { cmdHookMvGuard } from "./hooks/index.js";
|
|
35
25
|
|
|
36
26
|
export async function cliMain(): Promise<void> {
|
|
37
27
|
const [cmd, ...a] = process.argv.slice(2);
|
|
@@ -57,35 +47,14 @@ export async function cliMain(): Promise<void> {
|
|
|
57
47
|
cmdStats(a);
|
|
58
48
|
} else if (cmd === "telemetry") {
|
|
59
49
|
await cmdTelemetry(a);
|
|
60
|
-
} else if (cmd === "
|
|
61
|
-
|
|
62
|
-
} else if (cmd === "mem") {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!overrideMemDir || overrideMemDir.startsWith("--")) {
|
|
69
|
-
console.error("fapony mem: --mem-dir needs a value");
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
if (!existsSync(overrideMemDir)) {
|
|
73
|
-
console.error(
|
|
74
|
-
`fapony mem: --mem-dir path does not exist: ${overrideMemDir}`,
|
|
75
|
-
);
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
rest = a.filter((_, i) => i !== memDirIdx && i !== memDirIdx + 1);
|
|
79
|
-
}
|
|
80
|
-
initStore(process.cwd(), overrideMemDir);
|
|
81
|
-
try {
|
|
82
|
-
await cmdMem(rest, overrideMemDir);
|
|
83
|
-
} catch (e) {
|
|
84
|
-
console.error(
|
|
85
|
-
`fapony mem: ${e instanceof Error ? e.message : String(e)}`,
|
|
86
|
-
);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
50
|
+
} else if (cmd === "plan") {
|
|
51
|
+
cmdPlan(a);
|
|
52
|
+
} else if (cmd === "mem" || cmd === "mcp" || cmd === "init-mem") {
|
|
53
|
+
// Memory moved to fael (2026-09-25) — say where, don't just "unknown".
|
|
54
|
+
console.error(
|
|
55
|
+
`fapony: memory moved to fael (npm i -g @inonix/fael) — "fapony mem <sub>" → "fael <sub>", plans → "fapony plan [sweep|check]"`,
|
|
56
|
+
);
|
|
57
|
+
process.exit(1);
|
|
89
58
|
} else if (cmd === "init") {
|
|
90
59
|
await cmdInit(a);
|
|
91
60
|
} else if (cmd === "install") {
|
|
@@ -94,18 +63,8 @@ export async function cliMain(): Promise<void> {
|
|
|
94
63
|
await cmdSetup();
|
|
95
64
|
} else if (cmd === "update") {
|
|
96
65
|
await cmdUpdate();
|
|
97
|
-
} else if (cmd === "hook-stop") {
|
|
98
|
-
await cmdHookStop();
|
|
99
|
-
} else if (cmd === "hook-read-hint") {
|
|
100
|
-
await cmdHookReadHint();
|
|
101
|
-
} else if (cmd === "hook-edit-hint") {
|
|
102
|
-
await cmdHookEditHint();
|
|
103
66
|
} else if (cmd === "hook-mv-guard") {
|
|
104
67
|
await cmdHookMvGuard();
|
|
105
|
-
} else if (cmd === "hook-session-start") {
|
|
106
|
-
await cmdHookSessionStart();
|
|
107
|
-
} else if (cmd === "mcp") {
|
|
108
|
-
cmdMcp();
|
|
109
68
|
} else if (cmd === "report") {
|
|
110
69
|
cmdReport(a);
|
|
111
70
|
} else if (cmd === "report-web") {
|
|
@@ -118,7 +77,7 @@ export async function cliMain(): Promise<void> {
|
|
|
118
77
|
cmdUsageWeb(a);
|
|
119
78
|
} else {
|
|
120
79
|
console.error(`fapony: unknown command "${cmd ?? ""}"`);
|
|
121
|
-
const hints = suggestCommand(cmd ?? ""
|
|
80
|
+
const hints = suggestCommand(cmd ?? "");
|
|
122
81
|
if (hints.length > 0) {
|
|
123
82
|
console.error(`did you mean ${hints.map((h) => `"${h}"`).join(" or ")}?`);
|
|
124
83
|
}
|
|
@@ -15,24 +15,7 @@ export {
|
|
|
15
15
|
sessionKey,
|
|
16
16
|
utcStamp,
|
|
17
17
|
} from "../../core/hook-helpers.js";
|
|
18
|
-
export {
|
|
19
|
-
BUG_MARKERS,
|
|
20
|
-
hasBugMarker,
|
|
21
|
-
isBugfixCommit,
|
|
22
|
-
} from "./bug-markers.js";
|
|
23
18
|
export { computeHintImpact } from "./compute-hint-impact.js";
|
|
24
|
-
export {
|
|
25
|
-
type ContextLineData,
|
|
26
|
-
readContextData,
|
|
27
|
-
readContextLines,
|
|
28
|
-
} from "./context-data.js";
|
|
29
|
-
export {
|
|
30
|
-
cmdHookEditHint,
|
|
31
|
-
type EditHintInput,
|
|
32
|
-
type EditTrackRow,
|
|
33
|
-
editHintFor,
|
|
34
|
-
editTrackPath,
|
|
35
|
-
} from "./edit-hint.js";
|
|
36
19
|
export {
|
|
37
20
|
GIT_AUTONOMY_COMMIT_POLICY,
|
|
38
21
|
GIT_AUTONOMY_PLUGIN_FILE,
|
|
@@ -48,53 +31,3 @@ export {
|
|
|
48
31
|
rewriteGitAutonomyTool,
|
|
49
32
|
} from "./git-autonomy.js";
|
|
50
33
|
export { cmdHookMvGuard, mvGuardDecision } from "./mv-guard.js";
|
|
51
|
-
export {
|
|
52
|
-
COMMIT_HINT_MIN_COMMITS,
|
|
53
|
-
type CommitHintInput,
|
|
54
|
-
cmdHookReadHint,
|
|
55
|
-
commitHintFor,
|
|
56
|
-
READ_HINT_MIN_BYTES,
|
|
57
|
-
READ_HINT_MIN_LIMIT,
|
|
58
|
-
type ReadHintInput,
|
|
59
|
-
type ReadTrackRow,
|
|
60
|
-
type RereadHintInput,
|
|
61
|
-
readHintFor,
|
|
62
|
-
readTrackPath,
|
|
63
|
-
rereadHintFor,
|
|
64
|
-
} from "./read-hint.js";
|
|
65
|
-
export {
|
|
66
|
-
capContext,
|
|
67
|
-
cmdHookSessionStart,
|
|
68
|
-
SESSION_START_MAX_CHARS,
|
|
69
|
-
sessionStartContext,
|
|
70
|
-
} from "./session-start.js";
|
|
71
|
-
export {
|
|
72
|
-
bugDedupeKey,
|
|
73
|
-
bugSignalFromTranscript,
|
|
74
|
-
cmdHookStop,
|
|
75
|
-
countTicks,
|
|
76
|
-
cursorTranscriptPath,
|
|
77
|
-
decideHandoff,
|
|
78
|
-
decideStop,
|
|
79
|
-
fileAtRevision,
|
|
80
|
-
type HandoffDecision,
|
|
81
|
-
type HandoffPlanFile,
|
|
82
|
-
handoffBaseSha,
|
|
83
|
-
handoffBlockMessage,
|
|
84
|
-
handoffDedupeKey,
|
|
85
|
-
hasHandoffLiteral,
|
|
86
|
-
isCodexPayload,
|
|
87
|
-
isCursorPayload,
|
|
88
|
-
isPlanPath,
|
|
89
|
-
memHasHandoffForPlan,
|
|
90
|
-
mergeStopReasons,
|
|
91
|
-
type NormalizedStopInput,
|
|
92
|
-
normalizeStopInput,
|
|
93
|
-
type RawStopPayload,
|
|
94
|
-
type StopClient,
|
|
95
|
-
sessionPlanFiles,
|
|
96
|
-
stopBlockedBefore,
|
|
97
|
-
stopBlockPath,
|
|
98
|
-
stopBlockSurface,
|
|
99
|
-
stopOutput,
|
|
100
|
-
} from "./stop.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/adapters/hooks/mv-guard.ts — PreToolUse Bash guard: deny a raw `git mv`
|
|
2
|
-
// of a plan file into a done/ directory, point at `fapony
|
|
2
|
+
// of a plan file into a done/ directory, point at `fapony plan sweep
|
|
3
3
|
// --apply` instead.
|
|
4
4
|
//
|
|
5
5
|
// Manual `git mv` skips the link rewrite plan-sweep does — that produced two
|
|
@@ -23,7 +23,7 @@ export function mvGuardDecision(command: unknown): string | null {
|
|
|
23
23
|
const [, src] = m;
|
|
24
24
|
return (
|
|
25
25
|
`fapony: raw \`git mv\` of a plan into done/ skips the link rewrite — ` +
|
|
26
|
-
`run \`fapony
|
|
26
|
+
`run \`fapony plan sweep --apply ${src}\` instead, it moves the file ` +
|
|
27
27
|
`and fixes inbound/outbound links together.`
|
|
28
28
|
);
|
|
29
29
|
}
|
package/src/analyze/discover.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { join, relative, sep } from "node:path";
|
|
|
10
10
|
export const SCAN_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".pyi"]);
|
|
11
11
|
|
|
12
12
|
// "templates" for the same reason knip.json ignores templates/**: those files
|
|
13
|
-
// ship as a template
|
|
13
|
+
// ship as a template laid down in other repos by `fapony init` and never
|
|
14
14
|
// have real importers here — scanning them produces false wrapper/orphan
|
|
15
15
|
// signals (measured: conventions-seed flagged 8 "wrappers" that were all
|
|
16
16
|
// src/mem/commands/*.ts helpers matched against unrelated identically-
|
package/src/analyze/index.ts
CHANGED
package/src/commands.ts
CHANGED
|
@@ -32,9 +32,9 @@ const GROUP_ORDER: CommandGroup[] = [
|
|
|
32
32
|
|
|
33
33
|
export const COMMANDS: CommandInfo[] = [
|
|
34
34
|
{
|
|
35
|
-
name: "
|
|
35
|
+
name: "plan",
|
|
36
36
|
group: "core",
|
|
37
|
-
summary: "
|
|
37
|
+
summary: "where plans stand: next chunk, sweep shipped, check links/shas",
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
name: "debt",
|
|
@@ -46,7 +46,6 @@ export const COMMANDS: CommandInfo[] = [
|
|
|
46
46
|
group: "core",
|
|
47
47
|
summary: 'separate "already red" from "I made it red"',
|
|
48
48
|
},
|
|
49
|
-
{ name: "init-mem", group: "core", summary: "delete legacy .memory/ dirs" },
|
|
50
49
|
{
|
|
51
50
|
name: "digest",
|
|
52
51
|
group: "core",
|
|
@@ -78,41 +77,16 @@ export const COMMANDS: CommandInfo[] = [
|
|
|
78
77
|
group: "lookup",
|
|
79
78
|
summary: "write PLAN (+SPEC) with capped sections",
|
|
80
79
|
},
|
|
81
|
-
{
|
|
82
|
-
name: "mcp",
|
|
83
|
-
group: "hooks",
|
|
84
|
-
summary: "MCP server (stdio JSON-RPC, 3 tools)",
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "hook-stop",
|
|
88
|
-
group: "hooks",
|
|
89
|
-
summary: "Stop hook: block turns with commits but no mem row",
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: "hook-read-hint",
|
|
93
|
-
group: "hooks",
|
|
94
|
-
summary: "read/re-read annotations",
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
name: "hook-edit-hint",
|
|
98
|
-
group: "hooks",
|
|
99
|
-
summary: "importer count before editing shape",
|
|
100
|
-
},
|
|
101
80
|
{
|
|
102
81
|
name: "hook-mv-guard",
|
|
103
82
|
group: "hooks",
|
|
104
83
|
summary: "deny raw git mv of plan files into done/",
|
|
105
84
|
},
|
|
106
|
-
{
|
|
107
|
-
name: "hook-session-start",
|
|
108
|
-
group: "hooks",
|
|
109
|
-
summary: "SessionStart: kickoff into context",
|
|
110
|
-
},
|
|
111
85
|
{ name: "init", group: "setup", summary: "scaffold .fapony/ in a worktree" },
|
|
112
86
|
{
|
|
113
87
|
name: "install",
|
|
114
88
|
group: "setup",
|
|
115
|
-
summary: "wire
|
|
89
|
+
summary: "wire skills + plan-mv guard into clients",
|
|
116
90
|
},
|
|
117
91
|
{ name: "setup", group: "setup", summary: "interactive wizard" },
|
|
118
92
|
{ name: "update", group: "setup", summary: "self-update via git pull" },
|
|
@@ -130,7 +104,7 @@ export const COMMANDS: CommandInfo[] = [
|
|
|
130
104
|
// a `usage: fapony` line: scripts/smoke-publish.sh greps for it on a no-arg run.
|
|
131
105
|
export const renderUsage = (): string => {
|
|
132
106
|
const lines = [
|
|
133
|
-
"fapony —
|
|
107
|
+
"fapony — plan workflow + token-saving lookups for agent-written code",
|
|
134
108
|
"usage: fapony <command> [args]",
|
|
135
109
|
"",
|
|
136
110
|
];
|
|
@@ -140,7 +114,7 @@ export const renderUsage = (): string => {
|
|
|
140
114
|
}
|
|
141
115
|
lines.push(
|
|
142
116
|
"",
|
|
143
|
-
"
|
|
117
|
+
" memory (decisions, bugs, notes) lives in fael — fael --help",
|
|
144
118
|
);
|
|
145
119
|
return lines.join("\n");
|
|
146
120
|
};
|
|
@@ -162,19 +136,14 @@ export const levenshtein = (a: string, b: string): number => {
|
|
|
162
136
|
return prev[b.length];
|
|
163
137
|
};
|
|
164
138
|
|
|
165
|
-
// Up to 3 suggestions within distance ≤ 2
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
export const suggestCommand = (cmd: string, memSubs: string[]): string[] => {
|
|
139
|
+
// Up to 3 suggestions within distance ≤ 2 against top-level names. Sorted
|
|
140
|
+
// by distance, then alphabetically — deterministic for tests.
|
|
141
|
+
export const suggestCommand = (cmd: string): string[] => {
|
|
169
142
|
const scored: Array<{ text: string; d: number }> = [];
|
|
170
143
|
for (const c of COMMANDS) {
|
|
171
144
|
const d = levenshtein(cmd, c.name);
|
|
172
145
|
if (d <= 2) scored.push({ text: `fapony ${c.name}`, d });
|
|
173
146
|
}
|
|
174
|
-
for (const s of memSubs) {
|
|
175
|
-
const d = levenshtein(cmd, s);
|
|
176
|
-
if (d <= 2) scored.push({ text: `fapony mem ${s}`, d });
|
|
177
|
-
}
|
|
178
147
|
scored.sort((x, y) => x.d - y.d || (x.text < y.text ? -1 : 1));
|
|
179
148
|
return scored.slice(0, 3).map((s) => s.text);
|
|
180
149
|
};
|
package/src/core/config.ts
CHANGED
|
@@ -66,7 +66,6 @@ export interface Config {
|
|
|
66
66
|
stateDir?: string;
|
|
67
67
|
// plan/spec live in .fapony/{plan,spec} — not configurable (gitignored = private).
|
|
68
68
|
doneDir?: string;
|
|
69
|
-
memDir?: string;
|
|
70
69
|
evidenceFile?: string;
|
|
71
70
|
} | null;
|
|
72
71
|
safety?: {
|
|
@@ -100,7 +99,6 @@ export const SPEC_DIR = `${FAPONY_DIR}/spec`;
|
|
|
100
99
|
// Archive sits beside plan/, not inside it, so archiving never changes a file's
|
|
101
100
|
// depth and its relative links survive the move untouched.
|
|
102
101
|
export const DEFAULT_DONE_DIR = `${FAPONY_DIR}/done`;
|
|
103
|
-
export const DEFAULT_MEM_DIR = `${FAPONY_DIR}/.memory`;
|
|
104
102
|
export const DEFAULT_EVIDENCE_FILE = `${FAPONY_DIR}/${EVIDENCE_FILENAME}`;
|
|
105
103
|
|
|
106
104
|
export const DEFAULT_CONFIG: Config = {
|
|
@@ -136,10 +134,6 @@ export function doneDir(config?: Config): string {
|
|
|
136
134
|
return config?.paths?.doneDir ?? DEFAULT_DONE_DIR;
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
export function memoryDir(config?: Config): string {
|
|
140
|
-
return config?.paths?.memDir ?? DEFAULT_MEM_DIR;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
137
|
export function evidenceFile(config?: Config): string {
|
|
144
138
|
return config?.paths?.evidenceFile ?? DEFAULT_EVIDENCE_FILE;
|
|
145
139
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/core/fapony-dir.ts — where a repo's `.fapony/` (plan/ done/ spec/
|
|
2
|
+
// conventions.json) lives: the nearest one walking up from cwd, bounded by the
|
|
3
|
+
// git root, else `<root>/.fapony`. Monorepos keep one per app (apps/x/.fapony).
|
|
4
|
+
|
|
5
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { FAPONY_DIR } from "./config.js";
|
|
8
|
+
|
|
9
|
+
/** Physical path (symlinks resolved) so start and repo root compare like with
|
|
10
|
+
* like; falls back to a lexical resolve when the path does not exist yet. */
|
|
11
|
+
function physical(p: string): string {
|
|
12
|
+
try {
|
|
13
|
+
return realpathSync(p);
|
|
14
|
+
} catch {
|
|
15
|
+
return resolve(p);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `git rev-parse --show-toplevel` from a directory, or null outside a repo. */
|
|
20
|
+
function gitRootOf(fromDir: string): string | null {
|
|
21
|
+
try {
|
|
22
|
+
const { execSync } =
|
|
23
|
+
require("node:child_process") as typeof import("node:child_process");
|
|
24
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
25
|
+
cwd: fromDir,
|
|
26
|
+
encoding: "utf-8",
|
|
27
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
+
}).trim();
|
|
29
|
+
return root || null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The lexical ancestor of `fromDir` that is the git repo root, or null outside
|
|
37
|
+
* a repo. `git rev-parse` returns a physical path (it resolves /var → /private/
|
|
38
|
+
* var on macOS), so the walk compares `physical(dir)` against it and returns the
|
|
39
|
+
* path in the caller's own lexical form — the returned dir must match what the
|
|
40
|
+
* caller passed in, not a canonicalized stranger.
|
|
41
|
+
*/
|
|
42
|
+
export function repoRootOf(fromDir: string): string | null {
|
|
43
|
+
const start = resolve(fromDir);
|
|
44
|
+
const gitRoot = gitRootOf(start);
|
|
45
|
+
if (!gitRoot) return null;
|
|
46
|
+
const physicalRoot = physical(gitRoot);
|
|
47
|
+
let dir = start;
|
|
48
|
+
while (true) {
|
|
49
|
+
if (physical(dir) === physicalRoot) return dir;
|
|
50
|
+
const parent = dirname(dir);
|
|
51
|
+
if (parent === dir) return null;
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Nearest `.fapony/` at or above `fromDir` (never past the git root), else
|
|
57
|
+
* `<root>/.fapony` — which may not exist yet. */
|
|
58
|
+
export function faponyDirFrom(fromDir: string): string {
|
|
59
|
+
const start = resolve(fromDir);
|
|
60
|
+
const root = repoRootOf(start);
|
|
61
|
+
let dir = start;
|
|
62
|
+
while (true) {
|
|
63
|
+
const candidate = join(dir, FAPONY_DIR);
|
|
64
|
+
if (existsSync(candidate)) return candidate;
|
|
65
|
+
if (dir === (root ?? "/")) break;
|
|
66
|
+
const parent = dirname(dir);
|
|
67
|
+
if (parent === dir) break;
|
|
68
|
+
dir = parent;
|
|
69
|
+
}
|
|
70
|
+
return join(root ?? start, FAPONY_DIR);
|
|
71
|
+
}
|
package/src/core/hook-helpers.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// src/core/hook-helpers.ts — pure helpers shared by hook adapters
|
|
2
2
|
//
|
|
3
3
|
// Extracted from src/hook.ts (PLAN-lib-layer chunk 3). These have zero
|
|
4
|
-
// feature imports and are reused by
|
|
5
|
-
// session-start adapters.
|
|
4
|
+
// feature imports and are reused by the hook adapters and the hint log.
|
|
6
5
|
|
|
7
6
|
import { basename } from "node:path";
|
|
8
7
|
|
package/src/debt/load.ts
CHANGED
|
@@ -17,14 +17,12 @@ import {
|
|
|
17
17
|
CONVENTIONS_FILENAME,
|
|
18
18
|
FAPONY_DIR,
|
|
19
19
|
} from "../core/config.js";
|
|
20
|
-
import {
|
|
20
|
+
import { faponyDirFrom } from "../core/fapony-dir.js";
|
|
21
21
|
import type { Convention, LoadedConventions } from "./types.js";
|
|
22
22
|
|
|
23
23
|
export function resolveConventionsPath(worktree: string): string | null {
|
|
24
|
-
// Conventions live in the
|
|
25
|
-
|
|
26
|
-
const memDir = resolveMemDir(worktree);
|
|
27
|
-
const base = memDir ? join(memDir, "..") : join(worktree, FAPONY_DIR);
|
|
24
|
+
// Conventions live in the nearest .fapony/ — the same dir `fapony plan` uses.
|
|
25
|
+
const base = faponyDirFrom(worktree);
|
|
28
26
|
const app = join(base, CONVENTIONS_FILENAME);
|
|
29
27
|
if (existsSync(app)) return app;
|
|
30
28
|
// Monorepo where the app has not scaffolded .fapony/ yet, and single repos
|
package/src/debt/promotion.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// short and don't count, to avoid over-matching)
|
|
13
13
|
|
|
14
14
|
import { openDb } from "../db/store.js";
|
|
15
|
-
import {
|
|
15
|
+
import { readFaelLog } from "../fael.js";
|
|
16
16
|
import {
|
|
17
17
|
type Convention,
|
|
18
18
|
type DebtReport,
|
|
@@ -57,7 +57,7 @@ interface EvidenceRow {
|
|
|
57
57
|
function gatherEvidence(worktree: string): EvidenceRow[] {
|
|
58
58
|
const out: EvidenceRow[] = [];
|
|
59
59
|
try {
|
|
60
|
-
for (const r of
|
|
60
|
+
for (const r of readFaelLog(worktree).rows) {
|
|
61
61
|
if (r.kind !== "bug" && r.kind !== "decision") continue;
|
|
62
62
|
out.push({ ts: r.ts, files: r.files, hay: `${r.text}\n${r.spec ?? ""}` });
|
|
63
63
|
}
|
package/src/debt/scan.ts
CHANGED
|
@@ -187,7 +187,7 @@ export function debtScan(
|
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
/** Per-file lookup (
|
|
190
|
+
/** Per-file lookup (--files): which conventions flag this file. */
|
|
191
191
|
export function debtForFile(
|
|
192
192
|
worktree: string,
|
|
193
193
|
absFile: string,
|
package/src/digest/collect.ts
CHANGED
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
import { type HintImpact, hintLogPath } from "../core/hint-log.js";
|
|
17
17
|
import { parseSince } from "../core/since.js";
|
|
18
18
|
import { openDb } from "../db/store.js";
|
|
19
|
+
import { type MemRow, readFaelLog } from "../fael.js";
|
|
19
20
|
import { computeHintImpact } from "../hook.js";
|
|
20
|
-
import { type MemRow, readMemLog } from "../memory.js";
|
|
21
21
|
import { isPassFamily, VERDICT_GRADES } from "../parse.js";
|
|
22
22
|
import { imputeResult, loadPrices } from "../price/index.js";
|
|
23
23
|
import { EMPTY_RESULT, type PassiveUsageResult } from "../session/types.js";
|
|
@@ -528,15 +528,14 @@ export async function collectDigest(
|
|
|
528
528
|
let skippedMalformed = 0;
|
|
529
529
|
|
|
530
530
|
// 1. memory log
|
|
531
|
-
const mem =
|
|
531
|
+
const mem = readFaelLog(worktree, sinceIso);
|
|
532
532
|
skippedMalformed += mem.skipped;
|
|
533
533
|
sources.push({
|
|
534
534
|
name: "memory",
|
|
535
|
-
ok: mem.
|
|
536
|
-
detail:
|
|
537
|
-
mem.
|
|
538
|
-
|
|
539
|
-
: "no memory log — run: fapony init <path>",
|
|
535
|
+
ok: mem.ok,
|
|
536
|
+
detail: mem.ok
|
|
537
|
+
? `${mem.rows.length} rows from fael`
|
|
538
|
+
: "fael not found — memory lives in fael (npm i -g @inonix/fael)",
|
|
540
539
|
});
|
|
541
540
|
|
|
542
541
|
// 2. plans
|