Something went wrong. Try again.
An agent harness for spec-driven development.
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760#!/usr/bin/env nodeimport path from "node:path";import { buildImportPlan } from "./spec-files.js";import { createCharterDatabase } from "./db/factory.js";import { JazzCharterDatabase } from "./db/jazz.js";import { actionableChanges, buildEventsFromChanges, buildWorkItemsFromEvents, listSpecChanges, queuedWorkItems,} from "./changes.js";import { buildSpecIndex, saveSpecIndex, searchSpecIndex } from "./indexer.js";import { buildAgentPacket, buildAgentRequest, runAgentCommand } from "./agent-harness.js";import type { AgentConsumerRow, AgentPacketStatus, AgentReportStatus, AgentRequestStatus, CharterDatabaseSnapshot, SpecChange, SpecImportPlan, WorkItemStatus } from "./types.js";import type { CharterListenOptions, ServerContext } from "./server.js";
interface ParsedArgs { command: string; positionals: string[]; help: boolean; root: string; specDir: string; host: string; port: number; json: boolean; dryRun: boolean; jazzMemory: boolean; all: boolean; watch: boolean; watchIntervalMs?: number; eventType?: string; documentId?: string; since?: string; until?: string; status?: WorkItemStatus; requestId?: string; message?: string; limit?: number; next: boolean; execAgent: boolean; agentCommand?: string; lettaAgentId?: string;}
async function main(): Promise<void> { const args = parseArgs(process.argv.slice(2));
if (args.help || args.command === "help") { printHelp(); return; }
if (args.command === "watch") { const { startSpecWatcher } = await import("./server.js"); const context: ServerContext = { projectRoot: path.resolve(args.root), specDir: args.specDir, }; const watchOptions = { ...(args.watchIntervalMs === undefined ? {} : { intervalMs: args.watchIntervalMs }), log: (entry: unknown) => console.log(JSON.stringify(entry)), }; const watcher = startSpecWatcher(context, watchOptions); console.log(JSON.stringify({ ts: new Date().toISOString(), level: "info", event: "runtime.started", projectRoot: context.projectRoot, specDir: context.specDir, })); await new Promise<void>((resolve) => { const stop = () => { watcher.close(); resolve(); }; process.once("SIGINT", stop); process.once("SIGTERM", stop); }); return; }
if (args.command === "serve" || args.command === "start" || args.command === "frontend") { const { startCharterServer } = await import("./server.js"); const listenOptions: CharterListenOptions = { projectRoot: args.root, specDir: args.specDir, host: args.host, port: args.port, watch: args.watch, log: (entry) => console.log(JSON.stringify(entry)), }; const running = await startCharterServer(args.watchIntervalMs === undefined ? listenOptions : { ...listenOptions, watchIntervalMs: args.watchIntervalMs }); if (args.watch) { console.log(JSON.stringify({ ts: new Date().toISOString(), level: "info", event: "server.started", url: running.url, projectRoot: args.root, specDir: args.specDir, watch: true, })); } else { console.log(`Charter server listening at ${running.url}`); console.log("Press Ctrl+C to stop."); } await new Promise<void>((resolve) => { const stop = () => { running.server.close(() => resolve()); }; process.once("SIGINT", stop); process.once("SIGTERM", stop); }); return; }
if (args.command === "scan") { const plan = await buildImportPlan({ projectRoot: args.root, specDir: args.specDir }); if (args.json) { console.log(JSON.stringify(plan.documents, null, 2)); return; } printScan(plan); return; }
if (args.command === "index") { const plan = await buildImportPlan({ projectRoot: args.root, specDir: args.specDir }); const index = buildSpecIndex(plan); if (!args.dryRun) await saveSpecIndex(index); if (args.json) { console.log(JSON.stringify(index, null, 2)); return; } console.log(`${args.dryRun ? "Built" : "Wrote"} index for ${Object.keys(index.documents).length} spec documents`); console.log(`Terms: ${Object.keys(index.termIndex).length}`); console.log(`Backlink targets: ${Object.keys(index.backlinks).length}`); if (!args.dryRun) console.log("Index cache: .spec/state/index.json"); return; }
if (args.command === "search") { const query = args.positionals.join(" ").trim(); if (!query) throw new Error("search requires a query"); const plan = await buildImportPlan({ projectRoot: args.root, specDir: args.specDir }); const index = buildSpecIndex(plan); const results = searchSpecIndex(index, query); if (args.json) { console.log(JSON.stringify(results, null, 2)); return; } if (results.length === 0) { console.log(`No spec documents matched: ${query}`); return; } for (const result of results) { console.log(`- ${result.id} ${result.path} title=${result.title}`); } return; }
if (args.command === "changes") { const { plan, changes } = await loadCurrentChanges(args); if (args.json) { console.log(JSON.stringify(args.all ? changes : actionableChanges(changes), null, 2)); return; } printChanges(plan, changes, args.all); return; }
if (args.command === "events") { const db = createCharterDatabase({ projectRoot: args.root, projectId: "charter" }); const snapshot = await db.loadSnapshot(); const events = snapshot.events.filter((event) => { if (args.documentId && event.documentId !== args.documentId) return false; if (args.eventType && event.type !== args.eventType) return false; if (args.since && event.occurredAt < args.since) return false; if (args.until && event.occurredAt > args.until) return false; return true; }); if (args.json) { console.log(JSON.stringify(events, null, 2)); return; } if (events.length === 0) { console.log("No project events matched."); return; } for (const event of events) { const source = event.sourceCoValueId ? ` source=${event.sourceCoValueId}` : ""; const field = event.sourceField ? ` field=${event.sourceField}` : ""; console.log(`- ${event.occurredAt} ${event.type} ${event.documentId} ${event.path} sha=${event.sha256?.slice(0, 12) ?? "none"}${source}${field}`); } return; }
if (args.command === "work" || args.command === "queue") { const db = createCharterDatabase({ projectRoot: args.root, projectId: "charter" }); const snapshot = await db.loadSnapshot(); const itemLimit = args.next ? 1 : args.limit; const items = listWorkItems(snapshot, { command: args.command, status: args.status, documentId: args.documentId, all: args.all, limit: itemLimit, }); if (args.json) { console.log(JSON.stringify(items, null, 2)); return; } if (items.length === 0) { console.log(args.command === "queue" && !args.all ? "No queued work items matched." : "No work items matched."); return; } for (const item of items) { const request = item.requestId ? snapshot.requests[item.requestId] : undefined; const claim = item.requestId ? ` request=${item.requestId}${request ? `/${request.status}` : ""}` : ""; console.log(`- ${item.status} ${item.id} event=${item.eventId} document=${item.documentId} ${item.path}${claim}`); } return; }
if (args.command === "sync") { const { plan, snapshot, changes } = await loadCurrentChanges(args); const events = buildEventsFromChanges(plan, changes); const workItems = buildWorkItemsFromEvents(plan, events, changes, Object.values(snapshot.consumers));
if (args.dryRun) { printPlan(plan, changes, args.all, Object.values(snapshot.consumers)); return; }
const db = createCharterDatabase({ projectRoot: args.root, projectId: "charter" }); await db.loadSnapshot(); for (const row of plan.rows) await db.upsertDocument(row); for (const change of changes.filter((change) => change.type === "deleted")) { await db.deleteDocument(change.documentId); } for (const event of events) await db.appendEvent(event); for (const item of workItems) await db.upsertWorkItem(item); await db.saveSnapshot();
if (args.jazzMemory) { if (db instanceof JazzCharterDatabase) { console.log(JSON.stringify(await db.jazzManifest(), null, 2)); } else { const { materializePlanInJazz } = await import("./jazz/materialize.js"); const result = await materializePlanInJazz({ ...plan, events }, await db.loadSnapshot()); console.log(JSON.stringify(result, null, 2)); } return; }
if (args.json) { console.log(JSON.stringify(await db.loadSnapshot(), null, 2)); return; }
const counts = countChanges(changes); const changedText = [ `${counts.added} added`, `${counts.modified} modified`, `${counts.deleted} deleted`, ].join(", "); const previouslyMaterialized = Object.keys(snapshot.documents).length; console.log(`Synced ${plan.rows.length} spec documents into the configured database (${changedText}; ${previouslyMaterialized} previously materialized)`); console.log(`Queued ${workItems.length} durable work item${workItems.length === 1 ? "" : "s"} from project events.`); console.log("Default adapter: src/db/jazz.ts (Jazz CoValues, with .spec/state/spec-db.json as a local projection mirror)"); console.log("Use --jazz-values to print the active Jazz CoValue IDs."); return; }
if (args.command === "requests") { const db = createCharterDatabase({ projectRoot: args.root, projectId: "charter" }); const snapshot = await db.loadSnapshot(); const requests = Object.values(snapshot.requests) .filter((request) => (args.requestId ? request.id === args.requestId : true)) .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)); if (args.json) { console.log(JSON.stringify(requests, null, 2)); return; } if (requests.length === 0) { console.log("No agent requests matched."); return; } for (const request of requests) { const report = request.reportStatus ? ` report=${request.reportStatus}` : ""; const exit = request.exitCode === undefined ? "" : ` exit=${request.exitCode}`; console.log(`- ${request.status} ${request.id} work=${request.workItemIds.length} docs=${request.attachedDocumentIds.length}${report}${exit}`); if (args.requestId && request.resultText) console.log(indentBlock(request.resultText)); } return; }
if (args.command === "report") { const requestId = args.requestId ?? process.env.CHARTER_REQUEST_ID; if (!requestId) throw new Error("report requires --request or CHARTER_REQUEST_ID"); const reportStatus = parseReportStatus(args.status ?? "done"); const message = args.message ?? args.positionals.join(" ").trim(); if (!message) throw new Error("report requires --message or a positional message"); const db = createCharterDatabase({ projectRoot: args.root, projectId: "charter" }); const snapshot = await db.loadSnapshot(); const request = snapshot.requests[requestId]; if (!request) throw new Error(`Unknown agent request: ${requestId}`); const now = new Date().toISOString(); const requestStatus = requestStatusFromReport(reportStatus); await db.updateAgentRequestStatus(requestId, requestStatus, { resultText: message, reportStatus, reportedAt: now, }); if (request.packetId && snapshot.packets[request.packetId]) { const packetStatus: AgentPacketStatus = reportStatus === "ignored" ? "ignored" : requestStatus; await db.updateAgentPacketStatus(request.packetId, packetStatus, { resultText: message, reportStatus, reportedAt: now }); } for (const workItemId of request.workItemIds) { if (!snapshot.workItems[workItemId]) continue; await db.updateWorkItemStatus(workItemId, reportStatus, { resultText: message, reportedAt: now, }); } await db.saveSnapshot(); if (args.json) { console.log(JSON.stringify({ requestId, status: reportStatus, message }, null, 2)); return; } console.log(`Reported ${reportStatus} for request ${requestId}`); return; }
if (args.command === "agent") { const { plan, changes } = await loadCurrentChanges(args); const db = createCharterDatabase({ projectRoot: args.root, projectId: "charter" }); const snapshot = await db.loadSnapshot(); const requestedWorkItems = Object.values(snapshot.workItems) .filter((item) => item.status === (args.status ?? "queued")) .filter((item) => (args.documentId ? item.documentId === args.documentId : true)) .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)); const itemLimit = args.next ? 1 : args.limit; const workItems = itemLimit === undefined ? requestedWorkItems : requestedWorkItems.slice(0, itemLimit); const command = resolveAgentCommand(args); const request = buildAgentRequest({ plan, changes, command, workItems }); const packet = buildAgentPacket({ request, workItems, documents: snapshot.documents }); await db.upsertAgentPacket(packet); await db.upsertAgentRequest(request); await db.saveSnapshot();
if (args.json) { console.log(JSON.stringify({ request, packet, workItems, changes: actionableChanges(changes) }, null, 2)); return; }
console.log(`Created agent request in database: ${request.id}`); console.log(`Attached documents: ${request.attachedDocumentIds.length}`); console.log(`Selected work items: ${request.workItemIds.length}`); for (const item of workItems) console.log(`- ${item.id} ${item.documentId} ${item.path}`);
if (!args.execAgent) { console.log("No agent executed and no work items were claimed. Pass --exec with --agent-command/--letta-agent, or set CHARTER_AGENT_COMMAND/CHARTER_LETTA_AGENT_ID."); return; }
if (!command) throw new Error("--exec requires --agent-command/--letta-agent or CHARTER_AGENT_COMMAND/CHARTER_LETTA_AGENT_ID"); await db.updateAgentRequestStatus(request.id, "executing"); await db.updateAgentPacketStatus(packet.id, "executing"); for (const item of workItems) { await db.updateWorkItemStatus(item.id, "running", { requestId: request.id, claimedBy: "charter.agent", claimedAt: request.createdAt, }); } await db.saveSnapshot(); const result = await runAgentCommand({ command, projectRoot: args.root, request }); const resultDb = createCharterDatabase({ projectRoot: args.root, projectId: "charter" }); const resultSnapshot = await resultDb.loadSnapshot(); const latestRequest = resultSnapshot.requests[request.id]; if (!latestRequest) throw new Error(`Agent request disappeared during execution: ${request.id}`);
const outputFields = { exitCode: result.exitCode, stdoutText: result.stdoutText, stderrText: result.stderrText, outputText: result.outputText, resultText: latestRequest.resultText ?? fallbackResultText(result.exitCode, result.outputText), };
if (latestRequest.reportedAt) { await resultDb.updateAgentRequestStatus(request.id, latestRequest.status, outputFields); await resultDb.updateAgentPacketStatus(packet.id, latestRequest.reportStatus === "ignored" ? "ignored" : latestRequest.status, { resultText: outputFields.resultText, stdoutText: outputFields.stdoutText, stderrText: outputFields.stderrText, outputText: outputFields.outputText, exitCode: outputFields.exitCode, ...(latestRequest.reportStatus !== undefined ? { reportStatus: latestRequest.reportStatus } : {}), ...(latestRequest.reportedAt !== undefined ? { reportedAt: latestRequest.reportedAt } : {}), outputContract: `exit=${outputFields.exitCode}; ${outputFields.resultText}`.slice(0, 2000), }); } else { const requestStatus: AgentRequestStatus = result.exitCode === 0 ? "executed" : "failed"; const workStatus: WorkItemStatus = result.exitCode === 0 ? "done" : "failed"; await resultDb.updateAgentRequestStatus(request.id, requestStatus, outputFields); await resultDb.updateAgentPacketStatus(packet.id, requestStatus === "executed" ? "executed" : "failed", { resultText: outputFields.resultText, stdoutText: outputFields.stdoutText, stderrText: outputFields.stderrText, outputText: outputFields.outputText, exitCode: outputFields.exitCode, outputContract: `exit=${outputFields.exitCode}; ${outputFields.resultText}`.slice(0, 2000), }); for (const item of workItems) { if (resultSnapshot.workItems[item.id]) { await resultDb.updateWorkItemStatus(item.id, workStatus, { resultText: outputFields.resultText, }); } } } await resultDb.saveSnapshot(); if (result.exitCode !== 0) process.exitCode = result.exitCode; return; }
printHelp(); process.exitCode = 1;}
function parseArgs(argv: string[]): ParsedArgs { const [command = "help", ...rest] = argv; let help = false; let root = process.cwd(); let specDir = "spec"; let host = "127.0.0.1"; let port = 4173; let json = false; let dryRun = false; let jazzMemory = false; let all = false; let watch = false; let watchIntervalMs: number | undefined; let execAgent = false; let agentCommand: string | undefined; let lettaAgentId: string | undefined; let limit: number | undefined; let next = false; let eventType: string | undefined; let documentId: string | undefined; let since: string | undefined; let until: string | undefined; let status: WorkItemStatus | undefined; let requestId: string | undefined; let message: string | undefined; const positionals: string[] = [];
for (let index = 0; index < rest.length; index += 1) { const value = rest[index]; if (value === "--root") { const next = rest[index + 1]; if (!next) throw new Error("--root requires a value"); root = path.resolve(next); index += 1; } else if (value === "--spec-dir") { const next = rest[index + 1]; if (!next) throw new Error("--spec-dir requires a value"); specDir = next; index += 1; } else if (value === "--host") { const next = rest[index + 1]; if (!next) throw new Error("--host requires a value"); host = next; index += 1; } else if (value === "--port") { const nextValue = rest[index + 1]; if (!nextValue) throw new Error("--port requires a value"); port = parsePositiveInteger(nextValue, "--port"); index += 1; } else if (value === "--agent-command") { const nextValue = rest[index + 1]; if (!nextValue) throw new Error("--agent-command requires a value"); agentCommand = nextValue; index += 1; } else if (value === "--letta-agent" || value === "--letta-agent-id") { const nextValue = rest[index + 1]; if (!nextValue) throw new Error(`${value} requires a value`); lettaAgentId = nextValue; index += 1; } else if (value === "--limit") { const nextValue = rest[index + 1]; if (!nextValue) throw new Error("--limit requires a value"); limit = parsePositiveInteger(nextValue, "--limit"); index += 1; } else if (value === "--watch-interval" || value === "--watch-interval-ms") { const nextValue = rest[index + 1]; if (!nextValue) throw new Error(`${value} requires a value`); watchIntervalMs = parsePositiveInteger(nextValue, value); index += 1; } else if (value === "--event-type" || value === "--type") { const next = rest[index + 1]; if (!next) throw new Error(`${value} requires a value`); eventType = next; index += 1; } else if (value === "--document" || value === "--document-id") { const next = rest[index + 1]; if (!next) throw new Error(`${value} requires a value`); documentId = next; index += 1; } else if (value === "--status") { const next = rest[index + 1]; if (!next) throw new Error("--status requires a value"); status = parseWorkStatus(next); index += 1; } else if (value === "--request" || value === "--request-id") { const next = rest[index + 1]; if (!next) throw new Error(`${value} requires a value`); requestId = next; index += 1; } else if (value === "--message") { const next = rest[index + 1]; if (!next) throw new Error("--message requires a value"); message = next; index += 1; } else if (value === "--since") { const next = rest[index + 1]; if (!next) throw new Error("--since requires a value"); since = next; index += 1; } else if (value === "--until") { const next = rest[index + 1]; if (!next) throw new Error("--until requires a value"); until = next; index += 1; } else if (value === "--help" || value === "-h") { help = true; } else if (value === "--json") { json = true; } else if (value === "--dry-run") { dryRun = true; } else if (value === "--jazz-values" || value === "--jazz-memory") { jazzMemory = true; } else if (value === "--all") { all = true; } else if (value === "--watch") { watch = true; } else if (value === "--exec") { execAgent = true; } else if (value === "--next") { next = true; limit = 1; } else if (value?.startsWith("--")) { throw new Error(`Unknown argument: ${value}`); } else if (value === "next" && (command === "queue" || command === "work" || command === "agent")) { next = true; limit = 1; positionals.push(value); } else if (value !== undefined) { positionals.push(value); } }
const parsed: ParsedArgs = { command, positionals, help, root, specDir, host, port, json, dryRun, jazzMemory, all, watch, next, execAgent }; if (agentCommand !== undefined) parsed.agentCommand = agentCommand; if (eventType !== undefined) parsed.eventType = eventType; if (documentId !== undefined) parsed.documentId = documentId; if (since !== undefined) parsed.since = since; if (until !== undefined) parsed.until = until; if (status !== undefined) parsed.status = status; if (requestId !== undefined) parsed.requestId = requestId; if (message !== undefined) parsed.message = message; if (limit !== undefined) parsed.limit = limit; if (watchIntervalMs !== undefined) parsed.watchIntervalMs = watchIntervalMs; if (lettaAgentId !== undefined) parsed.lettaAgentId = lettaAgentId; return parsed;}
function listWorkItems(snapshot: CharterDatabaseSnapshot, options: { command: string; status: WorkItemStatus | undefined; documentId: string | undefined; all: boolean; limit: number | undefined;}): ReturnType<typeof queuedWorkItems> { const items = Object.values(snapshot.workItems) .filter((item) => { if (options.status) return item.status === options.status; if (options.command === "queue" && !options.all) return item.status === "queued"; return true; }) .filter((item) => (options.documentId ? item.documentId === options.documentId : true)) .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)); return options.limit === undefined ? items : items.slice(0, options.limit);}
function resolveAgentCommand(args: ParsedArgs): string { if (args.agentCommand) return args.agentCommand; if (process.env.CHARTER_AGENT_COMMAND) return process.env.CHARTER_AGENT_COMMAND; const lettaAgentId = args.lettaAgentId ?? process.env.CHARTER_LETTA_AGENT_ID; if (lettaAgentId) return `letta --agent ${shellQuote(lettaAgentId)} --new -p "$(cat {promptFile})"`; return "";}
function parsePositiveInteger(value: string, label: string): number { const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1) throw new Error(`${label} must be a positive integer`); return parsed;}
function shellQuote(value: string): string { return `'${value.replaceAll("'", "'\''")}'`;}
async function loadCurrentChanges(args: ParsedArgs): Promise<{ plan: SpecImportPlan; snapshot: CharterDatabaseSnapshot; changes: SpecChange[];}> { const plan = await buildImportPlan({ projectRoot: args.root, specDir: args.specDir }); const db = createCharterDatabase({ projectRoot: args.root, projectId: "charter" }); const snapshot = await db.loadSnapshot(); const changes = listSpecChanges(plan, snapshot); return { plan, snapshot, changes };}
function parseWorkStatus(value: string): WorkItemStatus { if (["queued", "claimed", "running", "done", "ignored", "failed", "blocked"].includes(value)) return value as WorkItemStatus; throw new Error(`Unknown work item status: ${value}`);}
function parseReportStatus(value: WorkItemStatus | string): AgentReportStatus { if (["done", "failed", "blocked", "ignored"].includes(value)) return value as AgentReportStatus; throw new Error(`Report status must be one of: done, failed, blocked, ignored`);}
function requestStatusFromReport(status: AgentReportStatus): AgentRequestStatus { if (status === "failed") return "failed"; if (status === "blocked") return "blocked"; return "executed";}
function fallbackResultText(exitCode: number, outputText: string): string { const trimmed = outputText.trim(); if (trimmed) return trimmed.slice(-12000); return `agent exit ${exitCode}`;}
function indentBlock(value: string): string { return value.split("\n").map((line) => ` ${line}`).join("\n");}
function printScan(plan: Awaited<ReturnType<typeof buildImportPlan>>): void { console.log(`Found ${plan.documents.length} spec document${plan.documents.length === 1 ? "" : "s"} in ${plan.specRoot}`); for (const document of plan.documents) { console.log(`- ${document.id} ${document.path} sha=${document.sha256.slice(0, 12)} title=${document.title}`); }}
function printChanges(plan: SpecImportPlan, changes: SpecChange[], includeUnchanged: boolean): void { const visible = includeUnchanged ? changes : actionableChanges(changes); const counts = countChanges(changes); console.log(`Compared ${plan.rows.length} current spec documents against configured database state`); console.log(`${counts.added} added, ${counts.modified} modified, ${counts.deleted} deleted, ${counts.unchanged} unchanged`); if (visible.length === 0) { console.log("No actionable spec changes."); return; } console.log(""); for (const change of visible) { const hashes = change.currentSha256 && change.previousSha256 && change.currentSha256 !== change.previousSha256 ? ` ${change.previousSha256.slice(0, 12)} -> ${change.currentSha256.slice(0, 12)}` : ""; console.log(`- ${change.type} ${change.documentId} ${change.path}${hashes}`); }}
function printPlan(plan: SpecImportPlan, changes: SpecChange[], includeUnchanged: boolean, consumers: AgentConsumerRow[]): void { printScan(plan); console.log(""); printChanges(plan, changes, includeUnchanged); const events = buildEventsFromChanges(plan, changes); const workItems = buildWorkItemsFromEvents(plan, events, changes, consumers); console.log(""); console.log("Planned project events and work items:"); if (events.length === 0) { console.log("- none"); return; } for (const event of events) { console.log(`- ${event.type} ${event.documentId} ${event.path} sha=${event.sha256?.slice(0, 12) ?? "none"}`); } console.log(""); console.log("Planned queued work:"); for (const item of workItems) console.log(`- ${item.id} ${item.documentId} ${item.path}`);}
function countChanges(changes: SpecChange[]): Record<SpecChange["type"], number> { return { added: changes.filter((change) => change.type === "added").length, modified: changes.filter((change) => change.type === "modified").length, deleted: changes.filter((change) => change.type === "deleted").length, unchanged: changes.filter((change) => change.type === "unchanged").length, };}
function printHelp(): void { console.log(`charter <command> [options]
Commands: watch Start the local Charter runtime: watch spec files, ingest changes, emit JSONL logs serve Start the optional local Charter HTTP inspection API/status page start Alias for serve frontend Alias for serve scan Parse spec/**/*.md and print normalized document ids index Build .spec/state/index.json cache search <query> Search the current spec index lexically changes List projection changes against the configured database events List project event feed entries, with optional filters work List durable work items queued from project events queue List queued work items; use queue next for one item sync Upsert parsed docs into the configured database and queue work agent Create a database agent request from queued work items; use agent next for one item report Report request completion from an agent runner requests List agent requests and reports
Options: --root <path> Project root. Defaults to cwd. --spec-dir <dir> Spec directory relative to root. Defaults to spec. --host <host> For serve/start/frontend. Defaults to 127.0.0.1. --port <port> For serve/start/frontend. Defaults to 4173. --watch Deprecated for serve/start/frontend; prefer charter watch. --watch-interval <ms> Watch polling interval. Defaults to 750. --json Print JSON output. --dry-run For sync/index: show or build without writing state. --all Include unchanged docs in change output. --jazz-values For sync: print the active Jazz CoValue IDs. --jazz-memory Alias for --jazz-values. --event-type <type> For events: filter by event type. --document <id> For events/work/agent: filter by document id. --status <status> For work/queue/report: queued, claimed, running, done, ignored, failed, blocked. --limit <n> For work/queue/agent: only take the first n matching work items. --next For work/queue/agent: shorthand for --limit 1. --since <iso> For events: filter by lower timestamp bound. --until <iso> For events: filter by upper timestamp bound. --request <id> For report/requests: target request id. Defaults to CHARTER_REQUEST_ID for report. --message <text> For report: human-readable completion report. --agent-command <command> Command for agent execution. Receives prompt on stdin; {promptFile} exports a temp file. --letta-agent <id> Convenience runner: letta --agent <id> --new -p "$(cat {promptFile})". --exec Execute the configured agent command.`);}
main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1;});