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.
- package/README.md +2 -2
- package/dist/adapters/claude.d.ts +5 -1
- package/dist/adapters/claude.js +41 -0
- package/dist/adapters/codex.d.ts +26 -28
- package/dist/adapters/codex.js +209 -148
- package/dist/adapters/recovering.d.ts +3 -1
- package/dist/adapters/recovering.js +2 -0
- package/dist/adapters/types.d.ts +29 -0
- package/dist/branch/planner.js +1 -1
- package/dist/branch/runner.d.ts +6 -0
- package/dist/branch/runner.js +149 -16
- package/dist/buildRuns.d.ts +9 -2
- package/dist/buildRuns.js +110 -7
- package/dist/cli/start.d.ts +4 -0
- package/dist/cli/start.js +283 -28
- package/dist/cli/status.js +25 -2
- package/dist/cli/tickets.d.ts +1 -0
- package/dist/cli/tickets.js +67 -27
- package/dist/foreman.d.ts +18 -3
- package/dist/foreman.js +112 -5
- package/dist/log.d.ts +1 -1
- package/dist/operationRecovery.d.ts +25 -0
- package/dist/operationRecovery.js +46 -0
- package/dist/outcomes.d.ts +16 -0
- package/dist/outcomes.js +27 -0
- package/dist/qaReview.d.ts +52 -0
- package/dist/qaReview.js +103 -0
- package/dist/qaSnapshot.d.ts +19 -0
- package/dist/qaSnapshot.js +88 -0
- package/dist/sessionLifecycle.d.ts +37 -0
- package/dist/sessionLifecycle.js +104 -0
- package/dist/sources/sourceRegistry.d.ts +49 -0
- package/dist/sources/sourceRegistry.js +577 -0
- package/dist/statusReporter.d.ts +40 -0
- package/dist/statusReporter.js +31 -0
- package/dist/ticketPopulation.d.ts +54 -0
- package/dist/ticketPopulation.js +209 -0
- package/dist/tickets/blockers.d.ts +1 -1
- package/dist/tickets/blockers.js +2 -0
- package/dist/tickets/commands.d.ts +1 -0
- package/dist/tickets/commands.js +31 -1
- package/dist/tickets/delivery.d.ts +36 -0
- package/dist/tickets/delivery.js +204 -4
- package/dist/tickets/queue.d.ts +3 -0
- package/dist/tickets/queue.js +50 -1
- package/dist/tickets/setupConfig.js +30 -2
- package/dist/tickets/sourceFetch.js +6 -1
- package/dist/tickets/stateDb.d.ts +2 -1
- package/dist/tickets/stateDb.js +24 -1
- package/dist/tickets/ticketSchema.d.ts +53 -0
- package/dist/tickets/ticketSchema.js +11 -0
- package/dist/tickets/validate.js +1 -1
- package/dist/workflowDb.d.ts +113 -0
- package/dist/workflowDb.js +261 -0
- package/package.json +20 -3
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
export const WORKFLOW_DB_FILE = ".rafi/recovery.sqlite3";
|
|
7
|
+
/** Authoritative persistence for every resumable workflow in a project. */
|
|
8
|
+
export class WorkflowDb {
|
|
9
|
+
projectDir;
|
|
10
|
+
path;
|
|
11
|
+
db;
|
|
12
|
+
constructor(projectDir, path = join(resolve(projectDir), WORKFLOW_DB_FILE)) {
|
|
13
|
+
this.projectDir = projectDir;
|
|
14
|
+
this.path = path;
|
|
15
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
16
|
+
ensureRecoveryGitignore(resolve(projectDir));
|
|
17
|
+
this.db = new Database(path);
|
|
18
|
+
this.db.pragma("journal_mode = WAL");
|
|
19
|
+
this.db.pragma("synchronous = FULL");
|
|
20
|
+
this.db.pragma("foreign_keys = ON");
|
|
21
|
+
this.migrate();
|
|
22
|
+
this.importLegacyOnce();
|
|
23
|
+
}
|
|
24
|
+
close() { this.db.close(); }
|
|
25
|
+
createRun(input, now = new Date()) {
|
|
26
|
+
const at = now.toISOString();
|
|
27
|
+
const run = {
|
|
28
|
+
runId: input.runId ?? randomUUID(), kind: input.kind, status: "running", checkpoint: input.checkpoint ?? "created",
|
|
29
|
+
originalWork: input.originalWork ?? {}, remainingWork: input.remainingWork ?? input.originalWork ?? {}, state: input.state ?? {},
|
|
30
|
+
legacy: Boolean(input.legacy), createdAt: at, updatedAt: at,
|
|
31
|
+
};
|
|
32
|
+
this.db.transaction(() => {
|
|
33
|
+
this.db.prepare(`INSERT INTO workflow_runs(run_id,kind,status,checkpoint,original_work_json,remaining_work_json,state_json,legacy,created_at,updated_at)
|
|
34
|
+
VALUES(?,?,?,?,?,?,?,?,?,?)`).run(run.runId, run.kind, run.status, run.checkpoint, json(run.originalWork), json(run.remainingWork), json(run.state), run.legacy ? 1 : 0, at, at);
|
|
35
|
+
this.insertEvent(run.runId, "run_created", run.checkpoint, { kind: run.kind }, at);
|
|
36
|
+
})();
|
|
37
|
+
return run;
|
|
38
|
+
}
|
|
39
|
+
getRun(runId) {
|
|
40
|
+
const row = this.db.prepare("SELECT * FROM workflow_runs WHERE run_id=?").get(runId);
|
|
41
|
+
return row ? rowToRun(row) : undefined;
|
|
42
|
+
}
|
|
43
|
+
activeRuns() {
|
|
44
|
+
return this.db.prepare("SELECT * FROM workflow_runs WHERE status IN ('running','paused','blocked') ORDER BY created_at").all().map(rowToRun);
|
|
45
|
+
}
|
|
46
|
+
transition(runId, update, now = new Date()) {
|
|
47
|
+
const at = now.toISOString();
|
|
48
|
+
return this.db.transaction(() => {
|
|
49
|
+
const current = this.getRun(runId);
|
|
50
|
+
if (!current)
|
|
51
|
+
throw new Error(`workflow run not found: ${runId}`);
|
|
52
|
+
const next = { ...current, status: update.status ?? current.status, checkpoint: update.checkpoint, remainingWork: update.remainingWork ?? current.remainingWork, state: update.state ?? current.state, updatedAt: at };
|
|
53
|
+
this.db.prepare("UPDATE workflow_runs SET status=?,checkpoint=?,remaining_work_json=?,state_json=?,updated_at=? WHERE run_id=?")
|
|
54
|
+
.run(next.status, next.checkpoint, json(next.remainingWork), json(next.state), at, runId);
|
|
55
|
+
this.insertEvent(runId, update.event ?? "checkpoint", update.checkpoint, update.payload ?? {}, at);
|
|
56
|
+
return next;
|
|
57
|
+
})();
|
|
58
|
+
}
|
|
59
|
+
events(runId) {
|
|
60
|
+
const rows = this.db.prepare("SELECT sequence,event_type,checkpoint,payload_json,created_at FROM workflow_events WHERE run_id=? ORDER BY sequence").all(runId);
|
|
61
|
+
return rows.map((row) => ({ sequence: row.sequence, type: row.event_type, checkpoint: row.checkpoint, payload: parseJson(row.payload_json), at: row.created_at }));
|
|
62
|
+
}
|
|
63
|
+
recordSettings(runId, role, boundary, settings, now = new Date()) {
|
|
64
|
+
this.db.transaction(() => {
|
|
65
|
+
this.db.prepare("INSERT OR REPLACE INTO role_settings(run_id,role,boundary,revision,settings_json,created_at) VALUES(?,?,?,?,?,?)")
|
|
66
|
+
.run(runId, role, boundary, settings.settings_revision, json(settings), now.toISOString());
|
|
67
|
+
this.insertEvent(runId, "settings_boundary", `role:${role}:${boundary}`, { revision: settings.settings_revision }, now.toISOString());
|
|
68
|
+
})();
|
|
69
|
+
}
|
|
70
|
+
recordProjectSettingsRevision(revision, defaults, now = new Date()) {
|
|
71
|
+
this.db.prepare("INSERT OR REPLACE INTO project_settings_revisions(revision,defaults_json,created_at) VALUES(?,?,?)").run(revision, json(defaults), now.toISOString());
|
|
72
|
+
}
|
|
73
|
+
recordTelemetry(runId, snapshot, now = new Date()) {
|
|
74
|
+
this.db.prepare("INSERT INTO workflow_telemetry(run_id,snapshot_json,created_at) VALUES(?,?,?)").run(runId, json(snapshot), now.toISOString());
|
|
75
|
+
}
|
|
76
|
+
recordSession(runId, role, stream, sessionId, transition, settings, now = new Date()) {
|
|
77
|
+
this.db.prepare(`INSERT INTO provider_sessions(run_id,role,stream,provider,model,session_id,transition,settings_revision,created_at)
|
|
78
|
+
VALUES(?,?,?,?,?,?,?,?,?)`).run(runId, role, stream, settings.make, settings.model, sessionId ?? null, transition, settings.settings_revision, now.toISOString());
|
|
79
|
+
}
|
|
80
|
+
recordIssue(runId, issue) {
|
|
81
|
+
const result = this.db.prepare("INSERT INTO workflow_issues(run_id,code,issue_json,created_at) VALUES(?,?,?,?)").run(runId, issue.code, json(issue), issue.occurred_at);
|
|
82
|
+
this.insertEvent(runId, "issue", issue.phase, { issueId: Number(result.lastInsertRowid), code: issue.code }, issue.occurred_at);
|
|
83
|
+
return Number(result.lastInsertRowid);
|
|
84
|
+
}
|
|
85
|
+
issues(runId) {
|
|
86
|
+
return this.db.prepare("SELECT issue_json FROM workflow_issues WHERE run_id=? ORDER BY issue_id").all(runId).map((row) => parseJson(row.issue_json));
|
|
87
|
+
}
|
|
88
|
+
planOperation(input, now = new Date()) {
|
|
89
|
+
const at = now.toISOString();
|
|
90
|
+
this.db.prepare(`INSERT INTO operation_journal(idempotency_key,run_id,kind,status,intent_json,created_at,updated_at)
|
|
91
|
+
VALUES(?,?,?,'planned',?,?,?) ON CONFLICT(idempotency_key) DO NOTHING`).run(input.idempotencyKey, input.runId, input.kind, json(input.intent), at, at);
|
|
92
|
+
return this.operation(input.idempotencyKey);
|
|
93
|
+
}
|
|
94
|
+
updateOperation(idempotencyKey, status, details = {}, now = new Date()) {
|
|
95
|
+
if (!this.operation(idempotencyKey))
|
|
96
|
+
throw new Error(`operation not found: ${idempotencyKey}`);
|
|
97
|
+
this.db.prepare("UPDATE operation_journal SET status=?,result_json=?,external_id=?,error=?,updated_at=? WHERE idempotency_key=?")
|
|
98
|
+
.run(status, details.result === undefined ? null : json(details.result), details.externalId ?? null, details.error ?? null, now.toISOString(), idempotencyKey);
|
|
99
|
+
return this.operation(idempotencyKey);
|
|
100
|
+
}
|
|
101
|
+
operation(idempotencyKey) {
|
|
102
|
+
const row = this.db.prepare("SELECT * FROM operation_journal WHERE idempotency_key=?").get(idempotencyKey);
|
|
103
|
+
return row ? operationFromRow(row) : undefined;
|
|
104
|
+
}
|
|
105
|
+
operations(runId) {
|
|
106
|
+
return this.db.prepare("SELECT * FROM operation_journal WHERE run_id=? ORDER BY created_at,idempotency_key").all(runId).map(operationFromRow);
|
|
107
|
+
}
|
|
108
|
+
putEvidence(kind, value, now = new Date()) {
|
|
109
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(sanitizeText(value));
|
|
110
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
111
|
+
this.db.prepare("INSERT OR IGNORE INTO content_refs(digest,kind,content,created_at) VALUES(?,?,?,?)").run(digest, kind, bytes, now.toISOString());
|
|
112
|
+
return digest;
|
|
113
|
+
}
|
|
114
|
+
getEvidence(digest) {
|
|
115
|
+
const row = this.db.prepare("SELECT content FROM content_refs WHERE digest=?").get(digest);
|
|
116
|
+
return row?.content;
|
|
117
|
+
}
|
|
118
|
+
beginPublication(runId, intent, previousDigests, now = new Date()) {
|
|
119
|
+
const transactionId = randomUUID();
|
|
120
|
+
const at = now.toISOString();
|
|
121
|
+
this.db.prepare("INSERT INTO publication_transactions(transaction_id,run_id,status,intent_json,previous_digests_json,created_at,updated_at) VALUES(?,?,'prepared',?,?,?,?)")
|
|
122
|
+
.run(transactionId, runId, json(intent), json(previousDigests), at, at);
|
|
123
|
+
this.insertEvent(runId, "publication_prepared", "publication", { transactionId }, at);
|
|
124
|
+
return { transactionId, runId, status: "prepared", intent, previousDigests, createdAt: at, updatedAt: at };
|
|
125
|
+
}
|
|
126
|
+
updatePublication(transactionId, status, now = new Date()) {
|
|
127
|
+
const current = this.publication(transactionId);
|
|
128
|
+
if (!current)
|
|
129
|
+
throw new Error(`publication transaction not found: ${transactionId}`);
|
|
130
|
+
this.db.prepare("UPDATE publication_transactions SET status=?,updated_at=? WHERE transaction_id=?").run(status, now.toISOString(), transactionId);
|
|
131
|
+
this.insertEvent(current.runId, `publication_${status}`, "publication", { transactionId }, now.toISOString());
|
|
132
|
+
return { ...current, status, updatedAt: now.toISOString() };
|
|
133
|
+
}
|
|
134
|
+
publication(transactionId) {
|
|
135
|
+
const row = this.db.prepare("SELECT * FROM publication_transactions WHERE transaction_id=?").get(transactionId);
|
|
136
|
+
return row ? { transactionId: row.transaction_id, runId: row.run_id, status: row.status, intent: parseJson(row.intent_json), previousDigests: parseJson(row.previous_digests_json), createdAt: row.created_at, updatedAt: row.updated_at } : undefined;
|
|
137
|
+
}
|
|
138
|
+
incompletePublications() {
|
|
139
|
+
const rows = this.db.prepare("SELECT transaction_id FROM publication_transactions WHERE status NOT IN ('committed','rolled_back') ORDER BY created_at").all();
|
|
140
|
+
return rows.map((row) => this.publication(row.transaction_id));
|
|
141
|
+
}
|
|
142
|
+
acquireLease(runId, owner = `${hostname()}:${process.pid}:${randomUUID()}`, now = new Date(), staleMs = 45_000) {
|
|
143
|
+
const at = now.toISOString();
|
|
144
|
+
const host = hostname();
|
|
145
|
+
const pid = process.pid;
|
|
146
|
+
const processStart = processStartIdentity();
|
|
147
|
+
return this.db.transaction(() => {
|
|
148
|
+
const current = this.currentLease();
|
|
149
|
+
if (current && leaseVerifiedLive(current, now, staleMs))
|
|
150
|
+
throw new Error(`project workflow lease is held by ${current.owner} for run ${current.runId}`);
|
|
151
|
+
const generation = (current?.generation ?? 0) + 1;
|
|
152
|
+
this.db.prepare(`INSERT INTO project_lease(singleton,owner,generation,pid,host,process_start,heartbeat_at,run_id)
|
|
153
|
+
VALUES(1,?,?,?,?,?,?,?) ON CONFLICT(singleton) DO UPDATE SET owner=excluded.owner,generation=excluded.generation,pid=excluded.pid,host=excluded.host,process_start=excluded.process_start,heartbeat_at=excluded.heartbeat_at,run_id=excluded.run_id`)
|
|
154
|
+
.run(owner, generation, pid, host, processStart, at, runId);
|
|
155
|
+
this.db.prepare("UPDATE workflow_runs SET lease_generation=? WHERE run_id=?").run(generation, runId);
|
|
156
|
+
this.insertEvent(runId, current ? "lease_takeover" : "lease_acquired", "lease", { owner, generation, previous: current?.owner }, at);
|
|
157
|
+
return { owner, generation, pid, host, processStart, heartbeatAt: at, runId };
|
|
158
|
+
})();
|
|
159
|
+
}
|
|
160
|
+
heartbeatLease(lease, now = new Date()) {
|
|
161
|
+
const at = now.toISOString();
|
|
162
|
+
const result = this.db.prepare("UPDATE project_lease SET heartbeat_at=? WHERE singleton=1 AND owner=? AND generation=?").run(at, lease.owner, lease.generation);
|
|
163
|
+
if (result.changes !== 1)
|
|
164
|
+
throw new Error("workflow lease ownership changed");
|
|
165
|
+
return { ...lease, heartbeatAt: at };
|
|
166
|
+
}
|
|
167
|
+
releaseLease(lease, now = new Date()) {
|
|
168
|
+
this.db.transaction(() => {
|
|
169
|
+
const result = this.db.prepare("DELETE FROM project_lease WHERE singleton=1 AND owner=? AND generation=?").run(lease.owner, lease.generation);
|
|
170
|
+
if (result.changes === 1)
|
|
171
|
+
this.insertEvent(lease.runId, "lease_released", "lease", { generation: lease.generation }, now.toISOString());
|
|
172
|
+
})();
|
|
173
|
+
}
|
|
174
|
+
currentLease() {
|
|
175
|
+
const row = this.db.prepare("SELECT * FROM project_lease WHERE singleton=1").get();
|
|
176
|
+
return row ? { owner: row.owner, generation: row.generation, pid: row.pid, host: row.host, processStart: row.process_start, heartbeatAt: row.heartbeat_at, runId: row.run_id } : undefined;
|
|
177
|
+
}
|
|
178
|
+
importLegacyOnce(now = new Date()) {
|
|
179
|
+
const sources = [join(this.projectDir, ".foreman", "runs"), join(this.projectDir, ".rafi", "interviews"), join(this.projectDir, ".tickets", "delivery-sessions")];
|
|
180
|
+
let count = 0;
|
|
181
|
+
for (const directory of sources) {
|
|
182
|
+
if (!existsSync(directory) || !statSync(directory).isDirectory())
|
|
183
|
+
continue;
|
|
184
|
+
for (const name of readdirSync(directory).filter((entry) => entry.endsWith(".json"))) {
|
|
185
|
+
const path = join(directory, name);
|
|
186
|
+
const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
187
|
+
if (this.db.prepare("SELECT 1 FROM legacy_imports WHERE source_path=? AND digest=?").get(path, digest))
|
|
188
|
+
continue;
|
|
189
|
+
let parsed;
|
|
190
|
+
try {
|
|
191
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
parsed = { unreadable: true };
|
|
195
|
+
}
|
|
196
|
+
const runId = `legacy_${digest.slice(0, 24)}`;
|
|
197
|
+
if (!this.getRun(runId)) {
|
|
198
|
+
this.createRun({ runId, kind: "legacy", checkpoint: "legacy-imported", originalWork: { source: path }, remainingWork: {}, state: { source: path, record: parsed }, legacy: true }, now);
|
|
199
|
+
this.transition(runId, { status: "superseded", checkpoint: "legacy-imported", remainingWork: {}, event: "legacy_record_preserved" }, now);
|
|
200
|
+
}
|
|
201
|
+
this.db.prepare("INSERT INTO legacy_imports(source_path,digest,run_id,imported_at) VALUES(?,?,?,?)").run(path, digest, runId, now.toISOString());
|
|
202
|
+
count += 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return count;
|
|
206
|
+
}
|
|
207
|
+
insertEvent(runId, type, checkpoint, payload, at) {
|
|
208
|
+
this.db.prepare("INSERT INTO workflow_events(run_id,event_type,checkpoint,payload_json,created_at) VALUES(?,?,?,?,?)").run(runId, type, checkpoint, json(payload), at);
|
|
209
|
+
}
|
|
210
|
+
migrate() {
|
|
211
|
+
this.db.exec(`
|
|
212
|
+
CREATE TABLE IF NOT EXISTS workflow_runs(run_id TEXT PRIMARY KEY,kind TEXT NOT NULL,status TEXT NOT NULL,checkpoint TEXT NOT NULL,original_work_json TEXT NOT NULL,remaining_work_json TEXT NOT NULL,state_json TEXT NOT NULL,lease_generation INTEGER,legacy INTEGER NOT NULL DEFAULT 0,created_at TEXT NOT NULL,updated_at TEXT NOT NULL);
|
|
213
|
+
CREATE TABLE IF NOT EXISTS workflow_events(sequence INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),event_type TEXT NOT NULL,checkpoint TEXT NOT NULL,payload_json TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
214
|
+
CREATE TABLE IF NOT EXISTS role_settings(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,boundary INTEGER NOT NULL,revision INTEGER NOT NULL,settings_json TEXT NOT NULL,created_at TEXT NOT NULL,PRIMARY KEY(run_id,role,boundary));
|
|
215
|
+
CREATE TABLE IF NOT EXISTS project_settings_revisions(revision INTEGER PRIMARY KEY,defaults_json TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
216
|
+
CREATE TABLE IF NOT EXISTS workflow_telemetry(id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),snapshot_json TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
217
|
+
CREATE TABLE IF NOT EXISTS provider_sessions(id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),role TEXT NOT NULL,stream TEXT NOT NULL,provider TEXT NOT NULL,model TEXT NOT NULL,session_id TEXT,transition TEXT NOT NULL,settings_revision INTEGER NOT NULL,created_at TEXT NOT NULL);
|
|
218
|
+
CREATE TABLE IF NOT EXISTS workflow_issues(issue_id INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),code TEXT NOT NULL,issue_json TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
219
|
+
CREATE TABLE IF NOT EXISTS operation_journal(idempotency_key TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),kind TEXT NOT NULL,status TEXT NOT NULL,intent_json TEXT NOT NULL,result_json TEXT,external_id TEXT,error TEXT,created_at TEXT NOT NULL,updated_at TEXT NOT NULL);
|
|
220
|
+
CREATE TABLE IF NOT EXISTS content_refs(digest TEXT PRIMARY KEY,kind TEXT NOT NULL,content BLOB NOT NULL,created_at TEXT NOT NULL);
|
|
221
|
+
CREATE TABLE IF NOT EXISTS project_lease(singleton INTEGER PRIMARY KEY CHECK(singleton=1),owner TEXT NOT NULL,generation INTEGER NOT NULL,pid INTEGER NOT NULL,host TEXT NOT NULL,process_start TEXT NOT NULL,heartbeat_at TEXT NOT NULL,run_id TEXT NOT NULL);
|
|
222
|
+
CREATE TABLE IF NOT EXISTS legacy_imports(source_path TEXT NOT NULL,digest TEXT NOT NULL,run_id TEXT NOT NULL,imported_at TEXT NOT NULL,PRIMARY KEY(source_path,digest));
|
|
223
|
+
CREATE TABLE IF NOT EXISTS publication_transactions(transaction_id TEXT PRIMARY KEY,run_id TEXT NOT NULL,status TEXT NOT NULL,intent_json TEXT NOT NULL,previous_digests_json TEXT NOT NULL,created_at TEXT NOT NULL,updated_at TEXT NOT NULL);
|
|
224
|
+
CREATE INDEX IF NOT EXISTS workflow_events_run ON workflow_events(run_id,sequence);
|
|
225
|
+
CREATE INDEX IF NOT EXISTS operations_run ON operation_journal(run_id,status);
|
|
226
|
+
`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function rowToRun(row) { return { runId: row.run_id, kind: row.kind, status: row.status, checkpoint: row.checkpoint, originalWork: parseJson(row.original_work_json), remainingWork: parseJson(row.remaining_work_json), state: parseJson(row.state_json), ...(row.lease_generation === null ? {} : { leaseGeneration: row.lease_generation }), legacy: Boolean(row.legacy), createdAt: row.created_at, updatedAt: row.updated_at }; }
|
|
230
|
+
function operationFromRow(row) { return { idempotencyKey: row.idempotency_key, runId: row.run_id, kind: row.kind, status: row.status, intent: parseJson(row.intent_json), ...(row.result_json ? { result: parseJson(row.result_json) } : {}), ...(row.external_id ? { externalId: row.external_id } : {}), ...(row.error ? { error: row.error } : {}), createdAt: row.created_at, updatedAt: row.updated_at }; }
|
|
231
|
+
function json(value) { return JSON.stringify(value ?? null); }
|
|
232
|
+
function parseJson(value) { return JSON.parse(value); }
|
|
233
|
+
function sanitizeText(value) { return value.replace(/\b(sk-[A-Za-z0-9_-]{12,}|(?:api[_-]?key|token|password)\s*[:=]\s*\S+)/gi, "[REDACTED]"); }
|
|
234
|
+
function processStartIdentity(pid = process.pid) { try {
|
|
235
|
+
return readFileSync(`/proc/${pid}/stat`, "utf8").split(" ")[21] ?? "unavailable";
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return "unavailable";
|
|
239
|
+
} }
|
|
240
|
+
function leaseVerifiedLive(lease, now, staleMs) {
|
|
241
|
+
if (now.getTime() - new Date(lease.heartbeatAt).getTime() > staleMs)
|
|
242
|
+
return false;
|
|
243
|
+
if (lease.host !== hostname())
|
|
244
|
+
return true;
|
|
245
|
+
try {
|
|
246
|
+
process.kill(lease.pid, 0);
|
|
247
|
+
return processStartIdentity(lease.pid) === lease.processStart;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function ensureRecoveryGitignore(projectDir) {
|
|
254
|
+
const localExclude = join(projectDir, ".git", "info", "exclude");
|
|
255
|
+
const path = existsSync(localExclude) ? localExclude : join(projectDir, ".gitignore");
|
|
256
|
+
const entries = [WORKFLOW_DB_FILE, `${WORKFLOW_DB_FILE}-wal`, `${WORKFLOW_DB_FILE}-shm`];
|
|
257
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
258
|
+
const missing = entries.filter((entry) => !existing.split(/\r?\n/).includes(entry));
|
|
259
|
+
if (missing.length)
|
|
260
|
+
appendFileSync(path, `${existing && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`, "utf8");
|
|
261
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-foreman",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "An orchestrator that keeps Codex CLI / Claude Code builders moving through their step list unattended. Part of Rafi.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -41,6 +41,18 @@
|
|
|
41
41
|
"types": "./dist/buildRuns.d.ts",
|
|
42
42
|
"default": "./dist/buildRuns.js"
|
|
43
43
|
},
|
|
44
|
+
"./workflow-db.js": {
|
|
45
|
+
"types": "./dist/workflowDb.d.ts",
|
|
46
|
+
"default": "./dist/workflowDb.js"
|
|
47
|
+
},
|
|
48
|
+
"./session-lifecycle.js": {
|
|
49
|
+
"types": "./dist/sessionLifecycle.d.ts",
|
|
50
|
+
"default": "./dist/sessionLifecycle.js"
|
|
51
|
+
},
|
|
52
|
+
"./operation-recovery.js": {
|
|
53
|
+
"types": "./dist/operationRecovery.d.ts",
|
|
54
|
+
"default": "./dist/operationRecovery.js"
|
|
55
|
+
},
|
|
44
56
|
"./ticket-planning.js": {
|
|
45
57
|
"types": "./dist/ticketPlanning.d.ts",
|
|
46
58
|
"default": "./dist/ticketPlanning.js"
|
|
@@ -53,6 +65,10 @@
|
|
|
53
65
|
"types": "./dist/tickets/setupConfig.d.ts",
|
|
54
66
|
"default": "./dist/tickets/setupConfig.js"
|
|
55
67
|
},
|
|
68
|
+
"./sources/source-registry.js": {
|
|
69
|
+
"types": "./dist/sources/sourceRegistry.d.ts",
|
|
70
|
+
"default": "./dist/sources/sourceRegistry.js"
|
|
71
|
+
},
|
|
56
72
|
"./cli/tickets.js": {
|
|
57
73
|
"types": "./dist/cli/tickets.d.ts",
|
|
58
74
|
"default": "./dist/cli/tickets.js"
|
|
@@ -81,6 +97,7 @@
|
|
|
81
97
|
"dev": "tsx src/index.ts",
|
|
82
98
|
"test": "tsx --test test/*.test.ts",
|
|
83
99
|
"typecheck": "tsc --noEmit",
|
|
100
|
+
"test:live-providers": "RAFI_LIVE_PROVIDER_SESSIONS=1 tsx --test test/providerSmoke.test.ts",
|
|
84
101
|
"prepublishOnly": "pnpm build && pnpm test && pnpm typecheck"
|
|
85
102
|
},
|
|
86
103
|
"keywords": [
|
|
@@ -101,8 +118,8 @@
|
|
|
101
118
|
"@anthropic-ai/claude-agent-sdk": "^0.3.148"
|
|
102
119
|
},
|
|
103
120
|
"dependencies": {
|
|
104
|
-
"special-agents": "0.
|
|
105
|
-
"rafi-spec": "0.
|
|
121
|
+
"special-agents": "0.8.1",
|
|
122
|
+
"rafi-spec": "0.8.1",
|
|
106
123
|
"@clack/prompts": "^1.4.0",
|
|
107
124
|
"ajv": "^8.20.0",
|
|
108
125
|
"ajv-formats": "^3.0.1",
|