fapony 0.3.0 → 0.3.4
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 +87 -65
- package/fapony.ts +3 -116
- package/package.json +6 -5
- package/skill/move-to-done/SKILL.md +22 -32
- package/skill/review-pony/SKILL.md +27 -58
- package/src/adapters/cli.ts +123 -0
- package/src/adapters/hooks/compute-hint-impact.ts +107 -0
- package/src/adapters/hooks/context-data.ts +102 -0
- package/src/adapters/hooks/edit-hint.ts +195 -0
- package/src/adapters/hooks/git-autonomy.ts +178 -0
- package/src/adapters/hooks/index.ts +79 -0
- package/src/adapters/hooks/mv-guard.ts +52 -0
- package/src/adapters/hooks/read-hint.ts +383 -0
- package/src/adapters/hooks/session-start.ts +101 -0
- package/src/adapters/hooks/stop.ts +302 -0
- package/src/{mcp → adapters/mcp}/evidence.ts +2 -2
- package/src/{mcp → adapters/mcp}/primitives.ts +3 -3
- package/src/{mcp → adapters/mcp}/tools/check.ts +1 -1
- package/src/{mcp → adapters/mcp}/tools/collect.ts +1 -1
- package/src/{mcp → adapters/mcp}/tools/index.ts +0 -74
- package/src/{mcp → adapters/mcp}/tools/mem.ts +3 -3
- package/src/{mcp → adapters/mcp}/tools/report.ts +4 -4
- package/src/{mcp → adapters/mcp}/transport.ts +4 -39
- package/src/adapters/mcp/types.ts +18 -0
- package/src/{mcp → adapters/mcp}/worktree.ts +2 -2
- package/src/analyze.ts +2 -2
- package/src/conventions-seed.ts +1 -1
- package/src/core/config.ts +199 -0
- package/src/core/debt-format.ts +107 -0
- package/src/core/debt-types.ts +79 -0
- package/src/core/defaults.ts +8 -0
- package/src/core/enums.ts +34 -0
- package/src/core/format.ts +33 -0
- package/src/core/hint-log.ts +68 -0
- package/src/core/hook-helpers.ts +31 -0
- package/src/core/mem-log.ts +357 -0
- package/src/core/parse.ts +71 -0
- package/src/core/pricing.ts +217 -0
- package/src/core/safety.ts +18 -0
- package/src/core/types.ts +142 -0
- package/src/core/util.ts +105 -0
- package/src/db/store.ts +2 -2
- package/src/debt/cli.ts +1 -1
- package/src/debt/format.ts +2 -107
- package/src/debt/load.ts +1 -1
- package/src/debt/promotion.ts +1 -1
- package/src/debt/types.ts +14 -79
- package/src/digest/collect.ts +4 -3
- package/src/gate.ts +5 -5
- package/src/gates.ts +1 -1
- package/src/hook.ts +69 -1337
- package/src/init-mem.ts +58 -72
- package/src/init.ts +13 -17
- package/src/install/antigravity.ts +112 -0
- package/src/install/claude.ts +19 -123
- package/src/install/detect.ts +17 -7
- package/src/install/opencode.ts +167 -26
- package/src/install.ts +23 -7
- package/src/lint-baseline.ts +1 -2
- package/src/map.ts +29 -8
- package/src/mem/commands/plan.ts +70 -42
- package/src/mem/commands/read.ts +219 -146
- package/src/mem/index.ts +4 -13
- package/src/mem/store.ts +5 -1
- package/src/memory.ts +24 -387
- package/src/parse.ts +9 -71
- package/src/price/fetch.ts +4 -16
- package/src/price/resolve.ts +12 -213
- package/src/report/cli.ts +3 -3
- package/src/safety.ts +2 -18
- package/src/{plan-seed.ts → seed/plan-seed.ts} +18 -32
- package/src/seed/primitives.ts +60 -0
- package/src/{review-seed.ts → seed/review-seed.ts} +7 -54
- package/src/session/types.ts +14 -128
- package/src/setup.ts +1 -1
- package/src/stats/data.ts +4 -4
- package/src/telemetry.ts +3 -3
- package/src/usage/cache.ts +1 -2
- package/src/usage/cli.ts +1 -1
- package/src/usage/scan.ts +2 -1
- package/src/util.ts +10 -32
- package/src/web/html.ts +2 -33
- package/templates/SPEC.md +8 -1
- package/images/logo.png +0 -0
- package/images/logo.webp +0 -0
- package/images/logo@400.webp +0 -0
- package/images/sample.webp +0 -0
- package/images/summary.webp +0 -0
- package/src/db/defaults.ts +0 -34
- package/src/db/getters.ts +0 -35
- package/src/db/index.ts +0 -7
- package/src/db/load.ts +0 -57
- package/src/db/types.ts +0 -77
- package/src/math.ts +0 -13
- package/src/mcp/tools/verdict.ts +0 -161
- package/src/mcp/types.ts +0 -54
package/src/stats/data.ts
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { readdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { type Event,
|
|
6
|
-
import {
|
|
5
|
+
import { type Event, loadConfig, PLAN_DIR, type Run } from "../core/config.js";
|
|
6
|
+
import { REASON_CODES, REGIME_CODES } from "../core/enums.js";
|
|
7
|
+
import { openDb } from "../db/store.js";
|
|
7
8
|
import { enrichGateWindows } from "../gates.js";
|
|
8
|
-
import { avg, minutesBetween } from "../math.js";
|
|
9
|
-
import { REASON_CODES, REGIME_CODES } from "../mcp/types.js";
|
|
10
9
|
import {
|
|
11
10
|
isPassFamily,
|
|
12
11
|
qualityScore,
|
|
@@ -20,6 +19,7 @@ import {
|
|
|
20
19
|
readPassiveUsage,
|
|
21
20
|
readZcodeUsage,
|
|
22
21
|
} from "../session/index.js";
|
|
22
|
+
import { avg, minutesBetween } from "../util.js";
|
|
23
23
|
|
|
24
24
|
// Walks events per run in order and pairs up spawn→route (executor time)
|
|
25
25
|
// and route→gate (review turnaround) per round, since one run row can span
|
package/src/telemetry.ts
CHANGED
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
type Config,
|
|
14
14
|
type Event,
|
|
15
15
|
loadConfig,
|
|
16
|
-
openDb,
|
|
17
16
|
type Run,
|
|
18
|
-
} from "./
|
|
17
|
+
} from "./core/config.js";
|
|
18
|
+
import { openDb } from "./db/store.js";
|
|
19
19
|
import { enrichGateWindows } from "./gates.js";
|
|
20
|
-
import { avg, minutesBetween } from "./math.js";
|
|
21
20
|
import { readPassiveUsage } from "./session/index.js";
|
|
21
|
+
import { avg, minutesBetween } from "./util.js";
|
|
22
22
|
|
|
23
23
|
// ─── Schema version ────────────────────────────────────────────────────
|
|
24
24
|
|
package/src/usage/cache.ts
CHANGED
|
@@ -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 "../
|
|
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 "../
|
|
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
|
|
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,32 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
/** True for "y"/"yes" (case-insensitive, trimmed) — the only affirmative answers. */
|
|
29
|
-
export function isAffirmative(answer: string): boolean {
|
|
30
|
-
const normalized = answer.trim().toLowerCase();
|
|
31
|
-
return normalized === "y" || normalized === "yes";
|
|
32
|
-
}
|
|
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/
|
|
2
|
-
|
|
3
|
-
/** HTML-escape interpolated strings. */
|
|
4
|
-
export function esc(s: string): string {
|
|
5
|
-
return s
|
|
6
|
-
.replaceAll("&", "&")
|
|
7
|
-
.replaceAll("<", "<")
|
|
8
|
-
.replaceAll(">", ">")
|
|
9
|
-
.replaceAll('"', """);
|
|
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";
|
package/templates/SPEC.md
CHANGED
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## Non-goals
|
|
18
|
+
What this explicitly does **not** do, one line of why each. Widening scope is an
|
|
19
|
+
agent's default; a spec that only says what to build never stopped anything.
|
|
20
|
+
|
|
17
21
|
## Shape (data / API / schema)
|
|
18
22
|
Concrete types, request/response bodies, DB columns — whatever the code needs.
|
|
19
23
|
|
|
20
24
|
## Edge cases
|
|
21
|
-
|
|
25
|
+
Three columns: input → expected → **verify** (a command that actually runs).
|
|
26
|
+
An edge case with no check after it is an opinion, not a requirement — and a
|
|
27
|
+
verify built from a substring of the prose is not a verify (real case: `grep "sed"`
|
|
28
|
+
matched "superseded", so the check passed while the work was missing).
|
|
22
29
|
|
|
23
30
|
## Examples
|
|
24
31
|
Before / after, request / response, sample payloads — as long as it needs to be.
|
package/images/logo.png
DELETED
|
Binary file
|
package/images/logo.webp
DELETED
|
Binary file
|
package/images/logo@400.webp
DELETED
|
Binary file
|
package/images/sample.webp
DELETED
|
Binary file
|
package/images/summary.webp
DELETED
|
Binary file
|
package/src/db/defaults.ts
DELETED
|
@@ -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
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/math.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// src/math.ts — pure numeric helpers shared across modules
|
|
2
|
-
|
|
3
|
-
/** Minutes between two ISO-like timestamps (space separator, UTC assumed). */
|
|
4
|
-
export function minutesBetween(a: string, b: string): number {
|
|
5
|
-
const t0 = new Date(`${a.replace(" ", "T")}Z`).getTime();
|
|
6
|
-
const t1 = new Date(`${b.replace(" ", "T")}Z`).getTime();
|
|
7
|
-
return (t1 - t0) / 60000;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Arithmetic mean of a numeric array. 0 for empty arrays. */
|
|
11
|
-
export function avg(xs: number[]): number {
|
|
12
|
-
return xs.length ? xs.reduce((s, x) => s + x, 0) / xs.length : 0;
|
|
13
|
-
}
|
package/src/mcp/tools/verdict.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
// src/mcp/tools/verdict.ts — verdict_submit tool
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
findOpenRun,
|
|
5
|
-
findOpenRunWithNullPlan,
|
|
6
|
-
getRun,
|
|
7
|
-
newRun,
|
|
8
|
-
openDb,
|
|
9
|
-
patchLastGateEvent,
|
|
10
|
-
} from "../../db/index.js";
|
|
11
|
-
import { gateOnce } from "../../gate.js";
|
|
12
|
-
import { VERDICT_GRADES, type VerdictGrade } from "../../parse.js";
|
|
13
|
-
import {
|
|
14
|
-
errorResult,
|
|
15
|
-
jsonResult,
|
|
16
|
-
REASON_CODES,
|
|
17
|
-
REGIME_CODES,
|
|
18
|
-
type ReasonCode,
|
|
19
|
-
type RegimeCode,
|
|
20
|
-
type ToolResult,
|
|
21
|
-
} from "../types.js";
|
|
22
|
-
import { resolveWorktreeArg } from "../worktree.js";
|
|
23
|
-
|
|
24
|
-
// --- Tool implementation ---
|
|
25
|
-
|
|
26
|
-
export function toolVerdictSubmit(args: Record<string, unknown>): ToolResult {
|
|
27
|
-
const {
|
|
28
|
-
run_id,
|
|
29
|
-
verdict,
|
|
30
|
-
reason_code,
|
|
31
|
-
regime,
|
|
32
|
-
note,
|
|
33
|
-
worktree,
|
|
34
|
-
plan,
|
|
35
|
-
session_id,
|
|
36
|
-
files,
|
|
37
|
-
} = args;
|
|
38
|
-
|
|
39
|
-
if (typeof verdict !== "string" || !VERDICT_GRADES.has(verdict)) {
|
|
40
|
-
return errorResult(
|
|
41
|
-
`verdict must be one of: ${[...VERDICT_GRADES].join(", ")}`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
const grade = verdict as VerdictGrade;
|
|
45
|
-
if (!REASON_CODES.includes(reason_code as ReasonCode)) {
|
|
46
|
-
return errorResult(
|
|
47
|
-
`reason_code must be one of: ${REASON_CODES.join(", ")}`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (reason_code === "other" && (!note || typeof note !== "string")) {
|
|
51
|
-
return errorResult("reason_code 'other' requires a note");
|
|
52
|
-
}
|
|
53
|
-
if (!REGIME_CODES.includes(regime as RegimeCode)) {
|
|
54
|
-
return errorResult(`regime must be one of: ${REGIME_CODES.join(", ")}`);
|
|
55
|
-
}
|
|
56
|
-
const regimeCode = regime as RegimeCode;
|
|
57
|
-
|
|
58
|
-
const db = openDb();
|
|
59
|
-
|
|
60
|
-
// Resolve or create run_id
|
|
61
|
-
let resolvedRunId: number;
|
|
62
|
-
if (typeof run_id === "number" && Number.isInteger(run_id)) {
|
|
63
|
-
const run = getRun(db, run_id);
|
|
64
|
-
if (!run) {
|
|
65
|
-
return jsonResult({ stored: false, error: "run not found" });
|
|
66
|
-
}
|
|
67
|
-
resolvedRunId = run_id;
|
|
68
|
-
} else {
|
|
69
|
-
// Bind to the latest still-open run for the same worktree+plan so a
|
|
70
|
-
// round-2+ verdict lands on the original row (round keeps counting and
|
|
71
|
-
// review.maxRounds can actually trigger). Only when no open run matches
|
|
72
|
-
// is a fresh row created. plan=null always opens a new run (no guess).
|
|
73
|
-
// Free-text plans are normalized (trim+lowercase) so "Fix Login" and
|
|
74
|
-
// "fix login" bind to the same run. When no exact match exists, falls
|
|
75
|
-
// back to any open run with plan=null (the "no PLAN file" flow).
|
|
76
|
-
const resolvedWorktree =
|
|
77
|
-
typeof worktree === "string" && worktree
|
|
78
|
-
? resolveWorktreeArg(worktree)
|
|
79
|
-
: "mcp-external";
|
|
80
|
-
const resolvedPlan =
|
|
81
|
-
typeof plan === "string" && plan ? plan.trim().toLowerCase() : null;
|
|
82
|
-
// BEGIN IMMEDIATE makes the find-then-create below atomic across
|
|
83
|
-
// concurrent MCP processes: without it, two agents closing the same
|
|
84
|
-
// worktree+plan at once can both see "no open run" and each insert a
|
|
85
|
-
// row. The write lock taken here blocks (busy_timeout, not throws) the
|
|
86
|
-
// second caller until the first commits, so it re-reads and finds the
|
|
87
|
-
// row already there instead of duplicating it.
|
|
88
|
-
db.run("BEGIN IMMEDIATE");
|
|
89
|
-
try {
|
|
90
|
-
let open = resolvedPlan
|
|
91
|
-
? findOpenRun(db, resolvedWorktree, resolvedPlan)
|
|
92
|
-
: null;
|
|
93
|
-
// Fallback: if no exact match and plan is non-null, try any open run with
|
|
94
|
-
// plan=null. This lets a verdict with free-text intent bind to a run that
|
|
95
|
-
// was created without a plan (the common "no PLAN file" flow).
|
|
96
|
-
if (!open && resolvedPlan) {
|
|
97
|
-
open = findOpenRunWithNullPlan(db, resolvedWorktree);
|
|
98
|
-
}
|
|
99
|
-
if (open) {
|
|
100
|
-
resolvedRunId = open.id;
|
|
101
|
-
} else {
|
|
102
|
-
// worktree/plan let callers (e.g. move-to-done) attribute the verdict
|
|
103
|
-
// so byReasonCode/bestPassing aggregate correctly instead of collapsing
|
|
104
|
-
// into "mcp-external".
|
|
105
|
-
resolvedRunId = newRun(db, resolvedWorktree, resolvedPlan, null, "mcp");
|
|
106
|
-
}
|
|
107
|
-
db.run("COMMIT");
|
|
108
|
-
} catch (e) {
|
|
109
|
-
db.run("ROLLBACK");
|
|
110
|
-
throw e;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Normalize files: must be a non-empty array of strings.
|
|
115
|
-
const resolvedFiles =
|
|
116
|
-
Array.isArray(files) && files.length > 0
|
|
117
|
-
? files.filter((f): f is string => typeof f === "string" && f.length > 0)
|
|
118
|
-
: undefined;
|
|
119
|
-
|
|
120
|
-
// Route through gateOnce for consistent status/round/memory handling.
|
|
121
|
-
const mcpNote =
|
|
122
|
-
typeof note === "string" && note
|
|
123
|
-
? `[${reason_code}] ${note}`
|
|
124
|
-
: `[${reason_code}]`;
|
|
125
|
-
const result = gateOnce(resolvedRunId, grade, mcpNote, resolvedFiles);
|
|
126
|
-
|
|
127
|
-
if (result.error) {
|
|
128
|
-
return jsonResult({
|
|
129
|
-
stored: false,
|
|
130
|
-
error: result.error,
|
|
131
|
-
run_id: resolvedRunId,
|
|
132
|
-
status: result.status,
|
|
133
|
-
round: result.round,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Patch the gate event with MCP-specific fields (reason_code, source).
|
|
138
|
-
// session_id lets gates.ts resolve model from client session logs when the
|
|
139
|
-
// window has no spawn events (the old execute→review loop that wrote spawns
|
|
140
|
-
// is gone). Optional — agents that can't expose it just omit it.
|
|
141
|
-
const patch: Record<string, unknown> = {
|
|
142
|
-
reason_code,
|
|
143
|
-
regime: regimeCode,
|
|
144
|
-
source: "mcp",
|
|
145
|
-
};
|
|
146
|
-
if (typeof session_id === "string" && session_id) {
|
|
147
|
-
patch.session_id = session_id;
|
|
148
|
-
}
|
|
149
|
-
patchLastGateEvent(db, resolvedRunId, patch);
|
|
150
|
-
|
|
151
|
-
return jsonResult({
|
|
152
|
-
stored: true,
|
|
153
|
-
run_id: resolvedRunId,
|
|
154
|
-
verdict: grade,
|
|
155
|
-
reason_code,
|
|
156
|
-
regime: regimeCode,
|
|
157
|
-
status: result.status,
|
|
158
|
-
round: result.round,
|
|
159
|
-
...(resolvedFiles ? { files: resolvedFiles } : {}),
|
|
160
|
-
});
|
|
161
|
-
}
|
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
|
-
}
|