ai-foreman 1.7.12 → 1.7.13
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 +15 -0
- package/dist/activity.d.ts +5 -1
- package/dist/activity.js +1 -1
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +133 -7
- package/dist/adapters/codex.d.ts +5 -0
- package/dist/adapters/codex.js +121 -11
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +11 -1
- package/dist/adapters/types.d.ts +47 -1
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +7 -1
- package/dist/branch/resume.js +11 -1
- package/dist/branch/runner.d.ts +11 -3
- package/dist/branch/runner.js +114 -60
- package/dist/buildRuns.d.ts +6 -2
- package/dist/buildRuns.js +76 -32
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +84 -1
- package/dist/cli/manager.d.ts +17 -0
- package/dist/cli/manager.js +135 -0
- package/dist/cli/recovery.d.ts +4 -0
- package/dist/cli/recovery.js +80 -0
- package/dist/cli/start.js +289 -53
- package/dist/cli/status.js +13 -2
- package/dist/cli/tickets.js +26 -8
- package/dist/config.d.ts +2 -0
- package/dist/config.js +23 -0
- package/dist/continuity.js +14 -9
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.js +428 -0
- package/dist/foreman.d.ts +16 -6
- package/dist/foreman.js +70 -82
- package/dist/handoffs.d.ts +5 -1
- package/dist/handoffs.js +4 -1
- package/dist/index.js +6 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.js +4 -1
- package/dist/managerPacket.d.ts +25 -0
- package/dist/managerPacket.js +83 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +4 -1
- package/dist/observability.d.ts +248 -0
- package/dist/observability.js +1018 -0
- package/dist/planChanges.d.ts +4 -0
- package/dist/planChanges.js +16 -0
- package/dist/projectDiagnostics.d.ts +27 -0
- package/dist/projectDiagnostics.js +407 -0
- package/dist/qaRecovery.d.ts +140 -0
- package/dist/qaRecovery.js +595 -0
- package/dist/qaReview.d.ts +128 -14
- package/dist/qaReview.js +974 -57
- package/dist/qaSnapshot.d.ts +31 -5
- package/dist/qaSnapshot.js +209 -134
- package/dist/recoveryPolicy.d.ts +37 -0
- package/dist/recoveryPolicy.js +251 -0
- package/dist/roles.js +1 -0
- package/dist/supervisor.d.ts +40 -0
- package/dist/supervisor.js +102 -0
- package/dist/ticketHistoryReader.d.ts +19 -0
- package/dist/ticketHistoryReader.js +58 -0
- package/dist/tickets/details.d.ts +8 -0
- package/dist/tickets/details.js +59 -11
- package/dist/tickets/output.d.ts +5 -0
- package/dist/tickets/output.js +73 -0
- package/dist/tickets/stateDb.d.ts +1 -0
- package/dist/tickets/stateDb.js +5 -0
- package/dist/workflowDb.d.ts +31 -2
- package/dist/workflowDb.js +192 -23
- package/dist/workflowReader.d.ts +42 -0
- package/dist/workflowReader.js +171 -0
- package/foreman.yaml +9 -0
- package/package.json +51 -3
package/dist/adapters/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Agent-agnostic interface over a coding agent ("builder").
|
|
3
3
|
*/
|
|
4
4
|
import type { ConfigurableAgentRole, ProviderSessionRefV1, RuntimeProbeCategory, RuntimeProbePhase, SessionAvailabilityV1 } from "rafi-spec";
|
|
5
|
+
import type { RunObserver } from "../observability.js";
|
|
5
6
|
/** A permission request surfaced by a builder before it runs a tool. */
|
|
6
7
|
export interface PermissionRequest {
|
|
7
8
|
toolName: string;
|
|
@@ -58,16 +59,53 @@ export interface TurnResult {
|
|
|
58
59
|
costAuthoritative?: boolean;
|
|
59
60
|
inputTokens?: number;
|
|
60
61
|
outputTokens?: number;
|
|
62
|
+
/** Explicit counter semantics. Compatibility counters above must not be summed without this scope. */
|
|
63
|
+
usage?: {
|
|
64
|
+
scope: "turn-delta" | "session-cumulative";
|
|
65
|
+
inputTokens?: number;
|
|
66
|
+
outputTokens?: number;
|
|
67
|
+
totalTokens?: number;
|
|
68
|
+
costUsd?: number;
|
|
69
|
+
};
|
|
61
70
|
failure?: RuntimeFailure;
|
|
71
|
+
/** Stable host/provider correlation ID for this turn. */
|
|
72
|
+
turnId?: string;
|
|
73
|
+
/** Exact instruction requested by the immediate host caller. */
|
|
74
|
+
hostInstruction?: string;
|
|
75
|
+
/** Exact instruction dispatched to the provider after all wrappers. */
|
|
76
|
+
providerInstruction?: string;
|
|
77
|
+
/** Exact provider response before continuity or contract cleanup. */
|
|
78
|
+
rawResponse?: string;
|
|
79
|
+
/** Effective response consumed by the host contract parser. */
|
|
80
|
+
cleanedResponse?: string;
|
|
81
|
+
providerMetadata?: {
|
|
82
|
+
provider: "claude" | "codex";
|
|
83
|
+
sessionId?: string;
|
|
84
|
+
sessionRef?: ProviderSessionRefV1;
|
|
85
|
+
};
|
|
62
86
|
}
|
|
63
87
|
/** Observability events emitted while a builder works. */
|
|
64
|
-
export type BuilderEvent = {
|
|
88
|
+
export type BuilderEvent = ({
|
|
65
89
|
kind: "text";
|
|
66
90
|
text: string;
|
|
91
|
+
byteCount?: number;
|
|
92
|
+
digest?: string;
|
|
67
93
|
} | {
|
|
68
94
|
kind: "tool";
|
|
69
95
|
name: string;
|
|
70
96
|
input: unknown;
|
|
97
|
+
inputCompleteness?: "complete" | "truncated" | "unavailable";
|
|
98
|
+
lifecycle?: "started" | "progress" | "completed";
|
|
99
|
+
callId?: string;
|
|
100
|
+
status?: string;
|
|
101
|
+
durationMs?: number;
|
|
102
|
+
exitCode?: number;
|
|
103
|
+
outputSummary?: string;
|
|
104
|
+
outputDigest?: string;
|
|
105
|
+
outputCompleteness?: "complete" | "truncated" | "unavailable";
|
|
106
|
+
rawOutputBytes?: number;
|
|
107
|
+
completionKnown?: boolean;
|
|
108
|
+
providerTurnId?: string;
|
|
71
109
|
} | {
|
|
72
110
|
kind: "activity";
|
|
73
111
|
state: string;
|
|
@@ -83,9 +121,12 @@ export type BuilderEvent = {
|
|
|
83
121
|
maximum?: number;
|
|
84
122
|
delayMs?: number;
|
|
85
123
|
managedBy: "provider" | "rafi";
|
|
124
|
+
retryId?: string;
|
|
125
|
+
providerTurnId?: string;
|
|
86
126
|
} | {
|
|
87
127
|
kind: "turn-complete";
|
|
88
128
|
result: TurnResult;
|
|
129
|
+
turnId?: string;
|
|
89
130
|
} | {
|
|
90
131
|
kind: "session-transition";
|
|
91
132
|
transition: "started" | "resumed" | "compacting" | "compacted" | "fresh-fallback";
|
|
@@ -100,6 +141,9 @@ export type BuilderEvent = {
|
|
|
100
141
|
} | {
|
|
101
142
|
kind: "error";
|
|
102
143
|
message: string;
|
|
144
|
+
}) & {
|
|
145
|
+
eventId?: string;
|
|
146
|
+
observedAt?: string;
|
|
103
147
|
};
|
|
104
148
|
export interface ContextUsage {
|
|
105
149
|
used: number;
|
|
@@ -185,6 +229,8 @@ export interface BuilderAdapterOptions {
|
|
|
185
229
|
skills?: string[];
|
|
186
230
|
/** Provider-native context ceiling, as a percentage of that provider's model window. */
|
|
187
231
|
autoCompactThresholdPercent?: number;
|
|
232
|
+
/** Raw-adapter observability; wrapper adapters must not persist re-emitted events. */
|
|
233
|
+
observer?: RunObserver;
|
|
188
234
|
}
|
|
189
235
|
export interface BuilderAdapter {
|
|
190
236
|
readonly agent: "claude" | "codex";
|
package/dist/agentRun.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface RoleBuilderOptions {
|
|
|
22
22
|
permissionConfig?: PermissionConfig;
|
|
23
23
|
extraSkills?: string[];
|
|
24
24
|
sandboxMode?: BuilderAdapterOptions["sandboxMode"];
|
|
25
|
+
/** Disable recovery-session persistence for isolated, non-recoverable roles such as Manager. */
|
|
26
|
+
persistSessionBindings?: boolean;
|
|
25
27
|
resumeSessionId?: string;
|
|
26
28
|
/** Location-scoped exact session. Raw IDs are resolved through durable project bindings or treated as legacy candidates. */
|
|
27
29
|
resumeSessionRef?: ProviderSessionRefV1;
|
package/dist/agentRun.js
CHANGED
|
@@ -199,6 +199,8 @@ export async function createRoleBuilder(opts) {
|
|
|
199
199
|
};
|
|
200
200
|
const initial = await makeAdapter(runtime, opts.resumeSessionId, requestedResumeRef);
|
|
201
201
|
const persistSessionRef = (ref) => {
|
|
202
|
+
if (opts.persistSessionBindings === false)
|
|
203
|
+
return;
|
|
202
204
|
const bindingDb = new WorkflowDb(opts.projectDir);
|
|
203
205
|
try {
|
|
204
206
|
bindingDb.recordProviderSessionBinding(ref);
|
|
@@ -240,6 +242,8 @@ function phaseForRole(role) {
|
|
|
240
242
|
return "qa";
|
|
241
243
|
if (role === "uninstaller")
|
|
242
244
|
return "uninstaller";
|
|
245
|
+
if (role === "manager")
|
|
246
|
+
return "manager";
|
|
243
247
|
return "builder";
|
|
244
248
|
}
|
|
245
249
|
export function readRoleDefaultsForExecution(projectDir, role) {
|
|
@@ -253,6 +257,8 @@ export function readRoleDefaultsForExecution(projectDir, role) {
|
|
|
253
257
|
: undefined;
|
|
254
258
|
if (!saved)
|
|
255
259
|
return undefined;
|
|
260
|
+
if (role === "manager")
|
|
261
|
+
return saved;
|
|
256
262
|
if (isFullyInitializedTracker(projectDir))
|
|
257
263
|
return saved;
|
|
258
264
|
const { make: _pendingMake, ...active } = saved;
|
|
@@ -333,7 +339,7 @@ export async function runRoleInstruction(opts) {
|
|
|
333
339
|
}
|
|
334
340
|
}
|
|
335
341
|
function configurableRole(role) {
|
|
336
|
-
return ["builder", "qa", "planner", "ticket-maker", "uninstaller"].includes(role)
|
|
342
|
+
return ["builder", "qa", "planner", "ticket-maker", "uninstaller", "manager"].includes(role)
|
|
337
343
|
? role
|
|
338
344
|
: "builder";
|
|
339
345
|
}
|
package/dist/branch/resume.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { WorkflowReader } from "../workflowReader.js";
|
|
3
4
|
export function findResumableBranchSessions(foremanDir) {
|
|
4
5
|
if (!existsSync(foremanDir))
|
|
5
6
|
return [];
|
|
7
|
+
const reader = new WorkflowReader(dirname(foremanDir));
|
|
8
|
+
try {
|
|
9
|
+
const structured = reader.branchResumeSessions().filter(session => existsSync(session.worktreePath));
|
|
10
|
+
if (structured.length)
|
|
11
|
+
return structured.sort((a, b) => a.ticket.localeCompare(b.ticket, undefined, { numeric: true }));
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
reader.close();
|
|
15
|
+
}
|
|
6
16
|
const byTicket = new Map();
|
|
7
17
|
const logs = readdirSync(foremanDir)
|
|
8
18
|
.filter((file) => file.endsWith(".jsonl"))
|
package/dist/branch/runner.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { BuilderAdapter, EffortLevel } from "../adapters/types.js";
|
|
2
|
+
import type { RunObserver } from "../observability.js";
|
|
2
3
|
import type { ProviderSessionRefV1, SessionStrategy } from "rafi-spec";
|
|
3
|
-
import { type QaNonconvergenceContext, type QaNonconvergenceDecision } from "../qaReview.js";
|
|
4
|
+
import { type QaNonconvergenceContext, type QaNonconvergenceDecision, type QaReportRecoveryHandler, type QaSessionBoundaryRecovery, type QaSessionHandle } from "../qaReview.js";
|
|
4
5
|
import type { Log } from "../log.js";
|
|
5
6
|
import type { BranchPlan, BranchPlanNode, BranchRunSummary, CompletionMode, MergeMethod, ReviewProvider } from "./types.js";
|
|
6
7
|
import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
7
8
|
import { SessionUnavailableContinuityError } from "../continuity.js";
|
|
9
|
+
import type { QaRecoveryPacket } from "../qaRecovery.js";
|
|
8
10
|
export interface DeliveryUnitSession {
|
|
9
11
|
unitId: string;
|
|
10
12
|
branch: string;
|
|
@@ -27,6 +29,7 @@ export interface BranchRunnerOptions {
|
|
|
27
29
|
notificationsEnabled: boolean;
|
|
28
30
|
terminalBellEnabled?: boolean;
|
|
29
31
|
qaEnabled: boolean;
|
|
32
|
+
qaMaxFixAttempts?: number;
|
|
30
33
|
createPr: boolean;
|
|
31
34
|
completionMode?: CompletionMode;
|
|
32
35
|
reviewProvider?: ReviewProvider;
|
|
@@ -53,7 +56,7 @@ export interface BranchRunnerOptions {
|
|
|
53
56
|
recordQaSession?: (session: string | ProviderSessionRefV1, ticketId: string, worktreePath: string) => void;
|
|
54
57
|
/** Notify the host and stop the branch plan without discarding its worktree. */
|
|
55
58
|
onSessionUnavailable?: (error: SessionUnavailableError | SessionUnavailableContinuityError) => void;
|
|
56
|
-
createQa?: (cwd: string, sessionId?: string) => Promise<BuilderAdapter>;
|
|
59
|
+
createQa?: (cwd: string, sessionId?: string) => Promise<BuilderAdapter | QaSessionHandle>;
|
|
57
60
|
builderSessionStrategy?: SessionStrategy;
|
|
58
61
|
qaSessionStrategy?: SessionStrategy;
|
|
59
62
|
observeBuilder?: (builder: BuilderAdapter) => Promise<void>;
|
|
@@ -62,7 +65,12 @@ export interface BranchRunnerOptions {
|
|
|
62
65
|
qaNonconvergence?: (context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>;
|
|
63
66
|
beforeBuilderTurn?: (adapter: BuilderAdapter, frozenAction: string, cwd: string) => Promise<BuilderAdapter>;
|
|
64
67
|
builderSessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>;
|
|
65
|
-
qaSessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>;
|
|
68
|
+
qaSessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string, recovery?: QaSessionBoundaryRecovery) => Promise<BuilderAdapter>;
|
|
69
|
+
observer?: RunObserver;
|
|
70
|
+
qaRuntimeContext?: unknown;
|
|
71
|
+
qaContinuityManaged?: boolean;
|
|
72
|
+
qaReportRecovery?: QaReportRecoveryHandler;
|
|
73
|
+
qaResumedRecovery?: QaRecoveryPacket;
|
|
66
74
|
}
|
|
67
75
|
export declare function runBranchPlan(opts: BranchRunnerOptions): Promise<BranchRunSummary[]>;
|
|
68
76
|
export declare function buildBranchTicketInstruction(node: BranchPlanNode, trackerPaths?: {
|
package/dist/branch/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildDurableQaFixHandoff, compactWithRetry, runIsolatedQa } from "../qaReview.js";
|
|
2
|
-
import { changeManifestAsync } from "../qaSnapshot.js";
|
|
2
|
+
import { changeManifestAsync, deterministicChangeSummaryAsync } from "../qaSnapshot.js";
|
|
3
3
|
import { Foreman, MARKER_SPEC, parseStepStatus } from "../foreman.js";
|
|
4
4
|
import { fireNotification } from "../notify.js";
|
|
5
5
|
import { cmdBlock, cmdComplete, cmdUnblock, cmdUpdate } from "../tickets/commands.js";
|
|
@@ -27,6 +27,7 @@ function deliverySessionPath(projectDir, unitId) {
|
|
|
27
27
|
return join(projectDir, ".foreman", "delivery-sessions", `${unitId}.json`);
|
|
28
28
|
}
|
|
29
29
|
export async function runBranchPlan(opts) {
|
|
30
|
+
let qaResumedRecovery = opts.qaResumedRecovery;
|
|
30
31
|
const baseWorktreePolicy = opts.baseWorktreePolicy ?? "enforce";
|
|
31
32
|
if (baseWorktreePolicy !== "skip") {
|
|
32
33
|
try {
|
|
@@ -128,7 +129,7 @@ export async function runBranchPlan(opts) {
|
|
|
128
129
|
const resumeSession = opts.resumeSessions?.get(node.ticket.id);
|
|
129
130
|
const worktreePath = resumeSession?.worktreePath
|
|
130
131
|
?? (sharedUnit ? findWorktreeForBranch(opts.projectDir, node.branch) : undefined)
|
|
131
|
-
?? createTicketWorktree(opts.projectDir, opts.runId, node.branch, node.baseBranch);
|
|
132
|
+
?? await observeNode(opts, node, "git", "creating ticket worktree", () => createTicketWorktree(opts.projectDir, opts.runId, node.branch, node.baseBranch));
|
|
132
133
|
node.worktreePath = worktreePath;
|
|
133
134
|
let builder;
|
|
134
135
|
let viewer;
|
|
@@ -178,7 +179,10 @@ export async function runBranchPlan(opts) {
|
|
|
178
179
|
builderWorkSessions += 1;
|
|
179
180
|
viewer = opts.observeBuilder?.(builder);
|
|
180
181
|
const foreman = new Foreman(builder, opts.log, { desktop: opts.notificationsEnabled, terminalBell: opts.terminalBellEnabled ?? true }, opts.qaEnabled, 3, undefined, undefined, undefined, opts.qaSessionStrategy ?? "compact", undefined, opts.builderSessionStrategy ?? "compact", undefined, opts.beforeBuilderTurn ? (adapter, action) => opts.beforeBuilderTurn(adapter, action, worktreePath) : undefined, undefined, undefined, undefined, async (adapter) => opts.observeBuilderNativeCompactions?.(adapter, worktreePath));
|
|
181
|
-
const
|
|
182
|
+
const turn = () => foreman.runInstruction(ticketInstruction);
|
|
183
|
+
const { result, status } = opts.observer
|
|
184
|
+
? await opts.observer.withContext({ role: "builder", stream: "builder", ticketId: node.ticket.id, deliveryUnitId: node.deliveryUnitId }, turn)
|
|
185
|
+
: await turn();
|
|
182
186
|
builder = foreman.builderAdapter();
|
|
183
187
|
workflowCheckpoint(opts.projectDir, opts.runId, "builder-after", node.ticket.id, { status: status.kind, sessionId: builder.sessionId(), worktree: worktreePath });
|
|
184
188
|
const sessionId = builder.sessionId();
|
|
@@ -190,7 +194,7 @@ export async function runBranchPlan(opts) {
|
|
|
190
194
|
workflowCheckpoint(opts.projectDir, opts.runId, "builder-session-scoped", node.ticket.id, { sessionId, sessionRef, worktree: worktreePath, branch: node.branch });
|
|
191
195
|
}
|
|
192
196
|
if (sessionId) {
|
|
193
|
-
|
|
197
|
+
const resumeSession = {
|
|
194
198
|
ticket: node.ticket.id,
|
|
195
199
|
branch: node.branch,
|
|
196
200
|
base: node.baseBranch,
|
|
@@ -209,7 +213,17 @@ export async function runBranchPlan(opts) {
|
|
|
209
213
|
keepWorktrees: opts.keepWorktrees,
|
|
210
214
|
deliveryUnitId: node.deliveryUnitId,
|
|
211
215
|
deliveryUnitFinal: node.deliveryUnitFinal,
|
|
212
|
-
|
|
216
|
+
logPath: "structured-recovery",
|
|
217
|
+
ts: new Date().toISOString(),
|
|
218
|
+
};
|
|
219
|
+
opts.log.write("branch-session", resumeSession);
|
|
220
|
+
const resumeDb = new WorkflowDb(opts.projectDir);
|
|
221
|
+
try {
|
|
222
|
+
resumeDb.recordBranchResumeSession(opts.runId, resumeSession);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
resumeDb.close();
|
|
226
|
+
}
|
|
213
227
|
if (node.deliveryUnitId) {
|
|
214
228
|
const path = deliverySessionPath(opts.projectDir, node.deliveryUnitId);
|
|
215
229
|
mkdirSync(join(opts.projectDir, ".foreman", "delivery-sessions"), { recursive: true });
|
|
@@ -243,18 +257,25 @@ export async function runBranchPlan(opts) {
|
|
|
243
257
|
throw new Error("independent disposable QA factory is required when QA is enabled");
|
|
244
258
|
workflowCheckpoint(opts.projectDir, opts.runId, "qa-before", node.ticket.id, { worktree: worktreePath });
|
|
245
259
|
const qa = await runIsolatedQa({
|
|
246
|
-
ticket: node.ticket, builderWorktree: worktreePath, builderSummary:
|
|
247
|
-
qaStrategy: opts.qaSessionStrategy ?? "compact", state: qaStream, createQa: opts.createQa, maxCycles: 3,
|
|
260
|
+
ticket: node.ticket, builderWorktree: worktreePath, builderSummary: result.text,
|
|
261
|
+
qaStrategy: opts.qaSessionStrategy ?? "compact", state: qaStream, createQa: opts.createQa, maxCycles: opts.qaMaxFixAttempts ?? 3,
|
|
262
|
+
recovery: { projectDir: opts.projectDir, runId: opts.runId },
|
|
263
|
+
observer: opts.observer,
|
|
264
|
+
qaRuntimeContext: opts.qaRuntimeContext,
|
|
265
|
+
continuityManaged: opts.qaContinuityManaged,
|
|
266
|
+
onReportRecovery: opts.qaReportRecovery,
|
|
267
|
+
resumedRecovery: qaResumedRecovery,
|
|
248
268
|
sessionBoundary: opts.qaSessionBoundary,
|
|
249
269
|
observeNativeCompactions: opts.observeQaNativeCompactions,
|
|
250
|
-
resolveBlocked: (adapter, reason) => foreman.resolveBlocker(adapter, reason),
|
|
270
|
+
resolveBlocked: (adapter, reason) => foreman.resolveBlocker(adapter, reason, "qa"),
|
|
251
271
|
evidence: (entry) => opts.log.write("qa-evidence", { ticket: node.ticket.id, ...entry }),
|
|
252
|
-
fix: async (
|
|
272
|
+
fix: async (request) => {
|
|
253
273
|
if (!builder)
|
|
254
274
|
return { ok: false, detail: "Builder session unavailable" };
|
|
255
275
|
builderWorkSessions += 1;
|
|
256
276
|
const digest = (await withActivityPhase("recording QA fix changes", () => changeManifestAsync(worktreePath, (state, detail) => currentActivity()?.update(state, detail), "recording QA fix changes"))).diffDigest;
|
|
257
|
-
const
|
|
277
|
+
const changeSummary = await deterministicChangeSummaryAsync(worktreePath);
|
|
278
|
+
const fixInstruction = buildDurableQaFixHandoff(node.ticket, request, worktreePath, request.latestBuilderResult, digest, changeSummary);
|
|
258
279
|
const strategy = opts.builderSessionStrategy ?? "compact";
|
|
259
280
|
if (opts.builderSessionBoundary) {
|
|
260
281
|
builder = await opts.builderSessionBoundary(builder, fixInstruction, strategy, worktreePath);
|
|
@@ -287,10 +308,11 @@ export async function runBranchPlan(opts) {
|
|
|
287
308
|
opts.recordBuilderSession?.(fixedSessionRef ?? fixedSessionId, node.ticket.id, worktreePath);
|
|
288
309
|
workflowCheckpoint(opts.projectDir, opts.runId, "builder-session-scoped", node.ticket.id, { sessionId: fixedSessionId, sessionRef: fixedSessionRef, worktree: worktreePath, branch: node.branch });
|
|
289
310
|
}
|
|
290
|
-
return { ok: !fixed.isError && fixedStatus.kind === "done", detail: fixedStatus.error
|
|
311
|
+
return { ok: !fixed.isError && fixedStatus.kind === "done", detail: fixedStatus.error, response: fixed.text, summary: fixedStatus.summary ?? fixed.text };
|
|
291
312
|
},
|
|
292
313
|
onNonconvergence: opts.qaNonconvergence,
|
|
293
314
|
});
|
|
315
|
+
qaResumedRecovery = undefined;
|
|
294
316
|
if (qaStream.sessionId)
|
|
295
317
|
opts.recordQaSession?.(qaStream.sessionRef ?? qaStream.sessionId, node.ticket.id, worktreePath);
|
|
296
318
|
workflowCheckpoint(opts.projectDir, opts.runId, "qa-after", node.ticket.id, { outcome: qa.outcome, detail: qa.detail });
|
|
@@ -326,7 +348,7 @@ export async function runBranchPlan(opts) {
|
|
|
326
348
|
const operation = `${opts.runId}:commit:${node.ticket.id}`;
|
|
327
349
|
planJournal(opts.projectDir, opts.runId, operation, "commit", { ticket: node.ticket.id, branch: node.branch, worktreePath });
|
|
328
350
|
try {
|
|
329
|
-
commit = commitAll(worktreePath, `${node.ticket.id}: ${node.ticket.title}`);
|
|
351
|
+
commit = await observeNode(opts, node, "git", "committing ticket changes", () => commitAll(worktreePath, `${node.ticket.id}: ${node.ticket.title}`));
|
|
330
352
|
confirmJournal(opts.projectDir, operation, commit, { sha: commit });
|
|
331
353
|
workflowCheckpoint(opts.projectDir, opts.runId, "commit-after", node.ticket.id, { sha: commit });
|
|
332
354
|
}
|
|
@@ -344,12 +366,12 @@ export async function runBranchPlan(opts) {
|
|
|
344
366
|
workflowCheckpoint(opts.projectDir, opts.runId, "push-before", node.ticket.id, { branch: node.branch, sha: commit });
|
|
345
367
|
const pushOperation = `${opts.runId}:push:${node.branch}`;
|
|
346
368
|
planJournal(opts.projectDir, opts.runId, pushOperation, "push", { branch: node.branch, sha: commit, remote: "origin" });
|
|
347
|
-
const push = await withActivityPhase(`pushing ${node.branch}`, async () => {
|
|
369
|
+
const push = await observeNode(opts, node, "git", `pushing ${node.branch}`, () => withActivityPhase(`pushing ${node.branch}`, async () => {
|
|
348
370
|
currentActivity()?.update(`pushing ${node.branch}`, `publishing ${node.ticket.id}`);
|
|
349
371
|
return reviewProvider === "gitlab"
|
|
350
372
|
? pushBranchForMr(worktreePath, node.branch)
|
|
351
373
|
: pushBranchForPr(worktreePath, node.branch);
|
|
352
|
-
});
|
|
374
|
+
}));
|
|
353
375
|
if (push.ok) {
|
|
354
376
|
confirmJournal(opts.projectDir, pushOperation, node.branch, { sha: commit });
|
|
355
377
|
opts.log.write("branch-push", { ticket: node.ticket.id, branch: node.branch, status: "pushed" });
|
|
@@ -386,7 +408,7 @@ export async function runBranchPlan(opts) {
|
|
|
386
408
|
workflowCheckpoint(opts.projectDir, opts.runId, "review-creation-before", node.ticket.id, { provider: reviewProvider, head: node.branch, base: node.baseBranch });
|
|
387
409
|
const reviewOperation = `${opts.runId}:${reviewProvider === "gitlab" ? "mr" : "pr"}:${node.branch}:${node.baseBranch}`;
|
|
388
410
|
planJournal(opts.projectDir, opts.runId, reviewOperation, reviewProvider === "gitlab" ? "mr-create" : "pr-create", { head: node.branch, base: node.baseBranch, sha: commit });
|
|
389
|
-
const pr = await withActivityPhase(`creating ${reviewProvider === "gitlab" ? "merge request" : "pull request"}`, async () => {
|
|
411
|
+
const pr = await observeNode(opts, node, "external_check", `creating ${reviewProvider === "gitlab" ? "merge request" : "pull request"}`, () => withActivityPhase(`creating ${reviewProvider === "gitlab" ? "merge request" : "pull request"}`, async () => {
|
|
390
412
|
currentActivity()?.update(`creating ${reviewProvider === "gitlab" ? "merge request" : "pull request"}`, node.ticket.id);
|
|
391
413
|
return reviewProvider === "gitlab"
|
|
392
414
|
? createOrReuseMr(opts.projectDir, {
|
|
@@ -406,7 +428,7 @@ export async function runBranchPlan(opts) {
|
|
|
406
428
|
qaEvidence: qaSummary,
|
|
407
429
|
commit,
|
|
408
430
|
});
|
|
409
|
-
});
|
|
431
|
+
}));
|
|
410
432
|
summary.pr = pr;
|
|
411
433
|
if (pr.status === "created" || pr.status === "existing") {
|
|
412
434
|
confirmJournal(opts.projectDir, reviewOperation, pr.url, { head: node.branch, base: node.baseBranch, url: pr.url });
|
|
@@ -425,10 +447,10 @@ export async function runBranchPlan(opts) {
|
|
|
425
447
|
continue;
|
|
426
448
|
}
|
|
427
449
|
if (completionMode === "auto-merge" && reviewProvider === "github") {
|
|
428
|
-
const autoMerge = await withActivityPhase("enabling GitHub auto-merge", async () => {
|
|
450
|
+
const autoMerge = await observeNode(opts, node, "external_check", "enabling GitHub auto-merge", () => withActivityPhase("enabling GitHub auto-merge", async () => {
|
|
429
451
|
currentActivity()?.update("enabling GitHub auto-merge", node.branch);
|
|
430
452
|
return enableGitHubAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
431
|
-
});
|
|
453
|
+
}));
|
|
432
454
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
433
455
|
if (autoMerge.status === "failed") {
|
|
434
456
|
opts.log.write("pr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -439,10 +461,10 @@ export async function runBranchPlan(opts) {
|
|
|
439
461
|
opts.log.write("pr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
|
|
440
462
|
}
|
|
441
463
|
else if (completionMode === "auto-merge" && reviewProvider === "gitlab") {
|
|
442
|
-
const autoMerge = await withActivityPhase("enabling GitLab auto-merge", async () => {
|
|
464
|
+
const autoMerge = await observeNode(opts, node, "external_check", "enabling GitLab auto-merge", () => withActivityPhase("enabling GitLab auto-merge", async () => {
|
|
443
465
|
currentActivity()?.update("enabling GitLab auto-merge", node.branch);
|
|
444
466
|
return enableGitLabAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
445
|
-
});
|
|
467
|
+
}));
|
|
446
468
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
447
469
|
if (autoMerge.status === "failed") {
|
|
448
470
|
opts.log.write("mr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -460,7 +482,7 @@ export async function runBranchPlan(opts) {
|
|
|
460
482
|
else if (commit && completionMode === "direct-merge" && completesSharedUnit) {
|
|
461
483
|
if (!opts.keepWorktrees)
|
|
462
484
|
removeTicketWorktree(opts.projectDir, worktreePath);
|
|
463
|
-
const mergeCommit = mergeBranchToLocalBase(opts.projectDir, node.branch, node.baseBranch, `${node.ticket.id}: ${node.ticket.title}`, opts.mergeMethod ?? "squash");
|
|
485
|
+
const mergeCommit = await observeNode(opts, node, "git", "merging ticket branch to local base", () => mergeBranchToLocalBase(opts.projectDir, node.branch, node.baseBranch, `${node.ticket.id}: ${node.ticket.title}`, opts.mergeMethod ?? "squash"));
|
|
464
486
|
summary.pr = { status: "merged", url: mergeCommit };
|
|
465
487
|
opts.log.write("branch-direct-merge", {
|
|
466
488
|
ticket: node.ticket.id,
|
|
@@ -492,10 +514,17 @@ export async function runBranchPlan(opts) {
|
|
|
492
514
|
status: "done",
|
|
493
515
|
detail: summary.detail,
|
|
494
516
|
});
|
|
517
|
+
const resumeDb = new WorkflowDb(opts.projectDir);
|
|
518
|
+
try {
|
|
519
|
+
resumeDb.completeBranchResumeSession(opts.runId, node.ticket.id);
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
resumeDb.close();
|
|
523
|
+
}
|
|
495
524
|
if (node.deliveryUnitId && node.deliveryUnitFinal)
|
|
496
525
|
rmSync(deliverySessionPath(opts.projectDir, node.deliveryUnitId), { force: true });
|
|
497
526
|
if (!opts.keepWorktrees && completionMode !== "direct-merge" && completesSharedUnit)
|
|
498
|
-
removeTicketWorktree(opts.projectDir, worktreePath);
|
|
527
|
+
await observeNode(opts, node, "cleanup", "removing ticket worktree", () => removeTicketWorktree(opts.projectDir, worktreePath));
|
|
499
528
|
}
|
|
500
529
|
catch (err) {
|
|
501
530
|
if (err instanceof SessionUnavailableError || err instanceof SessionUnavailableContinuityError) {
|
|
@@ -591,49 +620,69 @@ async function waitForAutoMergeDependenciesInternal(opts, node, provider) {
|
|
|
591
620
|
? null
|
|
592
621
|
: opts.autoMergeTimeoutMinutes * 60_000;
|
|
593
622
|
const deadline = opts.autoMergeWait && timeoutMs !== null ? Date.now() + timeoutMs : null;
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
623
|
+
let waitSpanId;
|
|
624
|
+
let waitOutcome = "completed";
|
|
625
|
+
let waitLogged = false;
|
|
626
|
+
try {
|
|
627
|
+
while (true) {
|
|
628
|
+
const pending = [];
|
|
629
|
+
for (const dep of dependencyNodes) {
|
|
630
|
+
const status = provider === "gitlab"
|
|
631
|
+
? checkGitLabMrMerged(opts.projectDir, dep.branch)
|
|
632
|
+
: checkGitHubPrMerged(opts.projectDir, dep.branch);
|
|
633
|
+
if (!status.ok) {
|
|
634
|
+
waitOutcome = "failed";
|
|
635
|
+
return {
|
|
636
|
+
ok: false,
|
|
637
|
+
code: status.code,
|
|
638
|
+
message: `dependency ${dep.ticket.id} merge check failed: ${status.message}`,
|
|
639
|
+
repairCommands: status.repairCommands,
|
|
640
|
+
command: status.command,
|
|
641
|
+
output: status.output,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (!status.merged)
|
|
645
|
+
pending.push(`${dep.ticket.id}${status.state ? ` (${status.state})` : ""}`);
|
|
646
|
+
}
|
|
647
|
+
if (pending.length === 0)
|
|
648
|
+
return { ok: true };
|
|
649
|
+
const message = `auto-merge dependency ${pending.join(", ")} has not merged into the root base yet`;
|
|
650
|
+
currentActivity()?.update("waiting for dependency merge", pending.join(", "));
|
|
651
|
+
if (!opts.autoMergeWait) {
|
|
601
652
|
return {
|
|
602
653
|
ok: false,
|
|
603
|
-
code:
|
|
604
|
-
message:
|
|
605
|
-
repairCommands: status.repairCommands,
|
|
606
|
-
command: status.command,
|
|
607
|
-
output: status.output,
|
|
654
|
+
code: "dependency_unavailable",
|
|
655
|
+
message: `${message}; rerun after it merges or enable auto-merge wait in ticket setup`,
|
|
608
656
|
};
|
|
609
657
|
}
|
|
610
|
-
if (!
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
658
|
+
if (!waitSpanId && opts.observer) {
|
|
659
|
+
waitSpanId = opts.observer.store.startSpan({ runId: opts.runId, executionId: opts.observer.executionId, role: "host", stream: "dependency", ticketId: node.ticket.id }, { kind: "dependency_wait", name: "waiting for dependency merge", attributes: { dependencies: pending } });
|
|
660
|
+
opts.observer.store.updateCurrentState({ runId: opts.runId, role: "host", stream: "dependency", executionId: opts.observer.executionId, ticketId: node.ticket.id, phase: "waiting for dependency merge", activeSpanId: waitSpanId, activeSpanKind: "dependency_wait", lastSemanticProgressAt: new Date().toISOString() });
|
|
661
|
+
}
|
|
662
|
+
if (!waitLogged) {
|
|
663
|
+
opts.log.write("branch-auto-merge-wait", {
|
|
664
|
+
ticket: node.ticket.id,
|
|
665
|
+
pending,
|
|
666
|
+
timeoutMinutes: opts.autoMergeTimeoutMinutes ?? null,
|
|
667
|
+
});
|
|
668
|
+
waitLogged = true;
|
|
669
|
+
}
|
|
670
|
+
if (deadline !== null && Date.now() >= deadline) {
|
|
671
|
+
waitOutcome = "timed_out";
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
code: "dependency_unavailable",
|
|
675
|
+
message: `${message}; timed out waiting for dependency merge`,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
await sleep(autoMergePollMs());
|
|
630
679
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
680
|
+
}
|
|
681
|
+
finally {
|
|
682
|
+
if (waitSpanId && opts.observer)
|
|
683
|
+
opts.observer.store.finishSpan(waitSpanId, { outcome: waitOutcome });
|
|
684
|
+
if (waitLogged)
|
|
685
|
+
opts.log.write("branch-auto-merge-wait", { ticket: node.ticket.id, outcome: waitOutcome, terminal: true });
|
|
637
686
|
}
|
|
638
687
|
}
|
|
639
688
|
function autoMergePollMs() {
|
|
@@ -646,6 +695,11 @@ function autoMergePollMs() {
|
|
|
646
695
|
function sleep(ms) {
|
|
647
696
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
648
697
|
}
|
|
698
|
+
function observeNode(opts, node, kind, name, operation) {
|
|
699
|
+
if (!opts.observer)
|
|
700
|
+
return Promise.resolve().then(operation);
|
|
701
|
+
return opts.observer.withContext({ role: "host", stream: kind, ticketId: node.ticket.id, deliveryUnitId: node.deliveryUnitId }, () => opts.observer.span(kind, name, operation));
|
|
702
|
+
}
|
|
649
703
|
function canonical(path) {
|
|
650
704
|
try {
|
|
651
705
|
return realpathSync.native(path);
|
package/dist/buildRuns.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { BuildRunRecordV2, ProviderSessionRefV1, ResolvedAgentSettings, SessionAvailabilityV1 } from "rafi-spec";
|
|
1
|
+
import type { AutonomyProfile, BuildRunRecordV2, ProviderSessionRefV1, ResolvedAgentSettings, ResolvedAutonomyPolicy, SessionAvailabilityV1 } from "rafi-spec";
|
|
2
2
|
import { type ResolveSessionAvailabilityOptions } from "./sessionAvailability.js";
|
|
3
|
-
export declare const BUILD_RUN_VERSION =
|
|
3
|
+
export declare const BUILD_RUN_VERSION = 3;
|
|
4
4
|
export declare const BUILD_RUN_DIRECTORY = ".foreman/runs";
|
|
5
5
|
export declare const BUILD_HEARTBEAT_MS = 10000;
|
|
6
6
|
export declare const BUILD_LEASE_STALE_MS = 45000;
|
|
7
7
|
export interface CreateBuildRunInput {
|
|
8
|
+
runId?: string;
|
|
8
9
|
tickets: string[];
|
|
9
10
|
deliveryUnit?: string;
|
|
10
11
|
branchMode?: BuildRunRecordV2["branchMode"];
|
|
@@ -17,6 +18,9 @@ export interface CreateBuildRunInput {
|
|
|
17
18
|
builder?: LegacyResolvedAgentSettings;
|
|
18
19
|
qa?: LegacyResolvedAgentSettings;
|
|
19
20
|
runDecisions?: BuildRunRecordV2["runDecisions"];
|
|
21
|
+
autonomyProfile?: AutonomyProfile;
|
|
22
|
+
frozenPolicy?: ResolvedAutonomyPolicy;
|
|
23
|
+
qaEnabled?: boolean;
|
|
20
24
|
now?: Date;
|
|
21
25
|
}
|
|
22
26
|
export declare function createBuildRun(input: CreateBuildRunInput): BuildRunRecordV2;
|