Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
150 changes: 129 additions & 21 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const COMMANDS = {
reuse: "proof-carrying code cache — query <spec> / mint <spec> --file <path> / stats",
context: "budgeted context assembly + completeness gate — what an edit NEEDS known",
preflight: "assumption check — what a task names that the repo doesn't define",
config: "provider setup — show / switch / add providers, set default model",
route: "recommend the cheapest capable model for a task (+ gateway config)",
impact: "predict blast radius for a symbol or file from the atlas graph",
substrate: "one pre-action gate: assumptions, route, impact, scope, memory, verify",
Expand Down Expand Up @@ -67,13 +68,23 @@ async function run(argv) {
}
if (cmd === "init") {
const { init } = await import("./init.js");
const { report, bytes } = init({ targetRoot: process.cwd() });
const noSettings = argv.includes("--no-settings");
const { report, bytes, settings } = init({ targetRoot: process.cwd(), noSettings });
const wrote = report.filter((r) => r.action === "written").map((r) => r.target);
console.log(`${BRAND.brand} init — this repo now speaks every AI tool from one source.\n`);
console.log(` emitted: ${wrote.length ? wrote.join(", ") : "(all up to date)"}`);
console.log(
` source: AGENTS.md (${bytes} B) — edit rules in source/, re-run \`${BRAND.cli} sync\``,
);
if (settings?.action === "merged") {
console.log(
` settings: merged ${settings.added.join(", ")} into ${settings.path}`,
);
} else if (settings?.action === "unchanged") {
console.log(` settings: already up to date (${settings.path})`);
} else if (settings?.action === "skipped") {
console.log(" settings: skipped (--no-settings)");
}
console.log(` active: tools · crew · guards → \`${BRAND.cli} catalog\``);
console.log(` verify: \`${BRAND.cli} doctor\``);
return;
Expand Down Expand Up @@ -678,9 +689,19 @@ async function run(argv) {
}
console.log(out.trim());
} catch {
console.log(
" ccusage not found. Install for real spend (reads local JSONL, nothing leaves your machine):\n npm i -g ccusage # then: forge cost",
);
const { estimateSpendFromLogs } = await import("./cost_report.js");
const est = estimateSpendFromLogs();
if (est && est.totalCost > 0) {
console.log(` $${est.totalCost.toFixed(2)} estimated from Claude session logs (${est.sessions} session(s))`);
if (est.byModel.length) {
for (const m of est.byModel) console.log(` ${m.model.padEnd(30)} $${m.cost.toFixed(4)} (${m.inTokens} in / ${m.outTokens} out)`);
}
console.log("\n install ccusage for precise tracking: npm i -g ccusage");
} else {
console.log(
" ccusage not found. Install for real spend (reads local JSONL, nothing leaves your machine):\n npm i -g ccusage # then: forge cost",
);
}
}
console.log(
`\n ceiling: FORGE_COST_CEILING (default $10) — the cost-budget guard warns when a day exceeds it.`,
Expand Down Expand Up @@ -832,6 +853,75 @@ async function run(argv) {
console.log(json ? JSON.stringify(r, null, 2) : renderSubstrate(r));
return;
}
if (cmd === "config") {
const sub = argv[1] || "show";
const { loadProviders, activeProvider, setProvider, addProvider, listProviders, applyRoute } =
await import("./providers.js");
const json = argv.includes("--json");
if (sub === "show") {
const prov = activeProvider(process.cwd());
const config = loadProviders(process.cwd());
if (json) return console.log(JSON.stringify({ active: config.active, provider: prov }, null, 2));
console.log(`${BRAND.brand} config\n`);
console.log(` provider: ${prov.name} (${prov.label || prov.name})`);
console.log(` base URL: ${prov.baseUrl}`);
console.log(` env key: ${prov.envKey || "(none)"}${prov.envKey ? (process.env[prov.envKey] ? " ✓ set" : " ✗ not set") : ""}`);
console.log(` models:`);
for (const [tier, id] of Object.entries(prov.models || {}))
console.log(` ${tier.padEnd(8)} ${id}`);
return;
}
if (sub === "providers") {
const list = listProviders(process.cwd());
if (json) return console.log(JSON.stringify(list, null, 2));
console.log(`${BRAND.brand} config providers\n`);
for (const p of list)
console.log(
` ${p.active ? "▸" : " "} ${p.name.padEnd(14)} ${p.label.padEnd(20)} ${p.envKey ? (p.hasKey ? "✓ key set" : "✗ key missing") : ""}`,
);
console.log(`\n switch: \`${BRAND.cli} config provider <name>\``);
return;
}
if (sub === "provider") {
const name = argv[2];
if (!name) {
console.error(`usage: ${BRAND.cli} config provider <name> | ${BRAND.cli} config provider add <name> --base-url <url> [--key-env <VAR>]`);
process.exitCode = 1;
return;
}
if (name === "add") {
const addName = argv[3];
const flagVal = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
const baseUrl = flagVal("--base-url");
const envKey = flagVal("--key-env");
const label = flagVal("--label");
const r = addProvider(process.cwd(), addName, { baseUrl, envKey, label });
if (!r.ok) { console.error(` ${r.reason}`); process.exitCode = 1; return; }
console.log(` added provider "${addName}" → ${r.provider.baseUrl}`);
return;
}
const r = setProvider(process.cwd(), name);
if (!r.ok) { console.error(` ${r.reason}`); process.exitCode = 1; return; }
console.log(` switched to provider "${name}" (${r.provider.label || name})`);
return;
}
if (sub === "model") {
const tier = argv[2];
if (!tier) {
console.error(`usage: ${BRAND.cli} config model <haiku|sonnet|opus|fable>`);
process.exitCode = 1;
return;
}
const r = applyRoute(tier);
if (!r.ok) { console.error(` ${r.reason}`); process.exitCode = 1; return; }
console.log(` model set to ${r.model} (${r.modelId})${r.prev ? ` — was: ${r.prev}` : ""}`);
console.log(` written to ${r.path}`);
return;
}
console.error(`config: unknown subcommand "${sub}" (show | providers | provider <name> | model <tier>)`);
process.exitCode = 1;
return;
}
if (cmd === "route") {
const r = await import("./route.js");
if (argv[1] === "gateway") {
Expand All @@ -844,35 +934,53 @@ async function run(argv) {
return;
}
const json = argv.includes("--json");
const apply = argv.includes("--apply");
const providerIdx = argv.indexOf("--provider");
const providerName = providerIdx >= 0 ? argv[providerIdx + 1] : undefined;
const FLAGS = new Set(["--json", "--apply"]);
const task = argv
.slice(1)
.filter((a) => a !== "--json")
.filter((a, i) => !FLAGS.has(a) && a !== "--provider" && argv[i] !== "--provider")
.join(" ");
if (!task) {
console.error('usage: forge route "<task>" [--json] | forge route gateway');
console.error('usage: forge route "<task>" [--apply] [--provider <name>] [--json] | forge route gateway');
process.exitCode = 1;
return;
}
if (providerName) {
const { setProvider } = await import("./providers.js");
const sr = setProvider(process.cwd(), providerName);
if (!sr.ok) { console.error(` ${sr.reason}`); process.exitCode = 1; return; }
}
const rec = r.routeTask(process.cwd(), task);
// P8 route metering — explicit CLI path only (hooks that route stay write-free);
// best-effort, a metrics failure must never block the recommendation.
r.meterRoute(process.cwd(), task, rec);
if (json) {
console.log(JSON.stringify(rec, null, 2));
return;
} else {
console.log(`${BRAND.brand} route — cheapest capable model\n`);
console.log(
` → ${rec.model.name} (${rec.tier}, $${rec.model.inCost}/$${rec.model.outCost} per M tok)`,
);
console.log(` ${rec.model.use}`);
console.log(
` complexity ${rec.score.toFixed(2)}${rec.reasons.length ? ` · driven by: ${rec.reasons.join(", ")}` : ""}`,
);
console.log(
` signals: ${rec.signals.files} file(s), fan-out ${rec.signals.fanout}, churn ${rec.signals.churn}, past-mistakes ${rec.signals.pastMistakes}, ambiguity ${rec.signals.ambiguity.toFixed(2)}`,
);
}
if (apply) {
const { applyRoute } = await import("./providers.js");
const ar = applyRoute(rec.key);
if (ar.ok) {
if (!json) console.log(`\n applied: model set to ${ar.model} (${ar.modelId}) in ${ar.path}`);
} else {
if (!json) console.error(`\n apply failed: ${ar.reason}`);
process.exitCode = 1;
}
} else if (!json) {
console.log(`\n advisory · apply: \`${BRAND.cli} route "<task>" --apply\` · gateway: \`${BRAND.cli} route gateway\``);
}
console.log(`${BRAND.brand} route — cheapest capable model\n`);
console.log(
` → ${rec.model.name} (${rec.tier}, $${rec.model.inCost}/$${rec.model.outCost} per M tok)`,
);
console.log(` ${rec.model.use}`);
console.log(
` complexity ${rec.score.toFixed(2)}${rec.reasons.length ? ` · driven by: ${rec.reasons.join(", ")}` : ""}`,
);
console.log(
` signals: ${rec.signals.files} file(s), fan-out ${rec.signals.fanout}, churn ${rec.signals.churn}, past-mistakes ${rec.signals.pastMistakes}, ambiguity ${rec.signals.ambiguity.toFixed(2)}`,
);
console.log("\n advisory · auto-routing: `forge route gateway`");
return;
}
if (cmd === "anchor") {
Expand Down
11 changes: 9 additions & 2 deletions src/cortex_hook_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,15 @@ async function main() {
// allowBuild:false keeps it cheap and never writes .forge/ from a hook; advisory only.
if (typeof hook.prompt === "string" && hook.prompt.trim()) {
const result = substrateCheck(root, hook.prompt, { allowBuild: false });
// Opt-in enforcing mode (FORGE_ENFORCE=1) turns the gate from advisory into a real halt on
// a vacuous prompt. Default: advisory, never blocks.
// Best-effort metrics recording — fills the cost dashboard pipeline without
// blocking the hook. A failing write is silently swallowed.
try {
const { record } = await import("./metrics.js");
record(root, { stage: "gate", outcome: result.gate?.halted ? "halt" : "pass" });
if (result.route?.key) {
record(root, { stage: "route", tier: result.route.tier });
}
} catch {}
const gate = enforceDecision(result);
if (gate.block) {
process.stdout.write(JSON.stringify({ decision: "block", reason: gate.reason }));
Expand Down
102 changes: 97 additions & 5 deletions src/cortex_mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { routeTask } from "./route.js";
import { decompose } from "./scope.js";
import { predictImpact, substrateCheck } from "./substrate.js";
import { epochDay } from "./util.js";
import { report as costReport } from "./cost_report.js";
import { dashData } from "./dash.js";
import { diagnose } from "./diagnose.js";
import { doctor } from "./doctor.js";

const PKG_VERSION = (() => {
try {
Expand Down Expand Up @@ -114,9 +118,57 @@ const TOOLS = [
required: ["files"],
},
},
{
name: "forge_cost",
description:
"Cost report — measured stage factors (gate, cache, route, context) from .forge/metrics.jsonl with multiplicative composition.",
inputSchema: { type: "object", properties: {} },
},
{
name: "forge_dash_data",
description:
"Dashboard JSON payload — ledger stats, metrics, atlas info. Same data forge dash serves at /api/data.",
inputSchema: { type: "object", properties: {} },
},
{
name: "forge_brain",
description:
"Project memory index — list all remembered facts stored in .forge/brain/.",
inputSchema: { type: "object", properties: {} },
},
{
name: "forge_ledger_query",
description:
"Query the proof-carrying memory ledger with a natural language query. Returns ranked matching claims.",
inputSchema: {
type: "object",
properties: { query: { type: "string", description: "what you are about to do or looking for" } },
required: ["query"],
},
},
{
name: "forge_diagnose",
description:
"Doom-loop check — record a failure and check if the same signature has recurred (3x = escalation). Prevents thrashing.",
inputSchema: {
type: "object",
properties: {
errorText: { type: "string", description: "the error message" },
file: { type: "string", description: "file where the error occurred" },
symbol: { type: "string", description: "symbol involved" },
},
required: ["errorText"],
},
},
{
name: "forge_doctor",
description:
"Health check — verify installed tools, guards, MCP auth, config drift, and system state.",
inputSchema: { type: "object", properties: {} },
},
];

function callTool(name, args = {}) {
async function callTool(name, args = {}) {
if (name === "cortex_lessons") {
const files = args.files ?? [];
const symbols = args.symbols ?? [];
Expand Down Expand Up @@ -151,11 +203,50 @@ function callTool(name, args = {}) {
const d = decompose(root, args.files ?? []);
return JSON.stringify(d, null, 2);
}
if (name === "forge_cost") return JSON.stringify(costReport(root), null, 2);
if (name === "forge_dash_data") return JSON.stringify(dashData(root), null, 2);
if (name === "forge_brain") {
try {
const { brainStore, list, buildIndex } = await import("./brain.js");
const store = brainStore(root);
const idx = buildIndex(store);
const items = list(store);
return JSON.stringify({ items, indexed: idx.indexed, overflow: idx.overflow }, null, 2);
} catch { return "No brain store found — run `forge remember` to start."; }
}
if (name === "forge_ledger_query") {
try {
const { loadClaims, repoLedger } = await import("./ledger_store.js");
const { retrieve, claimText } = await import("./ledger.js");
const { claimSim, simLabel } = await import("./embed.js");
const dir = repoLedger(root);
const q = String(args.query ?? "");
const claims = loadClaims(dir);
const sim = claimSim(root, q, claims, claimText);
const ranked = retrieve(q, claims, { nowDay: today(), budget: 8, sim });
return JSON.stringify({
sim: simLabel(sim),
results: ranked.map((r) => ({ id: r.claim.id, kind: r.claim.kind, score: r.score, text: claimText(r.claim).slice(0, 200) })),
}, null, 2);
} catch { return "No ledger claims found."; }
}
if (name === "forge_diagnose") {
const r = diagnose(root, {
errorText: String(args.errorText ?? ""),
file: args.file,
symbol: args.symbol,
});
return JSON.stringify(r, null, 2);
}
if (name === "forge_doctor") {
const { results, failed } = doctor({ targetRoot: root });
return JSON.stringify({ results, failed }, null, 2);
}
return null;
}

/** Handle one JSON-RPC message; returns a response object, or null for notifications. */
export function handle(msg) {
export async function handle(msg) {
const { id, method, params } = msg;
if (id === undefined || String(method).startsWith("notifications/")) return null;
if (method === "initialize") {
Expand All @@ -171,7 +262,7 @@ export function handle(msg) {
}
if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
if (method === "tools/call") {
const text = callTool(params?.name, params?.arguments);
const text = await callTool(params?.name, params?.arguments);
if (text === null) {
return {
jsonrpc: "2.0",
Expand Down Expand Up @@ -203,8 +294,9 @@ export function serve(input = process.stdin, output = process.stdout) {
} catch {
return; // ignore malformed frames
}
const res = handle(msg);
if (res) output.write(`${JSON.stringify(res)}\n`);
handle(msg).then((res) => {
if (res) output.write(`${JSON.stringify(res)}\n`);
}).catch(() => {});
});
}

Expand Down
Loading
Loading