/** * KHAELOR * File: src/cli/commands.ts * Description: Slash-command implementations backed by real services — registered into the TUI command registry. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { join } from "node:path"; import type { ResolvedConfig } from "../config/index.js"; import { MEMORY_FILE, parseMemory, purgeCandidates } from "../memory/index.js"; import { GoalStore, daemonDirFor, readDaemonStatus } from "../daemon/index.js"; import { SessionLog, buildUsageTotals, forkSession, listForkCheckpoints, renderSessionDiff, summarizeSessionRun, } from "../session/index.js"; import type { DurableEvent, Phase } from "../session/index.js"; import { mergeSubtaskBranch } from "../tasks/index.js"; import type { SubtaskManager } from "../tasks/index.js"; import type { CommandDef } from "../tui/index.js"; import type { PaletteItem } from "../tui/index.js"; import type { Engine } from "./engine.js"; import { replaySession } from "./replay.js"; import { listSessions } from "./sessions.js"; import { createSubtaskManager, workspaceExec } from "./subtasks.js"; // ───────────────────────── dependencies ───────────────────────── /** The slice of the TUI the commands drive — structural, stubbable in tests. */ export interface CommandUi { printBlock(lines: string[]): void; openSelector(items: PaletteItem[], onSelect: (id: string) => void): void; setModelLabel(model: string): void; } export interface CliCommandDeps { ui: CommandUi; /** Live engine accessor — the engine is rebuilt on /new and /resume. */ engine(): Engine; config: ResolvedConfig; cwd: string; sessionsDir: string; projectHash: string; actions: { newSession(): void; resumeSession(sessionId: string): void; quit(): void; /** Consume the next composer submit as command input instead of a model turn. */ captureNextSubmit?(consume: (text: string) => void): void; }; /** Live model listing (SDK /v1/models); injectable for tests. */ listModels?: () => Promise<{ id: string; displayName?: string }[]>; } /** Every slash command the CLI layer provides (TUI built-ins add /diff-expand, /help, /quit). */ export const CLI_SLASH_COMMANDS: readonly string[] = [ "/model", "/config", "/permissions", "/sessions", "/resume", "/new", "/rename", "/clear", "/compact", "/cost", "/context", "/diff", "/processes", "/status", "/phase", "/fork", "/sdiff", "/replay", "/memory", "/verify", "/spawn", "/tasks", "/merge", "/goals", ]; // ───────────────────────── pure report builders ───────────────────────── /** * Session cost report from REAL usage events only (Absolute Rule #4): * every number is summed from ModelResponseCompleted.usage. No pricing is * configured in V1, so no dollar figure is invented. */ export function costReportLines(events: readonly DurableEvent[]): string[] { const usage = buildUsageTotals(events); const lines: string[] = ["", " cost · this session (real API usage)"]; const fmt = (n: number): string => n.toLocaleString("en-US").padStart(12); const row = (label: string, u: { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; requests: number }): string[] => [ ` ${label}`, ` requests ${String(u.requests).padStart(12)}`, ` input tokens ${fmt(u.inputTokens)}`, ` output tokens ${fmt(u.outputTokens)}`, ` cache write ${fmt(u.cacheWriteTokens)}`, ` cache read ${fmt(u.cacheReadTokens)}`, ]; for (const [model, perModel] of Object.entries(usage.perModel)) { lines.push(...row(model, perModel)); } if (Object.keys(usage.perModel).length > 1) { lines.push(...row("total", usage.totals)); } if (usage.totals.requests === 0) { lines.push(" no model requests yet"); } lines.push(" cost estimate n/a — no pricing configured (tokens above are exact)"); return lines; } function elapsed(sinceMs: number): string { const s = Math.max(0, Math.floor((Date.now() - sinceMs) / 1000)); const mm = String(Math.floor(s / 60)).padStart(2, "0"); const ss = String(s % 60).padStart(2, "0"); return `${mm}:${ss}`; } // ───────────────────────── command construction ───────────────────────── /** Build every CLI slash command. Pure with respect to the registry. */ export function buildCliCommands(deps: CliCommandDeps): CommandDef[] { const { ui } = deps; const print = (lines: string[]): void => { ui.printBlock(lines); }; const printError = (what: string, error: unknown): void => { const message = error instanceof Error ? error.message : String(error); print(["", ` ${what} failed`, ` ${message}`]); }; const runAsync = (what: string, fn: () => Promise): void => { fn().catch((error: unknown) => { printError(what, error); deps.engine().logger.error(`${what} command failed`, { error: error instanceof Error ? error.message : String(error), }); }); }; const commands: CommandDef[] = []; // /model — selector listing live models when reachable, config models otherwise. commands.push({ id: "model.select", title: "Change model", slash: "/model", description: "switch the Anthropic model", run: () => runAsync("/model", async () => { const engine = deps.engine(); let models: { id: string; displayName?: string }[] = []; if (deps.listModels !== undefined) { try { models = await deps.listModels(); } catch { models = []; } } if (models.length === 0) { const fallback = new Set([deps.config.model, deps.config.auxModel, engine.model]); models = [...fallback].map((id) => ({ id })); } const items: PaletteItem[] = models.map((m) => { const item: PaletteItem = { id: m.id, label: m.id }; if (m.displayName !== undefined) item.detail = m.displayName; if (m.id === engine.model) item.detail = `${item.detail ?? ""} · current`.trim(); return item; }); ui.openSelector(items, (id) => { const current = deps.engine(); if (current.turnActive) { print(["", " /model — cannot switch while a turn is running (Esc to interrupt first)"]); return; } current.switchModel(id, "user"); ui.setModelLabel(id); print(["", ` model switched to ${id} (new prompt-cache lineage)`]); }); }), }); // /config — redacted resolved configuration + sources. commands.push({ id: "config.open", title: "Configuration", slash: "/config", description: "resolved configuration and sources", run: () => { const engine = deps.engine(); const lines = [ "", " configuration", ` model ${engine.model}`, ` auxModel ${deps.config.auxModel}`, ` thinking ${deps.config.thinking}`, ` maxOutputTokens ${deps.config.maxOutputTokens}`, ` apiKey ${deps.config.hasApiKey ? "[redacted]" : "(not set)"}`, " sources (highest precedence first)", ...(deps.config.sources.length > 0 ? deps.config.sources.map((s) => ` ${s}`) : [" defaults only"]), " edit: .khaelor/config.json (project) · ~/.khaelor/config.json (user)", ]; print(lines); }, }); // /permissions — the effective rule stack. commands.push({ id: "permissions.open", title: "Permissions", slash: "/permissions", description: "effective permission rules", run: () => { const rules = deps.engine().permissions.effectiveRules(); const lines = ["", " permissions · effective rules (later rules win)"]; for (const rule of rules) { const pattern = rule.pattern ?? "*"; lines.push( ` ${rule.action.padEnd(5)} ${rule.capability.padEnd(26)} ${pattern.padEnd(24)} ${rule.source ?? "default"}`, ); } lines.push(` ${rules.length} rules · unmatched destructive capabilities ask`); print(lines); }, }); // /sessions — list from the session directory. commands.push({ id: "sessions.open", title: "Sessions", slash: "/sessions", description: "list this project's sessions", run: () => { const sessions = listSessions(deps.sessionsDir, deps.projectHash); const current = deps.engine().session.sessionId; const lines = ["", ` sessions · ${deps.cwd}`]; if (sessions.length === 0) lines.push(" none yet"); for (const s of sessions.slice(0, 20)) { const marker = s.sessionId === current ? "●" : " "; const when = new Date(s.updatedAt).toISOString().slice(0, 16).replace("T", " "); lines.push(` ${marker} ${s.sessionId} ${when} ${s.preview}`); } lines.push(" /resume opens a session picker"); print(lines); }, }); // /resume — picker over recorded sessions; resume = replay. commands.push({ id: "sessions.resume", title: "Resume session", slash: "/resume", description: "resume a previous session (replay)", run: () => { const current = deps.engine().session.sessionId; const sessions = listSessions(deps.sessionsDir, deps.projectHash).filter( (s) => s.sessionId !== current, ); if (sessions.length === 0) { print(["", " no other sessions to resume in this project"]); return; } const items: PaletteItem[] = sessions.slice(0, 30).map((s) => ({ id: s.sessionId, label: s.preview, detail: s.sessionId, })); ui.openSelector(items, (id) => { deps.actions.resumeSession(id); }); }, }); // /new and /clear — a fresh session; logs are never truncated (§5.5). commands.push({ id: "session.new", title: "New session", slash: "/new", description: "start a fresh session", run: () => deps.actions.newSession(), }); // /rename — retitle the current session (§8). The next composer submit is the title. commands.push({ id: "session.rename", title: "Rename session", slash: "/rename", description: "rename the current session", run: () => { const capture = deps.actions.captureNextSubmit; if (capture === undefined) { print(["", " /rename is unavailable in this mode"]); return; } print(["", " rename — type the new session title and press Enter (empty cancels)"]); capture((text) => { const title = text.trim(); if (title === "") { print([" rename cancelled"]); return; } deps.engine().session.publishDurable({ type: "session.renamed", payload: { title }, }); print([` session renamed · ${title}`]); }); }, }); commands.push({ id: "session.clear", title: "Clear", slash: "/clear", description: "start a fresh session (the old log is kept)", run: () => deps.actions.newSession(), }); // /compact — manual compaction: prune first, then summarize-compact. commands.push({ id: "context.compact", title: "Compact context", slash: "/compact", description: "compact the conversation context now", run: () => runAsync("/compact", async () => { const engine = deps.engine(); if (engine.turnActive) { print(["", " /compact — wait for the current turn to finish (or Esc to interrupt)"]); return; } const events = engine.session.events(); const prune = engine.contextEngine.pruneToolResults({ events }); if (prune.toolUseIds.length > 0) { engine.session.publishDurable({ type: "context.pruned", payload: prune }); print([ "", ` context pruned · ${prune.toolUseIds.length} old tool results blanked`, ` ~${prune.tokensReclaimedEstimate.toLocaleString("en-US")} tokens reclaimed (estimate)`, ]); return; } const checkpoint = await engine.contextEngine.compress({ events }); engine.session.publishDurable({ type: "context.compacted", payload: checkpoint }); print([ "", ` context compacted · events ${checkpoint.cut.fromSeq}–${checkpoint.cut.toSeq} → checkpoint`, ` summary model ${checkpoint.summaryModel} · trigger ${checkpoint.trigger}`, ]); }), }); // /cost — real usage projection over the durable log. commands.push({ id: "cost.show", title: "Show cost", slash: "/cost", description: "session token usage from real API data", run: () => { print(costReportLines(deps.engine().session.events())); }, }); // /context — budget breakdown from the Context Engine. commands.push({ id: "context.open", title: "Context inspector", slash: "/context", description: "context budget breakdown", run: () => runAsync("/context", async () => { const engine = deps.engine(); const built = await engine.contextEngine.selectContext({ events: engine.session.events(), }); const budget = engine.budget; const lines = ["", " context · section estimates (~4 chars/token)"]; for (const section of built.stats.sections) { lines.push( ` ${section.name.padEnd(28)} ~${section.estimatedTokens.toLocaleString("en-US").padStart(9)} tokens`, ); } lines.push( ` ${"total (estimated)".padEnd(28)} ~${built.stats.estimatedInputTokens.toLocaleString("en-US").padStart(9)} tokens`, ); lines.push( ` window ${budget.modelWindow.toLocaleString("en-US")} · usable ${budget.usableWindow.toLocaleString("en-US")} (output + compaction reserve held back)`, ); if (budget.hasObservedUsage) { lines.push( ` last real total ${budget.lastTotalTokens.toLocaleString("en-US")} tokens · pressure ${(budget.pressure() * 100).toFixed(0)}%`, ); } else { lines.push(" no real usage observed yet this session"); } print(lines); }), }); // /diff — working-tree changes vs the recorded baseline, attributed. commands.push({ id: "diff.show", title: "View diff", slash: "/diff", key: "d", description: "working-tree changes since the session baseline", run: () => runAsync("/diff", async () => { const engine = deps.engine(); const [diff, attribution] = await Promise.all([ engine.git.diff(), engine.git.attributeChanges(), ]); if (diff.kind === "not-a-repo") { print(["", " /diff — not a git repository"]); return; } if (diff.kind === "error") { print(["", ` /diff — git failed: ${diff.message}`]); return; } const khaelor = new Set(attribution.kind === "ok" ? attribution.value.khaelor : []); const preExisting = new Set( attribution.kind === "ok" ? attribution.value.preExisting : [], ); const lines = ["", ` diff · vs ${diff.value.base}`]; if (diff.value.entries.length === 0) lines.push(" working tree clean"); for (const entry of diff.value.entries) { const added = entry.added === null ? "bin" : `+${entry.added}`; const removed = entry.removed === null ? "" : `−${entry.removed}`; const who = khaelor.has(entry.path) ? "khaelor" : preExisting.has(entry.path) ? "pre-existing" : ""; lines.push( ` ${entry.status.padEnd(9)} ${entry.path.padEnd(44)} ${`${added} ${removed}`.padEnd(12)} ${who}`, ); } lines.push(" press d after an edit to expand its unified diff"); print(lines); }), }); // /processes — the background process manager. commands.push({ id: "processes.open", title: "Show processes", slash: "/processes", description: "background processes", run: () => { const list = deps.engine().processes.list(); const lines = ["", " processes"]; if (list.length === 0) lines.push(" none"); for (const proc of list) { const glyph = proc.status === "running" ? "●" : "○"; const status = proc.status === "running" ? `running ${elapsed(proc.startedAt)}` : `${proc.status} ${proc.exitCode === null ? "" : `code ${proc.exitCode}`}`; lines.push(` ${glyph} ${proc.id.padEnd(5)} ${proc.command.slice(0, 40).padEnd(42)} ${status}`); } print(lines); }, }); // /status — where this session stands. commands.push({ id: "status.show", title: "Status", slash: "/status", description: "session status", run: () => runAsync("/status", async () => { const engine = deps.engine(); const events = engine.session.events(); const branch = await engine.git.currentBranch(); const running = engine.processes.list().filter((p) => p.status === "running").length; print([ "", " status", ` session ${engine.session.sessionId}`, ` model ${engine.model}`, ` cwd ${deps.cwd}`, ` branch ${branch.kind === "ok" ? branch.value : "(not a repo)"}`, ` events ${events.length} durable`, ` turn ${engine.turnActive ? "running" : "idle"}`, ` processes ${running} running`, ` log ${engine.log.filePath}`, ]); }), }); // ───────────────────────── v2 commands ───────────────────────── let subtasks: SubtaskManager | null = null; const getSubtasks = (): SubtaskManager => { if (subtasks === null) { const engine = deps.engine(); subtasks = createSubtaskManager({ config: deps.config, workspace: engine.workspace, projectRoot: deps.cwd, logger: engine.logger, publish: (event) => engine.session.publishDurable(event), parentSessionId: engine.session.sessionId, }); } return subtasks; }; // /phase — the escape hatch: show the phase, force a transition (logged as override). commands.push({ id: "phase.show", title: "Phase gate", slash: "/phase", description: "current phase; select to force a transition", run: () => { const engine = deps.engine(); const state = engine.phases.state(); const items: PaletteItem[] = (["understand", "design", "implement"] as Phase[]).map((phase) => ({ id: phase, label: phase, detail: phase === state.phase ? "current" : "force transition (logged as user-override)", })); print([ "", ` phase · ${state.phase} (gate mode: ${engine.phases.mode})`, state.pendingArtifact !== null ? ` pending design: ${state.pendingArtifact.artifact.goal.slice(0, 60)}` : ` design approved: ${state.designApproved ? "yes" : "no"}`, ]); ui.openSelector(items, (id) => { deps.engine().phases.forcePhase(id as Phase); print(["", ` phase forced to ${id} (recorded as user-override)`]); }); }, }); // /fork — pick a checkpoint, copy the JSONL prefix, resume the fork. commands.push({ id: "session.fork", title: "Fork session", slash: "/fork", description: "fork this session at a checkpoint", run: () => { const engine = deps.engine(); const checkpoints = listForkCheckpoints(engine.session.events()); if (checkpoints.length === 0) { print(["", " /fork — no checkpoints yet (user turns, approved designs, compactions)"]); return; } const items: PaletteItem[] = checkpoints .slice(-30) .reverse() .map((cp) => ({ id: String(cp.seq), label: cp.label, detail: `seq ${cp.seq} · ${cp.kind}` })); ui.openSelector(items, (id) => { runAsync("/fork", async () => { const result = await forkSession({ sessionsDir: deps.sessionsDir, projectHash: deps.projectHash, sourceSessionId: deps.engine().session.sessionId, uptoSeq: Number.parseInt(id, 10), }); print([ "", ` forked at seq ${result.forkPoint} → session ${result.sessionId}`, ` ${result.copiedEvents} events copied · opening the fork…`, ]); deps.actions.resumeSession(result.sessionId); }); }); }, }); // /sdiff — structured diff between two runs: " " (A defaults to this session). commands.push({ id: "session.sdiff", title: "Diff two sessions", slash: "/sdiff", description: "structured diff between two session runs", run: () => { const capture = deps.actions.captureNextSubmit; if (capture === undefined) { print(["", " /sdiff is unavailable in this mode"]); return; } print(["", ' sdiff — type " " (or just "" to compare with this one)']); capture((text) => { runAsync("/sdiff", async () => { const parts = text.trim().split(/\s+/).filter((part) => part.length > 0); if (parts.length === 0) { print([" sdiff cancelled"]); return; } const engine = deps.engine(); const idA = parts.length >= 2 ? (parts[0] as string) : engine.session.sessionId; const idB = parts.length >= 2 ? (parts[1] as string) : (parts[0] as string); const load = async (id: string): Promise> => { if (id === engine.session.sessionId) { return summarizeSessionRun(id, engine.session.events()); } const log = await SessionLog.open({ projectHash: deps.projectHash, sessionId: id, sessionsDir: deps.sessionsDir, }); return summarizeSessionRun(id, log.replayedEvents); }; const [a, b] = await Promise.all([load(idA), load(idB)]); print(["", " sdiff", ...renderSessionDiff(a, b)]); }); }); }, }); // /replay — re-run a session's user turns with the current model. commands.push({ id: "session.replay", title: "Replay session", slash: "/replay", description: "re-run a session's user turns (current model)", run: () => { const capture = deps.actions.captureNextSubmit; if (capture === undefined) { print(["", " /replay is unavailable in this mode"]); return; } print(["", ' replay — type "" to re-run its user turns with the current model']); capture((text) => { const sourceId = text.trim(); if (sourceId === "") { print([" replay cancelled"]); return; } runAsync("/replay", async () => { const engine = deps.engine(); print(["", ` replaying ${sourceId} — tool calls re-execute for real (use a clean tree)`]); const result = await replaySession({ config: deps.config, cwd: deps.cwd, sourceSessionId: sourceId, sessionsDir: deps.sessionsDir, logger: engine.logger, sandbox: { exec: workspaceExec(engine.workspace) }, onProgress: (message) => print([` ${message}`]), }); print([ "", ` replay done → session ${result.newSessionId} (${result.turnsReplayed} turns, sandboxed worktree)`, ` compare: /sdiff ${result.newSessionId}`, ]); }); }); }, }); // /memory — the project memory with provenance; low-confidence purge candidates flagged. commands.push({ id: "memory.open", title: "Project memory", slash: "/memory", description: "auto-maintained project memory with provenance", run: () => runAsync("/memory", async () => { const engine = deps.engine(); let content = ""; try { content = await engine.workspace.readFile(join(deps.cwd, MEMORY_FILE)); } catch { print(["", " memory — empty (the agent writes durable facts via the remember tool)"]); return; } const entries = parseMemory(content); const lines = ["", ` memory · ${MEMORY_FILE} (${entries.length} entries)`]; let section = ""; for (const entry of entries) { if (entry.section !== section) { section = entry.section; lines.push(` ${section}`); } const provenance = entry.provenance !== null ? ` [${entry.provenance.confidence} · ${entry.provenance.date} · session ${entry.provenance.session.slice(0, 8)}]` : ""; lines.push(` ${entry.text.replace(/^-\s*/, "· ").slice(0, 100)}${provenance}`); } const purgeable = purgeCandidates(entries); if (purgeable.length > 0) { lines.push(` ${purgeable.length} low-confidence entr${purgeable.length === 1 ? "y" : "ies"} — purge candidates at the next /compact`); } print(lines); }), }); // /verify — run the configured checks now, results recorded as verify.result events. commands.push({ id: "verify.run", title: "Run verification", slash: "/verify", description: "run the project's verify checks now", run: () => runAsync("/verify", async () => { const engine = deps.engine(); if (!engine.verifyRunner.hasChecks) { print(["", " verify — no checks configured or detected (.khaelor/verify.json)"]); return; } print(["", " verify — running checks…"]); const outcome = await engine.verifyRunner.runAll(); const lines = ["", ` verify · ${outcome.ok ? "✓ all passed" : "✗ failures"}`]; for (const result of outcome.results) { const mark = result.ok ? "✓" : "✗"; lines.push( ` ${mark} ${result.check.padEnd(12)} ${(result.durationMs / 1000).toFixed(1)}s${result.ok ? "" : ` (exit ${result.exitCode ?? "killed"})`}`, ); } print(lines); }), }); // /spawn — a parallel subtask in its own worktree + child session. commands.push({ id: "tasks.spawn", title: "Spawn subtask", slash: "/spawn", description: "run a task in an isolated worktree", run: () => { const capture = deps.actions.captureNextSubmit; if (capture === undefined) { print(["", " /spawn is unavailable in this mode"]); return; } print(["", " spawn — type the subtask description and press Enter (empty cancels)"]); capture((text) => { const description = text.trim(); if (description === "") { print([" spawn cancelled"]); return; } runAsync("/spawn", async () => { const record = await getSubtasks().spawn(description); print([ "", ` subtask ${record.taskId} spawned`, ` worktree ${record.worktree.path}`, ` branch ${record.worktree.branch} · child session ${record.childSessionId}`, " /tasks shows progress; completion lands in this session's log", ]); }); }); }, }); // /tasks — the subtask board. commands.push({ id: "tasks.open", title: "Subtasks", slash: "/tasks", description: "parallel subtasks and their state", run: () => { const records = subtasks?.list() ?? []; const lines = ["", " tasks"]; if (records.length === 0) lines.push(" none — /spawn starts one"); for (const record of records) { const glyph = record.status === "running" ? "●" : record.status === "done" ? "✓" : "✗"; const verify = record.verifyOk === null ? "" : record.verifyOk ? " · verify ✓" : " · verify ✗"; lines.push( ` ${glyph} ${record.taskId} ${record.status.padEnd(11)} +${record.diff.added} −${record.diff.removed}${verify} ${record.description.slice(0, 44)}`, ); } if (records.some((record) => record.status === "done")) { lines.push(" merge a finished task with /merge"); } print(lines); }, }); // /merge — supervised --no-ff merge of a finished subtask branch. commands.push({ id: "tasks.merge", title: "Merge subtask", slash: "/merge", description: "merge a finished subtask branch (--no-ff)", run: () => { const records = (subtasks?.list() ?? []).filter((record) => record.status === "done"); if (records.length === 0) { print(["", " /merge — no finished subtasks"]); return; } const items: PaletteItem[] = records.map((record) => ({ id: record.taskId, label: `${record.taskId} · ${record.description.slice(0, 40)}`, detail: `+${record.diff.added} −${record.diff.removed} · ${record.diff.files.length} file(s)`, })); ui.openSelector(items, (id) => { runAsync("/merge", async () => { const record = getSubtasks().get(id); if (record === undefined) return; const engine = deps.engine(); const result = await mergeSubtaskBranch( workspaceExec(engine.workspace), deps.cwd, record.worktree.branch, `khaelor subtask ${record.taskId}: ${record.description.slice(0, 60)}`, ); if (result.ok) { print(["", ` merged ${record.worktree.branch} (--no-ff)`]); } else if (result.conflict) { print([ "", ` merge conflict on ${record.worktree.branch} — aborted cleanly`, " ask KHAELOR to resolve it with both designs in context (v2 §6)", ]); } else { print(["", ` merge failed: ${result.detail}`]); } }); }); }, }); // /goals — daemon goals + status, read from the project's daemon directory. commands.push({ id: "goals.open", title: "Daemon goals", slash: "/goals", description: "long-term goals and daemon status", run: () => runAsync("/goals", async () => { const daemonDir = daemonDirFor(deps.cwd); const status = await readDaemonStatus(daemonDir); const store = new GoalStore(daemonDir); const goals = await store.list(); const lines = ["", ` goals · daemon ${status !== null ? `running (pid ${status.pid})` : "not running — khaelord start"}`]; if (goals.length === 0) lines.push(' none — khaelord goal add ""'); for (const goal of goals) { const runs = await store.runsToday(goal.id); const glyph = goal.status === "active" ? "●" : "○"; lines.push( ` ${glyph} ${goal.id} ${goal.type.padEnd(8)} ${goal.schedule.padEnd(16)} $${goal.budget.maxUsdPerDay}/day · runs ${runs}/${goal.budget.maxRunsPerDay}`, ); lines.push(` ${goal.description.slice(0, 70)}`); } print(lines); }), }); return commands; } /** Register every CLI command into the TUI registry (overrides Phase-2 placeholders by id). */ export function registerCliCommands( registry: { register(def: CommandDef): void }, deps: CliCommandDeps, ): void { for (const def of buildCliCommands(deps)) registry.register(def); }