ai-foreman 1.6.2 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/claude.d.ts +5 -1
  3. package/dist/adapters/claude.js +41 -0
  4. package/dist/adapters/codex.d.ts +26 -28
  5. package/dist/adapters/codex.js +209 -148
  6. package/dist/adapters/recovering.d.ts +3 -1
  7. package/dist/adapters/recovering.js +2 -0
  8. package/dist/adapters/types.d.ts +29 -0
  9. package/dist/branch/planner.js +1 -1
  10. package/dist/branch/runner.d.ts +6 -0
  11. package/dist/branch/runner.js +149 -16
  12. package/dist/buildRuns.d.ts +9 -2
  13. package/dist/buildRuns.js +110 -7
  14. package/dist/cli/start.d.ts +4 -0
  15. package/dist/cli/start.js +283 -28
  16. package/dist/cli/status.js +25 -2
  17. package/dist/cli/tickets.d.ts +1 -0
  18. package/dist/cli/tickets.js +67 -27
  19. package/dist/foreman.d.ts +18 -3
  20. package/dist/foreman.js +112 -5
  21. package/dist/log.d.ts +1 -1
  22. package/dist/operationRecovery.d.ts +25 -0
  23. package/dist/operationRecovery.js +46 -0
  24. package/dist/outcomes.d.ts +16 -0
  25. package/dist/outcomes.js +27 -0
  26. package/dist/qaReview.d.ts +52 -0
  27. package/dist/qaReview.js +103 -0
  28. package/dist/qaSnapshot.d.ts +19 -0
  29. package/dist/qaSnapshot.js +88 -0
  30. package/dist/sessionLifecycle.d.ts +37 -0
  31. package/dist/sessionLifecycle.js +104 -0
  32. package/dist/sources/sourceRegistry.d.ts +49 -0
  33. package/dist/sources/sourceRegistry.js +577 -0
  34. package/dist/statusReporter.d.ts +40 -0
  35. package/dist/statusReporter.js +31 -0
  36. package/dist/ticketPopulation.d.ts +54 -0
  37. package/dist/ticketPopulation.js +209 -0
  38. package/dist/tickets/blockers.d.ts +1 -1
  39. package/dist/tickets/blockers.js +2 -0
  40. package/dist/tickets/commands.d.ts +1 -0
  41. package/dist/tickets/commands.js +31 -1
  42. package/dist/tickets/delivery.d.ts +36 -0
  43. package/dist/tickets/delivery.js +204 -4
  44. package/dist/tickets/queue.d.ts +3 -0
  45. package/dist/tickets/queue.js +50 -1
  46. package/dist/tickets/setupConfig.js +30 -2
  47. package/dist/tickets/sourceFetch.js +6 -1
  48. package/dist/tickets/stateDb.d.ts +2 -1
  49. package/dist/tickets/stateDb.js +24 -1
  50. package/dist/tickets/ticketSchema.d.ts +53 -0
  51. package/dist/tickets/ticketSchema.js +11 -0
  52. package/dist/tickets/validate.js +1 -1
  53. package/dist/workflowDb.d.ts +113 -0
  54. package/dist/workflowDb.js +261 -0
  55. package/package.json +20 -3
@@ -1,5 +1,6 @@
1
1
  import type { TicketDef } from "./ticketSchema.js";
2
2
  import type { TicketState } from "./stateDb.js";
3
+ import type { DeliveryConfig } from "./delivery.js";
3
4
  export interface QueueRow {
4
5
  rank: number;
5
6
  ticket: string;
@@ -34,3 +35,5 @@ export interface ActiveStatusRow {
34
35
  }
35
36
  export declare function buildNextQueue(ticketDefs: TicketDef[], states: Map<string, TicketState>, queueLimit: number): QueueRow[];
36
37
  export declare function buildActiveStatusRows(queueRows: QueueRow[], ticketDefs: TicketDef[], states: Map<string, TicketState>): ActiveStatusRow[];
38
+ /** Human-readable queue blocks keep a whole unfinished stack and its resume point visible. */
39
+ export declare function formatStackAwareQueue(ticketDefs: TicketDef[], states: Map<string, TicketState>, delivery?: DeliveryConfig): string[];
@@ -1,4 +1,5 @@
1
1
  import { resolveBlockers, computeDisplayStatus } from "./blockers.js";
2
+ import { normalizeStackNodes } from "./delivery.js";
2
3
  function defaultNextAction(ticket, displayStatus, blockedBy) {
3
4
  if (displayStatus === "blocked")
4
5
  return `Resolve blockers: ${blockedBy.join(", ")}`;
@@ -11,7 +12,7 @@ export function buildNextQueue(ticketDefs, states, queueLimit) {
11
12
  .filter((t) => {
12
13
  const s = states.get(t.id);
13
14
  const status = s?.status ?? "planned";
14
- return status !== "done" && status !== "canceled";
15
+ return status !== "done" && status !== "canceled" && status !== "obsolete";
15
16
  })
16
17
  .sort((a, b) => a.order - b.order);
17
18
  const window = remaining.slice(0, Math.min(queueLimit, remaining.length));
@@ -60,3 +61,51 @@ export function buildActiveStatusRows(queueRows, ticketDefs, states) {
60
61
  };
61
62
  });
62
63
  }
64
+ /** Human-readable queue blocks keep a whole unfinished stack and its resume point visible. */
65
+ export function formatStackAwareQueue(ticketDefs, states, delivery) {
66
+ if (!delivery?.stacks?.length)
67
+ return buildNextQueue(ticketDefs, states, Number.MAX_SAFE_INTEGER).map((row) => `${row.ticket} ${row.status} ${row.title}`);
68
+ const defs = new Map(ticketDefs.map((ticket) => [ticket.id, ticket]));
69
+ const unitById = new Map(delivery.units.map((unit) => [unit.id, unit]));
70
+ const stackedTickets = new Set();
71
+ const lines = [];
72
+ for (const stack of delivery.stacks) {
73
+ const nodes = normalizeStackNodes(stack, delivery.units, ticketDefs);
74
+ const ticketIds = stack.units.flatMap((id) => unitById.get(id)?.tickets ?? []);
75
+ ticketIds.forEach((id) => stackedTickets.add(id));
76
+ const completed = ticketIds.filter((id) => terminal(states.get(id)?.status));
77
+ if (completed.length === ticketIds.length && (stack.status ?? "planned") === "merged")
78
+ continue;
79
+ const next = ticketIds.find((id) => !terminal(states.get(id)?.status));
80
+ const blockers = next && defs.get(next) ? resolveBlockers(defs.get(next), states) : [];
81
+ const state = deriveStackState(stack, ticketIds, states, blockers);
82
+ lines.push(`=== STACK START: ${stack.name} (${stack.id}) ===`);
83
+ lines.push(`status=${state}; size=${ticketIds.length}; pr_depth=${nodes.length}; blockers=${blockers.join(",") || "none"}; completed_prefix=${completed.join(",") || "none"}; next=${next ?? "none"}`);
84
+ lines.push(`reviews=${stack.review_links?.join(",") || "none"}; remote_checked=${stack.remote_checked_at ?? "never"}${stack.remote_stale ? " (stale; run --refresh for a live check)" : ""}`);
85
+ for (const id of ticketIds)
86
+ lines.push(`${id} ${states.get(id)?.status ?? "planned"} ${defs.get(id)?.title ?? "unknown ticket"}`);
87
+ lines.push(`=== STACK END: ${stack.name} (${stack.id}) ===`);
88
+ }
89
+ for (const ticket of ticketDefs.sort((a, b) => a.order - b.order)) {
90
+ if (stackedTickets.has(ticket.id) || terminal(states.get(ticket.id)?.status))
91
+ continue;
92
+ const blockers = resolveBlockers(ticket, states);
93
+ lines.push(`${ticket.id} ${computeDisplayStatus(states.get(ticket.id)?.status ?? "planned", blockers)} ${ticket.title}`);
94
+ }
95
+ return lines;
96
+ }
97
+ function terminal(status) { return status === "done" || status === "canceled" || status === "obsolete"; }
98
+ function deriveStackState(stack, tickets, states, blockers) {
99
+ if (stack.status === "merged")
100
+ return "merged";
101
+ if (stack.status === "awaiting_review")
102
+ return "awaiting_review";
103
+ if (blockers.length)
104
+ return "blocked";
105
+ const completed = tickets.filter((id) => terminal(states.get(id)?.status)).length;
106
+ if (completed === 0)
107
+ return stack.status ?? "planned";
108
+ if (completed < tickets.length)
109
+ return "partial";
110
+ return "awaiting_review";
111
+ }
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { parse, stringify } from "yaml";
5
5
  import { loadDefaults } from "special-agents";
6
+ import { appendLegacyTicketSources, loadSourceRegistry } from "../sources/sourceRegistry.js";
6
7
  export const RAFI_CONFIG_FILE = "rafi-config.yaml";
7
8
  const AGENT_NAMES = ["builder", "qa", "planner", "ticket-maker", "uninstaller"];
8
9
  const SKILL_NAMES = [
@@ -48,7 +49,10 @@ export function loadTicketSetupConfig(projectDir) {
48
49
  const raw = loadRafiConfigObject(projectDir);
49
50
  if (!raw || raw.tickets === undefined)
50
51
  return undefined;
51
- return normalizeTicketsSetupConfig(raw.tickets, `${RAFI_CONFIG_FILE}.tickets`);
52
+ const setup = normalizeTicketsSetupConfig(raw.tickets, `${RAFI_CONFIG_FILE}.tickets`);
53
+ if (setup.sources.length === 0)
54
+ setup.sources = legacySourcesFromRegistry(loadSourceRegistry(projectDir).registry.entries);
55
+ return setup;
52
56
  }
53
57
  export function loadTicketSetupConfigWithDefaults(projectDir) {
54
58
  return loadTicketSetupConfig(projectDir) ?? cloneTicketSetup(DEFAULT_TICKET_SETUP);
@@ -59,7 +63,17 @@ export function hasTicketSetupConfig(projectDir) {
59
63
  }
60
64
  export function saveTicketSetupConfig(projectDir, setup, opts = {}) {
61
65
  const config = loadRafiConfigObject(projectDir) ?? minimalRafiConfig(projectDir, opts);
62
- config.tickets = denormalizeTicketsSetupConfig(normalizeTicketsSetupConfig(setup, "tickets"));
66
+ const normalized = normalizeTicketsSetupConfig(setup, "tickets");
67
+ const ticketConfig = denormalizeTicketsSetupConfig(normalized);
68
+ delete ticketConfig.sources;
69
+ config.tickets = ticketConfig;
70
+ config.sources = appendLegacyTicketSources(loadSourceRegistry(projectDir).registry, normalized.sources);
71
+ const planning = config.planning && typeof config.planning === "object" && !Array.isArray(config.planning) ? config.planning : undefined;
72
+ if (planning) {
73
+ delete planning.sources;
74
+ if (Object.keys(planning).length === 0)
75
+ delete config.planning;
76
+ }
63
77
  mkdirSync(projectDir, { recursive: true });
64
78
  writeFileSync(join(projectDir, RAFI_CONFIG_FILE), stringify(config, { lineWidth: 100 }), "utf8");
65
79
  }
@@ -424,3 +438,17 @@ function cloneTicketSetup(setup) {
424
438
  function unique(values) {
425
439
  return [...new Set(values)];
426
440
  }
441
+ function legacySourcesFromRegistry(entries) {
442
+ const out = [];
443
+ for (const entry of entries.filter((item) => item.active)) {
444
+ if (entry.type === "local" && entry.locator.path)
445
+ out.push({ type: "local", paths: [entry.locator.path] });
446
+ else if (entry.type === "url" && entry.locator.url)
447
+ out.push({ type: "url", url: entry.locator.url });
448
+ else if (entry.type === "linear")
449
+ out.push({ type: "linear", api_key_env: entry.locator.api_key_env ?? "LINEAR_API_KEY", team_key: entry.locator.team_key, filter: entry.locator.filter });
450
+ else if (entry.type === "jira" && entry.locator.site && entry.locator.jql)
451
+ out.push({ type: "jira", site: entry.locator.site, email_env: entry.locator.email_env ?? "JIRA_EMAIL", token_env: entry.locator.token_env ?? "JIRA_API_TOKEN", jql: entry.locator.jql });
452
+ }
453
+ return dedupeTicketSources(out);
454
+ }
@@ -23,7 +23,7 @@ export async function fetchAndSnapshotUrl(projectDir, input, limits = {}) {
23
23
  response = await fetch(current, {
24
24
  redirect: "manual",
25
25
  signal: controller.signal,
26
- headers: { Accept: "text/html,text/plain,text/markdown,application/pdf;q=0.9" },
26
+ headers: { Accept: "text/html,text/plain,text/markdown,application/json,application/yaml,text/yaml,application/pdf;q=0.9" },
27
27
  });
28
28
  }
29
29
  finally {
@@ -154,6 +154,11 @@ function detectContentType(header, bytes, url) {
154
154
  return "html";
155
155
  if (["text/markdown", "text/x-markdown"].includes(mime) || /\.md(?:own)?$/i.test(new URL(url).pathname))
156
156
  return "markdown";
157
+ if (["application/json", "application/yaml", "application/x-yaml", "text/yaml", "text/x-yaml"].includes(mime) || /\.(?:json|ya?ml)$/i.test(new URL(url).pathname)) {
158
+ if (bytes.subarray(0, Math.min(bytes.length, 1024)).includes(0))
159
+ throw new Error("binary URL content is not supported");
160
+ return "text";
161
+ }
157
162
  if (!mime || mime.startsWith("text/") || mime === "application/octet-stream") {
158
163
  if (bytes.subarray(0, Math.min(bytes.length, 1024)).includes(0))
159
164
  throw new Error("binary URL content is not supported");
@@ -1,4 +1,4 @@
1
- export type TicketStatus = "planned" | "next" | "in_progress" | "blocked" | "done" | "canceled";
1
+ export type TicketStatus = "planned" | "next" | "in_progress" | "blocked" | "done" | "canceled" | "obsolete";
2
2
  export type ValidationResult = "passed" | "failed" | "not_run" | "not_applicable";
3
3
  export interface TicketState {
4
4
  ticket_id: string;
@@ -92,6 +92,7 @@ export interface OperationReceipt {
92
92
  export declare class StateDb {
93
93
  private db;
94
94
  constructor(dbPath: string);
95
+ private migrateObsoleteStatus;
95
96
  close(): void;
96
97
  getState(ticketId: string): TicketState | undefined;
97
98
  getAllStates(): Map<string, TicketState>;
@@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS ticket_state (
25
25
  validation_notes TEXT,
26
26
  updated_at TEXT NOT NULL,
27
27
  updated_by TEXT,
28
- CHECK (status IN ('planned','next','in_progress','blocked','done','canceled')),
28
+ CHECK (status IN ('planned','next','in_progress','blocked','done','canceled','obsolete')),
29
29
  CHECK (validation_result IS NULL OR validation_result IN ('passed','failed','not_run','not_applicable'))
30
30
  );
31
31
 
@@ -120,6 +120,29 @@ export class StateDb {
120
120
  this.db = new Database(dbPath);
121
121
  this.db.pragma("journal_mode = WAL");
122
122
  this.db.exec(INIT_SQL);
123
+ this.migrateObsoleteStatus();
124
+ }
125
+ migrateObsoleteStatus() {
126
+ const row = this.db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='ticket_state'").get();
127
+ if (!row || row.sql.includes("'obsolete'"))
128
+ return;
129
+ this.db.pragma("foreign_keys = OFF");
130
+ this.db.exec(`
131
+ DROP INDEX IF EXISTS idx_ticket_state_status;
132
+ ALTER TABLE ticket_state RENAME TO ticket_state_legacy;
133
+ CREATE TABLE ticket_state (
134
+ ticket_id TEXT PRIMARY KEY,status TEXT NOT NULL DEFAULT 'planned',owner TEXT,current_step TEXT,next_action TEXT,
135
+ blocked_by_json TEXT NOT NULL DEFAULT '[]',blocker_type TEXT,blocker_notes TEXT,first_blocked_at TEXT,last_checked_at TEXT,
136
+ last_worked_at TEXT,completed_at TEXT,attempt_count INTEGER NOT NULL DEFAULT 0,last_error TEXT,evidence TEXT,
137
+ validation_result TEXT,validation_commands TEXT,validation_notes TEXT,updated_at TEXT NOT NULL,updated_by TEXT,
138
+ CHECK (status IN ('planned','next','in_progress','blocked','done','canceled','obsolete')),
139
+ CHECK (validation_result IS NULL OR validation_result IN ('passed','failed','not_run','not_applicable'))
140
+ );
141
+ INSERT INTO ticket_state SELECT * FROM ticket_state_legacy;
142
+ DROP TABLE ticket_state_legacy;
143
+ CREATE INDEX idx_ticket_state_status ON ticket_state(status);
144
+ `);
145
+ this.db.pragma("foreign_keys = ON");
123
146
  }
124
147
  close() {
125
148
  this.db.close();
@@ -11,10 +11,16 @@ export interface TicketExternalRef {
11
11
  export interface TicketSourceRef {
12
12
  source: string;
13
13
  item: string;
14
+ source_id?: string;
14
15
  url?: string | null;
15
16
  fingerprint?: string | null;
16
17
  note?: string | null;
17
18
  }
19
+ export interface TicketPlanRef {
20
+ plan_id: string;
21
+ revision: number;
22
+ slice_ref: string;
23
+ }
18
24
  export interface TicketDef {
19
25
  id: string;
20
26
  order: number;
@@ -34,6 +40,7 @@ export interface TicketDef {
34
40
  source_refs?: TicketSourceRef[];
35
41
  superseded_by?: string[];
36
42
  supersedes?: string[];
43
+ plan_ref?: TicketPlanRef;
37
44
  }
38
45
  export declare const TICKET_JSON_SCHEMA: {
39
46
  readonly type: "object";
@@ -143,6 +150,10 @@ export declare const TICKET_JSON_SCHEMA: {
143
150
  readonly type: "string";
144
151
  readonly minLength: 1;
145
152
  };
153
+ readonly source_id: {
154
+ readonly type: "string";
155
+ readonly pattern: "^src_[A-Za-z0-9_-]+$";
156
+ };
146
157
  readonly url: {
147
158
  readonly type: readonly ["string", "null"];
148
159
  };
@@ -169,6 +180,25 @@ export declare const TICKET_JSON_SCHEMA: {
169
180
  };
170
181
  readonly uniqueItems: true;
171
182
  };
183
+ readonly plan_ref: {
184
+ readonly type: "object";
185
+ readonly required: readonly ["plan_id", "revision", "slice_ref"];
186
+ readonly additionalProperties: false;
187
+ readonly properties: {
188
+ readonly plan_id: {
189
+ readonly type: "string";
190
+ readonly minLength: 1;
191
+ };
192
+ readonly revision: {
193
+ readonly type: "integer";
194
+ readonly minimum: 1;
195
+ };
196
+ readonly slice_ref: {
197
+ readonly type: "string";
198
+ readonly minLength: 1;
199
+ };
200
+ };
201
+ };
172
202
  };
173
203
  };
174
204
  export declare const TICKETS_FILE_SCHEMA: {
@@ -285,6 +315,10 @@ export declare const TICKETS_FILE_SCHEMA: {
285
315
  readonly type: "string";
286
316
  readonly minLength: 1;
287
317
  };
318
+ readonly source_id: {
319
+ readonly type: "string";
320
+ readonly pattern: "^src_[A-Za-z0-9_-]+$";
321
+ };
288
322
  readonly url: {
289
323
  readonly type: readonly ["string", "null"];
290
324
  };
@@ -311,6 +345,25 @@ export declare const TICKETS_FILE_SCHEMA: {
311
345
  };
312
346
  readonly uniqueItems: true;
313
347
  };
348
+ readonly plan_ref: {
349
+ readonly type: "object";
350
+ readonly required: readonly ["plan_id", "revision", "slice_ref"];
351
+ readonly additionalProperties: false;
352
+ readonly properties: {
353
+ readonly plan_id: {
354
+ readonly type: "string";
355
+ readonly minLength: 1;
356
+ };
357
+ readonly revision: {
358
+ readonly type: "integer";
359
+ readonly minimum: 1;
360
+ };
361
+ readonly slice_ref: {
362
+ readonly type: "string";
363
+ readonly minLength: 1;
364
+ };
365
+ };
366
+ };
314
367
  };
315
368
  };
316
369
  };
@@ -43,6 +43,7 @@ export const TICKET_JSON_SCHEMA = {
43
43
  properties: {
44
44
  source: { type: "string", minLength: 1 },
45
45
  item: { type: "string", minLength: 1 },
46
+ source_id: { type: "string", pattern: "^src_[A-Za-z0-9_-]+$" },
46
47
  url: { type: ["string", "null"] },
47
48
  fingerprint: { type: ["string", "null"] },
48
49
  note: { type: ["string", "null"] },
@@ -51,6 +52,16 @@ export const TICKET_JSON_SCHEMA = {
51
52
  },
52
53
  superseded_by: { type: "array", items: { type: "string" }, uniqueItems: true },
53
54
  supersedes: { type: "array", items: { type: "string" }, uniqueItems: true },
55
+ plan_ref: {
56
+ type: "object",
57
+ required: ["plan_id", "revision", "slice_ref"],
58
+ additionalProperties: false,
59
+ properties: {
60
+ plan_id: { type: "string", minLength: 1 },
61
+ revision: { type: "integer", minimum: 1 },
62
+ slice_ref: { type: "string", minLength: 1 },
63
+ },
64
+ },
54
65
  },
55
66
  };
56
67
  export const TICKETS_FILE_SCHEMA = {
@@ -40,7 +40,7 @@ export function runAllValidation(config, projectDir, ticketDefs, states, db) {
40
40
  const queueRows = buildNextQueue(ticketDefs, states, config.implementationLimit);
41
41
  const remaining = ticketDefs.filter((t) => {
42
42
  const s = states.get(t.id);
43
- return s?.status !== "done" && s?.status !== "canceled";
43
+ return s?.status !== "done" && s?.status !== "canceled" && s?.status !== "obsolete";
44
44
  });
45
45
  const expectedLen = Math.min(config.implementationLimit, remaining.length);
46
46
  if (queueRows.length !== expectedLen) {
@@ -0,0 +1,113 @@
1
+ import type { OperationLifecycle, ResolvedAgentSettings, WorkflowIssue } from "rafi-spec";
2
+ export declare const WORKFLOW_DB_FILE = ".rafi/recovery.sqlite3";
3
+ export type WorkflowKind = "plan" | "ticket-plan" | "ticket-populate" | "uninstall" | "build" | "qa-remediation" | "recovery" | "legacy";
4
+ export type WorkflowRunStatus = "running" | "paused" | "blocked" | "completed" | "failed" | "cancelled" | "superseded";
5
+ export interface WorkflowRunSnapshot {
6
+ runId: string;
7
+ kind: WorkflowKind;
8
+ status: WorkflowRunStatus;
9
+ checkpoint: string;
10
+ originalWork: unknown;
11
+ remainingWork: unknown;
12
+ state: Record<string, unknown>;
13
+ leaseGeneration?: number;
14
+ legacy: boolean;
15
+ createdAt: string;
16
+ updatedAt: string;
17
+ }
18
+ export interface OperationRecord {
19
+ idempotencyKey: string;
20
+ runId: string;
21
+ kind: string;
22
+ status: OperationLifecycle;
23
+ intent: unknown;
24
+ result?: unknown;
25
+ externalId?: string;
26
+ error?: string;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ }
30
+ export interface ProjectLease {
31
+ owner: string;
32
+ generation: number;
33
+ pid: number;
34
+ host: string;
35
+ processStart: string;
36
+ heartbeatAt: string;
37
+ runId: string;
38
+ }
39
+ export interface PublicationTransaction {
40
+ transactionId: string;
41
+ runId: string;
42
+ status: "prepared" | "staged" | "tracker_committed" | "published" | "committed" | "rolled_back";
43
+ intent: unknown;
44
+ previousDigests: unknown;
45
+ createdAt: string;
46
+ updatedAt: string;
47
+ }
48
+ /** Authoritative persistence for every resumable workflow in a project. */
49
+ export declare class WorkflowDb {
50
+ readonly projectDir: string;
51
+ readonly path: string;
52
+ private readonly db;
53
+ constructor(projectDir: string, path?: string);
54
+ close(): void;
55
+ createRun(input: {
56
+ runId?: string;
57
+ kind: WorkflowKind;
58
+ checkpoint?: string;
59
+ originalWork?: unknown;
60
+ remainingWork?: unknown;
61
+ state?: Record<string, unknown>;
62
+ legacy?: boolean;
63
+ }, now?: Date): WorkflowRunSnapshot;
64
+ getRun(runId: string): WorkflowRunSnapshot | undefined;
65
+ activeRuns(): WorkflowRunSnapshot[];
66
+ transition(runId: string, update: {
67
+ status?: WorkflowRunStatus;
68
+ checkpoint: string;
69
+ remainingWork?: unknown;
70
+ state?: Record<string, unknown>;
71
+ event?: string;
72
+ payload?: unknown;
73
+ }, now?: Date): WorkflowRunSnapshot;
74
+ events(runId: string): Array<{
75
+ sequence: number;
76
+ type: string;
77
+ checkpoint: string;
78
+ payload: unknown;
79
+ at: string;
80
+ }>;
81
+ recordSettings(runId: string, role: string, boundary: number, settings: ResolvedAgentSettings, now?: Date): void;
82
+ recordProjectSettingsRevision(revision: number, defaults: unknown, now?: Date): void;
83
+ recordTelemetry(runId: string, snapshot: unknown, now?: Date): void;
84
+ recordSession(runId: string, role: string, stream: string, sessionId: string | undefined, transition: string, settings: ResolvedAgentSettings, now?: Date): void;
85
+ recordIssue(runId: string, issue: WorkflowIssue): number;
86
+ issues(runId: string): WorkflowIssue[];
87
+ planOperation(input: {
88
+ runId: string;
89
+ idempotencyKey: string;
90
+ kind: string;
91
+ intent: unknown;
92
+ }, now?: Date): OperationRecord;
93
+ updateOperation(idempotencyKey: string, status: OperationLifecycle, details?: {
94
+ result?: unknown;
95
+ externalId?: string;
96
+ error?: string;
97
+ }, now?: Date): OperationRecord;
98
+ operation(idempotencyKey: string): OperationRecord | undefined;
99
+ operations(runId: string): OperationRecord[];
100
+ putEvidence(kind: "handoff" | "diff" | "test", value: string | Buffer, now?: Date): string;
101
+ getEvidence(digest: string): Buffer | undefined;
102
+ beginPublication(runId: string, intent: unknown, previousDigests: unknown, now?: Date): PublicationTransaction;
103
+ updatePublication(transactionId: string, status: PublicationTransaction["status"], now?: Date): PublicationTransaction;
104
+ publication(transactionId: string): PublicationTransaction | undefined;
105
+ incompletePublications(): PublicationTransaction[];
106
+ acquireLease(runId: string, owner?: string, now?: Date, staleMs?: number): ProjectLease;
107
+ heartbeatLease(lease: ProjectLease, now?: Date): ProjectLease;
108
+ releaseLease(lease: ProjectLease, now?: Date): void;
109
+ currentLease(): ProjectLease | undefined;
110
+ importLegacyOnce(now?: Date): number;
111
+ private insertEvent;
112
+ private migrate;
113
+ }