ai-foreman 1.7.4 → 1.7.6
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 +6 -1
- package/dist/activity.d.ts +63 -0
- package/dist/activity.js +244 -0
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +41 -1
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +65 -5
- package/dist/adapters/recovering.js +7 -1
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +5 -0
- package/dist/agentRun.js +5 -1
- package/dist/branch/github.js +5 -3
- package/dist/branch/gitlab.js +5 -3
- package/dist/branch/planner.d.ts +5 -0
- package/dist/branch/planner.js +42 -3
- package/dist/branch/presentation.d.ts +17 -0
- package/dist/branch/presentation.js +38 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +2 -0
- package/dist/branch/runner.js +44 -25
- package/dist/buildRuns.d.ts +41 -17
- package/dist/buildRuns.js +243 -22
- package/dist/cli/events.js +24 -5
- package/dist/cli/runtimeAuthPrompt.js +5 -1
- package/dist/cli/start.js +138 -45
- package/dist/cli/tickets.d.ts +10 -1
- package/dist/cli/tickets.js +418 -79
- package/dist/foreman.d.ts +8 -5
- package/dist/foreman.js +38 -14
- package/dist/index.js +3 -1
- package/dist/providerQuestions.d.ts +23 -0
- package/dist/providerQuestions.js +24 -0
- package/dist/qaReview.d.ts +1 -0
- package/dist/qaReview.js +23 -15
- package/dist/qaSnapshot.d.ts +14 -0
- package/dist/qaSnapshot.js +117 -1
- package/dist/runtimeReadiness.js +5 -0
- package/dist/sources/sourceRegistry.js +13 -6
- package/dist/tickets/details.d.ts +17 -0
- package/dist/tickets/details.js +94 -0
- package/dist/tickets/eligibility.d.ts +28 -0
- package/dist/tickets/eligibility.js +71 -0
- package/dist/tickets/importer.js +13 -8
- package/dist/tickets/reset.d.ts +27 -0
- package/dist/tickets/reset.js +213 -0
- package/dist/tickets/reviewStandards.d.ts +7 -0
- package/dist/tickets/reviewStandards.js +53 -0
- package/dist/tickets/setupConfig.d.ts +22 -0
- package/dist/tickets/setupConfig.js +105 -1
- package/dist/tickets/sourceFetch.js +19 -0
- package/dist/tickets/stateDb.d.ts +8 -1
- package/dist/tickets/stateDb.js +54 -23
- package/dist/workflowDb.d.ts +3 -0
- package/dist/workflowDb.js +26 -0
- package/package.json +15 -3
package/dist/buildRuns.js
CHANGED
|
@@ -2,16 +2,22 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
5
7
|
import { WorkflowDb } from "./workflowDb.js";
|
|
6
|
-
|
|
8
|
+
import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "./tickets/config.js";
|
|
9
|
+
import { loadTickets } from "./tickets/ticketLoader.js";
|
|
10
|
+
export const BUILD_RUN_VERSION = 2;
|
|
7
11
|
export const BUILD_RUN_DIRECTORY = ".foreman/runs";
|
|
8
12
|
export const BUILD_HEARTBEAT_MS = 10_000;
|
|
9
13
|
export const BUILD_LEASE_STALE_MS = 45_000;
|
|
10
14
|
export function createBuildRun(input) {
|
|
11
15
|
const now = input.now ?? new Date();
|
|
12
16
|
const stamp = now.toISOString();
|
|
17
|
+
const worktree = resolve(input.worktree ?? input.repositoryRoot);
|
|
18
|
+
const snapshot = captureGitSnapshot(worktree, input);
|
|
13
19
|
const run = {
|
|
14
|
-
version:
|
|
20
|
+
version: 2,
|
|
15
21
|
runId: randomUUID(),
|
|
16
22
|
status: "running",
|
|
17
23
|
tickets: [...input.tickets],
|
|
@@ -23,12 +29,14 @@ export function createBuildRun(input) {
|
|
|
23
29
|
qa: input.qa ? { settings: normalizeCapturedSettings(input.qa) } : undefined,
|
|
24
30
|
repository: {
|
|
25
31
|
root: resolve(input.repositoryRoot),
|
|
26
|
-
worktree
|
|
27
|
-
branch: input.branch,
|
|
28
|
-
baseHead: input.baseHead,
|
|
29
|
-
startHead: input.startHead,
|
|
30
|
-
|
|
32
|
+
worktree,
|
|
33
|
+
branch: input.branch ?? snapshot.branch,
|
|
34
|
+
baseHead: input.baseHead ?? snapshot.baselineHead,
|
|
35
|
+
startHead: input.startHead ?? snapshot.startHead,
|
|
36
|
+
git: snapshot,
|
|
37
|
+
baselineComplete: Boolean(snapshot.baselineHead && snapshot.startHead && snapshot.branch),
|
|
31
38
|
},
|
|
39
|
+
progress: { completedTickets: [], completedOperations: [], remainingTickets: [...input.tickets], nextAction: input.tickets[0] ? `Start ${input.tickets[0]}` : "Plan next work" },
|
|
32
40
|
receipts: {},
|
|
33
41
|
lease: currentLease(now),
|
|
34
42
|
createdAt: stamp,
|
|
@@ -71,7 +79,12 @@ export function saveBuildRun(projectDir, run, now = new Date()) {
|
|
|
71
79
|
validateBuildRun(run);
|
|
72
80
|
const directory = join(resolve(projectDir), BUILD_RUN_DIRECTORY);
|
|
73
81
|
mkdirSync(directory, { recursive: true });
|
|
74
|
-
const
|
|
82
|
+
const completedOperations = Object.keys(run.receipts).sort();
|
|
83
|
+
const next = {
|
|
84
|
+
...run,
|
|
85
|
+
progress: { ...run.progress, completedOperations, remainingTickets: remainingTickets(run) },
|
|
86
|
+
updatedAt: now.toISOString(),
|
|
87
|
+
};
|
|
75
88
|
const target = join(directory, `${run.runId}.json`);
|
|
76
89
|
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
77
90
|
writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
@@ -145,7 +158,7 @@ export function releaseBuildLease(projectDir, run, status = "interrupted") {
|
|
|
145
158
|
return saved;
|
|
146
159
|
}
|
|
147
160
|
export function completeBuildRun(projectDir, run, now = new Date()) {
|
|
148
|
-
const saved = saveBuildRun(projectDir, { ...run, status: "completed", checkpoint: "complete", lease: undefined, completedAt: now.toISOString() }, now);
|
|
161
|
+
const saved = saveBuildRun(projectDir, { ...run, status: "completed", checkpoint: "complete", lease: undefined, completedAt: now.toISOString(), progress: { ...run.progress, completedTickets: [...run.tickets], remainingTickets: [], nextAction: "None; build complete" } }, now);
|
|
149
162
|
releaseWorkflowLease(projectDir, run.runId, now);
|
|
150
163
|
return saved;
|
|
151
164
|
}
|
|
@@ -157,7 +170,7 @@ export function readBuildRuns(projectDir) {
|
|
|
157
170
|
try {
|
|
158
171
|
const run = JSON.parse(readFileSync(join(directory, name), "utf8"));
|
|
159
172
|
validateBuildRun(run);
|
|
160
|
-
return [run];
|
|
173
|
+
return [upgradeBuildRun(run)];
|
|
161
174
|
}
|
|
162
175
|
catch {
|
|
163
176
|
return [];
|
|
@@ -167,7 +180,42 @@ export function readBuildRuns(projectDir) {
|
|
|
167
180
|
export function recoverableBuildRuns(projectDir, now = new Date()) {
|
|
168
181
|
return readBuildRuns(projectDir)
|
|
169
182
|
.filter((run) => run.status !== "completed")
|
|
170
|
-
.map((run) => ({ ...run, active: isLeaseActive(run, now) }));
|
|
183
|
+
.map((run) => ({ ...inferLegacyRunTickets(projectDir, run), active: isLeaseActive(run, now) }));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Current-branch runs created before ticket checkpointing did not retain their
|
|
187
|
+
* selected ticket. Recover it from Foreman's tracker events so --ticket can
|
|
188
|
+
* still address those durable runs without rewriting the legacy record.
|
|
189
|
+
*/
|
|
190
|
+
function inferLegacyRunTickets(projectDir, run) {
|
|
191
|
+
if (run.tickets.length > 0 || !isTicketsInitialized(projectDir))
|
|
192
|
+
return run;
|
|
193
|
+
let db;
|
|
194
|
+
try {
|
|
195
|
+
const paths = resolveTicketPaths(loadTicketsConfig(projectDir), projectDir);
|
|
196
|
+
if (!existsSync(paths.stateDb))
|
|
197
|
+
return run;
|
|
198
|
+
db = new Database(paths.stateDb, { readonly: true, fileMustExist: true });
|
|
199
|
+
const rows = db.prepare(`
|
|
200
|
+
SELECT ticket_id
|
|
201
|
+
FROM ticket_events
|
|
202
|
+
WHERE actor = 'foreman'
|
|
203
|
+
AND ticket_id IS NOT NULL
|
|
204
|
+
AND julianday(timestamp) >= julianday(?)
|
|
205
|
+
AND julianday(timestamp) <= julianday(?)
|
|
206
|
+
ORDER BY timestamp, id
|
|
207
|
+
`).all(run.createdAt, run.updatedAt);
|
|
208
|
+
const tickets = [...new Set(rows.map((row) => row.ticket_id).filter(Boolean))];
|
|
209
|
+
if (tickets.length === 0)
|
|
210
|
+
return run;
|
|
211
|
+
return { ...run, tickets, currentTicket: tickets.at(-1) };
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return run;
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
db?.close();
|
|
218
|
+
}
|
|
171
219
|
}
|
|
172
220
|
export function isLeaseActive(run, now = new Date()) {
|
|
173
221
|
if (!run.lease)
|
|
@@ -193,12 +241,106 @@ export function buildRecoveryPreview(run) {
|
|
|
193
241
|
`checkpoint ${run.checkpoint}`,
|
|
194
242
|
`worktree ${run.repository.worktree}`,
|
|
195
243
|
`branch ${run.repository.branch ?? "current"}`,
|
|
196
|
-
`
|
|
244
|
+
`baseline ${run.repository.baselineComplete ? `${run.repository.git.baseRef ?? "recorded"} @ ${run.repository.git.baselineHead}` : "incomplete legacy baseline"}`,
|
|
245
|
+
`preserved paths ${run.repository.git.runOwnedPaths.join(", ") || run.repository.git.statusPaths.join(", ") || "none recorded"}`,
|
|
197
246
|
`completed operations ${completed.length ? completed.join(", ") : "none"}`,
|
|
198
247
|
`session ${run.builder?.sessionId ? "exact Builder session available" : "fresh Builder session required"}`,
|
|
199
248
|
`QA session ${run.qa?.sessionId ? "exact QA session available" : "fresh QA session required"}`,
|
|
200
249
|
];
|
|
201
250
|
}
|
|
251
|
+
export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOverride) {
|
|
252
|
+
let operationNames = [];
|
|
253
|
+
let issues = [];
|
|
254
|
+
let workflowState = {};
|
|
255
|
+
if (existsSync(join(resolve(projectDir), ".rafi", "recovery.sqlite3"))) {
|
|
256
|
+
const workflow = new WorkflowDb(projectDir);
|
|
257
|
+
try {
|
|
258
|
+
operationNames = workflow.operations(run.runId).filter((operation) => operation.status === "confirmed").map((operation) => operation.kind);
|
|
259
|
+
issues = workflow.issues(run.runId).map((issue) => issue.detail);
|
|
260
|
+
workflowState = workflow.getRun(run.runId)?.state ?? {};
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
workflow.close();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const stateTicket = typeof workflowState.currentTicket === "string" ? workflowState.currentTicket : undefined;
|
|
267
|
+
const ticketId = ticketOverride ?? stateTicket ?? run.currentTicket ?? run.progress.remainingTickets[0] ?? run.tickets[0];
|
|
268
|
+
let ticketTitle;
|
|
269
|
+
if (ticketId && isTicketsInitialized(projectDir)) {
|
|
270
|
+
try {
|
|
271
|
+
const paths = resolveTicketPaths(loadTicketsConfig(projectDir), projectDir);
|
|
272
|
+
ticketTitle = loadTickets(paths.tickets).find((ticket) => ticket.id === ticketId)?.title;
|
|
273
|
+
}
|
|
274
|
+
catch { /* durable run data remains enough for a legacy preview */ }
|
|
275
|
+
}
|
|
276
|
+
const stateWorktree = typeof workflowState.worktree === "string" ? workflowState.worktree : undefined;
|
|
277
|
+
const recoveryWorktree = stateWorktree && existsSync(stateWorktree) ? stateWorktree : run.repository.worktree;
|
|
278
|
+
const stateBranch = typeof workflowState.branch === "string" ? workflowState.branch : undefined;
|
|
279
|
+
const expectedChanges = gitStatusPaths(recoveryWorktree);
|
|
280
|
+
const baseChanges = resolve(run.repository.root) === resolve(recoveryWorktree) ? [] : gitStatusPaths(run.repository.root);
|
|
281
|
+
const identityChanged = recoveryWorktree === run.repository.worktree && run.repository.git.worktreeIdentity && worktreeIdentity(recoveryWorktree) !== run.repository.git.worktreeIdentity;
|
|
282
|
+
const branchNow = gitValue(recoveryWorktree, ["branch", "--show-current"]);
|
|
283
|
+
const expectedBranch = stateBranch ?? run.repository.git.branch;
|
|
284
|
+
const branchChanged = Boolean(expectedBranch && branchNow && branchNow !== expectedBranch);
|
|
285
|
+
const unexpectedChanges = baseChanges.map((path) => ({ path, risk: "base-worktree changes may conflict with recovery" }));
|
|
286
|
+
if (identityChanged)
|
|
287
|
+
unexpectedChanges.push({ path: recoveryWorktree, risk: "preserved worktree identity changed" });
|
|
288
|
+
if (branchChanged)
|
|
289
|
+
unexpectedChanges.push({ path: recoveryWorktree, risk: `branch changed from ${expectedBranch} to ${branchNow}` });
|
|
290
|
+
const completed = [...new Set([...run.progress.completedTickets, ...run.progress.completedOperations, ...operationNames])];
|
|
291
|
+
const remaining = run.progress.remainingTickets.length ? run.progress.remainingTickets : remainingTickets(run);
|
|
292
|
+
const interruption = run.interruption?.lastError ?? run.interruption?.summary ?? run.failure?.summary ?? issues.at(-1) ?? run.status;
|
|
293
|
+
const validation = run.progress.validation
|
|
294
|
+
? `${run.progress.validation.status}${run.progress.validation.qa ? `; QA ${run.progress.validation.qa}` : ""}`
|
|
295
|
+
: run.qa?.sessionId ? "QA session checkpointed" : "no completed QA/validation checkpoint recorded";
|
|
296
|
+
const nextAction = ticketOverride
|
|
297
|
+
? `Resume ${ticketOverride} from ${run.checkpoint}`
|
|
298
|
+
: run.progress.nextAction ?? (ticketId ? `Resume ${ticketId} from ${run.checkpoint}` : `Resume from ${run.checkpoint}`);
|
|
299
|
+
const primary = ticketId ? `${ticketId}${ticketTitle ? `: ${ticketTitle}` : ""}` : "legacy run (ticket unavailable)";
|
|
300
|
+
return {
|
|
301
|
+
run, ticketId, ticketTitle,
|
|
302
|
+
compactLabel: `${primary} — ${run.status}`,
|
|
303
|
+
compactHint: `${run.builder?.settings.make ?? "runtime unavailable"}; ${stateBranch ?? branchNow ?? run.repository.branch ?? "current branch"}; updated ${relativeTime(run.updatedAt, now)}`,
|
|
304
|
+
completed, remaining,
|
|
305
|
+
lastSuccess: run.progress.lastSuccessfulAction ?? run.checkpoint,
|
|
306
|
+
interruption,
|
|
307
|
+
validation,
|
|
308
|
+
expectedChanges,
|
|
309
|
+
unexpectedChanges,
|
|
310
|
+
nextAction,
|
|
311
|
+
worktree: recoveryWorktree,
|
|
312
|
+
branch: stateBranch ?? branchNow ?? run.repository.git.branch ?? run.repository.branch,
|
|
313
|
+
exactSessionId: typeof workflowState.sessionId === "string" ? workflowState.sessionId : run.builder?.sessionId,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
export function formatBuildRecoveryProjection(projection) {
|
|
317
|
+
const { run } = projection;
|
|
318
|
+
const lines = [
|
|
319
|
+
`run ${run.runId}`,
|
|
320
|
+
`ticket ${projection.ticketId ?? "unavailable"}${projection.ticketTitle ? `: ${projection.ticketTitle}` : ""}`,
|
|
321
|
+
`status ${run.status}; checkpoint ${run.checkpoint}`,
|
|
322
|
+
`completed ${projection.completed.join(", ") || "none recorded"}`,
|
|
323
|
+
`current ${run.progress.currentStep ?? projection.ticketId ?? "not recorded"}`,
|
|
324
|
+
`remaining ${projection.remaining.join(", ") || "none"}`,
|
|
325
|
+
`last successful action ${projection.lastSuccess}`,
|
|
326
|
+
`interruption/failure ${projection.interruption}`,
|
|
327
|
+
`validation/QA ${projection.validation}`,
|
|
328
|
+
`branch ${projection.branch ?? "current"}; worktree ${projection.worktree}`,
|
|
329
|
+
`baseline ${run.repository.baselineComplete ? `${run.repository.git.baseRef ?? "recorded"} @ ${run.repository.git.baselineHead}` : "incomplete; automatic rollback is unavailable"}`,
|
|
330
|
+
`session ${projection.exactSessionId ? "exact Builder session available" : "fresh Builder session required"}`,
|
|
331
|
+
`runtime ${run.builder?.settings.make ?? "unavailable"}; model ${run.builder?.settings.model ?? "unavailable"}`,
|
|
332
|
+
`updated ${run.updatedAt}`,
|
|
333
|
+
`preserved expected in-progress paths ${projection.expectedChanges.join(", ") || "none currently dirty"}`,
|
|
334
|
+
];
|
|
335
|
+
if (projection.unexpectedChanges.length) {
|
|
336
|
+
lines.push("WARNING: unexpected recovery state (recovery remains available):");
|
|
337
|
+
for (const item of projection.unexpectedChanges)
|
|
338
|
+
lines.push(` ${item.path}: ${item.risk}`);
|
|
339
|
+
lines.push("recommended action: preserve or move these changes before recovery if they overlap the interrupted work");
|
|
340
|
+
}
|
|
341
|
+
lines.push(`next action ${projection.nextAction}`);
|
|
342
|
+
return lines;
|
|
343
|
+
}
|
|
202
344
|
function currentLease(now) {
|
|
203
345
|
return { hostname: hostname(), pid: process.pid, processStart: processStartIdentity(process.pid), heartbeatAt: now.toISOString() };
|
|
204
346
|
}
|
|
@@ -210,17 +352,8 @@ function processStartIdentity(pid) {
|
|
|
210
352
|
return pid === process.pid ? `${Date.now() - Math.round(process.uptime() * 1000)}` : "unknown";
|
|
211
353
|
}
|
|
212
354
|
}
|
|
213
|
-
function fingerprintWorkingFiles(worktree) {
|
|
214
|
-
try {
|
|
215
|
-
const stat = statSync(resolve(worktree));
|
|
216
|
-
return createHash("sha256").update(`${stat.dev}:${stat.ino}:${stat.mtimeMs}`).digest("hex");
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
return undefined;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
355
|
function validateBuildRun(run) {
|
|
223
|
-
if (run.version
|
|
356
|
+
if (![1, 2].includes(run.version) || !run.runId || !run.repository?.root || !run.repository.worktree || !run.createdAt || !run.updatedAt) {
|
|
224
357
|
throw new Error("invalid build run record");
|
|
225
358
|
}
|
|
226
359
|
}
|
|
@@ -229,6 +362,8 @@ function workflowStatus(status) {
|
|
|
229
362
|
return "completed";
|
|
230
363
|
if (status === "failed")
|
|
231
364
|
return "failed";
|
|
365
|
+
if (status === "superseded")
|
|
366
|
+
return "superseded";
|
|
232
367
|
if (status === "blocked")
|
|
233
368
|
return "blocked";
|
|
234
369
|
if (status === "interrupted" || status === "recoverable")
|
|
@@ -239,6 +374,92 @@ function remainingTickets(run) {
|
|
|
239
374
|
const current = run.currentTicket ? run.tickets.indexOf(run.currentTicket) : 0;
|
|
240
375
|
return run.status === "completed" ? [] : run.tickets.slice(Math.max(0, current));
|
|
241
376
|
}
|
|
377
|
+
function captureGitSnapshot(worktree, input) {
|
|
378
|
+
const git = (args) => {
|
|
379
|
+
try {
|
|
380
|
+
return execFileSync("git", args, { cwd: worktree, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const statusPaths = (git(["status", "--porcelain=v1", "-z"]) ?? "").split("\0").filter(Boolean).map((line) => line.slice(3)).sort();
|
|
387
|
+
const head = input.startHead ?? git(["rev-parse", "HEAD"]);
|
|
388
|
+
const branch = input.branch ?? git(["branch", "--show-current"]);
|
|
389
|
+
return {
|
|
390
|
+
baselineHead: input.baseHead ?? head,
|
|
391
|
+
baseRef: input.baseRef ?? branch,
|
|
392
|
+
branch,
|
|
393
|
+
startHead: head,
|
|
394
|
+
worktree,
|
|
395
|
+
worktreeIdentity: worktreeIdentity(worktree),
|
|
396
|
+
statusPaths,
|
|
397
|
+
initialStatusPaths: [...statusPaths],
|
|
398
|
+
runOwnedPaths: [],
|
|
399
|
+
createdBranch: false,
|
|
400
|
+
createdWorktree: worktree !== resolve(input.repositoryRoot),
|
|
401
|
+
upstream: git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function worktreeIdentity(worktree) {
|
|
405
|
+
try {
|
|
406
|
+
const stat = statSync(resolve(worktree));
|
|
407
|
+
return createHash("sha256").update(`${stat.dev}:${stat.ino}`).digest("hex");
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function gitValue(cwd, args) {
|
|
414
|
+
try {
|
|
415
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function gitStatusPaths(cwd) {
|
|
422
|
+
return (gitValue(cwd, ["status", "--porcelain=v1", "-z"]) ?? "").split("\0").filter(Boolean).map((line) => line.slice(3)).sort();
|
|
423
|
+
}
|
|
424
|
+
function relativeTime(value, now) {
|
|
425
|
+
const seconds = Math.max(0, Math.floor((now.getTime() - new Date(value).getTime()) / 1000));
|
|
426
|
+
if (seconds < 60)
|
|
427
|
+
return `${seconds}s ago`;
|
|
428
|
+
if (seconds < 3600)
|
|
429
|
+
return `${Math.floor(seconds / 60)}m ago`;
|
|
430
|
+
if (seconds < 86_400)
|
|
431
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
432
|
+
return `${Math.floor(seconds / 86_400)}d ago`;
|
|
433
|
+
}
|
|
434
|
+
function upgradeBuildRun(run) {
|
|
435
|
+
if (run.version === 2)
|
|
436
|
+
return run;
|
|
437
|
+
const legacy = run;
|
|
438
|
+
const remaining = legacy.status === "completed" ? [] : legacy.tickets.slice(Math.max(0, legacy.currentTicket ? legacy.tickets.indexOf(legacy.currentTicket) : 0));
|
|
439
|
+
return {
|
|
440
|
+
...legacy,
|
|
441
|
+
version: 2,
|
|
442
|
+
legacy: true,
|
|
443
|
+
repository: {
|
|
444
|
+
...legacy.repository,
|
|
445
|
+
baselineComplete: false,
|
|
446
|
+
git: {
|
|
447
|
+
baselineHead: legacy.repository.baseHead,
|
|
448
|
+
branch: legacy.repository.branch,
|
|
449
|
+
startHead: legacy.repository.startHead,
|
|
450
|
+
worktree: legacy.repository.worktree,
|
|
451
|
+
statusPaths: [], initialStatusPaths: [], runOwnedPaths: [], createdBranch: false, createdWorktree: false,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
progress: {
|
|
455
|
+
completedTickets: legacy.status === "completed" ? [...legacy.tickets] : [],
|
|
456
|
+
completedOperations: Object.keys(legacy.receipts).sort(),
|
|
457
|
+
remainingTickets: remaining,
|
|
458
|
+
lastSuccessfulAction: legacy.checkpoint,
|
|
459
|
+
nextAction: remaining[0] ? `Resume ${remaining[0]}` : "Inspect legacy run",
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
242
463
|
function releaseWorkflowLease(projectDir, runId, now = new Date()) {
|
|
243
464
|
const workflow = new WorkflowDb(projectDir);
|
|
244
465
|
try {
|
package/dist/cli/events.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import { currentActivity } from "../activity.js";
|
|
1
2
|
/** Print a compact live feed of builder activity. */
|
|
2
3
|
export async function printEvents(events) {
|
|
3
4
|
let atLineStart = true;
|
|
4
5
|
for await (const ev of events) {
|
|
5
6
|
if (ev.kind === "text") {
|
|
6
7
|
if (ev.text) {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
const reporter = currentActivity();
|
|
9
|
+
if (reporter) {
|
|
10
|
+
reporter.writePersistent(ev.text);
|
|
11
|
+
atLineStart = true;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
process.stdout.write(ev.text);
|
|
15
|
+
atLineStart = ev.text.endsWith("\n");
|
|
16
|
+
}
|
|
9
17
|
}
|
|
10
18
|
}
|
|
11
19
|
else if (ev.kind === "tool") {
|
|
@@ -13,7 +21,7 @@ export async function printEvents(events) {
|
|
|
13
21
|
process.stdout.write("\n");
|
|
14
22
|
atLineStart = true;
|
|
15
23
|
}
|
|
16
|
-
|
|
24
|
+
writeLine(` -> ${ev.name} ${briefInput(ev.input)}`);
|
|
17
25
|
}
|
|
18
26
|
else if (ev.kind === "turn-complete") {
|
|
19
27
|
if (!atLineStart) {
|
|
@@ -22,19 +30,30 @@ export async function printEvents(events) {
|
|
|
22
30
|
}
|
|
23
31
|
const tag = ev.result.isError ? "turn errored" : "turn complete";
|
|
24
32
|
const cost = ev.result.costUsd > 0 ? ` ($${ev.result.costUsd.toFixed(4)})` : "";
|
|
25
|
-
|
|
33
|
+
writeLine(` - ${tag}${cost}`);
|
|
26
34
|
}
|
|
27
35
|
else if (ev.kind === "error") {
|
|
28
36
|
if (!atLineStart) {
|
|
29
37
|
process.stdout.write("\n");
|
|
30
38
|
atLineStart = true;
|
|
31
39
|
}
|
|
32
|
-
|
|
40
|
+
writeLine(` ! error: ${ev.message}`);
|
|
41
|
+
}
|
|
42
|
+
else if (ev.kind === "retry" && !currentActivity()) {
|
|
43
|
+
const attempt = ev.attempt ? ` (${ev.attempt}${ev.maximum ? `/${ev.maximum}` : ""})` : "";
|
|
44
|
+
writeLine(` ! ${ev.provider}: ${ev.reason}; retrying${attempt}`);
|
|
33
45
|
}
|
|
34
46
|
}
|
|
35
47
|
if (!atLineStart)
|
|
36
48
|
process.stdout.write("\n");
|
|
37
49
|
}
|
|
50
|
+
function writeLine(line) {
|
|
51
|
+
const reporter = currentActivity();
|
|
52
|
+
if (reporter)
|
|
53
|
+
reporter.writePersistent(line);
|
|
54
|
+
else
|
|
55
|
+
console.log(line);
|
|
56
|
+
}
|
|
38
57
|
function briefInput(input) {
|
|
39
58
|
if (input && typeof input === "object") {
|
|
40
59
|
const o = input;
|
|
@@ -3,6 +3,7 @@ import { requireClaudeSDK } from "../adapters/claude.js";
|
|
|
3
3
|
import { checkRuntimeReady, RuntimeAuthError, runtimeCommandLabel, } from "../runtimeAuth.js";
|
|
4
4
|
import { resolveExecutablePath } from "../runtimeReadiness.js";
|
|
5
5
|
import { otherRuntime, runtimeDisplayName } from "./runtimeSelection.js";
|
|
6
|
+
import { currentActivity } from "../activity.js";
|
|
6
7
|
export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrOptions) {
|
|
7
8
|
const opts = typeof labelOrOptions === "string"
|
|
8
9
|
? { label: labelOrOptions }
|
|
@@ -37,9 +38,12 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
|
|
|
37
38
|
const choice = opts.choose
|
|
38
39
|
? await opts.choose(failure, { otherRuntime: fallbackRuntime, allowSwitch })
|
|
39
40
|
: await promptRuntimeRecovery(failure, opts.label, fallbackRuntime, allowSwitch);
|
|
40
|
-
if (choice === "retry")
|
|
41
|
+
if (choice === "retry") {
|
|
42
|
+
currentActivity()?.note(`rafi: retrying ${runtime} readiness check`);
|
|
41
43
|
continue;
|
|
44
|
+
}
|
|
42
45
|
if (choice === "switch" && allowSwitch) {
|
|
46
|
+
currentActivity()?.note(`rafi: checking fallback runtime ${fallbackRuntime}`);
|
|
43
47
|
try {
|
|
44
48
|
const probe = await check(projectDir, fallbackRuntime);
|
|
45
49
|
if (fallbackRuntime === "claude") {
|