Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4d5b275
feat(learn-from-fable): staged mining CLI — config, enumeration, tran…
genesiscz Jul 26, 2026
77ab25a
refactor(claude): uuid-dedupe option on parseJsonlTranscript so compa…
genesiscz Jul 26, 2026
e5384a4
feat(ai-proxy): AiProxyClient and grok ACP leader pool, with the lear…
genesiscz Jul 26, 2026
c7dfc58
refactor(models): central curated registry; catalogs and pricing beco…
genesiscz Jul 26, 2026
38878b0
feat(ai-proxy): tiered billing priceRules, curated model list, accoun…
genesiscz Jul 26, 2026
a140f23
feat(ai-proxy): per-call transcripts, x-gt-* job tags and a phase tim…
genesiscz Jul 26, 2026
778a56b
fix(ai-proxy): keep long streams alive and record the ones that die
genesiscz Jul 26, 2026
3a10508
feat(json): repairJson for broken LLM payloads, wired into extractJso…
genesiscz Jul 26, 2026
60c21d9
perf(pipeline): flow-through streaming stages with per-scope profiling
genesiscz Jul 26, 2026
1a50296
feat(markdown): fit CLI tables to the terminal, stacking them as card…
genesiscz Jul 26, 2026
a070f27
feat(learn-from-fable): mine, filter and judge stages — windowed extr…
genesiscz Jul 26, 2026
b394151
feat(learn-from-fable): consolidation tournament, A/B eval against th…
genesiscz Jul 26, 2026
5f7e3ae
feat(learn-from-fable): spec stage — synthesize a FABLE-SPEC proposal…
genesiscz Jul 26, 2026
2130688
feat(learn-from-fable): pipeline report, and the probe harnesses behi…
genesiscz Jul 26, 2026
b666438
fix(ai-config): re-login merges onto the stored entry so it cannot dr…
genesiscz Jul 26, 2026
f5569f4
fix(logger): serialize Error values passed as err/error instead of lo…
genesiscz Jul 26, 2026
8df30f8
style(learn-from-fable): biome import order and quote normalization
genesiscz Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 61 additions & 98 deletions .claude/commands/learn-from-fable.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,10 @@ Two cleanly separated layers (the 2026-05 logger+out overhaul):

## LLM Model Library & Pricing (one place — do not add new rate tables)

- **Curated static registry**: `src/utils/ai/models/registry.ts` — `MODEL_REGISTRY: CanonicalModel[]` (id, provider, family, displayName, contextWindow, thinking, list `pricing`, aliases, `flags.supports1m`/`native1m`, `cli` label) with `byProvider()` / `byId()` / `aliasMapFor()` / `pricingFor()`. Anthropic models live ONLY here; `src/utils/ai/anthropic/models.ts` (`ANTHROPIC_SUB_STATIC_CATALOG`, alias map, `inferAnthropicContextWindow`), `src/claude/lib/models.ts` (`CLAUDE_MODELS`), and `src/ai-spend/lib/pricing.ts` (`DEFAULT_PRICING`) are derived views over it. Grok (`src/utils/ai/grok/models.ts`) and Codex/WHAM (`src/utils/ai/openai/sub-models.ts`) stay authoritative in their own files — their records carry provider-specific fields (probe status, speed, visibility, modalities).
- **Canonical model list + pricing**: `providerManager.detectProviders()` (`src/ask/providers/ProviderManager.ts`) returns `ModelInfo[]` with `pricing?: PricingInfo` (`inputPer1M`/`outputPer1M`, `src/ask/types/provider.ts`). Pricing is populated by `dynamicPricingManager.getPricing(provider, modelId)` (`src/ask/providers/DynamicPricing.ts`), sourced from LiteLLM's `model_prices_and_context_window.json` (cached as `litellm-pricing.json`) + OpenRouter.
- **Cost math (pure)**: `src/utils/ai/llm-cost.ts` — `estimateLlmCallCostUsd({pricing, inputTokens, outputTokens})`, `estimateSpeechTokens(durationSec)`. No rates in there, math only.
- **Deliberate exception**: `src/ai-proxy/lib/billing/pricing.ts` keeps a small STATIC table — it's the ai-proxy client-ledger invoicing source of truth (deterministic, offline). Don't "deduplicate" it into the dynamic path, and don't copy it anywhere else.
- **Deliberate exception**: `src/ai-proxy/lib/billing/pricing.ts` keeps a small STATIC table — it's the ai-proxy client-ledger invoicing source of truth (deterministic, offline). Don't "deduplicate" it into the dynamic path or into `MODEL_REGISTRY`, and don't copy it anywhere else; its rates may intentionally differ from list prices. Cost is booked at WRITE time (`recordClientUsage()` in `src/ai-proxy/lib/usage/client-ledger.ts` adds `estimateCostUsd(...)` into the month total), so a later rate edit never rewrites past invoices — readers (`tools ai-proxy clients usage`) print the stored `cost_usd` and never recompute. `pricing.test.ts` asserts every priced prefix still names a model in `MODEL_REGISTRY` / the grok / Codex catalogs, but never asserts rate equality.
- Resolving a user's provider/model choice (default account, `-sub` suffix handling): `resolveProviderChoice()` in `src/youtube/lib/provider-choice.ts` wraps `modelSelector`/`providerManager`.
- **Accounts** live in `~/.genesis-tools/ai/config.json` (`AIConfig`, types in `src/utils/config/ai.types.ts`). Subscription billing is flagged by `DetectedProvider.subscription` (set by the `*SubResolver`s in `src/utils/ai/resolvers/`) — never infer it from name suffixes. API-key accounts may REFERENCE an env var via `tokens.apiKeyEnv` (resolved by `AIConfig.resolveApiKey`, never copied); `grok-sub` accounts reference the Grok CLI auth file via `tokens.authFile` (default `~/.grok/auth.json`, resolved by `resolveGrokSubToken` in `src/utils/ai/grok/account.ts`). ai-proxy's subscription providers bill these same accounts by `accountName` reference.

3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
"ink-text-input": "^6.0.0",
"jsdom": "^29.1.1",
"jsonpath": "^1.1.1",
"jsonrepair": "^3.15.0",
"katex": "^0.16.46",
"kysely": "^0.28.16",
"linkedom": "^0.18.12",
Expand Down
95 changes: 95 additions & 0 deletions scripts/ai-proxy/structured-output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 95 added lines in scripts/ai-proxy/structured-output.ts

This PR adds 95 lines to scripts/ai-proxy/structured-output.ts with no touching test change (no changed test names structured-output and none under scripts/ai-proxy/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: structured-output

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not acting on these. All three files are single-purpose diagnostic probes under scripts/ (structured-output.ts, probe-extractor-latency.ts, probe-judge-batch.ts): they are run by hand against a live proxy and a real account to answer one question (does structured output round-trip, where does extractor latency go, does a judge batch of N hang), print timings, and exit. They export no behaviour, nothing imports them, and a test would have to mock the very network path the probe exists to observe. This matches the escape hatch in the finding itself ("ignore if the change is genuinely untestable"). The library code they exercise is what got tests this round: sse-keepalive.test.ts, transcripts.test.ts and calls.test.ts in ebde0daa8.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 95 added lines in scripts/ai-proxy/structured-output.ts

This PR adds 95 lines to scripts/ai-proxy/structured-output.ts with no touching test change (no changed test names structured-output and none under scripts/ai-proxy/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: structured-output

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 95 added lines in scripts/ai-proxy/structured-output.ts

This PR adds 95 lines to scripts/ai-proxy/structured-output.ts with no touching test change (no changed test names structured-output and none under scripts/ai-proxy/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: structured-output

* Probe: does the ai-proxy deliver schema-shaped output per provider?
*
* For each model (args, default: grok-4.5 + sonnet) runs the same tiny task in
* both schema modes ("response_format" native, "prompt" injected) and reports
* whether the reply parsed against the expectation, plus the usage the proxy
* returned AND the last matching record in usage/requests.jsonl (proving the
* proxy recorded metrics for the call).
*
* Usage: bun scripts/ai-proxy/structured-output.ts [model...]
*/
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { AiProxyClient, type SchemaMode } from "@genesiscz/utils/ai/proxy/AiProxyClient";
import { SafeJSON } from "@genesiscz/utils/json";

const SCHEMA = {
name: "fruit_pick",
schema: {
type: "object",
additionalProperties: false,
required: ["fruit", "confidence", "reason"],
properties: {
fruit: { type: "string", enum: ["apple", "banana", "cherry"] },
confidence: { type: "number", minimum: 0, maximum: 100 },
reason: { type: "string" },
},
},
};

const PROMPT = "Pick the fruit that is a berry botanically-speaking from: apple, banana, cherry.";

function lastUsageRecord(model: string): unknown {
const path = join(homedir(), ".genesis-tools", "ai-proxy", "usage", "requests.jsonl");
if (!existsSync(path)) {
return "NO requests.jsonl";
}

const lines = readFileSync(path, "utf-8").trim().split("\n");
for (let i = lines.length - 1; i >= 0; i--) {
try {
const row = SafeJSON.parse(lines[i], { strict: true }) as { proxyModel?: string };
if (row.proxyModel === model || row.proxyModel?.endsWith(`/${model}`)) {
return row;
}
} catch {
// skip
}
}

return "NO record for model";
}

const models = process.argv.slice(2).length ? process.argv.slice(2) : ["grok-4.5", "sonnet"];
const client = new AiProxyClient();
let failures = 0;

if (!(await client.health())) {
console.error("ai-proxy is DOWN (start: tools ai-proxy up)");
process.exit(1);
}

for (const model of models) {
for (const mode of ["response_format", "prompt"] as SchemaMode[]) {
const label = `${model} [${mode}]`;
try {
const res = await client.chat({
model,
messages: [{ role: "user", content: PROMPT }],
jsonSchema: SCHEMA,
schemaMode: mode,
maxTokens: 2000,
timeoutMs: 120_000,
});
const ok = res.parsed && typeof res.parsed === "object" && (res.parsed as { fruit?: string }).fruit;
console.log(
`${ok ? "✅" : "❌"} ${label} ${res.elapsedMs}ms usage=${SafeJSON.stringify(res.usage ?? null)} ` +
`parsed=${SafeJSON.stringify(res.parsed ?? null)} parseError=${res.parseError ?? "none"}`
);

if (!ok) {
failures++;
console.log(` raw text: ${res.text.slice(0, 200)}`);
}
} catch (err) {
failures++;
console.log(`💥 ${label} ${err instanceof Error ? err.message.slice(0, 300) : err}`);
}
}

console.log(` usage record: ${SafeJSON.stringify(lastUsageRecord(model))}`);
}

process.exit(failures ? 1 : 0);
132 changes: 132 additions & 0 deletions scripts/learn-from-fable/audit-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 132 added lines in scripts/learn-from-fable/audit-spec.ts

This PR adds 132 lines to scripts/learn-from-fable/audit-spec.ts with no touching test change (no changed test names audit-spec and none under scripts/learn-from-fable/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: audit-spec

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 132 added lines in scripts/learn-from-fable/audit-spec.ts

This PR adds 132 lines to scripts/learn-from-fable/audit-spec.ts with no touching test change (no changed test names audit-spec and none under scripts/learn-from-fable/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: audit-spec

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 132 added lines in scripts/learn-from-fable/audit-spec.ts

This PR adds 132 lines to scripts/learn-from-fable/audit-spec.ts with no touching test change (no changed test names audit-spec and none under scripts/learn-from-fable/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: audit-spec

* Is a FABLE-SPEC proposal ready to generate the skill from?
*
* The spec stage reports what IT did (passes, rejections, bullet counts). This reads
* the finished document the way the skill stage will and answers the separate
* question: is this still a list of principles? The v6 proposal passed every stage
* metric and was still unusable — 55 of its 78 bullets over 400 characters, the worst
* holding 4_363, because a line budget is satisfied by making lines longer.
*
* Usage: bun scripts/learn-from-fable/audit-spec.ts <spec.md> [baseline.md]
*/
import { readFileSync } from "node:fs";

const CAP = 420;
/** Two bullets sharing this share of their vocabulary are saying the same thing twice. */
const DUPLICATE_OVERLAP = 0.6;

interface Bullet {
line: number;
text: string;
section: string;
}

function readBullets(markdown: string): { bullets: Bullet[]; sections: string[] } {
const bullets: Bullet[] = [];
const sections: string[] = [];
let section = "(preamble)";

markdown.split("\n").forEach((line, i) => {
const heading = line.match(/^##\s+(.*)$/);
if (heading) {
section = heading[1].trim();
sections.push(section);
return;
}

if (/^\s*[-*]\s/.test(line)) {
bullets.push({ line: i + 1, text: line, section });
}
});

return { bullets, sections };
}

function words(text: string): Set<string> {
return new Set(
text
.toLowerCase()
.replace(/[^a-z0-9\s]/g, " ")
.split(/\s+/)
.filter((w) => w.length > 3)
);
}

function overlap(a: Set<string>, b: Set<string>): number {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Quality | 🔵 Low · confidence 98/100

⚠️ Potential issue

overlap() divides by Math.min(a.size, b.size) which can be zero

overlap returns shared / Math.min(a.size, b.size). words() filters out tokens of length <= 3, so a short bullet like - do it now yields an empty set and overlap returns NaN (0/0). NaN >= DUPLICATE_OVERLAP is false so it silently never matches, meaning short bullets are excluded from duplicate detection without any signal. Guard the empty-set case explicitly.

🧩 Analysis

Grep evidence: shared / Math.min\(a.size, b.size\)

Provenance: found by claude-sub/opus-5 · verified by codex/gpt-5.6-sol · peer score 38/100

let shared = 0;
for (const w of a) {
if (b.has(w)) {
shared++;
}
}

return shared / Math.min(a.size, b.size);
}

function audit(path: string): void {
const markdown = readFileSync(path, "utf-8");
const { bullets, sections } = readBullets(markdown);
const lengths = bullets.map((b) => b.text.length).sort((a, b) => a - b);
const over = bullets.filter((b) => b.text.length > CAP);
// the skill is generated per-principle, so a bullet with no source is a bullet
// nobody can trace back to a session when it turns out to be wrong
const uncited = bullets.filter((b) => !/\*\([0-9a-f]{8}/.test(b.text));

const vocab = bullets.map((b) => words(b.text));
const dupes: string[] = [];
for (let i = 0; i < bullets.length; i++) {
for (let j = i + 1; j < bullets.length; j++) {
if (overlap(vocab[i], vocab[j]) >= DUPLICATE_OVERLAP) {
dupes.push(` L${bullets[i].line} ~ L${bullets[j].line}: ${bullets[i].text.slice(2, 90)}…`);
}
}
}

const total = lengths.reduce((a, b) => a + b, 0);
process.stdout.write(
[
`# ${path}`,
`lines ${markdown.split("\n").length}`,
`sections ${sections.length} (${sections.join(" · ")})`,
`bullets ${bullets.length}`,
`bullet len avg ${Math.round(total / bullets.length)} median ${lengths[Math.floor(lengths.length / 2)]} max ${lengths.at(-1)}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Quality | 🔵 Low · confidence 92/100

⚠️ Potential issue

Empty specs produce NaN and undefined audit metrics

When a document has no bullets, bullets.length is zero and lengths is empty, so this output reports an average of NaN, median/max as undefined, and the following over-cap percentage as NaN%. An empty or malformed proposal is precisely an input this readiness audit should diagnose clearly. Guard the empty case and emit zeroes (or an explicit “no bullets” failure) before calculating aggregate metrics.

🧩 Analysis

Grep evidence: Math\.round\(total / bullets\.length\).*lengths\[Math\.floor\(lengths\.length / 2\)\]

Provenance: found by codex/gpt-5.6-sol · verified by claude-sub/opus-5 · peer score 33/100

`over ${CAP} ${over.length} (${Math.round((over.length / bullets.length) * 100)}%)`,
`uncited ${uncited.length}`,
`near-dupes ${dupes.length}`,
"",
...(over.length
? [
"## oversized",
...over.slice(0, 8).map((b) => ` L${b.line} ${b.text.length}ch: ${b.text.slice(2, 90)}…`),
"",
]
: []),
...(dupes.length ? ["## near-duplicates", ...dupes.slice(0, 12), ""] : []),
...(uncited.length
? ["## uncited", ...uncited.slice(0, 8).map((b) => ` L${b.line}: ${b.text.slice(2, 90)}…`), ""]
: []),
"## bullets per section",
...sections.map(
(s) =>
` ${bullets
.filter((b) => b.section === s)
.length.toString()
.padStart(4)} ${s}`
),
"",
].join("\n")
);
}

const [path, baseline] = process.argv.slice(2);
if (!path) {
process.stdout.write("usage: bun scripts/learn-from-fable/audit-spec.ts <spec.md> [baseline.md]\n");
process.exit(1);
}

if (baseline) {
audit(baseline);
process.stdout.write(`${"=".repeat(72)}\n`);
}

audit(path);
71 changes: 71 additions & 0 deletions scripts/learn-from-fable/probe-claude-sub-concurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 71 added lines in scripts/learn-from-fable/probe-claude-sub-concurrency.ts

This PR adds 71 lines to scripts/learn-from-fable/probe-claude-sub-concurrency.ts with no touching test change (no changed test names probe-claude-sub-concurrency and none under scripts/learn-from-fable/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: probe-claude-sub-concurrency

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 71 added lines in scripts/learn-from-fable/probe-claude-sub-concurrency.ts

This PR adds 71 lines to scripts/learn-from-fable/probe-claude-sub-concurrency.ts with no touching test change (no changed test names probe-claude-sub-concurrency and none under scripts/learn-from-fable/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: probe-claude-sub-concurrency

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 71 added lines in scripts/learn-from-fable/probe-claude-sub-concurrency.ts

This PR adds 71 lines to scripts/learn-from-fable/probe-claude-sub-concurrency.ts with no touching test change (no changed test names probe-claude-sub-concurrency and none under scripts/learn-from-fable/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: probe-claude-sub-concurrency

* Are claude-sub's silent stalls caused by CONCURRENCY, or independent of it?
*
* The filter runs ~18 calls in flight against one subscription account and about
* a quarter of them never emit a byte. If that is a concurrency ceiling, running
* wider makes it worse and running narrower fixes it; if the stall rate is flat
* across widths, it is upstream flakiness and the answer is retries, not tuning.
*
* Usage: bun scripts/learn-from-fable/probe-claude-sub-concurrency.ts [model]
*/
import { AiProxyClient } from "@genesiscz/utils/ai/proxy/AiProxyClient";

const model = process.argv[2] ?? "olivia/claude-sub/claude-sonnet-5";
const WIDTHS = (process.argv[3] ?? "4,12").split(",").map(Number);
const CALLS_PER_WIDTH = 12;
const BUDGET_MS = 45_000;

const client = new AiProxyClient();

interface Outcome {
ok: boolean;
ms: number;
chars: number;
}

async function one(width: number, index: number): Promise<Outcome> {
const started = performance.now();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new Error("budget")), BUDGET_MS);
try {
const r = await client.chatStream({
model,
// varied so nothing is served from a cache
messages: [{ role: "user", content: `In one short sentence, why does step ${index} of a build matter?` }],
maxTokens: 120,
timeoutMs: BUDGET_MS,
signal: controller.signal,
tags: { session: "probe-cc", stage: "probe", label: `w${width}-${index}` },
});
const chars = r.text.trim().length;
return { ok: chars > 0, ms: Math.round(performance.now() - started), chars };
} catch {
return { ok: false, ms: Math.round(performance.now() - started), chars: 0 };
} finally {
clearTimeout(timer);
}
}

for (const width of WIDTHS) {
const started = performance.now();
const results: Outcome[] = [];
let next = 0;
const workers = Array.from({ length: width }, async () => {
while (next < CALLS_PER_WIDTH) {
const index = next++;
results.push(await one(width, index));
}
});
await Promise.all(workers);

const ok = results.filter((r) => r.ok);
const dead = results.filter((r) => !r.ok);
const lat = ok.map((r) => r.ms).sort((a, b) => a - b);
const median = lat.length ? lat[Math.floor(lat.length / 2)] : 0;
process.stdout.write(
`width=${String(width).padStart(2)} ok=${ok.length}/${results.length} ` +
`silent=${dead.length} medianOk=${median}ms wall=${Math.round(performance.now() - started)}ms\n`
);
}

process.exit(0);
20 changes: 20 additions & 0 deletions scripts/learn-from-fable/probe-episodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests | 🔵 Low · confidence 65/100

⚠️ Potential issue

No test changes accompany 20 added lines in scripts/learn-from-fable/probe-episodes.ts

This PR adds 20 lines to scripts/learn-from-fable/probe-episodes.ts with no touching test change (no changed test names probe-episodes and none under scripts/learn-from-fable/). If the change alters behavior, add or extend a test that pins it (deterministic static check — ignore if the change is genuinely untestable or covered elsewhere).

🧩 Analysis

Grep evidence: probe-episodes

* Where the probes read mined episodes from when no path is passed on argv.
* Resolved through the fable config so no probe carries a machine-specific path.
*/
import { join } from "node:path";
import { loadFableConfig, packPaths } from "../../src/learn-from-fable/lib/config";

const DEFAULT_ARTIFACT = "episodes.ai-proxy-martin-grok-grok-4.5.raw.jsonl";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Default artifact name still embeds a personal account slug.

The header states no probe carries a machine-specific path, yet episodes.ai-proxy-martin-grok-grok-4.5.raw.jsonl hardcodes the martin account and a specific model. Consider picking the newest episodes.*.raw.jsonl in episodesDir when no artifact is passed, so the probe works for any pack.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/learn-from-fable/probe-episodes.ts` at line 8, The default artifact
selection should not hardcode a personal account or model. Replace
DEFAULT_ARTIFACT-based fallback logic in the probe with discovery of the newest
episodes.*.raw.jsonl file within episodesDir when no artifact is provided, while
preserving explicit artifact arguments.


export function defaultEpisodesPath(artifact = DEFAULT_ARTIFACT): string {
const config = loadFableConfig();

if (!config) {
throw new Error(
"No fable config found. Run `tools learn-from-fable bootstrap` or pass an episodes path as the last argument."
);
}

return join(packPaths(config).episodesDir, artifact);
}
Loading
Loading