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.
Files changed (96) hide show
  1. package/README.md +87 -65
  2. package/fapony.ts +3 -116
  3. package/package.json +6 -5
  4. package/skill/move-to-done/SKILL.md +22 -32
  5. package/skill/review-pony/SKILL.md +27 -58
  6. package/src/adapters/cli.ts +123 -0
  7. package/src/adapters/hooks/compute-hint-impact.ts +107 -0
  8. package/src/adapters/hooks/context-data.ts +102 -0
  9. package/src/adapters/hooks/edit-hint.ts +195 -0
  10. package/src/adapters/hooks/git-autonomy.ts +178 -0
  11. package/src/adapters/hooks/index.ts +79 -0
  12. package/src/adapters/hooks/mv-guard.ts +52 -0
  13. package/src/adapters/hooks/read-hint.ts +383 -0
  14. package/src/adapters/hooks/session-start.ts +101 -0
  15. package/src/adapters/hooks/stop.ts +302 -0
  16. package/src/{mcp → adapters/mcp}/evidence.ts +2 -2
  17. package/src/{mcp → adapters/mcp}/primitives.ts +3 -3
  18. package/src/{mcp → adapters/mcp}/tools/check.ts +1 -1
  19. package/src/{mcp → adapters/mcp}/tools/collect.ts +1 -1
  20. package/src/{mcp → adapters/mcp}/tools/index.ts +0 -74
  21. package/src/{mcp → adapters/mcp}/tools/mem.ts +3 -3
  22. package/src/{mcp → adapters/mcp}/tools/report.ts +4 -4
  23. package/src/{mcp → adapters/mcp}/transport.ts +4 -39
  24. package/src/adapters/mcp/types.ts +18 -0
  25. package/src/{mcp → adapters/mcp}/worktree.ts +2 -2
  26. package/src/analyze.ts +2 -2
  27. package/src/conventions-seed.ts +1 -1
  28. package/src/core/config.ts +199 -0
  29. package/src/core/debt-format.ts +107 -0
  30. package/src/core/debt-types.ts +79 -0
  31. package/src/core/defaults.ts +8 -0
  32. package/src/core/enums.ts +34 -0
  33. package/src/core/format.ts +33 -0
  34. package/src/core/hint-log.ts +68 -0
  35. package/src/core/hook-helpers.ts +31 -0
  36. package/src/core/mem-log.ts +357 -0
  37. package/src/core/parse.ts +71 -0
  38. package/src/core/pricing.ts +217 -0
  39. package/src/core/safety.ts +18 -0
  40. package/src/core/types.ts +142 -0
  41. package/src/core/util.ts +105 -0
  42. package/src/db/store.ts +2 -2
  43. package/src/debt/cli.ts +1 -1
  44. package/src/debt/format.ts +2 -107
  45. package/src/debt/load.ts +1 -1
  46. package/src/debt/promotion.ts +1 -1
  47. package/src/debt/types.ts +14 -79
  48. package/src/digest/collect.ts +4 -3
  49. package/src/gate.ts +5 -5
  50. package/src/gates.ts +1 -1
  51. package/src/hook.ts +69 -1337
  52. package/src/init-mem.ts +58 -72
  53. package/src/init.ts +13 -17
  54. package/src/install/antigravity.ts +112 -0
  55. package/src/install/claude.ts +19 -123
  56. package/src/install/detect.ts +17 -7
  57. package/src/install/opencode.ts +167 -26
  58. package/src/install.ts +23 -7
  59. package/src/lint-baseline.ts +1 -2
  60. package/src/map.ts +29 -8
  61. package/src/mem/commands/plan.ts +70 -42
  62. package/src/mem/commands/read.ts +219 -146
  63. package/src/mem/index.ts +4 -13
  64. package/src/mem/store.ts +5 -1
  65. package/src/memory.ts +24 -387
  66. package/src/parse.ts +9 -71
  67. package/src/price/fetch.ts +4 -16
  68. package/src/price/resolve.ts +12 -213
  69. package/src/report/cli.ts +3 -3
  70. package/src/safety.ts +2 -18
  71. package/src/{plan-seed.ts → seed/plan-seed.ts} +18 -32
  72. package/src/seed/primitives.ts +60 -0
  73. package/src/{review-seed.ts → seed/review-seed.ts} +7 -54
  74. package/src/session/types.ts +14 -128
  75. package/src/setup.ts +1 -1
  76. package/src/stats/data.ts +4 -4
  77. package/src/telemetry.ts +3 -3
  78. package/src/usage/cache.ts +1 -2
  79. package/src/usage/cli.ts +1 -1
  80. package/src/usage/scan.ts +2 -1
  81. package/src/util.ts +10 -32
  82. package/src/web/html.ts +2 -33
  83. package/templates/SPEC.md +8 -1
  84. package/images/logo.png +0 -0
  85. package/images/logo.webp +0 -0
  86. package/images/logo@400.webp +0 -0
  87. package/images/sample.webp +0 -0
  88. package/images/summary.webp +0 -0
  89. package/src/db/defaults.ts +0 -34
  90. package/src/db/getters.ts +0 -35
  91. package/src/db/index.ts +0 -7
  92. package/src/db/load.ts +0 -57
  93. package/src/db/types.ts +0 -77
  94. package/src/math.ts +0 -13
  95. package/src/mcp/tools/verdict.ts +0 -161
  96. 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, openDb, PLAN_DIR, type Run } from "../db/index.js";
6
- import { loadConfig } from "../db/load.js";
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 "./db/index.js";
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
 
@@ -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,32 +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
- /** 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/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";
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
- Table or bullets: input → expected behavior.
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
Binary file
Binary file
Binary file
@@ -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/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
- }
@@ -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
- }