fapony 0.3.3 → 0.3.5

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 (87) hide show
  1. package/README.md +7 -1
  2. package/fapony.ts +3 -116
  3. package/package.json +5 -6
  4. package/skill/move-to-done/SKILL.md +3 -2
  5. package/src/adapters/cli.ts +128 -0
  6. package/src/adapters/hooks/compute-hint-impact.ts +107 -0
  7. package/src/adapters/hooks/context-data.ts +102 -0
  8. package/src/adapters/hooks/edit-hint.ts +195 -0
  9. package/src/adapters/hooks/git-autonomy.ts +178 -0
  10. package/src/adapters/hooks/index.ts +80 -0
  11. package/src/adapters/hooks/mv-guard.ts +52 -0
  12. package/src/adapters/hooks/read-hint.ts +383 -0
  13. package/src/adapters/hooks/session-start.ts +101 -0
  14. package/src/adapters/hooks/stop.ts +434 -0
  15. package/src/{mcp → adapters/mcp}/evidence.ts +2 -2
  16. package/src/{mcp → adapters/mcp}/primitives.ts +2 -2
  17. package/src/{mcp → adapters/mcp}/tools/check.ts +1 -1
  18. package/src/{mcp → adapters/mcp}/tools/collect.ts +1 -1
  19. package/src/{mcp → adapters/mcp}/tools/mem.ts +3 -3
  20. package/src/{mcp → adapters/mcp}/tools/report.ts +4 -4
  21. package/src/adapters/mcp/types.ts +18 -0
  22. package/src/{mcp → adapters/mcp}/worktree.ts +1 -1
  23. package/src/analyze.ts +1 -1
  24. package/src/commands.ts +180 -0
  25. package/src/conventions-seed.ts +1 -1
  26. package/src/core/config.ts +199 -0
  27. package/src/core/debt-format.ts +107 -0
  28. package/src/core/debt-types.ts +79 -0
  29. package/src/core/defaults.ts +8 -0
  30. package/src/core/enums.ts +34 -0
  31. package/src/core/format.ts +33 -0
  32. package/src/core/hint-log.ts +68 -0
  33. package/src/core/hook-helpers.ts +31 -0
  34. package/src/core/mem-log.ts +357 -0
  35. package/src/core/parse.ts +71 -0
  36. package/src/core/pricing.ts +217 -0
  37. package/src/core/safety.ts +18 -0
  38. package/src/core/types.ts +142 -0
  39. package/src/core/util.ts +105 -0
  40. package/src/db/store.ts +2 -2
  41. package/src/debt/cli.ts +1 -1
  42. package/src/debt/format.ts +2 -107
  43. package/src/debt/load.ts +1 -1
  44. package/src/debt/promotion.ts +1 -1
  45. package/src/debt/types.ts +14 -79
  46. package/src/digest/collect.ts +4 -3
  47. package/src/gate.ts +2 -2
  48. package/src/gates.ts +1 -1
  49. package/src/hook.ts +70 -1424
  50. package/src/init-mem.ts +1 -1
  51. package/src/init.ts +1 -1
  52. package/src/install/claude.ts +17 -0
  53. package/src/install/opencode.ts +114 -0
  54. package/src/install.ts +12 -5
  55. package/src/lint-baseline.ts +1 -2
  56. package/src/map.ts +29 -8
  57. package/src/mem/commands/plan.ts +197 -42
  58. package/src/mem/commands/read.ts +53 -12
  59. package/src/mem/index.ts +60 -1
  60. package/src/mem/store.ts +5 -1
  61. package/src/memory.ts +26 -388
  62. package/src/parse.ts +9 -71
  63. package/src/price/fetch.ts +4 -16
  64. package/src/price/resolve.ts +12 -213
  65. package/src/report/cli.ts +3 -3
  66. package/src/safety.ts +2 -18
  67. package/src/seed/plan-seed.ts +126 -32
  68. package/src/seed/primitives.ts +1 -7
  69. package/src/session/types.ts +14 -128
  70. package/src/setup.ts +1 -1
  71. package/src/stats/data.ts +3 -3
  72. package/src/telemetry.ts +2 -2
  73. package/src/update.ts +1 -1
  74. package/src/usage/cache.ts +1 -2
  75. package/src/usage/cli.ts +1 -1
  76. package/src/usage/scan.ts +2 -1
  77. package/src/util.ts +10 -93
  78. package/src/web/html.ts +2 -33
  79. package/src/db/defaults.ts +0 -34
  80. package/src/db/getters.ts +0 -35
  81. package/src/db/index.ts +0 -7
  82. package/src/db/load.ts +0 -57
  83. package/src/db/types.ts +0 -77
  84. package/src/mcp/types.ts +0 -54
  85. package/src/test.ts +0 -2
  86. /package/src/{mcp → adapters/mcp}/tools/index.ts +0 -0
  87. /package/src/{mcp → adapters/mcp}/transport.ts +0 -0
package/src/init-mem.ts CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
8
8
  import { join } from "node:path";
9
- import { DEFAULT_MEM_DIR, FAPONY_DIR, loadConfig } from "./db/index.js";
9
+ import { DEFAULT_MEM_DIR, FAPONY_DIR, loadConfig } from "./core/config.js";
10
10
  import { walkDir } from "./util.js";
11
11
 
12
12
  export function cmdInitMem(args: string[]): void {
package/src/init.ts CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  memoryDir,
22
22
  planDir,
23
23
  specDir,
24
- } from "./db/index.js";
24
+ } from "./core/config.js";
25
25
  import { isAffirmative } from "./util.js";
26
26
 
27
27
  const FAPONY_README = `# .fapony/ — fapony project dir (plans, specs, memory)
@@ -232,6 +232,7 @@ function installClaudeHooks(dryRun: boolean, deps: InstallDeps): void {
232
232
  installStopHook(dryRun, deps);
233
233
  installReadHintHook(dryRun, deps);
234
234
  installEditHintHook(dryRun, deps);
235
+ installMvGuardHook(dryRun, deps);
235
236
  installSessionStartHook(dryRun, deps);
236
237
  }
237
238
 
@@ -276,6 +277,22 @@ function installEditHintHook(dryRun: boolean, deps: InstallDeps): void {
276
277
  });
277
278
  }
278
279
 
280
+ /**
281
+ * PreToolUse hook on Bash: denies a raw `git mv` of a plan file into a done/
282
+ * directory, pointing at `fapony mem plan-sweep --apply` instead — that
283
+ * command does the link rewrite a plain `git mv` skips. The one fapony hook
284
+ * that blocks besides Stop; matcher "Bash" keeps the spawn off every other
285
+ * tool call.
286
+ */
287
+ function installMvGuardHook(dryRun: boolean, deps: InstallDeps): void {
288
+ ensureClaudeHook(dryRun, deps, {
289
+ event: "PreToolUse",
290
+ matcher: "Bash",
291
+ subcommand: "hook-mv-guard",
292
+ label: "plan-mv guard",
293
+ });
294
+ }
295
+
279
296
  /**
280
297
  * SessionStart hook: injects `fapony mem kickoff` as context when the repo has
281
298
  * a mem log, and stays silent when it does not. Context only — SessionStart
@@ -81,9 +81,15 @@ function isConfigured(mcp: Record<string, unknown> | undefined): boolean {
81
81
  );
82
82
  }
83
83
 
84
+ export interface OpencodeInstallOpts {
85
+ /** Opt-in only: write the git-autonomy rewrite plugin. Never default. */
86
+ gitAutonomy?: boolean;
87
+ }
88
+
84
89
  export function cmdInstallOpencode(
85
90
  dryRun: boolean,
86
91
  deps: InstallDeps = {},
92
+ opts: OpencodeInstallOpts = {},
87
93
  ): void {
88
94
  const exitFn = deps.exit ?? defaultExit;
89
95
  const getHome = deps.homedir ?? homedir;
@@ -115,6 +121,7 @@ export function cmdInstallOpencode(
115
121
  installCommitHintPlugin(dryRun, getHome);
116
122
  installEditHintPlugin(dryRun, getHome);
117
123
  installSessionStartPlugin(dryRun, getHome);
124
+ if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
118
125
  return;
119
126
  }
120
127
 
@@ -140,6 +147,7 @@ export function cmdInstallOpencode(
140
147
  installCommitHintPlugin(dryRun, getHome);
141
148
  installEditHintPlugin(dryRun, getHome);
142
149
  installSessionStartPlugin(dryRun, getHome);
150
+ if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
143
151
  return;
144
152
  }
145
153
 
@@ -163,6 +171,7 @@ export function cmdInstallOpencode(
163
171
  installCommitHintPlugin(dryRun, getHome);
164
172
  installEditHintPlugin(dryRun, getHome);
165
173
  installSessionStartPlugin(dryRun, getHome);
174
+ if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
166
175
  }
167
176
 
168
177
  /**
@@ -608,3 +617,108 @@ function installSessionStartPlugin(
608
617
  ` session start: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
609
618
  );
610
619
  }
620
+
621
+ /**
622
+ * OpenCode plugin — git-autonomy rewrites, the parity of the personal
623
+ * `git-autonomy.ts` plugin. Two hooks because the policy lives on two
624
+ * surfaces the other never sees:
625
+ * `experimental.chat.system.transform` (system prompt) + `tool.definition`
626
+ * (bash tool description).
627
+ *
628
+ * OPT-IN ONLY — written only when `fapony install` gets `--git-autonomy`.
629
+ * Never on the default path, never via `--all` unless the flag rides along.
630
+ * The policy is an opinion (commit-as-you-go), not a utility, and the file
631
+ * name is `fapony-git-autonomy.ts`: the user's own `git-autonomy.ts` is a
632
+ * different file and is never touched — delete it by hand after verifying.
633
+ *
634
+ * Logic lives in src/adapters/hooks/git-autonomy.ts
635
+ * (rewriteGitAutonomySystem / rewriteGitAutonomyTool) — the plugin imports
636
+ * it from the install root, so `git pull` updates every client. No baked
637
+ * regex in the generated body: a baked copy would rot silently when upstream
638
+ * rewords the policy, the same failure this module's gitAutonomyStatus
639
+ * exists to name. Never throws: a rewrite must not break a session start
640
+ * or hide a tool.
641
+ */
642
+ export function gitAutonomyPluginSource(root: string): string {
643
+ const hookModule = JSON.stringify(join(root, "src", "hook.ts"));
644
+ return `// fapony git-autonomy — rewrites opencode's ask-first git policy to
645
+ // commit-as-you-go on both surfaces that carry it (system prompt +
646
+ // bash tool description), and cuts the tone few-shot examples.
647
+ // OPT-IN: written only by \`fapony install --platform opencode --git-autonomy\`.
648
+ // Generated by \`fapony install\` — logic lives in src/adapters/hooks/git-autonomy.ts.
649
+ import { rewriteGitAutonomySystem, rewriteGitAutonomyTool } from ${hookModule};
650
+
651
+ export const FaponyGitAutonomy = async () => ({
652
+ "experimental.chat.system.transform": async (
653
+ _input: unknown,
654
+ output: { system: string[] },
655
+ ) => {
656
+ try {
657
+ for (let i = 0; i < output.system.length; i++) {
658
+ output.system[i] = rewriteGitAutonomySystem(output.system[i]).text;
659
+ }
660
+ } catch {
661
+ // a rewrite must never break a session start
662
+ }
663
+ },
664
+ "tool.definition": async (
665
+ input: { toolID: string },
666
+ output: { description: string; parameters: unknown },
667
+ ) => {
668
+ try {
669
+ const r = rewriteGitAutonomyTool(input.toolID, output.description);
670
+ output.description = r.description;
671
+ } catch {
672
+ // a rewrite must never hide a tool
673
+ }
674
+ },
675
+ });
676
+ `;
677
+ }
678
+
679
+ function installGitAutonomyPlugin(
680
+ dryRun: boolean,
681
+ getHome: () => string,
682
+ ): void {
683
+ const pluginsDir = join(getHome(), ".config", "opencode", "plugins");
684
+ const pluginPath = join(pluginsDir, "fapony-git-autonomy.ts");
685
+ if (existsSync(pluginPath)) {
686
+ let current = "";
687
+ try {
688
+ current = readFileSync(pluginPath, "utf-8");
689
+ } catch {
690
+ current = "";
691
+ }
692
+ if (current.includes("FaponyGitAutonomy")) {
693
+ // Ours — and generated, so never overwritten. Same stale-check shape
694
+ // as the session-start plugin: an older generation must say so instead
695
+ // of "no change", or a fixed rewrite never reaches the client.
696
+ if (current !== gitAutonomyPluginSource(INSTALL_ROOT)) {
697
+ console.error(
698
+ ` git-autonomy: stale fapony plugin — delete ${pluginPath} and re-run install --git-autonomy`,
699
+ );
700
+ return;
701
+ }
702
+ console.error(` git-autonomy: already installed — no change`);
703
+ return;
704
+ }
705
+ console.error(
706
+ ` git-autonomy: ${pluginPath} exists but isn't fapony's — not overwriting.`,
707
+ );
708
+ return;
709
+ }
710
+ if (!dryRun) {
711
+ try {
712
+ mkdirSync(pluginsDir, { recursive: true });
713
+ writeFileSync(pluginPath, gitAutonomyPluginSource(INSTALL_ROOT), "utf-8");
714
+ } catch (e) {
715
+ console.error(
716
+ ` git-autonomy: failed to write — ${(e as Error).message}`,
717
+ );
718
+ return;
719
+ }
720
+ }
721
+ console.error(
722
+ ` git-autonomy: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
723
+ );
724
+ }
package/src/install.ts CHANGED
@@ -37,6 +37,8 @@ export {
37
37
  cmdInstallOpencode,
38
38
  commitHintPluginSource,
39
39
  editHintPluginSource,
40
+ gitAutonomyPluginSource,
41
+ type OpencodeInstallOpts,
40
42
  readHintPluginSource,
41
43
  } from "./install/opencode.js";
42
44
  export {
@@ -58,6 +60,10 @@ export async function cmdInstall(
58
60
  const platform = args.find((a) => !a.startsWith("--"));
59
61
  const dryRun = args.includes("--dry-run");
60
62
  const installAll = args.includes("--all");
63
+ // Opt-in only: the git-autonomy rewrite is an opinion (commit-as-you-go),
64
+ // not a utility — never installed unless this flag rides along, including
65
+ // via --all. Opencode-only; other platforms ignore it.
66
+ const gitAutonomy = args.includes("--git-autonomy");
61
67
 
62
68
  // --- explicit platform: original behavior, unchanged ---
63
69
  if (platform === "antigravity") {
@@ -81,12 +87,12 @@ export async function cmdInstall(
81
87
  return;
82
88
  }
83
89
  if (platform === "opencode") {
84
- cmdInstallOpencode(dryRun, deps);
90
+ cmdInstallOpencode(dryRun, deps, { gitAutonomy });
85
91
  return;
86
92
  }
87
93
  if (platform !== undefined) {
88
94
  console.error(
89
- `usage: fapony install --platform antigravity|opencode|claude|cursor|zcode|codex [--dry-run]`,
95
+ `usage: fapony install --platform antigravity|opencode|claude|cursor|zcode|codex [--dry-run] [--git-autonomy]`,
90
96
  );
91
97
  console.error(
92
98
  ` supported platforms: antigravity, opencode, claude, cursor, zcode, codex`,
@@ -125,7 +131,7 @@ export async function cmdInstall(
125
131
  if (installAll) {
126
132
  console.error();
127
133
  for (const client of found) {
128
- installPlatform(client.platform, dryRun, deps);
134
+ installPlatform(client.platform, dryRun, deps, { gitAutonomy });
129
135
  }
130
136
  return;
131
137
  }
@@ -154,7 +160,7 @@ export async function cmdInstall(
154
160
  for (const client of found) {
155
161
  const answer = await askFn(`install into ${client.platform}?`, "Y");
156
162
  if (isAffirmative(answer)) {
157
- installPlatform(client.platform, dryRun, deps);
163
+ installPlatform(client.platform, dryRun, deps, { gitAutonomy });
158
164
  }
159
165
  }
160
166
  } finally {
@@ -166,6 +172,7 @@ function installPlatform(
166
172
  platform: string,
167
173
  dryRun: boolean,
168
174
  deps: InstallDeps,
175
+ opts: { gitAutonomy?: boolean } = {},
169
176
  ): void {
170
177
  switch (platform) {
171
178
  case "antigravity":
@@ -178,7 +185,7 @@ function installPlatform(
178
185
  cmdInstallCursor(dryRun, deps);
179
186
  break;
180
187
  case "opencode":
181
- cmdInstallOpencode(dryRun, deps);
188
+ cmdInstallOpencode(dryRun, deps, opts);
182
189
  break;
183
190
  case "zcode":
184
191
  cmdInstallZcode(dryRun, deps);
@@ -14,8 +14,7 @@
14
14
  import { execSync } from "node:child_process";
15
15
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
16
16
  import { join, relative } from "node:path";
17
- import { DEFAULT_EVIDENCE_FILE } from "./db/index.js";
18
- import { faponyDir } from "./db/load.js";
17
+ import { DEFAULT_EVIDENCE_FILE, faponyDir } from "./core/config.js";
19
18
  import { assertSafe } from "./safety.js";
20
19
 
21
20
  const USAGE =
package/src/map.ts CHANGED
@@ -41,19 +41,36 @@ export interface ExportScan {
41
41
  error: string | null;
42
42
  }
43
43
 
44
- // --- Parse gate ---
44
+ // --- Parse gate (injectable: default is Bun.Transpiler, no Node fallback —
45
+ // Bun-only is a repo constraint. Pass a custom scanner to extractExports()
46
+ // to reuse this module outside Bun or in tests.) ---
45
47
 
46
- let transpiler: Bun.Transpiler | null = null;
48
+ export interface ExportScanner {
49
+ scan(source: string): { exports: string[] };
50
+ }
51
+
52
+ function createBunScanner(): ExportScanner {
53
+ const transpiler = new Bun.Transpiler({ loader: "tsx" });
54
+ return {
55
+ scan: (source) => transpiler.scan(source) as { exports: string[] },
56
+ };
57
+ }
58
+
59
+ let defaultScanner: ExportScanner | null = null;
60
+
61
+ function getDefaultScanner(): ExportScanner {
62
+ if (!defaultScanner) defaultScanner = createBunScanner();
63
+ return defaultScanner;
64
+ }
47
65
 
48
66
  interface ScanResult {
49
67
  error: string | null;
50
68
  exports: string[];
51
69
  }
52
70
 
53
- function scanSource(source: string): ScanResult {
71
+ function scanSource(source: string, scanner: ExportScanner): ScanResult {
54
72
  try {
55
- if (!transpiler) transpiler = new Bun.Transpiler({ loader: "tsx" });
56
- const scanned = transpiler.scan(source) as { exports: string[] };
73
+ const scanned = scanner.scan(source);
57
74
  return { error: null, exports: scanned.exports };
58
75
  } catch (e) {
59
76
  return {
@@ -150,8 +167,12 @@ function braceNames(
150
167
 
151
168
  const VAR_DECL_RE = /^export\s+(?:const|let|var)\b/;
152
169
 
153
- export function extractExports(source: string): ExportScan {
154
- const scanned = scanSource(source);
170
+ export function extractExports(
171
+ source: string,
172
+ scanner?: ExportScanner,
173
+ ): ExportScan {
174
+ const s = scanner ?? getDefaultScanner();
175
+ const scanned = scanSource(source, s);
155
176
  if (scanned.error) return { symbols: [], error: scanned.error };
156
177
 
157
178
  // Ambient declarations (`export declare ...`) are invisible to scan(), so
@@ -161,7 +182,7 @@ export function extractExports(source: string): ExportScan {
161
182
  /^([ \t]*)export\s+declare\s+/gm,
162
183
  "$1export ",
163
184
  );
164
- const strippedScan = scanSource(stripped);
185
+ const strippedScan = scanSource(stripped, s);
165
186
  const valid = new Set([
166
187
  ...scanned.exports,
167
188
  ...(strippedScan.error ? [] : strippedScan.exports),