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/update.ts CHANGED
@@ -206,7 +206,7 @@ export async function cmdUpdate(deps: UpdateDeps = {}): Promise<void> {
206
206
  │ Update complete! │
207
207
  │ │
208
208
  │ Version: ${newVersion.padEnd(31)}│
209
- │ Run "fapony test" to verify. │
209
+ │ Run "bun run test" to verify. │
210
210
  └──────────────────────────────────────────┘
211
211
  `);
212
212
  }
@@ -12,8 +12,7 @@ import {
12
12
  writeFileSync,
13
13
  } from "node:fs";
14
14
  import { dirname, join } from "node:path";
15
- import { faponyDir } from "../db/load.js";
16
- import type { Config } from "../db/types.js";
15
+ import { type Config, faponyDir } from "../core/config.js";
17
16
 
18
17
  const CACHE_FILENAME = "usage-cache.jsonl";
19
18
 
package/src/usage/cli.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Reads from usage-cache.jsonl (written by `fapony usage-scan`).
4
4
  // No live session scanning — serves static HTML.
5
5
 
6
- import { loadConfig } from "../db/index.js";
6
+ import { loadConfig } from "../core/config.js";
7
7
  import type { PassiveUsageResult } from "../session/types.js";
8
8
  import { type CacheEntry, cacheMeta, readCache } from "./cache.js";
9
9
  import { renderUsageHtml } from "./render.js";
package/src/usage/scan.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  // Per-worktree + global aggregate — the cache now carries a worktree dimension.
6
6
  // Progress bar on TTY, plain lines on pipe/CI.
7
7
 
8
- import { loadConfig, openDb } from "../db/index.js";
8
+ import { loadConfig } from "../core/config.js";
9
+ import { openDb } from "../db/store.js";
9
10
  import { CLIENTS } from "../session/index.js";
10
11
  import type { PassiveUsageResult } from "../session/types.js";
11
12
  import {
package/src/util.ts CHANGED
@@ -1,93 +1,10 @@
1
- // Replacement values are inserted via a function so `$&`, `$'` etc. in the
2
- // value are treated as literal text, not replace() special patterns.
3
- export function templateArgs(
4
- arr: string[],
5
- vars: Record<string, string>,
6
- ): string[] {
7
- return arr.map((s) => {
8
- let out = s;
9
- for (const [k, v] of Object.entries(vars)) {
10
- out = out.replaceAll(`{${k}}`, () => v);
11
- }
12
- return out;
13
- });
14
- }
15
-
16
- /** Fill a prompt template with {{VARS}} (all occurrences). Missing vars → "". */
17
- export function fillPrompt(
18
- template: string,
19
- vars: Record<string, string>,
20
- ): string {
21
- let out = template;
22
- for (const [k, v] of Object.entries(vars)) {
23
- out = out.replaceAll(`{{${k}}}`, () => v);
24
- }
25
- return out;
26
- }
27
-
28
- /** Minutes between two ISO-like timestamps (space separator, UTC assumed). */
29
- export function minutesBetween(a: string, b: string): number {
30
- const t0 = new Date(`${a.replace(" ", "T")}Z`).getTime();
31
- const t1 = new Date(`${b.replace(" ", "T")}Z`).getTime();
32
- return (t1 - t0) / 60000;
33
- }
34
-
35
- /** Arithmetic mean of a numeric array. 0 for empty arrays. */
36
- export function avg(xs: number[]): number {
37
- return xs.length ? xs.reduce((s, x) => s + x, 0) / xs.length : 0;
38
- }
39
-
40
- /** True for "y"/"yes" (case-insensitive, trimmed) — the only affirmative answers. */
41
- export function isAffirmative(answer: string): boolean {
42
- const normalized = answer.trim().toLowerCase();
43
- return normalized === "y" || normalized === "yes";
44
- }
45
-
46
- // --- Recursive directory walker ---
47
-
48
- import { readdirSync } from "node:fs";
49
- import { join } from "node:path";
50
-
51
- export interface WalkDirOpts {
52
- /** Max recursion depth (default 4). */
53
- depth?: number;
54
- /** Skip node_modules (default true). */
55
- skipNodeModules?: boolean;
56
- /** Dot-directories to descend into (default []). Others starting with . are skipped. */
57
- includeDotDirs?: string[];
58
- /** Extra filter: return true to collect this dir. Omit to collect all reached dirs. */
59
- predicate?: (dirPath: string) => boolean;
60
- }
61
-
62
- /**
63
- * Recursively walk directories under `root`.
64
- * Returns absolute paths of dirs for which `predicate` returned true (or all
65
- * reached dirs when no predicate is given). Skips node_modules by default.
66
- */
67
- export function walkDir(root: string, opts?: WalkDirOpts): string[] {
68
- const maxDepth = opts?.depth ?? 4;
69
- const skipNm = opts?.skipNodeModules ?? true;
70
- const includeDot = new Set(opts?.includeDotDirs ?? []);
71
- const pred = opts?.predicate;
72
- const result: string[] = [];
73
-
74
- const walk = (dir: string, depth: number): void => {
75
- if (depth > maxDepth) return;
76
- if (!pred || pred(dir)) result.push(dir);
77
- try {
78
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
79
- if (!entry.isDirectory()) continue;
80
- if (entry.isSymbolicLink()) continue;
81
- if (skipNm && entry.name === "node_modules") continue;
82
- if (entry.name.startsWith(".") && !includeDot.has(entry.name)) {
83
- continue;
84
- }
85
- walk(join(dir, entry.name), depth + 1);
86
- }
87
- } catch {
88
- // ignore unreadable dirs
89
- }
90
- };
91
- walk(root, 0);
92
- return result;
93
- }
1
+ // shim — moved to src/core/util.ts
2
+ export {
3
+ avg,
4
+ fillPrompt,
5
+ isAffirmative,
6
+ minutesBetween,
7
+ templateArgs,
8
+ type WalkDirOpts,
9
+ walkDir,
10
+ } from "./core/util.js";
package/src/web/html.ts CHANGED
@@ -1,33 +1,2 @@
1
- // src/web/html.ts — shared HTML helpers for report-web and usage-web
2
-
3
- /** HTML-escape interpolated strings. */
4
- export function esc(s: string): string {
5
- return s
6
- .replaceAll("&", "&amp;")
7
- .replaceAll("<", "&lt;")
8
- .replaceAll(">", "&gt;")
9
- .replaceAll('"', "&quot;");
10
- }
11
-
12
- /** Common dark-theme CSS variables + base reset used by all HTML pages. */
13
- export const DARK_THEME_CSS = `
14
- :root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; --green: #3fb950; --red: #f85149; --yellow: #d29922; --muted: #8b949e; }
15
- * { box-sizing: border-box; margin: 0; padding: 0; }
16
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; padding: 2rem; max-width: 960px; margin: 0 auto; }
17
- `;
18
-
19
- /** Shared table + typography CSS used by both report and usage pages. */
20
- export const TABLE_CSS = `
21
- h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
22
- h2 { font-size: 1.1rem; color: var(--accent); margin: 1.5rem 0 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
23
- .meta { color: var(--muted); font-size: 0.85rem; margin-bottom: 1.5rem; }
24
- table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; font-size: 0.9rem; }
25
- th, td { padding: 0.5rem 0.8rem; text-align: left; border-bottom: 1px solid var(--border); }
26
- th { color: var(--muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; }
27
- tr:hover { background: #161b22; }
28
- .pass { color: var(--green); }
29
- .fail { color: var(--red); }
30
- .warn { color: var(--yellow); }
31
- .muted { color: var(--muted); }
32
- .sample { font-size: 0.8rem; color: var(--muted); }
33
- `;
1
+ // shim — moved to src/core/format.ts
2
+ export { DARK_THEME_CSS, esc, TABLE_CSS } from "../core/format.js";
@@ -1,34 +0,0 @@
1
- import type { Config } from "./types.js";
2
-
3
- export const DEFAULT_SAFETY_DENY = [
4
- "reset\\s+--hard",
5
- "clean\\s+-[a-z]*f",
6
- "checkout\\s+--\\s",
7
- "git\\s+stash",
8
- ];
9
- // --- .fapony/ layout (single source of truth — do not hardcode ".fapony" elsewhere) ---
10
- // plan/spec live in .fapony/ — not configurable (gitignored = private).
11
- export const FAPONY_DIR = ".fapony";
12
- export const CONFIG_FILENAME = "fapony.config.json";
13
- export const CONVENTIONS_FILENAME = "conventions.json";
14
- export const EVIDENCE_FILENAME = "evidence.json";
15
- export const CONVENTIONS_FILE = `${FAPONY_DIR}/${CONVENTIONS_FILENAME}`;
16
- // plan/spec live in .fapony/ — not configurable (gitignored = private).
17
- export const PLAN_DIR = `${FAPONY_DIR}/plan`;
18
- export const SPEC_DIR = `${FAPONY_DIR}/spec`;
19
- // Archive sits beside plan/, not inside it, so archiving never changes a file's
20
- // depth and its relative links survive the move untouched.
21
- export const DEFAULT_DONE_DIR = `${FAPONY_DIR}/done`;
22
- export const DEFAULT_MEM_DIR = `${FAPONY_DIR}/.memory`;
23
- export const DEFAULT_EVIDENCE_FILE = `${FAPONY_DIR}/${EVIDENCE_FILENAME}`;
24
-
25
- export const DEFAULT_CONFIG: Config = {
26
- worktrees: {},
27
- review: {
28
- maxRounds: 2,
29
- },
30
- memory: null,
31
- telemetry: null,
32
- paths: null,
33
- safety: null,
34
- };
package/src/db/getters.ts DELETED
@@ -1,35 +0,0 @@
1
- import {
2
- DEFAULT_DONE_DIR,
3
- DEFAULT_EVIDENCE_FILE,
4
- DEFAULT_MEM_DIR,
5
- DEFAULT_SAFETY_DENY,
6
- PLAN_DIR,
7
- SPEC_DIR,
8
- } from "./defaults.js";
9
- import type { Config } from "./types.js";
10
-
11
- export function safetyDeny(config?: Config): string[] {
12
- return config?.safety?.deny ?? DEFAULT_SAFETY_DENY;
13
- }
14
-
15
- /** Hardcoded — plan/spec live in .fapony/ (gitignored = private). */
16
- export function planDir(): string {
17
- return PLAN_DIR;
18
- }
19
-
20
- /** Hardcoded — plan/spec live in .fapony/ (gitignored = private). */
21
- export function specDir(): string {
22
- return SPEC_DIR;
23
- }
24
-
25
- export function doneDir(config?: Config): string {
26
- return config?.paths?.doneDir ?? DEFAULT_DONE_DIR;
27
- }
28
-
29
- export function memoryDir(config?: Config): string {
30
- return config?.paths?.memDir ?? DEFAULT_MEM_DIR;
31
- }
32
-
33
- export function evidenceFile(config?: Config): string {
34
- return config?.paths?.evidenceFile ?? DEFAULT_EVIDENCE_FILE;
35
- }
package/src/db/index.ts DELETED
@@ -1,7 +0,0 @@
1
- // barrel — re-export everything from db/* so existing imports from "./db/index.js" keep working
2
-
3
- export * from "./defaults.js";
4
- export * from "./getters.js";
5
- export * from "./load.js";
6
- export * from "./store.js";
7
- export * from "./types.js";
package/src/db/load.ts DELETED
@@ -1,57 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { CONFIG_FILENAME, DEFAULT_CONFIG } from "./defaults.js";
5
- import type { Config } from "./types.js";
6
-
7
- // XDG Base Directory convention (macOS ignores Apple's ~/Library/Application Support
8
- // for CLI tools by common practice — gh, ripgrep-adjacent tools, etc. use ~/.config too)
9
- // Override order: $FAPONY_STATE_DIR > config.paths.stateDir > $XDG_CONFIG_HOME > ~/.config
10
- export function faponyDir(config?: Config): string {
11
- if (process.env.FAPONY_STATE_DIR) return process.env.FAPONY_STATE_DIR;
12
- if (config?.paths?.stateDir) return config.paths.stateDir;
13
- const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
14
- return join(base, "fapony");
15
- }
16
-
17
- function _dbPath(config?: Config): string {
18
- return join(faponyDir(config), "state.db");
19
- }
20
-
21
- export function configFilePath(): string {
22
- if (process.env.FAPONY_CONFIG) return process.env.FAPONY_CONFIG;
23
- return join(process.cwd(), CONFIG_FILENAME);
24
- }
25
-
26
- function freshDefaultConfig(): Config {
27
- return {
28
- ...DEFAULT_CONFIG,
29
- worktrees: { ...DEFAULT_CONFIG.worktrees },
30
- review: {
31
- ...DEFAULT_CONFIG.review,
32
- },
33
- };
34
- }
35
-
36
- export function loadConfig(configPath?: string): Config {
37
- const resolved = configPath ?? configFilePath();
38
- if (!existsSync(resolved)) return freshDefaultConfig();
39
-
40
- try {
41
- const raw = readFileSync(resolved, "utf-8");
42
- const file = JSON.parse(raw) as Partial<Config>;
43
-
44
- return {
45
- ...DEFAULT_CONFIG,
46
- ...file,
47
- review: {
48
- ...DEFAULT_CONFIG.review,
49
- ...file.review,
50
- },
51
- paths: file.paths ? { ...file.paths } : DEFAULT_CONFIG.paths,
52
- safety: file.safety ? { ...file.safety } : DEFAULT_CONFIG.safety,
53
- };
54
- } catch {
55
- return freshDefaultConfig();
56
- }
57
- }
package/src/db/types.ts DELETED
@@ -1,77 +0,0 @@
1
- export type RunStatus =
2
- | "running"
3
- | "awaiting_review"
4
- | "fixing"
5
- | "passed"
6
- | "stopped"
7
- | "stalled";
8
-
9
- export interface Run {
10
- id: number;
11
- worktree: string;
12
- plan: string | null;
13
- mem_id: string | null;
14
- status: RunStatus;
15
- base_sha: string;
16
- round: number;
17
- created_at: string;
18
- updated_at: string;
19
- }
20
-
21
- export interface Event {
22
- id: number;
23
- run_id: number;
24
- ts: string;
25
- kind: string;
26
- data: string | null;
27
- }
28
-
29
- export interface Config {
30
- worktrees: Record<string, string>;
31
- review: {
32
- maxRounds: number;
33
- };
34
- memory: {
35
- claim: string[];
36
- close: string[];
37
- add: string[];
38
- kickoff?: string[];
39
- } | null;
40
- // opt-in only — omit or leave null to keep everything local. See TELEMETRY.md
41
- // for the exact payload shape (KPI numbers + event kind/timestamp, no
42
- // plan/commit/gate-note content, ever).
43
- telemetry?: {
44
- enabled: boolean;
45
- endpoint: string;
46
- /** Self-reported metadata — advisory, not machine-observed. */
47
- metadata?: {
48
- task_category?: string;
49
- stack?: string;
50
- notes?: string;
51
- };
52
- } | null;
53
- // --- Flexible paths / limits (all optional, defaults = old hardcodes) ---
54
- paths?: {
55
- // state dir override (default: $XDG_CONFIG_HOME/fapony or ~/.config/fapony).
56
- // $FAPONY_STATE_DIR env wins over this when set.
57
- stateDir?: string;
58
- // plan/spec live in .fapony/{plan,spec} — not configurable (gitignored = private).
59
- doneDir?: string;
60
- memDir?: string;
61
- evidenceFile?: string;
62
- } | null;
63
- safety?: {
64
- // regex sources tested against the joined argv; default = the 4 git patterns.
65
- deny?: string[];
66
- } | null;
67
- usageWeb?: {
68
- port?: number;
69
- hostname?: string;
70
- ownerName?: string;
71
- } | null;
72
- }
73
-
74
- export interface RolePricing {
75
- inputPer1k: number;
76
- outputPer1k: number;
77
- }
package/src/mcp/types.ts DELETED
@@ -1,54 +0,0 @@
1
- // src/mcp/types.ts — ReasonCode enum, ToolResult, helpers
2
-
3
- // --- ReasonCode enum (locked in step 0, append-only) ---
4
-
5
- export const REASON_CODES = [
6
- "missing_test",
7
- "scope_mismatch",
8
- "unsafe_command",
9
- "spec_gap",
10
- "timeout",
11
- "blocked",
12
- "incomplete",
13
- "none",
14
- "other",
15
- ] as const;
16
-
17
- export type ReasonCode = (typeof REASON_CODES)[number];
18
-
19
- // --- RegimeCode enum (task-shape axis for model × project × regime × quality) ---
20
-
21
- export const REGIME_CODES = [
22
- "code",
23
- "fix",
24
- "review",
25
- "plan",
26
- "inquiry",
27
- "test",
28
- ] as const;
29
-
30
- export type RegimeCode = (typeof REGIME_CODES)[number];
31
-
32
- // --- Tool result types ---
33
-
34
- export interface ToolResult {
35
- content: { type: "text"; text: string }[];
36
- isError?: boolean;
37
- }
38
-
39
- export function jsonResult(data: unknown): ToolResult {
40
- return {
41
- content: [{ type: "text", text: JSON.stringify(data) }],
42
- };
43
- }
44
-
45
- export function errorResult(message: string): ToolResult {
46
- return {
47
- content: [{ type: "text", text: JSON.stringify({ error: message }) }],
48
- isError: true,
49
- };
50
- }
51
-
52
- export function parseToolResult(result: ToolResult): unknown {
53
- return JSON.parse(result.content[0].text);
54
- }
package/src/test.ts DELETED
@@ -1,2 +0,0 @@
1
- // src/test.ts — thin CLI wrapper. Actual tests live in test/.
2
- export { cmdTest } from "../test/index.js";
File without changes
File without changes