Something went wrong. Try again.
An agent harness for spec-driven development.
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217import fs from "node:fs/promises";import path from "node:path";import type { CharterDatabase } from "./adapter.js";import type { AgentPacketRow, AgentPacketStatus, AgentRequestRow, AgentRequestStatus, AgentConsumerRow, CharterDatabaseSnapshot, ProjectEventRow, SpecDocumentRow, WorkItemRow, WorkItemStatus,} from "../types.js";
export class JsonCharterDatabase implements CharterDatabase { private snapshot: CharterDatabaseSnapshot | undefined;
constructor( private readonly options: { projectRoot: string; projectId?: string; statePath?: string; }, ) {}
async loadSnapshot(): Promise<CharterDatabaseSnapshot> { if (this.snapshot) return this.snapshot;
const statePath = this.statePath(); try { const raw = await fs.readFile(statePath, "utf8"); this.snapshot = normalizeSnapshot(JSON.parse(raw), this.options.projectId); return this.snapshot; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; this.snapshot = emptySnapshot(this.options.projectRoot, this.options.projectId); return this.snapshot; } }
async upsertDocument(row: SpecDocumentRow): Promise<void> { const snapshot = await this.loadSnapshot(); const previous = snapshot.documents[row.id]; if (previous && previous.path !== row.path) { delete snapshot.pathIndex[previous.path]; } snapshot.documents[row.id] = row; snapshot.pathIndex[row.path] = row.id; snapshot.updatedAt = new Date().toISOString(); }
async deleteDocument(documentId: string): Promise<void> { const snapshot = await this.loadSnapshot(); const existing = snapshot.documents[documentId]; if (existing) delete snapshot.pathIndex[existing.path]; delete snapshot.documents[documentId]; snapshot.updatedAt = new Date().toISOString(); }
async appendEvent(event: ProjectEventRow): Promise<void> { const snapshot = await this.loadSnapshot(); if (!snapshot.events.some((existing) => existing.id === event.id)) snapshot.events.push(event); snapshot.updatedAt = new Date().toISOString(); }
async upsertWorkItem(row: WorkItemRow): Promise<void> { const snapshot = await this.loadSnapshot(); snapshot.workItems[row.id] = row; snapshot.updatedAt = new Date().toISOString(); }
async updateWorkItemStatus(id: string, status: WorkItemStatus, fields: Partial<WorkItemRow> = {}): Promise<void> { const snapshot = await this.loadSnapshot(); const item = snapshot.workItems[id]; if (!item) throw new Error(`Unknown work item: ${id}`); snapshot.workItems[id] = { ...item, ...fields, status, updatedAt: new Date().toISOString(), }; snapshot.updatedAt = new Date().toISOString(); }
async upsertAgentConsumer(row: AgentConsumerRow): Promise<void> { const snapshot = await this.loadSnapshot(); snapshot.consumers[row.id] = row; snapshot.updatedAt = new Date().toISOString(); }
async upsertAgentPacket(row: AgentPacketRow): Promise<void> { const snapshot = await this.loadSnapshot(); snapshot.packets[row.id] = row; snapshot.updatedAt = new Date().toISOString(); }
async updateAgentPacketStatus(id: string, status: AgentPacketStatus, fields: Partial<AgentPacketRow> = {}): Promise<void> { const snapshot = await this.loadSnapshot(); const packet = snapshot.packets[id]; if (!packet) throw new Error(`Unknown agent packet: ${id}`); snapshot.packets[id] = { ...packet, ...fields, status, updatedAt: new Date().toISOString(), }; snapshot.updatedAt = new Date().toISOString(); }
async upsertAgentRequest(row: AgentRequestRow): Promise<void> { const snapshot = await this.loadSnapshot(); snapshot.requests[row.id] = row; snapshot.updatedAt = new Date().toISOString(); }
async updateAgentRequestStatus(id: string, status: AgentRequestStatus, fields: Partial<AgentRequestRow> = {}): Promise<void> { const snapshot = await this.loadSnapshot(); const request = snapshot.requests[id]; if (!request) throw new Error(`Unknown agent request: ${id}`); snapshot.requests[id] = { ...request, ...fields, status, updatedAt: new Date().toISOString(), }; snapshot.updatedAt = new Date().toISOString(); }
async saveSnapshot(): Promise<void> { const snapshot = await this.loadSnapshot(); const statePath = this.statePath(); await fs.mkdir(path.dirname(statePath), { recursive: true }); await fs.writeFile(statePath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8"); }
private statePath(): string { return this.options.statePath ?? path.join(this.options.projectRoot, ".spec", "state", "spec-db.json"); }}
function emptySnapshot(projectRoot: string, projectId: string | undefined): CharterDatabaseSnapshot { return { projectId: projectId ?? path.basename(projectRoot), updatedAt: new Date().toISOString(), documents: {}, pathIndex: {}, events: [], workItems: {}, consumers: {}, packets: {}, requests: {}, };}
function normalizeSnapshot(value: unknown, projectId: string | undefined): CharterDatabaseSnapshot { if (!value || typeof value !== "object") throw new Error("Invalid Charter database snapshot"); const record = value as Partial<CharterDatabaseSnapshot>; return { projectId: projectId ?? record.projectId ?? "charter", updatedAt: record.updatedAt ?? new Date().toISOString(), documents: record.documents ?? {}, pathIndex: record.pathIndex ?? {}, events: record.events ?? [], workItems: normalizeWorkItems(record.workItems ?? {}), consumers: record.consumers ?? {}, packets: normalizePackets(record.packets ?? {}), requests: normalizeRequests(record.requests ?? {}), };}
function normalizeWorkItems(workItems: Record<string, WorkItemRow>): Record<string, WorkItemRow> { return Object.fromEntries( Object.entries(workItems).map(([id, item]) => [ id, { ...item, attachedDocumentIds: item.attachedDocumentIds ?? [], matchedConsumerIds: item.matchedConsumerIds ?? [], }, ]), );}
function normalizePackets(packets: Record<string, AgentPacketRow>): Record<string, AgentPacketRow> { return Object.fromEntries( Object.entries(packets).map(([id, packet]) => [ id, { ...packet, workItemIds: packet.workItemIds ?? [], eventIds: packet.eventIds ?? [], affectedDocumentIds: packet.affectedDocumentIds ?? packet.attachedDocumentIds ?? [], promptDocumentIds: packet.promptDocumentIds ?? [], attachedDocumentIds: packet.attachedDocumentIds ?? [], documentVersionIds: packet.documentVersionIds ?? {}, outputContract: packet.outputContract ?? "Report files changed, behavior implemented, checks run, and unresolved ambiguity.", ...(packet.exitCode !== undefined ? { exitCode: packet.exitCode } : {}), }, ]), );}
function normalizeRequests(requests: Record<string, AgentRequestRow>): Record<string, AgentRequestRow> { return Object.fromEntries( Object.entries(requests).map(([id, request]) => [ id, { ...request, workItemIds: request.workItemIds ?? [], attachedDocumentIds: request.attachedDocumentIds ?? request.changeIds ?? [], }, ]), );}