ai-foreman 1.7.6 → 1.7.8
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 +26 -1
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +31 -6
- package/dist/adapters/claude.d.ts +61 -6
- package/dist/adapters/claude.js +544 -36
- package/dist/adapters/codex.d.ts +28 -1
- package/dist/adapters/codex.js +398 -27
- package/dist/adapters/recovering.d.ts +13 -2
- package/dist/adapters/recovering.js +32 -4
- package/dist/adapters/sessionFailure.d.ts +18 -0
- package/dist/adapters/sessionFailure.js +51 -0
- package/dist/adapters/types.d.ts +117 -9
- package/dist/agentRun.d.ts +4 -1
- package/dist/agentRun.js +96 -9
- package/dist/branch/currentGuard.d.ts +38 -0
- package/dist/branch/currentGuard.js +71 -0
- package/dist/branch/planner.d.ts +1 -1
- package/dist/branch/planner.js +2 -7
- package/dist/branch/prefix.d.ts +8 -0
- package/dist/branch/prefix.js +26 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +8 -0
- package/dist/branch/runner.d.ts +14 -2
- package/dist/branch/runner.js +77 -17
- package/dist/buildRuns.d.ts +13 -4
- package/dist/buildRuns.js +85 -19
- package/dist/cli/handoffs.d.ts +2 -0
- package/dist/cli/handoffs.js +74 -0
- package/dist/cli/start.js +1079 -43
- package/dist/cli/tickets.js +199 -3
- package/dist/continuity.d.ts +92 -0
- package/dist/continuity.js +415 -0
- package/dist/foreman.d.ts +5 -1
- package/dist/foreman.js +41 -6
- package/dist/handoffs.d.ts +69 -0
- package/dist/handoffs.js +546 -0
- package/dist/log.d.ts +1 -1
- package/dist/permissions/policy.d.ts +5 -1
- package/dist/permissions/policy.js +48 -1
- package/dist/qaReview.d.ts +6 -6
- package/dist/qaReview.js +24 -14
- package/dist/sessionAvailability.d.ts +15 -0
- package/dist/sessionAvailability.js +57 -0
- package/dist/sessionIdentity.d.ts +33 -0
- package/dist/sessionIdentity.js +132 -0
- package/dist/sessionLifecycle.d.ts +198 -5
- package/dist/sessionLifecycle.js +1373 -20
- package/dist/statusReporter.d.ts +84 -8
- package/dist/statusReporter.js +270 -14
- package/dist/ticketPlanning.js +8 -0
- package/dist/ticketPopulation.js +88 -12
- package/dist/tickets/commands.js +20 -12
- package/dist/tickets/groupReset.d.ts +25 -0
- package/dist/tickets/groupReset.js +279 -0
- package/dist/tickets/groups.d.ts +36 -0
- package/dist/tickets/groups.js +68 -0
- package/dist/tickets/importer.js +27 -3
- package/dist/tickets/recommendations.js +31 -7
- package/dist/tickets/reset.d.ts +1 -0
- package/dist/tickets/reset.js +1 -0
- package/dist/tickets/setupConfig.d.ts +1 -0
- package/dist/tickets/setupConfig.js +10 -2
- package/dist/tickets/stateDb.d.ts +34 -0
- package/dist/tickets/stateDb.js +207 -0
- package/dist/tickets/ticketLoader.js +5 -2
- package/dist/tickets/validate.js +4 -0
- package/dist/workflowDb.d.ts +145 -3
- package/dist/workflowDb.js +571 -10
- package/package.json +32 -4
package/dist/buildRuns.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import Database from "better-sqlite3";
|
|
7
7
|
import { WorkflowDb } from "./workflowDb.js";
|
|
8
|
+
import { canonicalSessionPath, captureWorkspaceIdentity, createProviderSessionRef, latestSessionBinding, upsertSessionBinding } from "./sessionIdentity.js";
|
|
9
|
+
import { resolveProviderSessionAvailability } from "./sessionAvailability.js";
|
|
10
|
+
import { captureCurrentWorkflowSessionIdentity, currentWorkflowIdentityKey } from "./branch/currentGuard.js";
|
|
8
11
|
import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "./tickets/config.js";
|
|
9
12
|
import { loadTickets } from "./tickets/ticketLoader.js";
|
|
10
13
|
export const BUILD_RUN_VERSION = 2;
|
|
@@ -37,6 +40,7 @@ export function createBuildRun(input) {
|
|
|
37
40
|
baselineComplete: Boolean(snapshot.baselineHead && snapshot.startHead && snapshot.branch),
|
|
38
41
|
},
|
|
39
42
|
progress: { completedTickets: [], completedOperations: [], remainingTickets: [...input.tickets], nextAction: input.tickets[0] ? `Start ${input.tickets[0]}` : "Plan next work" },
|
|
43
|
+
runDecisions: input.runDecisions,
|
|
40
44
|
receipts: {},
|
|
41
45
|
lease: currentLease(now),
|
|
42
46
|
createdAt: stamp,
|
|
@@ -65,15 +69,27 @@ export function resumeBuildRun(projectDir, runId, patch, now = new Date()) {
|
|
|
65
69
|
finally {
|
|
66
70
|
workflow.close();
|
|
67
71
|
}
|
|
72
|
+
const clearBuilderSession = patch.builderSessionId === null && patch.builderSessionRef === null;
|
|
73
|
+
const builderSessionId = clearBuilderSession
|
|
74
|
+
? undefined
|
|
75
|
+
: patch.builderSessionRef?.sessionId ?? patch.builderSessionId ?? existing.builder?.sessionId;
|
|
68
76
|
return saveBuildRun(projectDir, {
|
|
69
77
|
...existing, status: "running", checkpoint: "recovery-resumed", completedAt: undefined,
|
|
70
|
-
builder: patch.builder ? { settings: normalizeCapturedSettings(patch.builder), ...(
|
|
78
|
+
builder: patch.builder ? { settings: normalizeCapturedSettings(patch.builder), ...(builderSessionId ? { sessionId: builderSessionId } : {}) } : existing.builder,
|
|
71
79
|
qa: patch.qa ? { settings: normalizeCapturedSettings(patch.qa), sessionId: existing.qa?.sessionId } : existing.qa,
|
|
80
|
+
sessionBindings: patch.builderSessionRef ? upsertSessionBinding(existing.sessionBindings, patch.builderSessionRef) : existing.sessionBindings,
|
|
72
81
|
lease: currentLease(now),
|
|
73
82
|
}, now);
|
|
74
83
|
}
|
|
75
84
|
function normalizeCapturedSettings(settings) {
|
|
76
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
...settings,
|
|
87
|
+
session_strategy: settings.session_strategy ?? (["builder", "qa", "ticket-maker"].includes(settings.role) ? "compact" : "fresh"),
|
|
88
|
+
display_session_cost: settings.display_session_cost ?? false,
|
|
89
|
+
auto_compact_threshold_percent: settings.auto_compact_threshold_percent ?? 50,
|
|
90
|
+
compact_maximum: settings.compact_maximum ?? 10,
|
|
91
|
+
settings_revision: settings.settings_revision ?? 0,
|
|
92
|
+
};
|
|
77
93
|
}
|
|
78
94
|
export function saveBuildRun(projectDir, run, now = new Date()) {
|
|
79
95
|
validateBuildRun(run);
|
|
@@ -104,9 +120,9 @@ export function saveBuildRun(projectDir, run, now = new Date()) {
|
|
|
104
120
|
remainingWork: { tickets: remainingTickets(next) }, state: next, event: "build_snapshot",
|
|
105
121
|
}, now);
|
|
106
122
|
if (next.builder?.sessionId)
|
|
107
|
-
workflow.recordSession(next.runId, "builder", "builder", next.builder.sessionId, "checkpoint", next.builder.settings, now);
|
|
123
|
+
workflow.recordSession(next.runId, "builder", "builder", latestSessionBinding(next.sessionBindings, "builder", next.builder.sessionId) ?? next.builder.sessionId, "checkpoint", next.builder.settings, now);
|
|
108
124
|
if (next.qa?.sessionId)
|
|
109
|
-
workflow.recordSession(next.runId, "qa", "qa", next.qa.sessionId, "checkpoint", next.qa.settings, now);
|
|
125
|
+
workflow.recordSession(next.runId, "qa", "qa", latestSessionBinding(next.sessionBindings, "qa", next.qa.sessionId) ?? next.qa.sessionId, "checkpoint", next.qa.settings, now);
|
|
110
126
|
}
|
|
111
127
|
finally {
|
|
112
128
|
workflow.close();
|
|
@@ -116,11 +132,40 @@ export function saveBuildRun(projectDir, run, now = new Date()) {
|
|
|
116
132
|
export function checkpointBuildRun(projectDir, run, checkpoint, patch = {}) {
|
|
117
133
|
return saveBuildRun(projectDir, { ...run, ...patch, checkpoint, receipts: patch.receipts ?? run.receipts });
|
|
118
134
|
}
|
|
119
|
-
export function persistBuildSession(projectDir, run, role,
|
|
135
|
+
export function persistBuildSession(projectDir, run, role, session) {
|
|
120
136
|
const current = run[role];
|
|
121
137
|
if (!current)
|
|
122
138
|
throw new Error(`${role} settings were not captured for run ${run.runId}`);
|
|
123
|
-
|
|
139
|
+
const sessionId = typeof session === "string" ? session : session.sessionId;
|
|
140
|
+
const sessionBindings = typeof session === "string" ? run.sessionBindings : upsertSessionBinding(run.sessionBindings, session);
|
|
141
|
+
return checkpointBuildRun(projectDir, { ...run, sessionBindings, [role]: { ...current, sessionId } }, `${role}-session-ready`);
|
|
142
|
+
}
|
|
143
|
+
/** Latest scoped binding, with the narrow legacy Builder inference allowed by the recovery protocol. */
|
|
144
|
+
export function buildRunSessionBinding(run, role, sessionId) {
|
|
145
|
+
const bound = latestSessionBinding(run.sessionBindings, role, sessionId);
|
|
146
|
+
if (bound)
|
|
147
|
+
return bound;
|
|
148
|
+
if (role === "qa")
|
|
149
|
+
return undefined;
|
|
150
|
+
const raw = sessionId ?? run.builder?.sessionId;
|
|
151
|
+
if (!raw || !run.builder || !existsSync(run.repository.worktree))
|
|
152
|
+
return undefined;
|
|
153
|
+
return createProviderSessionRef({
|
|
154
|
+
provider: run.builder.settings.make,
|
|
155
|
+
sessionId: raw,
|
|
156
|
+
role: "builder",
|
|
157
|
+
stream: "builder",
|
|
158
|
+
generation: 0,
|
|
159
|
+
cwd: run.repository.worktree,
|
|
160
|
+
configRoot: run.repository.root,
|
|
161
|
+
workspaceIdentity: run.branchMode === "current" && run.repository.git.branch
|
|
162
|
+
? currentWorkflowIdentityKey({ worktree: canonicalSessionPath(run.repository.worktree), ref: `branch:${run.repository.git.branch}` })
|
|
163
|
+
: run.repository.git.worktreeIdentity ?? captureWorkspaceIdentity(run.repository.worktree),
|
|
164
|
+
ticketId: run.currentTicket,
|
|
165
|
+
deliveryUnitId: run.deliveryUnit,
|
|
166
|
+
source: "legacy-inferred",
|
|
167
|
+
createdAt: run.createdAt,
|
|
168
|
+
});
|
|
124
169
|
}
|
|
125
170
|
export function recordBuildReceipt(projectDir, run, operationId, detail) {
|
|
126
171
|
if (run.receipts[operationId])
|
|
@@ -244,11 +289,11 @@ export function buildRecoveryPreview(run) {
|
|
|
244
289
|
`baseline ${run.repository.baselineComplete ? `${run.repository.git.baseRef ?? "recorded"} @ ${run.repository.git.baselineHead}` : "incomplete legacy baseline"}`,
|
|
245
290
|
`preserved paths ${run.repository.git.runOwnedPaths.join(", ") || run.repository.git.statusPaths.join(", ") || "none recorded"}`,
|
|
246
291
|
`completed operations ${completed.length ? completed.join(", ") : "none"}`,
|
|
247
|
-
`session ${run
|
|
248
|
-
`QA session ${run
|
|
292
|
+
`session ${buildRunSessionBinding(run, "builder") ? "Builder session candidate requires validation" : "fresh Builder session required"}`,
|
|
293
|
+
`QA session ${buildRunSessionBinding(run, "qa") ? "QA session candidate requires validation" : "fresh QA session required"}`,
|
|
249
294
|
];
|
|
250
295
|
}
|
|
251
|
-
export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOverride) {
|
|
296
|
+
export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOverride, sessionAvailability) {
|
|
252
297
|
let operationNames = [];
|
|
253
298
|
let issues = [];
|
|
254
299
|
let workflowState = {};
|
|
@@ -297,6 +342,9 @@ export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOv
|
|
|
297
342
|
? `Resume ${ticketOverride} from ${run.checkpoint}`
|
|
298
343
|
: run.progress.nextAction ?? (ticketId ? `Resume ${ticketId} from ${run.checkpoint}` : `Resume from ${run.checkpoint}`);
|
|
299
344
|
const primary = ticketId ? `${ticketId}${ticketTitle ? `: ${ticketTitle}` : ""}` : "legacy run (ticket unavailable)";
|
|
345
|
+
const stateSessionId = typeof workflowState.sessionId === "string" ? workflowState.sessionId : undefined;
|
|
346
|
+
const sessionCandidateRef = buildRunSessionBinding(run, "builder", stateSessionId ?? run.builder?.sessionId);
|
|
347
|
+
const exactSessionRef = sessionAvailability?.status === "available" ? sessionAvailability.sessionRef ?? sessionCandidateRef : undefined;
|
|
300
348
|
return {
|
|
301
349
|
run, ticketId, ticketTitle,
|
|
302
350
|
compactLabel: `${primary} — ${run.status}`,
|
|
@@ -310,9 +358,33 @@ export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOv
|
|
|
310
358
|
nextAction,
|
|
311
359
|
worktree: recoveryWorktree,
|
|
312
360
|
branch: stateBranch ?? branchNow ?? run.repository.git.branch ?? run.repository.branch,
|
|
313
|
-
|
|
361
|
+
...(sessionCandidateRef ? { sessionCandidateRef } : {}),
|
|
362
|
+
...(sessionAvailability ? { sessionAvailability } : {}),
|
|
363
|
+
...(exactSessionRef ? { exactSessionRef, exactSessionId: exactSessionRef.sessionId } : {}),
|
|
314
364
|
};
|
|
315
365
|
}
|
|
366
|
+
export async function resolveBuildRecoveryProjection(projectDir, run, now = new Date(), ticketOverride, probe = {}) {
|
|
367
|
+
const frozen = projectBuildRecovery(projectDir, run, now, ticketOverride);
|
|
368
|
+
const candidate = frozen.sessionCandidateRef;
|
|
369
|
+
if (!candidate)
|
|
370
|
+
return frozen;
|
|
371
|
+
const availability = await resolveProviderSessionAvailability(candidate, {
|
|
372
|
+
...probe,
|
|
373
|
+
cwd: frozen.worktree,
|
|
374
|
+
configRoot: run.repository.root,
|
|
375
|
+
workspaceIdentity: run.branchMode === "current" ? safeCurrentIdentity(frozen.worktree) : captureWorkspaceIdentity(frozen.worktree),
|
|
376
|
+
now,
|
|
377
|
+
});
|
|
378
|
+
return projectBuildRecovery(projectDir, run, now, ticketOverride, availability);
|
|
379
|
+
}
|
|
380
|
+
function safeCurrentIdentity(worktree) {
|
|
381
|
+
try {
|
|
382
|
+
return captureCurrentWorkflowSessionIdentity(worktree);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
316
388
|
export function formatBuildRecoveryProjection(projection) {
|
|
317
389
|
const { run } = projection;
|
|
318
390
|
const lines = [
|
|
@@ -327,7 +399,7 @@ export function formatBuildRecoveryProjection(projection) {
|
|
|
327
399
|
`validation/QA ${projection.validation}`,
|
|
328
400
|
`branch ${projection.branch ?? "current"}; worktree ${projection.worktree}`,
|
|
329
401
|
`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"}`,
|
|
402
|
+
`session ${projection.exactSessionId ? "exact Builder session available (validated)" : projection.sessionCandidateRef ? `exact Builder session unavailable (${projection.sessionAvailability?.reason ?? "validation pending"})` : "fresh Builder session required"}`,
|
|
331
403
|
`runtime ${run.builder?.settings.make ?? "unavailable"}; model ${run.builder?.settings.model ?? "unavailable"}`,
|
|
332
404
|
`updated ${run.updatedAt}`,
|
|
333
405
|
`preserved expected in-progress paths ${projection.expectedChanges.join(", ") || "none currently dirty"}`,
|
|
@@ -402,13 +474,7 @@ function captureGitSnapshot(worktree, input) {
|
|
|
402
474
|
};
|
|
403
475
|
}
|
|
404
476
|
function worktreeIdentity(worktree) {
|
|
405
|
-
|
|
406
|
-
const stat = statSync(resolve(worktree));
|
|
407
|
-
return createHash("sha256").update(`${stat.dev}:${stat.ino}`).digest("hex");
|
|
408
|
-
}
|
|
409
|
-
catch {
|
|
410
|
-
return undefined;
|
|
411
|
-
}
|
|
477
|
+
return captureWorkspaceIdentity(worktree);
|
|
412
478
|
}
|
|
413
479
|
function gitValue(cwd, args) {
|
|
414
480
|
try {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { recoverableBuildRuns } from "../buildRuns.js";
|
|
4
|
+
import { HandoffService, writeHandoffInspection } from "../handoffs.js";
|
|
5
|
+
import { WorkflowDb } from "../workflowDb.js";
|
|
6
|
+
export function buildHandoffsCommand() {
|
|
7
|
+
const command = new Command("handoffs").description("Inspect and explicitly manage validated cumulative handoffs.");
|
|
8
|
+
command.command("inspect")
|
|
9
|
+
.requiredOption("--run <id>", "build run ID")
|
|
10
|
+
.option("--generation <n>", "handoff generation")
|
|
11
|
+
.option("--format <format>", "markdown or json", "markdown")
|
|
12
|
+
.option("--output <path>", "write inspection to a file")
|
|
13
|
+
.option("--project <dir>", "project directory", ".")
|
|
14
|
+
.action((opts) => {
|
|
15
|
+
const format = String(opts.format);
|
|
16
|
+
if (!["markdown", "json"].includes(format))
|
|
17
|
+
throw new Error("--format must be markdown or json");
|
|
18
|
+
const generation = opts.generation === undefined ? undefined : positiveInteger(opts.generation, "--generation");
|
|
19
|
+
const service = new HandoffService(resolve(String(opts.project)));
|
|
20
|
+
const value = service.inspect(String(opts.run), generation);
|
|
21
|
+
const content = format === "json"
|
|
22
|
+
? `${JSON.stringify({ version: 1, lineage: value.lineage, manifest: value.manifest }, null, 2)}\n`
|
|
23
|
+
: value.markdown;
|
|
24
|
+
if (opts.output) {
|
|
25
|
+
writeHandoffInspection(String(opts.output), content);
|
|
26
|
+
console.log(`rafi handoffs: wrote ${format} inspection to ${resolve(String(opts.output))}`);
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
|
|
30
|
+
});
|
|
31
|
+
command.command("prune-cache")
|
|
32
|
+
.requiredOption("--run <id>", "build run ID")
|
|
33
|
+
.option("--keep-latest <n>", "number of newest cache generations to keep", "1")
|
|
34
|
+
.option("--project <dir>", "project directory", ".")
|
|
35
|
+
.action((opts) => {
|
|
36
|
+
const keep = nonNegativeInteger(opts.keepLatest, "--keep-latest");
|
|
37
|
+
const removed = new HandoffService(resolve(String(opts.project))).pruneCache(String(opts.run), keep);
|
|
38
|
+
console.log(`rafi handoffs: pruned ${removed.length} disposable cache generation(s); durable history was preserved`);
|
|
39
|
+
});
|
|
40
|
+
command.command("delete-history")
|
|
41
|
+
.requiredOption("--run <id>", "build run ID")
|
|
42
|
+
.requiredOption("--yes", "confirm irreversible durable history deletion")
|
|
43
|
+
.option("--project <dir>", "project directory", ".")
|
|
44
|
+
.action((opts) => {
|
|
45
|
+
const projectDir = resolve(String(opts.project));
|
|
46
|
+
const runId = String(opts.run);
|
|
47
|
+
const recoverable = recoverableBuildRuns(projectDir).find((run) => run.runId === runId);
|
|
48
|
+
if (recoverable)
|
|
49
|
+
throw new Error(`run ${runId} is active or recoverable; finish or explicitly clean up the run before deleting durable handoff history`);
|
|
50
|
+
const db = new WorkflowDb(projectDir);
|
|
51
|
+
try {
|
|
52
|
+
const deleted = db.deleteHandoffHistory(runId);
|
|
53
|
+
console.log(`rafi handoffs: deleted ${deleted} durable handoff generation(s) for run ${runId}`);
|
|
54
|
+
console.log(`rafi handoffs: disposable copies remain until pruned; run: rafi handoffs prune-cache --run ${shellQuote(runId)} --keep-latest 1`);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
db.close();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return command;
|
|
61
|
+
}
|
|
62
|
+
function positiveInteger(value, label) {
|
|
63
|
+
const result = Number(value);
|
|
64
|
+
if (!Number.isSafeInteger(result) || result < 1)
|
|
65
|
+
throw new Error(`${label} must be a positive safe integer`);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
function nonNegativeInteger(value, label) {
|
|
69
|
+
const result = Number(value);
|
|
70
|
+
if (!Number.isSafeInteger(result) || result < 0)
|
|
71
|
+
throw new Error(`${label} must be a non-negative safe integer`);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
function shellQuote(value) { return `'${value.replace(/'/g, `'\\''`)}'`; }
|