ai-foreman 1.7.6 → 1.7.7

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 (69) hide show
  1. package/README.md +16 -1
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +29 -4
  4. package/dist/adapters/claude.d.ts +33 -2
  5. package/dist/adapters/claude.js +208 -17
  6. package/dist/adapters/codex.d.ts +11 -1
  7. package/dist/adapters/codex.js +195 -20
  8. package/dist/adapters/recovering.d.ts +8 -2
  9. package/dist/adapters/recovering.js +23 -3
  10. package/dist/adapters/sessionFailure.d.ts +18 -0
  11. package/dist/adapters/sessionFailure.js +51 -0
  12. package/dist/adapters/types.d.ts +52 -9
  13. package/dist/agentRun.d.ts +4 -1
  14. package/dist/agentRun.js +96 -9
  15. package/dist/branch/currentGuard.d.ts +34 -0
  16. package/dist/branch/currentGuard.js +65 -0
  17. package/dist/branch/planner.d.ts +1 -1
  18. package/dist/branch/planner.js +2 -7
  19. package/dist/branch/prefix.d.ts +8 -0
  20. package/dist/branch/prefix.js +26 -0
  21. package/dist/branch/resume.d.ts +2 -0
  22. package/dist/branch/resume.js +8 -0
  23. package/dist/branch/runner.d.ts +14 -2
  24. package/dist/branch/runner.js +77 -17
  25. package/dist/buildRuns.d.ts +13 -4
  26. package/dist/buildRuns.js +85 -19
  27. package/dist/cli/handoffs.d.ts +2 -0
  28. package/dist/cli/handoffs.js +74 -0
  29. package/dist/cli/start.js +940 -42
  30. package/dist/cli/tickets.js +199 -3
  31. package/dist/continuity.d.ts +86 -0
  32. package/dist/continuity.js +365 -0
  33. package/dist/foreman.d.ts +5 -1
  34. package/dist/foreman.js +41 -6
  35. package/dist/handoffs.d.ts +69 -0
  36. package/dist/handoffs.js +326 -0
  37. package/dist/log.d.ts +1 -1
  38. package/dist/permissions/policy.d.ts +5 -1
  39. package/dist/permissions/policy.js +48 -1
  40. package/dist/qaReview.d.ts +6 -6
  41. package/dist/qaReview.js +20 -14
  42. package/dist/sessionAvailability.d.ts +15 -0
  43. package/dist/sessionAvailability.js +57 -0
  44. package/dist/sessionIdentity.d.ts +33 -0
  45. package/dist/sessionIdentity.js +132 -0
  46. package/dist/sessionLifecycle.d.ts +102 -3
  47. package/dist/sessionLifecycle.js +578 -20
  48. package/dist/statusReporter.d.ts +58 -8
  49. package/dist/statusReporter.js +210 -14
  50. package/dist/ticketPlanning.js +8 -0
  51. package/dist/ticketPopulation.js +88 -12
  52. package/dist/tickets/commands.js +20 -12
  53. package/dist/tickets/groupReset.d.ts +25 -0
  54. package/dist/tickets/groupReset.js +279 -0
  55. package/dist/tickets/groups.d.ts +36 -0
  56. package/dist/tickets/groups.js +68 -0
  57. package/dist/tickets/importer.js +27 -3
  58. package/dist/tickets/recommendations.js +31 -7
  59. package/dist/tickets/reset.d.ts +1 -0
  60. package/dist/tickets/reset.js +1 -0
  61. package/dist/tickets/setupConfig.d.ts +1 -0
  62. package/dist/tickets/setupConfig.js +10 -2
  63. package/dist/tickets/stateDb.d.ts +34 -0
  64. package/dist/tickets/stateDb.js +207 -0
  65. package/dist/tickets/ticketLoader.js +5 -2
  66. package/dist/tickets/validate.js +4 -0
  67. package/dist/workflowDb.d.ts +99 -2
  68. package/dist/workflowDb.js +434 -8
  69. package/package.json +31 -3
@@ -1,6 +1,7 @@
1
1
  import Database from "better-sqlite3";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
+ import { createHash } from "node:crypto";
4
5
  /** The only valid initial/fully-reset active state for a ticket. */
5
6
  export function pristineTicketState(ticketId, now, actor = null) {
6
7
  return {
@@ -130,6 +131,38 @@ CREATE TABLE IF NOT EXISTS operation_receipts (
130
131
  payload_json TEXT NOT NULL DEFAULT '{}'
131
132
  );
132
133
 
134
+ CREATE TABLE IF NOT EXISTS ticket_groups (
135
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
136
+ origin TEXT NOT NULL,
137
+ created_at TEXT NOT NULL,
138
+ legacy INTEGER NOT NULL DEFAULT 0,
139
+ operation_id TEXT NOT NULL UNIQUE
140
+ );
141
+
142
+ CREATE TABLE IF NOT EXISTS ticket_group_members (
143
+ group_sequence INTEGER NOT NULL REFERENCES ticket_groups(sequence) ON DELETE RESTRICT,
144
+ ticket_id TEXT NOT NULL UNIQUE,
145
+ position INTEGER NOT NULL,
146
+ snapshot_json TEXT NOT NULL,
147
+ snapshot_digest TEXT NOT NULL,
148
+ snapshot_validated_at TEXT NOT NULL,
149
+ PRIMARY KEY(group_sequence, ticket_id),
150
+ UNIQUE(group_sequence, position),
151
+ CHECK(position >= 1)
152
+ );
153
+
154
+ CREATE TABLE IF NOT EXISTS ticket_group_receipts (
155
+ operation_id TEXT PRIMARY KEY,
156
+ group_sequence INTEGER NOT NULL UNIQUE REFERENCES ticket_groups(sequence) ON DELETE RESTRICT,
157
+ created_at TEXT NOT NULL
158
+ );
159
+
160
+ CREATE TABLE IF NOT EXISTS ticket_group_sequence_ledger (
161
+ sequence INTEGER PRIMARY KEY,
162
+ operation_id TEXT NOT NULL UNIQUE,
163
+ allocated_at TEXT NOT NULL
164
+ );
165
+
133
166
  CREATE INDEX IF NOT EXISTS idx_ticket_state_status ON ticket_state(status);
134
167
  CREATE INDEX IF NOT EXISTS idx_ticket_events_ticket_id ON ticket_events(ticket_id);
135
168
  CREATE INDEX IF NOT EXISTS idx_ticket_events_timestamp ON ticket_events(timestamp);
@@ -137,6 +170,7 @@ CREATE INDEX IF NOT EXISTS idx_validation_snapshots_timestamp ON validation_snap
137
170
  CREATE INDEX IF NOT EXISTS idx_future_work_disposition ON future_work(disposition);
138
171
  CREATE INDEX IF NOT EXISTS idx_review_recommendations_status ON review_recommendations(status);
139
172
  CREATE INDEX IF NOT EXISTS idx_operation_receipts_run ON operation_receipts(run_id);
173
+ CREATE INDEX IF NOT EXISTS idx_ticket_group_members_group ON ticket_group_members(group_sequence, position);
140
174
  `;
141
175
  export class StateDb {
142
176
  db;
@@ -145,6 +179,8 @@ export class StateDb {
145
179
  this.db = new Database(dbPath);
146
180
  this.db.pragma("journal_mode = WAL");
147
181
  this.db.exec(INIT_SQL);
182
+ this.db.prepare(`INSERT OR IGNORE INTO ticket_group_sequence_ledger(sequence,operation_id,allocated_at)
183
+ SELECT sequence,operation_id,created_at FROM ticket_groups`).run();
148
184
  this.migrateObsoleteStatus();
149
185
  }
150
186
  migrateObsoleteStatus() {
@@ -369,8 +405,179 @@ export class StateDb {
369
405
  `).run(receipt);
370
406
  return result.changes === 1;
371
407
  }
408
+ // ── immutable ticket creation groups ─────────────────────────────────────
409
+ createTicketGroup(input) {
410
+ const existing = this.getTicketGroupByOperation(input.operationId);
411
+ if (existing) {
412
+ const requestedIds = input.members.map((member) => member.ticketId);
413
+ const existingIds = existing.members.map((member) => member.ticketId);
414
+ if (existing.origin !== input.origin || JSON.stringify(existingIds) !== JSON.stringify(requestedIds))
415
+ throw new Error(`ticket group operation ${input.operationId} was retried with different immutable membership`);
416
+ return existing;
417
+ }
418
+ if (!input.operationId.trim())
419
+ throw new Error("ticket group operation ID must not be empty");
420
+ if (input.members.length === 0)
421
+ throw new Error("ticket creation updates/no-ops must not create an empty group");
422
+ const ids = input.members.map((member) => member.ticketId);
423
+ if (new Set(ids).size !== ids.length)
424
+ throw new Error("ticket group members must be unique");
425
+ const createdAt = input.createdAt ?? new Date().toISOString();
426
+ return this.transaction(() => {
427
+ const result = this.db.prepare("INSERT INTO ticket_groups(origin,created_at,legacy,operation_id) VALUES(?,?,?,?)")
428
+ .run(input.origin, createdAt, input.legacy ? 1 : 0, input.operationId);
429
+ const sequence = Number(result.lastInsertRowid);
430
+ this.db.prepare("INSERT INTO ticket_group_sequence_ledger(sequence,operation_id,allocated_at) VALUES(?,?,?)").run(sequence, input.operationId, createdAt);
431
+ const insert = this.db.prepare(`INSERT INTO ticket_group_members(group_sequence,ticket_id,position,snapshot_json,snapshot_digest,snapshot_validated_at)
432
+ VALUES(?,?,?,?,?,?)`);
433
+ input.members.forEach((member, index) => {
434
+ const snapshot = makeDefinitionSnapshot(member.ticketId, member.definition, member.validatedAt ?? createdAt);
435
+ insert.run(sequence, member.ticketId, index + 1, JSON.stringify(snapshot.definition), snapshot.digest, snapshot.validatedAt);
436
+ // The creation receipt, immutable membership, recoverable definition,
437
+ // and initial active state are one SQLite publication. Existing state
438
+ // is preserved on replay or when importing an externally completed
439
+ // ticket.
440
+ if (!this.getState(member.ticketId)) {
441
+ this.upsertState(member.ticketId, pristineTicketState(member.ticketId, createdAt, "rafi ticket publication"), createdAt);
442
+ }
443
+ });
444
+ this.db.prepare("INSERT INTO ticket_group_receipts(operation_id,group_sequence,created_at) VALUES(?,?,?)").run(input.operationId, sequence, createdAt);
445
+ return this.getTicketGroupBySequence(sequence);
446
+ });
447
+ }
448
+ /** Update only the recoverable definition; membership and order never change. */
449
+ updateTicketDefinitionSnapshot(ticketId, definition, validatedAt = new Date().toISOString()) {
450
+ const snapshot = makeDefinitionSnapshot(ticketId, definition, validatedAt);
451
+ const result = this.db.prepare(`UPDATE ticket_group_members SET snapshot_json=?,snapshot_digest=?,snapshot_validated_at=? WHERE ticket_id=?`)
452
+ .run(JSON.stringify(snapshot.definition), snapshot.digest, snapshot.validatedAt, ticketId);
453
+ if (result.changes !== 1)
454
+ throw new Error(`ticket ${ticketId} has no creation-group membership`);
455
+ return snapshot;
456
+ }
457
+ getTicketGroupByOperation(operationId) {
458
+ const row = this.db.prepare("SELECT sequence FROM ticket_groups WHERE operation_id=?").get(operationId);
459
+ return row ? this.getTicketGroupBySequence(row.sequence) : undefined;
460
+ }
461
+ getTicketGroup(groupId) {
462
+ const match = /^TG-([1-9]\d*)$/.exec(groupId);
463
+ return match ? this.getTicketGroupBySequence(Number(match[1])) : undefined;
464
+ }
465
+ getTicketGroupForTicket(ticketId) {
466
+ const row = this.db.prepare("SELECT group_sequence AS sequence FROM ticket_group_members WHERE ticket_id=?").get(ticketId);
467
+ return row ? this.getTicketGroupBySequence(row.sequence) : undefined;
468
+ }
469
+ listTicketGroups() {
470
+ const rows = this.db.prepare("SELECT sequence FROM ticket_groups ORDER BY sequence DESC").all();
471
+ return rows.map((row) => this.getTicketGroupBySequence(row.sequence));
472
+ }
473
+ /** One-time migration for repositories whose complete ticket set predates groups. */
474
+ ensureSyntheticLegacyGroup(definitions, now = new Date()) {
475
+ if (definitions.length === 0 || this.listTicketGroups().length > 0)
476
+ return undefined;
477
+ return this.createTicketGroup({
478
+ origin: "legacy",
479
+ operationId: "ticket-groups:synthetic-legacy:v1",
480
+ legacy: true,
481
+ createdAt: now.toISOString(),
482
+ members: definitions.map((definition) => ({ ticketId: definition.id, definition, validatedAt: now.toISOString() })),
483
+ });
484
+ }
485
+ ungroupedTicketIds(ticketIds) {
486
+ const grouped = new Set(this.db.prepare("SELECT ticket_id FROM ticket_group_members").all().map((row) => row.ticket_id));
487
+ return ticketIds.filter((ticketId) => !grouped.has(ticketId));
488
+ }
489
+ repairTicketGroups(definitions, operationId, now = new Date()) {
490
+ const ungrouped = new Set(this.ungroupedTicketIds(definitions.map((definition) => definition.id)));
491
+ if (ungrouped.size === 0)
492
+ return undefined;
493
+ return this.createTicketGroup({
494
+ origin: "repair", operationId, createdAt: now.toISOString(),
495
+ members: definitions.filter((definition) => ungrouped.has(definition.id)).map((definition) => ({ ticketId: definition.id, definition })),
496
+ });
497
+ }
498
+ validateTicketGroups(knownTicketIds = []) {
499
+ const issues = [];
500
+ const groups = this.listTicketGroups();
501
+ const known = new Set(knownTicketIds);
502
+ for (const group of groups) {
503
+ if (group.members.length === 0)
504
+ issues.push({ code: "empty-group", groupId: group.id, message: `${group.id} has no members` });
505
+ for (const member of group.members) {
506
+ if (known.size > 0 && !known.has(member.ticketId))
507
+ issues.push({ code: "unknown-member", groupId: group.id, ticketId: member.ticketId, message: `${group.id} references unknown ticket ${member.ticketId}` });
508
+ if (definitionDigest(member.snapshot.definition) !== member.snapshot.digest)
509
+ issues.push({ code: "snapshot-corruption", groupId: group.id, ticketId: member.ticketId, message: `${group.id} snapshot for ${member.ticketId} does not match its digest` });
510
+ }
511
+ }
512
+ for (const ticketId of known)
513
+ if (!this.getTicketGroupForTicket(ticketId))
514
+ issues.push({ code: "missing-membership", ticketId, message: `ticket ${ticketId} has no creation-group membership` });
515
+ const duplicates = this.db.prepare("SELECT ticket_id,COUNT(*) AS count FROM ticket_group_members GROUP BY ticket_id HAVING COUNT(*)<>1").all();
516
+ for (const row of duplicates)
517
+ issues.push({ code: "multiple-membership", ticketId: row.ticket_id, message: `ticket ${row.ticket_id} belongs to ${row.count} creation groups` });
518
+ const maximum = groups.reduce((value, group) => Math.max(value, group.sequence), 0);
519
+ const sequence = this.db.prepare("SELECT seq FROM sqlite_sequence WHERE name='ticket_groups'").get();
520
+ if (maximum > 0 && (!sequence || sequence.seq < maximum))
521
+ issues.push({ code: "sequence-corruption", message: `ticket group sequence is ${sequence?.seq ?? "missing"}, below allocated ${maximum}` });
522
+ const missingLedger = this.db.prepare("SELECT sequence FROM ticket_groups WHERE sequence NOT IN (SELECT sequence FROM ticket_group_sequence_ledger)").all();
523
+ for (const row of missingLedger)
524
+ issues.push({ code: "sequence-corruption", groupId: `TG-${row.sequence}`, message: `TG-${row.sequence} has no immutable allocation ledger entry` });
525
+ const reused = this.db.prepare(`SELECT g.sequence FROM ticket_groups g JOIN ticket_group_sequence_ledger l ON l.sequence=g.sequence WHERE g.operation_id<>l.operation_id`).all();
526
+ for (const row of reused)
527
+ issues.push({ code: "reused-id", groupId: `TG-${row.sequence}`, message: `TG-${row.sequence} was reused for a different creation operation` });
528
+ const deletedAllocations = this.db.prepare("SELECT sequence FROM ticket_group_sequence_ledger WHERE sequence NOT IN (SELECT sequence FROM ticket_groups)").all();
529
+ for (const row of deletedAllocations)
530
+ issues.push({ code: "sequence-corruption", groupId: `TG-${row.sequence}`, message: `allocated TG-${row.sequence} is missing; group IDs must never be deleted or reused` });
531
+ const orphanReceipts = this.db.prepare("SELECT operation_id FROM ticket_group_receipts WHERE group_sequence NOT IN (SELECT sequence FROM ticket_groups)").all();
532
+ for (const row of orphanReceipts)
533
+ issues.push({ code: "receipt-without-group", message: `creation receipt ${row.operation_id} has no group` });
534
+ return issues;
535
+ }
536
+ getTicketGroupBySequence(sequence) {
537
+ const row = this.db.prepare("SELECT * FROM ticket_groups WHERE sequence=?").get(sequence);
538
+ if (!row)
539
+ return undefined;
540
+ const members = this.db.prepare("SELECT * FROM ticket_group_members WHERE group_sequence=? ORDER BY position").all(sequence);
541
+ return {
542
+ id: `TG-${row.sequence}`,
543
+ sequence: row.sequence,
544
+ origin: row.origin,
545
+ createdAt: row.created_at,
546
+ legacy: Boolean(row.legacy),
547
+ operationId: row.operation_id,
548
+ members: members.map((member) => ({
549
+ groupId: `TG-${row.sequence}`,
550
+ ticketId: member.ticket_id,
551
+ position: member.position,
552
+ snapshot: { version: 1, ticketId: member.ticket_id, definition: parseSnapshot(member.snapshot_json), digest: member.snapshot_digest, validatedAt: member.snapshot_validated_at },
553
+ })),
554
+ };
555
+ }
372
556
  // ── transactions ──────────────────────────────────────────────────────────
373
557
  transaction(fn) {
374
558
  return this.db.transaction(fn)();
375
559
  }
376
560
  }
561
+ function makeDefinitionSnapshot(ticketId, definition, validatedAt) {
562
+ const cloned = JSON.parse(JSON.stringify(definition));
563
+ if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) {
564
+ const definitionId = cloned.id;
565
+ if (definitionId !== undefined && definitionId !== ticketId)
566
+ throw new Error(`ticket definition snapshot ID ${String(definitionId)} does not match group member ${ticketId}`);
567
+ }
568
+ return { version: 1, ticketId, definition: cloned, digest: definitionDigest(cloned), validatedAt };
569
+ }
570
+ function definitionDigest(value) { return createHash("sha256").update(stableJson(value)).digest("hex"); }
571
+ function stableJson(value) {
572
+ if (Array.isArray(value))
573
+ return `[${value.map(stableJson).join(",")}]`;
574
+ if (value && typeof value === "object")
575
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
576
+ return JSON.stringify(value) ?? "null";
577
+ }
578
+ function parseSnapshot(value) { try {
579
+ return JSON.parse(value);
580
+ }
581
+ catch {
582
+ return { __rafi_corrupt_snapshot: true, raw: value };
583
+ } }
@@ -1,4 +1,5 @@
1
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
1
+ import { readFileSync, renameSync, writeFileSync, existsSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { parse, stringify } from "yaml";
3
4
  import { Ajv } from "ajv";
4
5
  import { TICKET_JSON_SCHEMA } from "./ticketSchema.js";
@@ -13,7 +14,9 @@ export function loadTickets(ticketsPath) {
13
14
  return raw.tickets;
14
15
  }
15
16
  export function saveTickets(ticketsPath, tickets) {
16
- writeFileSync(ticketsPath, stringify({ tickets }, { lineWidth: 120 }), "utf8");
17
+ const temporary = `${ticketsPath}.${process.pid}.${randomUUID()}.tmp`;
18
+ writeFileSync(temporary, stringify({ tickets }, { lineWidth: 120 }), "utf8");
19
+ renameSync(temporary, ticketsPath);
17
20
  }
18
21
  export function validateTicketDefs(tickets) {
19
22
  const errors = [];
@@ -5,6 +5,7 @@ import { buildNextQueue } from "./queue.js";
5
5
  import { loadDeliveryConfig, validateDeliveryConfig } from "./delivery.js";
6
6
  export function runAllValidation(config, projectDir, ticketDefs, states, db) {
7
7
  const issues = [];
8
+ db.ensureSyntheticLegacyGroup(ticketDefs);
8
9
  // Pass 1: schema and source
9
10
  for (const err of validateTicketDefs(ticketDefs)) {
10
11
  issues.push({ pass: 1, severity: "error", message: `[schema] ${err.path}: ${err.message}` });
@@ -12,6 +13,9 @@ export function runAllValidation(config, projectDir, ticketDefs, states, db) {
12
13
  for (const cycle of detectCycles(ticketDefs)) {
13
14
  issues.push({ pass: 1, severity: "error", message: `[cycle] dependency cycle: ${cycle}` });
14
15
  }
16
+ for (const issue of db.validateTicketGroups(ticketDefs.map((ticket) => ticket.id))) {
17
+ issues.push({ pass: 1, severity: "error", message: `[ticket-group:${issue.code}] ${issue.message}` });
18
+ }
15
19
  try {
16
20
  const delivery = loadDeliveryConfig(projectDir);
17
21
  if (delivery) {
@@ -1,4 +1,4 @@
1
- import type { OperationLifecycle, ResolvedAgentSettings, WorkflowIssue } from "rafi-spec";
1
+ import type { BuildRecoveryDecisionReceipt, ContextSample, ContinuityCheckpoint, ContinuityDelta, ContinuityEvent, ContinuityHead, ContinuityHeadState, HandoffLineage, HandoffManifestV1, LiveSettingsAcknowledgment, OperationLifecycle, ProviderSessionRefV1, ResolvedAgentSettings, SessionUsageSample, WorkflowIssue } from "rafi-spec";
2
2
  export declare const WORKFLOW_DB_FILE = ".rafi/recovery.sqlite3";
3
3
  export type WorkflowKind = "plan" | "ticket-plan" | "ticket-populate" | "uninstall" | "build" | "qa-remediation" | "recovery" | "legacy";
4
4
  export type WorkflowRunStatus = "running" | "paused" | "blocked" | "completed" | "failed" | "cancelled" | "superseded";
@@ -47,6 +47,30 @@ export interface PublicationTransaction {
47
47
  createdAt: string;
48
48
  updatedAt: string;
49
49
  }
50
+ export interface CompactionAttemptRecord {
51
+ idempotencyKey: string;
52
+ runId: string;
53
+ role: "builder" | "qa";
54
+ providerSessionId?: string;
55
+ sessionRef?: ProviderSessionRefV1;
56
+ sessionKey?: string;
57
+ crossingKey: string;
58
+ status: "started" | "succeeded" | "failed";
59
+ beforeSample?: ContextSample;
60
+ afterSample?: ContextSample;
61
+ error?: string;
62
+ createdAt: string;
63
+ updatedAt: string;
64
+ }
65
+ export interface RoleMutationLease {
66
+ runId: string;
67
+ role: "builder" | "qa";
68
+ generation: number;
69
+ providerSessionId: string;
70
+ sessionRef?: ProviderSessionRefV1;
71
+ sessionKey?: string;
72
+ movedAt: string;
73
+ }
50
74
  /** Authoritative persistence for every resumable workflow in a project. */
51
75
  export declare class WorkflowDb {
52
76
  readonly projectDir: string;
@@ -54,6 +78,8 @@ export declare class WorkflowDb {
54
78
  private readonly db;
55
79
  constructor(projectDir: string, path?: string);
56
80
  close(): void;
81
+ /** Ensure non-workflow build records can use the same durable event store. */
82
+ ensureRun(runId: string, kind?: WorkflowKind, now?: Date): WorkflowRunSnapshot;
57
83
  createRun(input: {
58
84
  runId?: string;
59
85
  kind: WorkflowKind;
@@ -84,7 +110,40 @@ export declare class WorkflowDb {
84
110
  recordSettings(runId: string, role: string, boundary: number, settings: ResolvedAgentSettings, now?: Date): void;
85
111
  recordProjectSettingsRevision(revision: number, defaults: unknown, now?: Date): void;
86
112
  recordTelemetry(runId: string, snapshot: unknown, now?: Date): void;
87
- recordSession(runId: string, role: string, stream: string, sessionId: string | undefined, transition: string, settings: ResolvedAgentSettings, now?: Date): void;
113
+ recordContextSample(sample: ContextSample): void;
114
+ contextSamples(runId: string, role?: "builder" | "qa"): ContextSample[];
115
+ recordSessionUsage(sample: SessionUsageSample): void;
116
+ sessionUsageSamples(runId: string, role?: "builder" | "qa"): SessionUsageSample[];
117
+ acknowledgeSettings(ack: LiveSettingsAcknowledgment): void;
118
+ settingsAcknowledgments(revision: number): LiveSettingsAcknowledgment[];
119
+ appendContinuityEvent(input: {
120
+ runId: string;
121
+ role: "builder" | "qa" | "host";
122
+ kind: string;
123
+ payload: unknown;
124
+ authoritativeStateRevision: number;
125
+ sessionRef?: ProviderSessionRefV1;
126
+ sessionKey?: string;
127
+ }, now?: Date): ContinuityEvent;
128
+ continuityEvents(runId: string, afterSequence?: number): ContinuityEvent[];
129
+ publishContinuityCheckpoint(input: {
130
+ runId: string;
131
+ role: "builder" | "qa";
132
+ delta: ContinuityDelta;
133
+ state?: ContinuityHeadState;
134
+ authoritativeStateRevision: number;
135
+ sessionRef?: ProviderSessionRefV1;
136
+ sessionKey?: string;
137
+ }, now?: Date): ContinuityCheckpoint;
138
+ continuityCheckpoints(runId: string, role?: "builder" | "qa"): ContinuityCheckpoint[];
139
+ latestContinuityCheckpoint(runId: string, role: "builder" | "qa"): ContinuityCheckpoint | undefined;
140
+ continuityHead(runId: string, role: "builder" | "qa" | "run"): ContinuityHead | undefined;
141
+ setContinuityHeadState(runId: string, role: "builder" | "qa", state: ContinuityHeadState, now?: Date): ContinuityHead | undefined;
142
+ recordSession(runId: string, role: string, stream: string, session: string | ProviderSessionRefV1 | undefined, transition: string, settings: ResolvedAgentSettings, now?: Date): void;
143
+ providerSessionRefs(runId: string, role?: string): ProviderSessionRefV1[];
144
+ /** Project-local lookup used to resolve compatibility raw IDs into scoped references. */
145
+ recordProviderSessionBinding(ref: ProviderSessionRefV1, now?: Date): void;
146
+ providerSessionBindings(sessionId?: string, role?: string): ProviderSessionRefV1[];
88
147
  recordIssue(runId: string, issue: WorkflowIssue): number;
89
148
  issues(runId: string): WorkflowIssue[];
90
149
  planOperation(input: {
@@ -102,6 +161,41 @@ export declare class WorkflowDb {
102
161
  operations(runId: string): OperationRecord[];
103
162
  putEvidence(kind: "handoff" | "diff" | "test", value: string | Buffer, now?: Date): string;
104
163
  getEvidence(digest: string): Buffer | undefined;
164
+ stageHandoff(manifest: HandoffManifestV1, markdown: string): HandoffLineage;
165
+ acceptHandoff(runId: string, generation: number, successor: string | ProviderSessionRefV1, now?: Date): HandoffLineage;
166
+ /** Fence the initial role owner without overwriting a lease already moved by a handoff. */
167
+ claimInitialRoleLease(runId: string, role: "builder" | "qa", providerSession: string | ProviderSessionRefV1, now?: Date): RoleMutationLease;
168
+ roleMutationLease(runId: string, role: "builder" | "qa"): RoleMutationLease | undefined;
169
+ /** Move a dead predecessor's lease only after a fresh recovery produced a schema-valid checkpoint. */
170
+ moveRoleLeaseAfterValidatedRecovery(runId: string, role: "builder" | "qa", providerSession: string | ProviderSessionRefV1, reason: string, now?: Date): RoleMutationLease;
171
+ failHandoff(runId: string, generation: number, reason: string, now?: Date): HandoffLineage;
172
+ handoff(runId: string, generation: number): HandoffLineage | undefined;
173
+ handoffs(runId: string): HandoffLineage[];
174
+ handoffContent(runId: string, generation: number): {
175
+ lineage: HandoffLineage;
176
+ manifest: HandoffManifestV1;
177
+ markdown: string;
178
+ } | undefined;
179
+ deleteHandoffHistory(runId: string): number;
180
+ startCompactionAttempt(input: {
181
+ idempotencyKey: string;
182
+ runId: string;
183
+ role: "builder" | "qa";
184
+ providerSessionId?: string;
185
+ sessionRef?: ProviderSessionRefV1;
186
+ sessionKey?: string;
187
+ crossingKey: string;
188
+ beforeSample?: ContextSample;
189
+ }, now?: Date): CompactionAttemptRecord;
190
+ finishCompactionAttempt(idempotencyKey: string, input: {
191
+ ok: boolean;
192
+ afterSample?: ContextSample;
193
+ error?: string;
194
+ }, now?: Date): CompactionAttemptRecord;
195
+ compactionAttempt(idempotencyKey: string): CompactionAttemptRecord | undefined;
196
+ successfulCompactionCount(runId: string, role: "builder" | "qa", providerSession?: string | ProviderSessionRefV1): number;
197
+ recordRecoveryDecision(receipt: BuildRecoveryDecisionReceipt): void;
198
+ recoveryDecisions(runId: string): BuildRecoveryDecisionReceipt[];
105
199
  beginPublication(runId: string, intent: unknown, previousDigests: unknown, now?: Date): PublicationTransaction;
106
200
  updatePublication(transactionId: string, status: PublicationTransaction["status"], now?: Date): PublicationTransaction;
107
201
  publication(transactionId: string): PublicationTransaction | undefined;
@@ -111,6 +205,9 @@ export declare class WorkflowDb {
111
205
  releaseLease(lease: ProjectLease, now?: Date): void;
112
206
  currentLease(): ProjectLease | undefined;
113
207
  importLegacyOnce(now?: Date): number;
208
+ private upsertContinuityHead;
209
+ private refreshRunContinuityHead;
114
210
  private insertEvent;
115
211
  private migrate;
212
+ private ensureColumn;
116
213
  }