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.
- package/README.md +7 -1
- package/fapony.ts +3 -116
- package/package.json +5 -6
- package/skill/move-to-done/SKILL.md +3 -2
- package/src/adapters/cli.ts +128 -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 +80 -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 +434 -0
- package/src/{mcp → adapters/mcp}/evidence.ts +2 -2
- package/src/{mcp → adapters/mcp}/primitives.ts +2 -2
- 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/mem.ts +3 -3
- package/src/{mcp → adapters/mcp}/tools/report.ts +4 -4
- package/src/adapters/mcp/types.ts +18 -0
- package/src/{mcp → adapters/mcp}/worktree.ts +1 -1
- package/src/analyze.ts +1 -1
- package/src/commands.ts +180 -0
- 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 +2 -2
- package/src/gates.ts +1 -1
- package/src/hook.ts +70 -1424
- package/src/init-mem.ts +1 -1
- package/src/init.ts +1 -1
- package/src/install/claude.ts +17 -0
- package/src/install/opencode.ts +114 -0
- package/src/install.ts +12 -5
- package/src/lint-baseline.ts +1 -2
- package/src/map.ts +29 -8
- package/src/mem/commands/plan.ts +197 -42
- package/src/mem/commands/read.ts +53 -12
- package/src/mem/index.ts +60 -1
- package/src/mem/store.ts +5 -1
- package/src/memory.ts +26 -388
- 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/seed/plan-seed.ts +126 -32
- package/src/seed/primitives.ts +1 -7
- package/src/session/types.ts +14 -128
- package/src/setup.ts +1 -1
- package/src/stats/data.ts +3 -3
- package/src/telemetry.ts +2 -2
- package/src/update.ts +1 -1
- 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 -93
- package/src/web/html.ts +2 -33
- 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/mcp/types.ts +0 -54
- package/src/test.ts +0 -2
- /package/src/{mcp → adapters/mcp}/tools/index.ts +0 -0
- /package/src/{mcp → adapters/mcp}/transport.ts +0 -0
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";
|
package/src/seed/plan-seed.ts
CHANGED
|
@@ -35,14 +35,16 @@ import {
|
|
|
35
35
|
} from "node:fs";
|
|
36
36
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
37
37
|
import { collectSourceFiles, isSkippedDir, SCAN_EXTS } from "../analyze.js";
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
import {
|
|
39
|
+
CONFIG_FILENAME,
|
|
40
|
+
type Config,
|
|
41
|
+
doneDir,
|
|
42
|
+
loadConfig,
|
|
43
|
+
planDir,
|
|
44
|
+
specDir,
|
|
45
|
+
} from "../core/config.js";
|
|
43
46
|
import { extractExports } from "../map.js";
|
|
44
47
|
import { readRecentMemDecisions } from "../memory.js";
|
|
45
|
-
import { getStatsData } from "../stats/data.js";
|
|
46
48
|
import { capLines, execGit, SIG_MAX } from "./primitives.js";
|
|
47
49
|
|
|
48
50
|
// One chunk = one module's signatures — past ~40 lines a module is its own
|
|
@@ -56,6 +58,14 @@ const SCOPE_WARN_FILES = 300;
|
|
|
56
58
|
// Shipped plans/specs that already touched this scope. Capped low on purpose:
|
|
57
59
|
// this is a "go read that first" pointer, not a bibliography.
|
|
58
60
|
const MAX_PRIOR_ART = 5;
|
|
61
|
+
// Chunk 4 (PLAN-seed-and-surface): the PLAN names what is already in scope —
|
|
62
|
+
// one line per scope file with its export names. SPEC-only seeds never gave
|
|
63
|
+
// PLAN-only readers this pointer, so agents re-derived what export-lines.ts
|
|
64
|
+
// already knew. Capped low: a pointer, not a signature dump.
|
|
65
|
+
const MAX_EXISTING_IN_SCOPE = 15;
|
|
66
|
+
// The PLAN ≤ ~60 contract (§3.1) predates this block — the block yields to it,
|
|
67
|
+
// never grows it. Effective block cap = min(block cap, remaining budget).
|
|
68
|
+
const MAX_PLAN_LINES = 60;
|
|
59
69
|
// Anchor-safe slug: lowercase, non-alphanumerics → dash.
|
|
60
70
|
const slug = (s: string): string =>
|
|
61
71
|
s
|
|
@@ -145,7 +155,84 @@ function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
|
|
|
145
155
|
return shown.join("\n");
|
|
146
156
|
}
|
|
147
157
|
|
|
148
|
-
//
|
|
158
|
+
// Chunk 5 (PLAN-seed-and-surface): stdout ends with the plans that already
|
|
159
|
+
// exist — the seed that lands next to a shipped decision without knowing it
|
|
160
|
+
// is the expensive mistake (§8 prior art guards the file, this guards the
|
|
161
|
+
// glance). Active plans first (the ones a new seed must not duplicate),
|
|
162
|
+
// then shipped; capped like every other list here.
|
|
163
|
+
const MAX_PLAN_LIST = 10;
|
|
164
|
+
|
|
165
|
+
function listExistingPlans(
|
|
166
|
+
cwd: string,
|
|
167
|
+
config: Config,
|
|
168
|
+
exclude: string,
|
|
169
|
+
): string[] {
|
|
170
|
+
const items: string[] = [];
|
|
171
|
+
for (const [dir, where] of [
|
|
172
|
+
[planDir(), "plan"],
|
|
173
|
+
[doneDir(config), "done"],
|
|
174
|
+
] as const) {
|
|
175
|
+
let names: string[];
|
|
176
|
+
try {
|
|
177
|
+
names = readdirSync(join(cwd, dir))
|
|
178
|
+
.filter((n) => n.endsWith(".md"))
|
|
179
|
+
.sort();
|
|
180
|
+
} catch {
|
|
181
|
+
continue; // dir missing — nothing seeded yet
|
|
182
|
+
}
|
|
183
|
+
for (const n of names) {
|
|
184
|
+
if (n === exclude) continue; // the file just written, not "existing"
|
|
185
|
+
items.push(`- ${n} (${where})`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (items.length === 0) return ["- (none yet)"];
|
|
189
|
+
return capLines(items, MAX_PLAN_LIST, "plans");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Context (fapony): mem decisions + existing in scope ---
|
|
193
|
+
//
|
|
194
|
+
// No ledger-ranking line here (PLAN-seed-and-surface chunk 6): the ledger is
|
|
195
|
+
// frozen and Positioning rule 2 forbids cross-model ranking claims, so a
|
|
196
|
+
// seeded pointer at it teaches the reader to cite what cannot be cited.
|
|
197
|
+
// computeModelFit() itself stays — `fapony stats` reads it.
|
|
198
|
+
|
|
199
|
+
// One line per scope file naming its exports — `src/debt/scan.ts —
|
|
200
|
+
// scanDebt() · DebtHit`. Files with no exports (or unreadable) are skipped:
|
|
201
|
+
// a pointer lists what is there, not what is not. Sorted for determinism.
|
|
202
|
+
function renderExistingInScope(
|
|
203
|
+
roots: string[],
|
|
204
|
+
cwd: string,
|
|
205
|
+
scoped: boolean,
|
|
206
|
+
cap: number,
|
|
207
|
+
): string[] {
|
|
208
|
+
// No --scope means every file in the repo matches — a list of everything
|
|
209
|
+
// points at nothing (§8 prior art goes quiet for the same reason).
|
|
210
|
+
if (!scoped)
|
|
211
|
+
return ["- _(no --scope — re-seed with --scope <dir> to list exports)_"];
|
|
212
|
+
const abs: string[] = [];
|
|
213
|
+
for (const r of roots) for (const f of scopeSourceFiles(r)) abs.push(f);
|
|
214
|
+
const rels = [...new Set(abs.map((f) => relative(cwd, f) || "."))].sort();
|
|
215
|
+
const lines: string[] = [];
|
|
216
|
+
for (const rel of rels) {
|
|
217
|
+
let source: string;
|
|
218
|
+
try {
|
|
219
|
+
source = readFileSync(join(cwd, rel), "utf-8");
|
|
220
|
+
} catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const scan = extractExports(source);
|
|
224
|
+
if (scan.error || scan.symbols.length === 0) continue;
|
|
225
|
+
// Re-export-only files scan as one `*` per line — dedupe to a single `*`.
|
|
226
|
+
const names = [
|
|
227
|
+
...new Set(
|
|
228
|
+
scan.symbols.map((s) => (s.kind === "fn" ? `${s.name}()` : s.name)),
|
|
229
|
+
),
|
|
230
|
+
];
|
|
231
|
+
lines.push(`- ${rel} — ${names.join(" · ")}`);
|
|
232
|
+
}
|
|
233
|
+
if (lines.length === 0) return ["- _(no exports in scope)_"];
|
|
234
|
+
return capLines(lines, cap, "files in scope (narrow with --scope <path>)");
|
|
235
|
+
}
|
|
149
236
|
|
|
150
237
|
function renderContextFapony(worktree: string): string {
|
|
151
238
|
const lines: string[] = [];
|
|
@@ -160,17 +247,6 @@ function renderContextFapony(worktree: string): string {
|
|
|
160
247
|
.join(" · ")}`
|
|
161
248
|
: "- Decisions on record (mem): _(none — no mem log or empty)_",
|
|
162
249
|
);
|
|
163
|
-
const fits = computeModelFit(getStatsData().byRegime, worktree);
|
|
164
|
-
lines.push(
|
|
165
|
-
fits.length > 0
|
|
166
|
-
? `- Model fit (ledger, min N=5): ${fits
|
|
167
|
-
.map(
|
|
168
|
-
(f) =>
|
|
169
|
-
`${f.regime} → ${f.model} (N=${f.gates}, fail ${(f.failRate * 100).toFixed(0)}%)`,
|
|
170
|
-
)
|
|
171
|
-
.join(" · ")}`
|
|
172
|
-
: "- Model fit: _(not enough graded history yet)_",
|
|
173
|
-
);
|
|
174
250
|
return lines.join("\n");
|
|
175
251
|
}
|
|
176
252
|
|
|
@@ -180,6 +256,7 @@ function planTemplate(
|
|
|
180
256
|
name: string,
|
|
181
257
|
priorArt: string,
|
|
182
258
|
contextFapony: string,
|
|
259
|
+
existingScope: string[],
|
|
183
260
|
specLink: string | null,
|
|
184
261
|
planRel: string,
|
|
185
262
|
): string {
|
|
@@ -200,6 +277,8 @@ status: active
|
|
|
200
277
|
|
|
201
278
|
## Context (fapony)
|
|
202
279
|
${contextFapony}
|
|
280
|
+
### Existing in scope
|
|
281
|
+
${existingScope.join("\n")}
|
|
203
282
|
|
|
204
283
|
## 1. Goal (why)
|
|
205
284
|
_(agent fills in)_
|
|
@@ -217,19 +296,11 @@ _(agent fills in)_
|
|
|
217
296
|
_(agent fills in)_
|
|
218
297
|
|
|
219
298
|
## 6. Steps (what in which order)
|
|
220
|
-
One step = one chunk = one session: finish it, close it, **stop** — starting the
|
|
221
|
-
next step in the same session is what rule 9 forbids.
|
|
299
|
+
One step = one chunk = one session: finish it, close it, **stop** — starting the next step in the same session is what rule 9 forbids.
|
|
222
300
|
|
|
223
301
|
1. _(agent fills in — each step must be verifiable)_
|
|
224
302
|
|
|
225
|
-
**Closing a step:** tick
|
|
226
|
-
files only · then hand off:
|
|
227
|
-
|
|
228
|
-
\`\`\`bash
|
|
229
|
-
fapony mem add note "<what chunk N+1 must know>" --files <f1,f2> ${planRel}
|
|
230
|
-
\`\`\`
|
|
231
|
-
|
|
232
|
-
Next session opens with \`kickoff ${planRel}\` (or \`kickoff ${basename(planRel)}\` — kickoff resolves by filename too, so no need to retype the path).
|
|
303
|
+
**Closing a step:** tick TL;DR with sha · \`git commit\` files only · \`fapony mem add note "<what chunk N+1 must know>" --files <f1,f2> ${planRel}\` · next opens with \`kickoff ${planRel}\` (or \`kickoff PLAN-${name}.md\` — kickoff resolves by filename too).
|
|
233
304
|
|
|
234
305
|
## 7. Examples
|
|
235
306
|
${
|
|
@@ -562,6 +633,14 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
562
633
|
|
|
563
634
|
const priorArt = renderPriorArt(cwd, config, roots);
|
|
564
635
|
const contextFapony = renderContextFapony(worktree);
|
|
636
|
+
const scoped = requested.length > 0;
|
|
637
|
+
// First pass at the block cap — the total-cap check below may shrink it.
|
|
638
|
+
let existingScope = renderExistingInScope(
|
|
639
|
+
roots,
|
|
640
|
+
cwd,
|
|
641
|
+
scoped,
|
|
642
|
+
MAX_EXISTING_IN_SCOPE,
|
|
643
|
+
);
|
|
565
644
|
|
|
566
645
|
let specLink: string | null = null;
|
|
567
646
|
if (withSpec) {
|
|
@@ -589,15 +668,30 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
589
668
|
}
|
|
590
669
|
|
|
591
670
|
mkdirSync(planDirAbs, { recursive: true });
|
|
592
|
-
|
|
593
|
-
planPath,
|
|
671
|
+
const buildPlan = (existing: string[]): string =>
|
|
594
672
|
planTemplate(
|
|
595
673
|
name,
|
|
596
674
|
priorArt,
|
|
597
675
|
contextFapony,
|
|
676
|
+
existing,
|
|
598
677
|
specLink,
|
|
599
678
|
`${planDir()}/PLAN-${name}.md`,
|
|
600
|
-
)
|
|
601
|
-
);
|
|
679
|
+
);
|
|
680
|
+
let planBody = buildPlan(existingScope);
|
|
681
|
+
// The ≤ ~60 contract predates the §4 block — shrink the block (never the
|
|
682
|
+
// judgment sections) until the file fits. Each item is one line, so cutting
|
|
683
|
+
// `over` items fixes exactly; the re-render recounts the cut honestly.
|
|
684
|
+
const planLines = (b: string): number =>
|
|
685
|
+
b.replace(/\n$/, "").split("\n").length;
|
|
686
|
+
const over = planLines(planBody) - MAX_PLAN_LINES;
|
|
687
|
+
if (over > 0) {
|
|
688
|
+
const budget = Math.max(existingScope.length - over, 1);
|
|
689
|
+
existingScope = renderExistingInScope(roots, cwd, scoped, budget);
|
|
690
|
+
planBody = buildPlan(existingScope);
|
|
691
|
+
}
|
|
692
|
+
writeFileSync(planPath, planBody);
|
|
602
693
|
console.log(`wrote ${planPath}${specLink ? ` + SPEC-${name}.md` : ""}`);
|
|
694
|
+
console.log("Existing plans:");
|
|
695
|
+
for (const l of listExistingPlans(cwd, config, `PLAN-${name}.md`))
|
|
696
|
+
console.log(l);
|
|
603
697
|
}
|
package/src/seed/primitives.ts
CHANGED
|
@@ -53,13 +53,7 @@ export function gitValue(kind: string, value: string): string {
|
|
|
53
53
|
|
|
54
54
|
// Keep the head, always say how much was cut — a silent cut is
|
|
55
55
|
// indistinguishable from "that was everything".
|
|
56
|
-
export
|
|
57
|
-
if (lines.length <= cap) return lines;
|
|
58
|
-
const rest = lines.length - (cap - 1);
|
|
59
|
-
const kept = lines.slice(0, cap - 1);
|
|
60
|
-
kept.push(`… +${rest} more ${what}`);
|
|
61
|
-
return kept;
|
|
62
|
-
}
|
|
56
|
+
export { capLines } from "../core/util.js";
|
|
63
57
|
|
|
64
58
|
// --- Signature formatting ---
|
|
65
59
|
|
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
|
|
package/src/stats/data.ts
CHANGED
|
@@ -2,10 +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 { REASON_CODES, REGIME_CODES } from "../mcp/types.js";
|
|
9
9
|
import {
|
|
10
10
|
isPassFamily,
|
|
11
11
|
qualityScore,
|
package/src/telemetry.ts
CHANGED
|
@@ -13,9 +13,9 @@ 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
20
|
import { readPassiveUsage } from "./session/index.js";
|
|
21
21
|
import { avg, minutesBetween } from "./util.js";
|