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/price/resolve.ts
CHANGED
|
@@ -1,213 +1,12 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
rates: ModelRates | null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** client prefix prepended to every model id — strip it, then look up again */
|
|
17
|
-
const CLIENT_PREFIXES = ["openrouter/", "opencode-go/", "opencode/"];
|
|
18
|
-
|
|
19
|
-
/** OpenRouter's trailing tier — :free is a genuinely free endpoint, :batch is a discount */
|
|
20
|
-
function stripTier(id: string): string {
|
|
21
|
-
return id.endsWith(":free") || id.endsWith(":batch")
|
|
22
|
-
? id.slice(0, id.lastIndexOf(":"))
|
|
23
|
-
: id;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Candidate ids ordered by specificity: exact first, broad later
|
|
28
|
-
* (an exact match always wins — the bare slug is the broadest match and goes last)
|
|
29
|
-
*/
|
|
30
|
-
export function candidateIds(provider: string, model: string): string[] {
|
|
31
|
-
const full = provider ? `${provider}/${model}` : model;
|
|
32
|
-
const out: string[] = [full];
|
|
33
|
-
let rest = full;
|
|
34
|
-
for (const p of CLIENT_PREFIXES) {
|
|
35
|
-
if (rest.startsWith(p)) {
|
|
36
|
-
rest = rest.slice(p.length);
|
|
37
|
-
out.push(rest);
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// opencode appends -free to the slug of a free model (deepseek-v4-flash-free)
|
|
42
|
-
if (rest.endsWith("-free")) out.push(rest.slice(0, -"-free".length));
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Find rates for one model — null only for local, which skips the table
|
|
48
|
-
* (local never had a price to begin with, it is not "not found")
|
|
49
|
-
*/
|
|
50
|
-
function isLocalProvider(provider: string): boolean {
|
|
51
|
-
return provider === "lmstudio_local";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function resolvePrice(
|
|
55
|
-
provider: string,
|
|
56
|
-
model: string,
|
|
57
|
-
table: PriceTable,
|
|
58
|
-
): PriceResolution {
|
|
59
|
-
if (!model || model === "(no model id)" || model === "(unknown)")
|
|
60
|
-
return { status: "unpriced", rates: null };
|
|
61
|
-
if (isLocalProvider(provider)) return { status: "free", rates: null };
|
|
62
|
-
// ends with :free or -free = used a free endpoint / the client's free model
|
|
63
|
-
// the real price is 0 (not list price), whether or not OpenRouter maps it
|
|
64
|
-
if (model.endsWith(":free") || model.endsWith("-free"))
|
|
65
|
-
return { status: "free", rates: null };
|
|
66
|
-
for (const id of candidateIds(provider, model)) {
|
|
67
|
-
const rates = table.models[id] ?? table.models[stripTier(id)];
|
|
68
|
-
if (rates) {
|
|
69
|
-
// an all-zero rate row (:free / free model) = genuinely free, not unpriced
|
|
70
|
-
if (
|
|
71
|
-
rates.input === 0 &&
|
|
72
|
-
rates.output === 0 &&
|
|
73
|
-
rates.cacheRead === 0 &&
|
|
74
|
-
(rates.cacheWrite ?? 0) === 0
|
|
75
|
-
)
|
|
76
|
-
return { status: "free", rates };
|
|
77
|
-
return { status: "priced", rates };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// zcode stores only the slug with no vendor (GLM-5.3-Flash) — compare the suffix / exact
|
|
81
|
-
// case-insensitive match (exact, not fuzzy: the whole string must be equal in length)
|
|
82
|
-
// the table side strips the tier (:free/:batch) before comparing — so a bare slug hits the :free
|
|
83
|
-
// row that is truly 0 (e.g. ling-3.0-flash-fin) and becomes free, not unpriced
|
|
84
|
-
const slug = stripTier(
|
|
85
|
-
candidateIds(provider, model).at(-1) ?? "",
|
|
86
|
-
).toLowerCase();
|
|
87
|
-
if (slug) {
|
|
88
|
-
const ids = Object.keys(table.models).sort();
|
|
89
|
-
for (const id of ids) {
|
|
90
|
-
const rawSuffix = id.includes("/")
|
|
91
|
-
? id.slice(id.lastIndexOf("/") + 1)
|
|
92
|
-
: id;
|
|
93
|
-
if (stripTier(rawSuffix).toLowerCase() === slug) {
|
|
94
|
-
const rates = table.models[id];
|
|
95
|
-
if (
|
|
96
|
-
rates.input === 0 &&
|
|
97
|
-
rates.output === 0 &&
|
|
98
|
-
rates.cacheRead === 0 &&
|
|
99
|
-
(rates.cacheWrite ?? 0) === 0
|
|
100
|
-
)
|
|
101
|
-
return { status: "free", rates };
|
|
102
|
-
return { status: "priced", rates };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return { status: "unpriced", rates: null };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export interface TokenCounts {
|
|
110
|
-
input: number;
|
|
111
|
-
cacheRead: number;
|
|
112
|
-
cacheWrite: number;
|
|
113
|
-
output: number;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Pure costing: separate rates for input / cache-read / cache-write — never one rate for all
|
|
118
|
-
*
|
|
119
|
-
* reasoning is not multiplied separately: Anthropic-family thinking is already included in output
|
|
120
|
-
* (the reader keeps it separately just for visibility); multiplying separately = double counting · cache_write absent from the table
|
|
121
|
-
* → fall back to the input rate (writing cache costs more than or equal to a fresh read, never less)
|
|
122
|
-
*/
|
|
123
|
-
export function calcCost(t: TokenCounts, rates: ModelRates): number {
|
|
124
|
-
return (
|
|
125
|
-
t.input * rates.input +
|
|
126
|
-
t.cacheRead * rates.cacheRead +
|
|
127
|
-
t.cacheWrite * (rates.cacheWrite ?? rates.input) +
|
|
128
|
-
t.output * rates.output
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface ImputedModel {
|
|
133
|
-
provider: string;
|
|
134
|
-
model: string;
|
|
135
|
-
session_count: number;
|
|
136
|
-
tokens_input: number;
|
|
137
|
-
tokens_output: number;
|
|
138
|
-
tokens_cache_read: number;
|
|
139
|
-
tokens_cache_write: number;
|
|
140
|
-
status: PriceStatus;
|
|
141
|
-
/** dollars at list-price — 0 when free/unpriced (check status, do not read the number alone) */
|
|
142
|
-
imputed_cost: number;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export interface ImputeSummary {
|
|
146
|
-
total_imputed: number;
|
|
147
|
-
priced_sessions: number;
|
|
148
|
-
free_sessions: number;
|
|
149
|
-
unpriced_sessions: number;
|
|
150
|
-
unpriced_tokens: number;
|
|
151
|
-
by_model: ImputedModel[];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Price an entire PassiveUsageResult — works on live results (stats/usage) or cache rows
|
|
156
|
-
* (usage-web) alike, because it only takes tokens per model
|
|
157
|
-
*/
|
|
158
|
-
export function imputeResult(
|
|
159
|
-
result: PassiveUsageResult,
|
|
160
|
-
table: PriceTable,
|
|
161
|
-
): ImputeSummary {
|
|
162
|
-
const by_model: ImputedModel[] = [];
|
|
163
|
-
let total_imputed = 0;
|
|
164
|
-
let priced_sessions = 0;
|
|
165
|
-
let free_sessions = 0;
|
|
166
|
-
let unpriced_sessions = 0;
|
|
167
|
-
let unpriced_tokens = 0;
|
|
168
|
-
for (const m of result.by_model as ModelBreakdown[]) {
|
|
169
|
-
const r = resolvePrice(m.provider, m.model, table);
|
|
170
|
-
let cost = 0;
|
|
171
|
-
if (r.status === "priced" && r.rates) {
|
|
172
|
-
cost = calcCost(
|
|
173
|
-
{
|
|
174
|
-
input: m.tokens_input,
|
|
175
|
-
cacheRead: m.tokens_cache_read,
|
|
176
|
-
cacheWrite: m.tokens_cache_write,
|
|
177
|
-
output: m.tokens_output,
|
|
178
|
-
},
|
|
179
|
-
r.rates,
|
|
180
|
-
);
|
|
181
|
-
total_imputed += cost;
|
|
182
|
-
priced_sessions += m.session_count;
|
|
183
|
-
} else if (r.status === "free") {
|
|
184
|
-
free_sessions += m.session_count;
|
|
185
|
-
} else {
|
|
186
|
-
unpriced_sessions += m.session_count;
|
|
187
|
-
unpriced_tokens +=
|
|
188
|
-
m.tokens_input +
|
|
189
|
-
m.tokens_output +
|
|
190
|
-
m.tokens_cache_read +
|
|
191
|
-
m.tokens_cache_write;
|
|
192
|
-
}
|
|
193
|
-
by_model.push({
|
|
194
|
-
provider: m.provider,
|
|
195
|
-
model: m.model,
|
|
196
|
-
session_count: m.session_count,
|
|
197
|
-
tokens_input: m.tokens_input,
|
|
198
|
-
tokens_output: m.tokens_output,
|
|
199
|
-
tokens_cache_read: m.tokens_cache_read,
|
|
200
|
-
tokens_cache_write: m.tokens_cache_write,
|
|
201
|
-
status: r.status,
|
|
202
|
-
imputed_cost: cost,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
total_imputed,
|
|
207
|
-
priced_sessions,
|
|
208
|
-
free_sessions,
|
|
209
|
-
unpriced_sessions,
|
|
210
|
-
unpriced_tokens,
|
|
211
|
-
by_model,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
1
|
+
// shim — moved to src/core/pricing.ts
|
|
2
|
+
export {
|
|
3
|
+
calcCost,
|
|
4
|
+
candidateIds,
|
|
5
|
+
type ImputedModel,
|
|
6
|
+
type ImputeSummary,
|
|
7
|
+
imputeResult,
|
|
8
|
+
type PriceResolution,
|
|
9
|
+
type PriceStatus,
|
|
10
|
+
resolvePrice,
|
|
11
|
+
type TokenCounts,
|
|
12
|
+
} from "../core/pricing.js";
|
package/src/report/cli.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { parseToolResult } from "../
|
|
6
|
+
import { toolVerificationReport } from "../adapters/mcp/tools/report.js";
|
|
7
|
+
import { loadConfig } from "../core/config.js";
|
|
8
|
+
import { parseToolResult } from "../core/types.js";
|
|
9
9
|
import { getStatsData } from "../stats/data.js";
|
|
10
10
|
import { renderReportHtml } from "./render.js";
|
|
11
11
|
|
package/src/safety.ts
CHANGED
|
@@ -1,18 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// ones built from config templates (memory claim/close/add, evidence
|
|
4
|
-
// collector commands, install's `claude mcp add`).
|
|
5
|
-
// Patterns come from config.safety.deny (regex sources); built-in default
|
|
6
|
-
// covers the 4 known-destructive git invocations.
|
|
7
|
-
|
|
8
|
-
import { DEFAULT_SAFETY_DENY } from "./db/index.js";
|
|
9
|
-
|
|
10
|
-
export function assertSafe(argv: string[], denySources?: string[]): void {
|
|
11
|
-
const sources = denySources ?? DEFAULT_SAFETY_DENY;
|
|
12
|
-
const joined = argv.join(" ");
|
|
13
|
-
for (const src of sources) {
|
|
14
|
-
if (new RegExp(src).test(joined)) {
|
|
15
|
-
throw new Error(`refusing to run dangerous command: ${joined}`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
1
|
+
// shim — moved to src/core/safety.ts
|
|
2
|
+
export { assertSafe } from "./core/safety.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/plan-seed.ts — `fapony plan-seed <name> [--spec] [--scope <path>]...`
|
|
1
|
+
// src/seed/plan-seed.ts — `fapony plan-seed <name> [--spec] [--scope <path>]...`
|
|
2
2
|
//
|
|
3
3
|
// Writes PLAN + SPEC straight into planDir/specDir. What it pre-fills is the
|
|
4
4
|
// structure (frontmatter, the 8 sections, prior art, ledger context) — the
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
//
|
|
26
26
|
// Composes existing producers — no new parsing, no new table, no MCP tool.
|
|
27
27
|
|
|
28
|
-
import { execSync } from "node:child_process";
|
|
29
28
|
import {
|
|
30
29
|
existsSync,
|
|
31
30
|
mkdirSync,
|
|
@@ -35,15 +34,20 @@ import {
|
|
|
35
34
|
writeFileSync,
|
|
36
35
|
} from "node:fs";
|
|
37
36
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
38
|
-
import { collectSourceFiles, isSkippedDir, SCAN_EXTS } from "
|
|
39
|
-
import { computeModelFit } from "
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
import { collectSourceFiles, isSkippedDir, SCAN_EXTS } from "../analyze.js";
|
|
38
|
+
import { computeModelFit } from "../context/projectHealth.js";
|
|
39
|
+
import {
|
|
40
|
+
CONFIG_FILENAME,
|
|
41
|
+
type Config,
|
|
42
|
+
doneDir,
|
|
43
|
+
loadConfig,
|
|
44
|
+
planDir,
|
|
45
|
+
specDir,
|
|
46
|
+
} from "../core/config.js";
|
|
47
|
+
import { extractExports } from "../map.js";
|
|
48
|
+
import { readRecentMemDecisions } from "../memory.js";
|
|
49
|
+
import { getStatsData } from "../stats/data.js";
|
|
50
|
+
import { capLines, execGit, SIG_MAX } from "./primitives.js";
|
|
47
51
|
|
|
48
52
|
// One chunk = one module's signatures — past ~40 lines a module is its own
|
|
49
53
|
// reading task, and the whole-SPEC cap below does the final trim.
|
|
@@ -56,7 +60,6 @@ const SCOPE_WARN_FILES = 300;
|
|
|
56
60
|
// Shipped plans/specs that already touched this scope. Capped low on purpose:
|
|
57
61
|
// this is a "go read that first" pointer, not a bibliography.
|
|
58
62
|
const MAX_PRIOR_ART = 5;
|
|
59
|
-
const SIG_MAX = 90;
|
|
60
63
|
// Anchor-safe slug: lowercase, non-alphanumerics → dash.
|
|
61
64
|
const slug = (s: string): string =>
|
|
62
65
|
s
|
|
@@ -224,7 +227,7 @@ next step in the same session is what rule 9 forbids.
|
|
|
224
227
|
1. _(agent fills in — each step must be verifiable)_
|
|
225
228
|
|
|
226
229
|
**Closing a step:** tick its TL;DR box with the sha · \`git commit\` this step's
|
|
227
|
-
files only ·
|
|
230
|
+
files only · then hand off:
|
|
228
231
|
|
|
229
232
|
\`\`\`bash
|
|
230
233
|
fapony mem add note "<what chunk N+1 must know>" --files <f1,f2> ${planRel}
|
|
@@ -257,16 +260,6 @@ interface Chunk {
|
|
|
257
260
|
body: string;
|
|
258
261
|
}
|
|
259
262
|
|
|
260
|
-
// review-seed's cap shape: keep the head, always say how much was cut — a
|
|
261
|
-
// silent cut is indistinguishable from "that was everything".
|
|
262
|
-
function capLines(lines: string[], cap: number, what: string): string[] {
|
|
263
|
-
if (lines.length <= cap) return lines;
|
|
264
|
-
const rest = lines.length - (cap - 1);
|
|
265
|
-
const kept = lines.slice(0, cap - 1);
|
|
266
|
-
kept.push(`… +${rest} more ${what}`);
|
|
267
|
-
return kept;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
263
|
function capChunk(c: Chunk): Chunk {
|
|
271
264
|
return {
|
|
272
265
|
...c,
|
|
@@ -530,15 +523,8 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
530
523
|
// state.db uses (git rev-parse --show-toplevel). Running from a subdir
|
|
531
524
|
// would otherwise mismatch: stats return empty, Context (fapony) always
|
|
532
525
|
// prints "(not enough graded history yet)".
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
worktree = execSync("git rev-parse --show-toplevel", {
|
|
536
|
-
encoding: "utf-8",
|
|
537
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
538
|
-
}).trim();
|
|
539
|
-
} catch {
|
|
540
|
-
worktree = cwd;
|
|
541
|
-
}
|
|
526
|
+
const root = execGit("git rev-parse --show-toplevel", cwd);
|
|
527
|
+
const worktree = root.ok ? root.output.split("\n")[0] : cwd;
|
|
542
528
|
|
|
543
529
|
// Scope: explicit paths win; default is the cwd. Resolved absolutes,
|
|
544
530
|
// deduped — the same path twice is one scope. Nested roots are pruned:
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/seed/primitives.ts — shared helpers for plan-seed + review-seed
|
|
2
|
+
//
|
|
3
|
+
// Git execution, output capping, signature formatting — the pieces both
|
|
4
|
+
// commands need. §0 rule: add-only — never remove or rename exported symbols.
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
// --- Error class ---
|
|
9
|
+
|
|
10
|
+
export class SeedError extends Error {}
|
|
11
|
+
|
|
12
|
+
// --- Git helpers ---
|
|
13
|
+
|
|
14
|
+
export function execGit(
|
|
15
|
+
cmd: string,
|
|
16
|
+
cwd: string,
|
|
17
|
+
): { ok: boolean; output: string; error?: string } {
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync(cmd, {
|
|
20
|
+
cwd,
|
|
21
|
+
encoding: "utf-8",
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
timeout: 15_000,
|
|
24
|
+
});
|
|
25
|
+
return { ok: true, output: output.trim() };
|
|
26
|
+
} catch (e: unknown) {
|
|
27
|
+
const err = e as { stderr?: string; message?: string };
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
output: "",
|
|
31
|
+
error: (err.stderr ?? err.message ?? "").trim(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function gitOk(r: { ok: boolean; error?: string }, cmd: string): void {
|
|
37
|
+
if (!r.ok)
|
|
38
|
+
throw new SeedError(`git failed: ${cmd}\n${r.error ?? "unknown error"}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Values interpolated into a git command line must be plain refs/paths —
|
|
42
|
+
// blocks shell metacharacters before execSync ever sees them.
|
|
43
|
+
const GIT_VALUE_RE = /^[A-Za-z0-9._/{}^~+-]+$/;
|
|
44
|
+
|
|
45
|
+
export function gitValue(kind: string, value: string): string {
|
|
46
|
+
if (!GIT_VALUE_RE.test(value)) {
|
|
47
|
+
throw new SeedError(`invalid ${kind}: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Output capping ---
|
|
53
|
+
|
|
54
|
+
// Keep the head, always say how much was cut — a silent cut is
|
|
55
|
+
// indistinguishable from "that was everything".
|
|
56
|
+
export { capLines } from "../core/util.js";
|
|
57
|
+
|
|
58
|
+
// --- Signature formatting ---
|
|
59
|
+
|
|
60
|
+
export const SIG_MAX = 90;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/review-seed.ts — `fapony review-seed [--staged|--commit <sha>|--range <a...b>|--files f1,f2,dir|--plan <PLAN.md>]`
|
|
1
|
+
// src/seed/review-seed.ts — `fapony review-seed [--staged|--commit <sha>|--range <a...b>|--files f1,f2,dir|--plan <PLAN.md>]`
|
|
2
2
|
//
|
|
3
3
|
// Seeds a code review with the deterministic facts of the scope the agent
|
|
4
4
|
// asked about: which files changed (per the exact git expression, echoed),
|
|
@@ -12,14 +12,13 @@
|
|
|
12
12
|
// cite rule 5 "never write into a target worktree", dropped 2026-09-17 because
|
|
13
13
|
// four commands broke it; being read-only was always a property of this
|
|
14
14
|
// command, never of that rule. Facts only: nothing here says broken/fixed
|
|
15
|
-
// — judgment lives in the reviewer and
|
|
15
|
+
// — judgment lives in the reviewer and mem log, never in
|
|
16
16
|
// this output. Deterministic: same input, same bytes, no LLM.
|
|
17
17
|
//
|
|
18
18
|
// Composes existing producers — buildGraph (analyze.ts) for importers/untested,
|
|
19
19
|
// extractExports (map.ts) for signatures. One flag = one declared git call;
|
|
20
20
|
// no magic parsing.
|
|
21
21
|
|
|
22
|
-
import { execSync } from "node:child_process";
|
|
23
22
|
import type { Stats } from "node:fs";
|
|
24
23
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
25
24
|
import { join } from "node:path";
|
|
@@ -30,9 +29,10 @@ import {
|
|
|
30
29
|
isTestedThroughBarrels,
|
|
31
30
|
isTestFile,
|
|
32
31
|
SCAN_EXTS,
|
|
33
|
-
} from "
|
|
34
|
-
import { extractBody, extractExports } from "
|
|
35
|
-
import { assertSafe } from "
|
|
32
|
+
} from "../analyze.js";
|
|
33
|
+
import { extractBody, extractExports } from "../map.js";
|
|
34
|
+
import { assertSafe } from "../safety.js";
|
|
35
|
+
import { execGit, gitOk, gitValue, SeedError, SIG_MAX } from "./primitives.js";
|
|
36
36
|
|
|
37
37
|
const WRAP_WIDTH = 88;
|
|
38
38
|
// The changed list is the review's scope boundary, not context: a file hidden
|
|
@@ -58,7 +58,6 @@ const LOOKUP_OUTPUT_CAP = 120;
|
|
|
58
58
|
// files is already past "this component area" into "the whole tree" — cut
|
|
59
59
|
// there and say so, a folder-shaped wall is not an answer either.
|
|
60
60
|
// Signature text cap per symbol (same trim as map.ts's file view).
|
|
61
|
-
const SIG_MAX = 90;
|
|
62
61
|
const DISCLAIMER =
|
|
63
62
|
"static graph only — seed is where to enter, not what is verified";
|
|
64
63
|
const USAGE =
|
|
@@ -70,52 +69,6 @@ const MAX_BODY_LINES = 80;
|
|
|
70
69
|
const MAX_CALLER_FILES = 12;
|
|
71
70
|
const MAX_CALLER_HITS = 20;
|
|
72
71
|
|
|
73
|
-
export class SeedError extends Error {}
|
|
74
|
-
|
|
75
|
-
// --- Git helpers (same shape as collect.ts execGitSafe) ---
|
|
76
|
-
|
|
77
|
-
function execGit(
|
|
78
|
-
cmd: string,
|
|
79
|
-
cwd: string,
|
|
80
|
-
): { ok: boolean; output: string; error?: string } {
|
|
81
|
-
try {
|
|
82
|
-
const output = execSync(cmd, {
|
|
83
|
-
cwd,
|
|
84
|
-
encoding: "utf-8",
|
|
85
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
86
|
-
timeout: 15_000,
|
|
87
|
-
});
|
|
88
|
-
return { ok: true, output: output.trim() };
|
|
89
|
-
} catch (e: unknown) {
|
|
90
|
-
const err = e as { stderr?: string; message?: string };
|
|
91
|
-
return {
|
|
92
|
-
ok: false,
|
|
93
|
-
output: "",
|
|
94
|
-
error: (err.stderr ?? err.message ?? "").trim(),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// A scope's primary git call failing must never masquerade as "nothing
|
|
100
|
-
// changed" — surface it. merge-base failures are tolerated (label fallback).
|
|
101
|
-
function gitOk(r: { ok: boolean; error?: string }, cmd: string): void {
|
|
102
|
-
if (!r.ok)
|
|
103
|
-
throw new SeedError(
|
|
104
|
-
`review-seed: git failed: ${cmd}\n${r.error ?? "unknown error"}`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Values interpolated into a git command line must be plain refs/paths —
|
|
109
|
-
// blocks shell metacharacters before execSync ever sees them.
|
|
110
|
-
const GIT_VALUE_RE = /^[A-Za-z0-9._/{}^~+-]+$/;
|
|
111
|
-
|
|
112
|
-
function gitValue(kind: string, value: string): string {
|
|
113
|
-
if (!GIT_VALUE_RE.test(value)) {
|
|
114
|
-
throw new SeedError(`review-seed: invalid ${kind}: ${value}`);
|
|
115
|
-
}
|
|
116
|
-
return value;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
72
|
// --- Scope flags: exactly one source of scope ---
|
|
120
73
|
|
|
121
74
|
type Scope =
|
|
@@ -233,7 +186,7 @@ function parseScope(args: string[]): Scope {
|
|
|
233
186
|
|
|
234
187
|
// --- Scope resolution: one flag = one declared git call ---
|
|
235
188
|
|
|
236
|
-
interface FileEntry {
|
|
189
|
+
export interface FileEntry {
|
|
237
190
|
path: string;
|
|
238
191
|
ins: number | null;
|
|
239
192
|
del: number | null;
|
package/src/session/types.ts
CHANGED
|
@@ -1,128 +1,14 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export interface SessionDetail {
|
|
16
|
-
session_id: string;
|
|
17
|
-
model: string;
|
|
18
|
-
steps: number;
|
|
19
|
-
tools: Record<string, number>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ToolLatencyStat {
|
|
23
|
-
count: number;
|
|
24
|
-
avgMs: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface StepTimingSummary {
|
|
28
|
-
/** step-finish row count (mirrors UsageDetail.steps for SQLite providers). */
|
|
29
|
-
steps: number;
|
|
30
|
-
/** Mean per-part duration from embedded data.time (row-timestamp fallback when absent). */
|
|
31
|
-
avgStepMs: number | null;
|
|
32
|
-
/** How many durations were actually measured (vs. steps with no time signal). */
|
|
33
|
-
stepSamples: number;
|
|
34
|
-
/** Mean per-step tokens — averages only, never summed (sums overlap). */
|
|
35
|
-
avgStepInput: number | null;
|
|
36
|
-
avgStepOutput: number | null;
|
|
37
|
-
avgStepCost: number | null;
|
|
38
|
-
/** Mean tool latency grouped by tool name (from data.state.time). */
|
|
39
|
-
toolLatencyMsByType: Record<string, ToolLatencyStat>;
|
|
40
|
-
note: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface StaleReadFile {
|
|
44
|
-
file: string;
|
|
45
|
-
/** How many distinct sessions read this file (diversity, not just count). */
|
|
46
|
-
sessions: number;
|
|
47
|
-
/** Total Read calls across those sessions. */
|
|
48
|
-
reads: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface UsageDetail {
|
|
52
|
-
/** Global tool-call counts across the filtered sessions (activity signal, not quality). */
|
|
53
|
-
tool_breakdown: Record<string, number>;
|
|
54
|
-
/** Context bytes per tool — size of tool_result content blocks. */
|
|
55
|
-
bytes_by_tool?: Record<string, number>;
|
|
56
|
-
/** Total step-finish parts across the filtered sessions. */
|
|
57
|
-
steps: number;
|
|
58
|
-
/** Per-session breakdown (SQL-aggregated, never raw part rows). */
|
|
59
|
-
by_session: SessionDetail[];
|
|
60
|
-
/**
|
|
61
|
-
* Files read across 2+ sessions in this window and never edited/written —
|
|
62
|
-
* candidates for pasting into CLAUDE.md instead of re-reading every
|
|
63
|
-
* session. Top 20 by session count. Needs a wide `since` window (many
|
|
64
|
-
* sessions) before the pattern means anything — noise below that.
|
|
65
|
-
*/
|
|
66
|
-
stale_reads?: StaleReadFile[];
|
|
67
|
-
/**
|
|
68
|
-
* Step-token sums are NOT reported: per-step tokens overlap (each step
|
|
69
|
-
* carries the full context window), so SUM(step tokens) >> session tokens.
|
|
70
|
-
* Verified on real data — see testSessionDetailStepTokensNotSummed.
|
|
71
|
-
*/
|
|
72
|
-
note: string;
|
|
73
|
-
/** Per-step timing/token/latency signal — present only when detail:true was requested. */
|
|
74
|
-
timing?: StepTimingSummary | null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface PassiveUsageResult {
|
|
78
|
-
total_tokens_input: number;
|
|
79
|
-
total_tokens_output: number;
|
|
80
|
-
total_tokens_reasoning: number;
|
|
81
|
-
total_tokens_cache_read: number;
|
|
82
|
-
total_tokens_cache_write: number;
|
|
83
|
-
total_cost: number;
|
|
84
|
-
session_count: number;
|
|
85
|
-
by_model: ModelBreakdown[];
|
|
86
|
-
/** Present only when detail:true was requested (additive, default absent). */
|
|
87
|
-
detail?: UsageDetail | null;
|
|
88
|
-
/** ZCode sessions, when available. Absent when ZCode DB not found. */
|
|
89
|
-
zcode?: PassiveUsageResult | null;
|
|
90
|
-
/** Claude Code sessions, when available. Absent when projects dir not found. */
|
|
91
|
-
claude_code?: PassiveUsageResult | null;
|
|
92
|
-
/**
|
|
93
|
-
* Why this read came back short, when it did. Absent on success.
|
|
94
|
-
*
|
|
95
|
-
* Zeros with no `error` mean "no sessions matched"; zeros WITH an error mean
|
|
96
|
-
* "could not read the client's log" — most often schema drift after the
|
|
97
|
-
* client updated. The two used to be indistinguishable: a renamed column
|
|
98
|
-
* made the query throw, the catch-all swallowed it, and the numbers just
|
|
99
|
-
* quietly went missing.
|
|
100
|
-
*/
|
|
101
|
-
error?: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const EMPTY_RESULT: PassiveUsageResult = {
|
|
105
|
-
total_tokens_input: 0,
|
|
106
|
-
total_tokens_output: 0,
|
|
107
|
-
total_tokens_reasoning: 0,
|
|
108
|
-
total_tokens_cache_read: 0,
|
|
109
|
-
total_tokens_cache_write: 0,
|
|
110
|
-
total_cost: 0,
|
|
111
|
-
session_count: 0,
|
|
112
|
-
by_model: [],
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/** Common shape every client reader implements — see registry.ts. */
|
|
116
|
-
export type PassiveUsageReader = (
|
|
117
|
-
worktree?: string,
|
|
118
|
-
since?: number,
|
|
119
|
-
until?: number,
|
|
120
|
-
detail?: boolean,
|
|
121
|
-
full?: boolean,
|
|
122
|
-
) => PassiveUsageResult;
|
|
123
|
-
|
|
124
|
-
export const STEP_TOKENS_NOTE =
|
|
125
|
-
"step tokens overlap (per-step context window) — SUM(step tokens) != session tokens; steps is a count only";
|
|
126
|
-
|
|
127
|
-
export const TIMING_NOTE =
|
|
128
|
-
"timing from embedded part fields only (data.time/data.state.time/step-finish tokens) with row-timestamp fallback; averages, never raw I/O";
|
|
1
|
+
// shim — moved to src/core/types.ts
|
|
2
|
+
export {
|
|
3
|
+
EMPTY_RESULT,
|
|
4
|
+
type ModelBreakdown,
|
|
5
|
+
type PassiveUsageReader,
|
|
6
|
+
type PassiveUsageResult,
|
|
7
|
+
type SessionDetail,
|
|
8
|
+
STEP_TOKENS_NOTE,
|
|
9
|
+
type StaleReadFile,
|
|
10
|
+
type StepTimingSummary,
|
|
11
|
+
TIMING_NOTE,
|
|
12
|
+
type ToolLatencyStat,
|
|
13
|
+
type UsageDetail,
|
|
14
|
+
} from "../core/types.js";
|
package/src/setup.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, statSync, writeFileSync } from "node:fs";
|
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { seedConventionsFile } from "./conventions-seed.js";
|
|
9
|
-
import { CONFIG_FILENAME, FAPONY_DIR } from "./
|
|
9
|
+
import { CONFIG_FILENAME, FAPONY_DIR } from "./core/config.js";
|
|
10
10
|
import { initProject } from "./init.js";
|
|
11
11
|
import { isAffirmative } from "./util.js";
|
|
12
12
|
|