fapony 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +144 -156
  2. package/package.json +5 -6
  3. package/skill/define-convention/SKILL.md +3 -3
  4. package/skill/lookup-before-edit/SKILL.md +2 -1
  5. package/skill/move-to-done/SKILL.md +18 -22
  6. package/skill/plan-with-pony/SKILL.md +4 -4
  7. package/skill/review-pony/SKILL.md +10 -12
  8. package/src/adapters/cli.ts +11 -52
  9. package/src/adapters/hooks/index.ts +0 -67
  10. package/src/adapters/hooks/mv-guard.ts +2 -2
  11. package/src/analyze/discover.ts +1 -1
  12. package/src/analyze/index.ts +0 -1
  13. package/src/commands.ts +8 -39
  14. package/src/core/config.ts +0 -8
  15. package/src/core/fapony-dir.ts +71 -0
  16. package/src/core/hook-helpers.ts +2 -11
  17. package/src/debt/load.ts +3 -5
  18. package/src/debt/promotion.ts +2 -2
  19. package/src/debt/scan.ts +1 -1
  20. package/src/digest/collect.ts +6 -7
  21. package/src/fael.ts +139 -0
  22. package/src/hook.ts +1 -81
  23. package/src/init.ts +30 -90
  24. package/src/install/antigravity.ts +8 -99
  25. package/src/install/claude.ts +66 -200
  26. package/src/install/codex.ts +6 -170
  27. package/src/install/detect.ts +4 -14
  28. package/src/install/opencode.ts +50 -486
  29. package/src/install/types.ts +1 -39
  30. package/src/install/zcode.ts +5 -91
  31. package/src/install.ts +9 -35
  32. package/src/memory.ts +10 -69
  33. package/src/plan/index.ts +34 -0
  34. package/src/plan/next.ts +181 -0
  35. package/src/plan/store.ts +41 -0
  36. package/src/{mem/commands/plan.ts → plan/sweep.ts} +66 -46
  37. package/src/seed/plan-seed.ts +10 -9
  38. package/src/setup.ts +4 -22
  39. package/src/update.ts +1 -1
  40. package/templates/PLAN.md +1 -1
  41. package/src/adapters/hooks/bug-markers.ts +0 -60
  42. package/src/adapters/hooks/context-data.ts +0 -211
  43. package/src/adapters/hooks/edit-hint.ts +0 -215
  44. package/src/adapters/hooks/read-hint.ts +0 -400
  45. package/src/adapters/hooks/session-start.ts +0 -112
  46. package/src/adapters/hooks/stop.ts +0 -768
  47. package/src/adapters/mcp/tools/index.ts +0 -149
  48. package/src/adapters/mcp/tools/mem.ts +0 -258
  49. package/src/adapters/mcp/transport.ts +0 -147
  50. package/src/analyze/cache.ts +0 -162
  51. package/src/core/mem-log.ts +0 -394
  52. package/src/init-mem.ts +0 -143
  53. package/src/install/cursor.ts +0 -167
  54. package/src/install/utils.ts +0 -29
  55. package/src/mem/commands/read.ts +0 -792
  56. package/src/mem/commands/rotate.ts +0 -59
  57. package/src/mem/commands/where.ts +0 -56
  58. package/src/mem/commands/write.ts +0 -284
  59. package/src/mem/engine.ts +0 -329
  60. package/src/mem/index.ts +0 -201
  61. package/src/mem/key-registry.ts +0 -116
  62. package/src/mem/render.ts +0 -66
  63. package/src/mem/selectors.ts +0 -227
  64. package/src/mem/store.ts +0 -356
@@ -1,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 the project's mem log 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.
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 fapony has one.
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 (fapony)
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 `mem_add` once, after the report is shown. Don't block the report on it,
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 `bug` when a finding survived (something is broken), `decision` when
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. worktree is the absolute
179
- path (`git rev-parse --show-toplevel`), never a bare name. Only record into a
180
- project that already has a mem log — never create one uninvited.
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. mem_add(kind="bug",
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:
@@ -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 { cmdMem, MEM_SUBCOMMANDS } from "../mem/index.js";
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 === "init-mem") {
61
- cmdInitMem(a);
62
- } else if (cmd === "mem") {
63
- const memDirIdx = a.indexOf("--mem-dir");
64
- let overrideMemDir: string | undefined;
65
- let rest = a;
66
- if (memDirIdx !== -1) {
67
- overrideMemDir = a[memDirIdx + 1];
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 ?? "", MEM_SUBCOMMANDS);
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 mem plan-sweep
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 mem plan-sweep --apply ${src}\` instead, it moves the file ` +
26
+ `run \`fapony plan sweep --apply ${src}\` instead, it moves the file ` +
27
27
  `and fixes inbound/outbound links together.`
28
28
  );
29
29
  }
@@ -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 copied into other repos by `fapony init-mem` and never
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-
@@ -6,7 +6,6 @@
6
6
 
7
7
  export * from "./barrels.js";
8
8
  export * from "./blast.js";
9
- export * from "./cache.js";
10
9
  export * from "./cli.js";
11
10
  export * from "./criteria.js";
12
11
  export * from "./diagnose.js";
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: "mem",
35
+ name: "plan",
36
36
  group: "core",
37
- summary: "project pain memory: decisions, bugs, notes",
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 MCP + skills into clients",
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 — project pain memory for agent-written code",
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
- " fapony mem <sub> --help per-subcommand details (only mem has --help)",
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, against top-level names and mem
166
- // subs (so `fapony plan-check` offers `fapony mem plan-check`). Sorted by
167
- // distance, then alphabetically — deterministic for tests.
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
  };
@@ -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?: {
@@ -92,17 +91,14 @@ export interface RolePricing {
92
91
  export const FAPONY_DIR = ".fapony";
93
92
  export const CONFIG_FILENAME = "fapony.config.json";
94
93
  export const CONVENTIONS_FILENAME = "conventions.json";
95
- export const KEYS_FILENAME = "keys.json";
96
94
  export const EVIDENCE_FILENAME = "evidence.json";
97
95
  export const CONVENTIONS_FILE = `${FAPONY_DIR}/${CONVENTIONS_FILENAME}`;
98
- export const KEYS_FILE = `${FAPONY_DIR}/${KEYS_FILENAME}`;
99
96
  // plan/spec live in .fapony/ — not configurable (gitignored = private).
100
97
  export const PLAN_DIR = `${FAPONY_DIR}/plan`;
101
98
  export const SPEC_DIR = `${FAPONY_DIR}/spec`;
102
99
  // Archive sits beside plan/, not inside it, so archiving never changes a file's
103
100
  // depth and its relative links survive the move untouched.
104
101
  export const DEFAULT_DONE_DIR = `${FAPONY_DIR}/done`;
105
- export const DEFAULT_MEM_DIR = `${FAPONY_DIR}/.memory`;
106
102
  export const DEFAULT_EVIDENCE_FILE = `${FAPONY_DIR}/${EVIDENCE_FILENAME}`;
107
103
 
108
104
  export const DEFAULT_CONFIG: Config = {
@@ -138,10 +134,6 @@ export function doneDir(config?: Config): string {
138
134
  return config?.paths?.doneDir ?? DEFAULT_DONE_DIR;
139
135
  }
140
136
 
141
- export function memoryDir(config?: Config): string {
142
- return config?.paths?.memDir ?? DEFAULT_MEM_DIR;
143
- }
144
-
145
137
  export function evidenceFile(config?: Config): string {
146
138
  return config?.paths?.evidenceFile ?? DEFAULT_EVIDENCE_FILE;
147
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
+ }
@@ -1,11 +1,9 @@
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 stop, read-hint, edit-hint, and
5
- // session-start adapters.
4
+ // feature imports and are reused by the hook adapters and the hint log.
6
5
 
7
- import { homedir } from "node:os";
8
- import { basename, join } from "node:path";
6
+ import { basename } from "node:path";
9
7
 
10
8
  /** UTC 'YYYY-MM-DD HH:MM:SS' — the format events.ts is written in. */
11
9
  export function utcStamp(d: Date): string {
@@ -30,10 +28,3 @@ export function sessionKey(session: string): string {
30
28
  const base = basename(session).replace(/\.[^.]+$/, "");
31
29
  return base.replace(/[^A-Za-z0-9_-]/g, "-") || "unknown";
32
30
  }
33
-
34
- /** Per-session hint state dir (read log + shown mem ids) — disposable, never in a worktree. */
35
- export function readTrackDir(): string {
36
- const base =
37
- process.env.FAPONY_STATE_DIR || join(homedir(), ".config", "fapony");
38
- return join(base, "read-track");
39
- }
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 { resolveMemDir } from "../memory.js";
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 same .fapony/ dir as the mem log — derive from
25
- // the resolved mem dir so both resolvers cannot drift apart.
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
@@ -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 { readMemLog } from "../memory.js";
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 readMemLog(worktree).rows) {
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 (hook-read-hint + --files): which conventions flag this file. */
190
+ /** Per-file lookup (--files): which conventions flag this file. */
191
191
  export function debtForFile(
192
192
  worktree: string,
193
193
  absFile: string,
@@ -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 = readMemLog(worktree, sinceIso);
531
+ const mem = readFaelLog(worktree, sinceIso);
532
532
  skippedMalformed += mem.skipped;
533
533
  sources.push({
534
534
  name: "memory",
535
- ok: mem.filesFound > 0,
536
- detail:
537
- mem.filesFound > 0
538
- ? `${mem.rows.length} rows from ${mem.filesFound} file${mem.filesFound > 1 ? "s" : ""}`
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