ai-foreman 1.7.11 → 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 +8 -2
- package/dist/branch/resume.js +11 -1
- package/dist/branch/runner.d.ts +12 -3
- package/dist/branch/runner.js +131 -67
- 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 +446 -142
- package/dist/cli/status.js +13 -2
- package/dist/cli/tickets.js +27 -9
- package/dist/config.d.ts +4 -0
- package/dist/config.js +28 -1
- package/dist/continuity.js +14 -9
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.js +428 -0
- package/dist/foreman.d.ts +26 -6
- package/dist/foreman.js +147 -104
- package/dist/handoffs.d.ts +35 -3
- package/dist/handoffs.js +155 -17
- package/dist/index.js +6 -0
- package/dist/log.d.ts +3 -3
- 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/notify.d.ts +4 -0
- package/dist/notify.js +15 -0
- 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 +133 -14
- package/dist/qaReview.js +984 -59
- 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 +13 -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;
|
|
@@ -293,7 +299,7 @@ export async function runRoleInstruction(opts) {
|
|
|
293
299
|
const { builder, effort, roleBundle, skills } = roleBuilder;
|
|
294
300
|
const viewer = printEvents(builder.events());
|
|
295
301
|
const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
|
|
296
|
-
const foreman = new Foreman(builder, log, config.notifications.enabled, false, 3, opts.projectDir);
|
|
302
|
+
const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, false, 3, opts.projectDir);
|
|
297
303
|
try {
|
|
298
304
|
const turn = await foreman.runInstruction(opts.instruction);
|
|
299
305
|
await builder.close();
|
|
@@ -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;
|
|
@@ -25,7 +27,9 @@ export interface BranchRunnerOptions {
|
|
|
25
27
|
effort?: EffortLevel;
|
|
26
28
|
fast?: boolean;
|
|
27
29
|
notificationsEnabled: boolean;
|
|
30
|
+
terminalBellEnabled?: boolean;
|
|
28
31
|
qaEnabled: boolean;
|
|
32
|
+
qaMaxFixAttempts?: number;
|
|
29
33
|
createPr: boolean;
|
|
30
34
|
completionMode?: CompletionMode;
|
|
31
35
|
reviewProvider?: ReviewProvider;
|
|
@@ -52,7 +56,7 @@ export interface BranchRunnerOptions {
|
|
|
52
56
|
recordQaSession?: (session: string | ProviderSessionRefV1, ticketId: string, worktreePath: string) => void;
|
|
53
57
|
/** Notify the host and stop the branch plan without discarding its worktree. */
|
|
54
58
|
onSessionUnavailable?: (error: SessionUnavailableError | SessionUnavailableContinuityError) => void;
|
|
55
|
-
createQa?: (cwd: string, sessionId?: string) => Promise<BuilderAdapter>;
|
|
59
|
+
createQa?: (cwd: string, sessionId?: string) => Promise<BuilderAdapter | QaSessionHandle>;
|
|
56
60
|
builderSessionStrategy?: SessionStrategy;
|
|
57
61
|
qaSessionStrategy?: SessionStrategy;
|
|
58
62
|
observeBuilder?: (builder: BuilderAdapter) => Promise<void>;
|
|
@@ -61,7 +65,12 @@ export interface BranchRunnerOptions {
|
|
|
61
65
|
qaNonconvergence?: (context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>;
|
|
62
66
|
beforeBuilderTurn?: (adapter: BuilderAdapter, frozenAction: string, cwd: string) => Promise<BuilderAdapter>;
|
|
63
67
|
builderSessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>;
|
|
64
|
-
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;
|
|
65
74
|
}
|
|
66
75
|
export declare function runBranchPlan(opts: BranchRunnerOptions): Promise<BranchRunSummary[]>;
|
|
67
76
|
export declare function buildBranchTicketInstruction(node: BranchPlanNode, trackerPaths?: {
|
package/dist/branch/runner.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
-
import { cmdBlock, cmdComplete, cmdUpdate } from "../tickets/commands.js";
|
|
5
|
+
import { cmdBlock, cmdComplete, cmdUnblock, cmdUpdate } from "../tickets/commands.js";
|
|
6
6
|
import { commitAll, createTicketWorktree, findWorktreeForBranch, currentWorktreeBranch, deleteLocalBranch, ensureCleanBaseWorktree, ensureForemanExcluded, hasTrackerChanges, hasWorktreeChanges, headCommitIfAhead, removeTicketWorktree, mergeBranchToLocalBase, } from "./git.js";
|
|
7
7
|
import { checkGitHubPrMerged, createOrReusePr, enableGitHubAutoMerge, pushBranchForPr } from "./github.js";
|
|
8
8
|
import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranchForMr } from "./gitlab.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;
|
|
@@ -143,10 +144,14 @@ export async function runBranchPlan(opts) {
|
|
|
143
144
|
sessionRef: resumeSession.sessionRef,
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
|
-
journalTracker(opts.projectDir, opts.runId, `${opts.runId}:tracker-update:${node.ticket.id}:in-progress`, { ticket: node.ticket.id, status: "in_progress" }, () =>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
journalTracker(opts.projectDir, opts.runId, `${opts.runId}:tracker-update:${node.ticket.id}:in-progress`, { ticket: node.ticket.id, status: "in_progress" }, () => {
|
|
148
|
+
if (resumeSession)
|
|
149
|
+
cmdUnblock(opts.projectDir, node.ticket.id, { actor: "foreman", summary: `Reopened by explicit branch recovery for ${node.branch}` });
|
|
150
|
+
cmdUpdate(opts.projectDir, node.ticket.id, {
|
|
151
|
+
status: "in_progress", actor: "foreman",
|
|
152
|
+
summary: resumeSession ? `Resuming branch ${node.branch}` : `Starting branch ${node.branch}`,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
150
155
|
const ticketInstruction = resumeSession ? buildBranchTicketResumeInstruction(node, opts.trackerPaths) : buildBranchTicketInstruction(node, opts.trackerPaths);
|
|
151
156
|
// Reattach the predecessor even for a `fresh` strategy so the host can
|
|
152
157
|
// publish and validate a cumulative handoff before creating its successor.
|
|
@@ -173,8 +178,11 @@ export async function runBranchPlan(opts) {
|
|
|
173
178
|
}
|
|
174
179
|
builderWorkSessions += 1;
|
|
175
180
|
viewer = opts.observeBuilder?.(builder);
|
|
176
|
-
const foreman = new Foreman(builder, opts.log, opts.notificationsEnabled, 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, async (adapter) => opts.observeBuilderNativeCompactions?.(adapter, worktreePath));
|
|
177
|
-
const
|
|
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));
|
|
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();
|
|
178
186
|
builder = foreman.builderAdapter();
|
|
179
187
|
workflowCheckpoint(opts.projectDir, opts.runId, "builder-after", node.ticket.id, { status: status.kind, sessionId: builder.sessionId(), worktree: worktreePath });
|
|
180
188
|
const sessionId = builder.sessionId();
|
|
@@ -186,7 +194,7 @@ export async function runBranchPlan(opts) {
|
|
|
186
194
|
workflowCheckpoint(opts.projectDir, opts.runId, "builder-session-scoped", node.ticket.id, { sessionId, sessionRef, worktree: worktreePath, branch: node.branch });
|
|
187
195
|
}
|
|
188
196
|
if (sessionId) {
|
|
189
|
-
|
|
197
|
+
const resumeSession = {
|
|
190
198
|
ticket: node.ticket.id,
|
|
191
199
|
branch: node.branch,
|
|
192
200
|
base: node.baseBranch,
|
|
@@ -205,7 +213,17 @@ export async function runBranchPlan(opts) {
|
|
|
205
213
|
keepWorktrees: opts.keepWorktrees,
|
|
206
214
|
deliveryUnitId: node.deliveryUnitId,
|
|
207
215
|
deliveryUnitFinal: node.deliveryUnitFinal,
|
|
208
|
-
|
|
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
|
+
}
|
|
209
227
|
if (node.deliveryUnitId) {
|
|
210
228
|
const path = deliverySessionPath(opts.projectDir, node.deliveryUnitId);
|
|
211
229
|
mkdirSync(join(opts.projectDir, ".foreman", "delivery-sessions"), { recursive: true });
|
|
@@ -239,17 +257,25 @@ export async function runBranchPlan(opts) {
|
|
|
239
257
|
throw new Error("independent disposable QA factory is required when QA is enabled");
|
|
240
258
|
workflowCheckpoint(opts.projectDir, opts.runId, "qa-before", node.ticket.id, { worktree: worktreePath });
|
|
241
259
|
const qa = await runIsolatedQa({
|
|
242
|
-
ticket: node.ticket, builderWorktree: worktreePath, builderSummary:
|
|
243
|
-
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,
|
|
244
268
|
sessionBoundary: opts.qaSessionBoundary,
|
|
245
269
|
observeNativeCompactions: opts.observeQaNativeCompactions,
|
|
270
|
+
resolveBlocked: (adapter, reason) => foreman.resolveBlocker(adapter, reason, "qa"),
|
|
246
271
|
evidence: (entry) => opts.log.write("qa-evidence", { ticket: node.ticket.id, ...entry }),
|
|
247
|
-
fix: async (
|
|
272
|
+
fix: async (request) => {
|
|
248
273
|
if (!builder)
|
|
249
274
|
return { ok: false, detail: "Builder session unavailable" };
|
|
250
275
|
builderWorkSessions += 1;
|
|
251
276
|
const digest = (await withActivityPhase("recording QA fix changes", () => changeManifestAsync(worktreePath, (state, detail) => currentActivity()?.update(state, detail), "recording QA fix changes"))).diffDigest;
|
|
252
|
-
const
|
|
277
|
+
const changeSummary = await deterministicChangeSummaryAsync(worktreePath);
|
|
278
|
+
const fixInstruction = buildDurableQaFixHandoff(node.ticket, request, worktreePath, request.latestBuilderResult, digest, changeSummary);
|
|
253
279
|
const strategy = opts.builderSessionStrategy ?? "compact";
|
|
254
280
|
if (opts.builderSessionBoundary) {
|
|
255
281
|
builder = await opts.builderSessionBoundary(builder, fixInstruction, strategy, worktreePath);
|
|
@@ -267,8 +293,13 @@ export async function runBranchPlan(opts) {
|
|
|
267
293
|
}
|
|
268
294
|
if (opts.beforeBuilderTurn)
|
|
269
295
|
builder = await opts.beforeBuilderTurn(builder, fixInstruction, worktreePath);
|
|
270
|
-
|
|
271
|
-
|
|
296
|
+
let fixed = await builder.sendTurn(fixInstruction);
|
|
297
|
+
let fixedStatus = parseStepStatus(fixed.text);
|
|
298
|
+
if (!fixed.isError && fixedStatus.kind === "blocked") {
|
|
299
|
+
const resolved = await foreman.resolveBlocker(builder, fixedStatus.reason ?? "Builder QA fix reported an unspecified blocker");
|
|
300
|
+
fixed = resolved.result;
|
|
301
|
+
fixedStatus = resolved.status;
|
|
302
|
+
}
|
|
272
303
|
const fixedSessionId = builder.sessionId();
|
|
273
304
|
const fixedSessionRef = builder.sessionRef?.();
|
|
274
305
|
if (fixedSessionId)
|
|
@@ -277,10 +308,11 @@ export async function runBranchPlan(opts) {
|
|
|
277
308
|
opts.recordBuilderSession?.(fixedSessionRef ?? fixedSessionId, node.ticket.id, worktreePath);
|
|
278
309
|
workflowCheckpoint(opts.projectDir, opts.runId, "builder-session-scoped", node.ticket.id, { sessionId: fixedSessionId, sessionRef: fixedSessionRef, worktree: worktreePath, branch: node.branch });
|
|
279
310
|
}
|
|
280
|
-
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 };
|
|
281
312
|
},
|
|
282
313
|
onNonconvergence: opts.qaNonconvergence,
|
|
283
314
|
});
|
|
315
|
+
qaResumedRecovery = undefined;
|
|
284
316
|
if (qaStream.sessionId)
|
|
285
317
|
opts.recordQaSession?.(qaStream.sessionRef ?? qaStream.sessionId, node.ticket.id, worktreePath);
|
|
286
318
|
workflowCheckpoint(opts.projectDir, opts.runId, "qa-after", node.ticket.id, { outcome: qa.outcome, detail: qa.detail });
|
|
@@ -316,7 +348,7 @@ export async function runBranchPlan(opts) {
|
|
|
316
348
|
const operation = `${opts.runId}:commit:${node.ticket.id}`;
|
|
317
349
|
planJournal(opts.projectDir, opts.runId, operation, "commit", { ticket: node.ticket.id, branch: node.branch, worktreePath });
|
|
318
350
|
try {
|
|
319
|
-
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}`));
|
|
320
352
|
confirmJournal(opts.projectDir, operation, commit, { sha: commit });
|
|
321
353
|
workflowCheckpoint(opts.projectDir, opts.runId, "commit-after", node.ticket.id, { sha: commit });
|
|
322
354
|
}
|
|
@@ -334,12 +366,12 @@ export async function runBranchPlan(opts) {
|
|
|
334
366
|
workflowCheckpoint(opts.projectDir, opts.runId, "push-before", node.ticket.id, { branch: node.branch, sha: commit });
|
|
335
367
|
const pushOperation = `${opts.runId}:push:${node.branch}`;
|
|
336
368
|
planJournal(opts.projectDir, opts.runId, pushOperation, "push", { branch: node.branch, sha: commit, remote: "origin" });
|
|
337
|
-
const push = await withActivityPhase(`pushing ${node.branch}`, async () => {
|
|
369
|
+
const push = await observeNode(opts, node, "git", `pushing ${node.branch}`, () => withActivityPhase(`pushing ${node.branch}`, async () => {
|
|
338
370
|
currentActivity()?.update(`pushing ${node.branch}`, `publishing ${node.ticket.id}`);
|
|
339
371
|
return reviewProvider === "gitlab"
|
|
340
372
|
? pushBranchForMr(worktreePath, node.branch)
|
|
341
373
|
: pushBranchForPr(worktreePath, node.branch);
|
|
342
|
-
});
|
|
374
|
+
}));
|
|
343
375
|
if (push.ok) {
|
|
344
376
|
confirmJournal(opts.projectDir, pushOperation, node.branch, { sha: commit });
|
|
345
377
|
opts.log.write("branch-push", { ticket: node.ticket.id, branch: node.branch, status: "pushed" });
|
|
@@ -376,7 +408,7 @@ export async function runBranchPlan(opts) {
|
|
|
376
408
|
workflowCheckpoint(opts.projectDir, opts.runId, "review-creation-before", node.ticket.id, { provider: reviewProvider, head: node.branch, base: node.baseBranch });
|
|
377
409
|
const reviewOperation = `${opts.runId}:${reviewProvider === "gitlab" ? "mr" : "pr"}:${node.branch}:${node.baseBranch}`;
|
|
378
410
|
planJournal(opts.projectDir, opts.runId, reviewOperation, reviewProvider === "gitlab" ? "mr-create" : "pr-create", { head: node.branch, base: node.baseBranch, sha: commit });
|
|
379
|
-
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 () => {
|
|
380
412
|
currentActivity()?.update(`creating ${reviewProvider === "gitlab" ? "merge request" : "pull request"}`, node.ticket.id);
|
|
381
413
|
return reviewProvider === "gitlab"
|
|
382
414
|
? createOrReuseMr(opts.projectDir, {
|
|
@@ -396,7 +428,7 @@ export async function runBranchPlan(opts) {
|
|
|
396
428
|
qaEvidence: qaSummary,
|
|
397
429
|
commit,
|
|
398
430
|
});
|
|
399
|
-
});
|
|
431
|
+
}));
|
|
400
432
|
summary.pr = pr;
|
|
401
433
|
if (pr.status === "created" || pr.status === "existing") {
|
|
402
434
|
confirmJournal(opts.projectDir, reviewOperation, pr.url, { head: node.branch, base: node.baseBranch, url: pr.url });
|
|
@@ -415,10 +447,10 @@ export async function runBranchPlan(opts) {
|
|
|
415
447
|
continue;
|
|
416
448
|
}
|
|
417
449
|
if (completionMode === "auto-merge" && reviewProvider === "github") {
|
|
418
|
-
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 () => {
|
|
419
451
|
currentActivity()?.update("enabling GitHub auto-merge", node.branch);
|
|
420
452
|
return enableGitHubAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
421
|
-
});
|
|
453
|
+
}));
|
|
422
454
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
423
455
|
if (autoMerge.status === "failed") {
|
|
424
456
|
opts.log.write("pr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -429,10 +461,10 @@ export async function runBranchPlan(opts) {
|
|
|
429
461
|
opts.log.write("pr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
|
|
430
462
|
}
|
|
431
463
|
else if (completionMode === "auto-merge" && reviewProvider === "gitlab") {
|
|
432
|
-
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 () => {
|
|
433
465
|
currentActivity()?.update("enabling GitLab auto-merge", node.branch);
|
|
434
466
|
return enableGitLabAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
435
|
-
});
|
|
467
|
+
}));
|
|
436
468
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
437
469
|
if (autoMerge.status === "failed") {
|
|
438
470
|
opts.log.write("mr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -450,7 +482,7 @@ export async function runBranchPlan(opts) {
|
|
|
450
482
|
else if (commit && completionMode === "direct-merge" && completesSharedUnit) {
|
|
451
483
|
if (!opts.keepWorktrees)
|
|
452
484
|
removeTicketWorktree(opts.projectDir, worktreePath);
|
|
453
|
-
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"));
|
|
454
486
|
summary.pr = { status: "merged", url: mergeCommit };
|
|
455
487
|
opts.log.write("branch-direct-merge", {
|
|
456
488
|
ticket: node.ticket.id,
|
|
@@ -482,10 +514,17 @@ export async function runBranchPlan(opts) {
|
|
|
482
514
|
status: "done",
|
|
483
515
|
detail: summary.detail,
|
|
484
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
|
+
}
|
|
485
524
|
if (node.deliveryUnitId && node.deliveryUnitFinal)
|
|
486
525
|
rmSync(deliverySessionPath(opts.projectDir, node.deliveryUnitId), { force: true });
|
|
487
526
|
if (!opts.keepWorktrees && completionMode !== "direct-merge" && completesSharedUnit)
|
|
488
|
-
removeTicketWorktree(opts.projectDir, worktreePath);
|
|
527
|
+
await observeNode(opts, node, "cleanup", "removing ticket worktree", () => removeTicketWorktree(opts.projectDir, worktreePath));
|
|
489
528
|
}
|
|
490
529
|
catch (err) {
|
|
491
530
|
if (err instanceof SessionUnavailableError || err instanceof SessionUnavailableContinuityError) {
|
|
@@ -581,49 +620,69 @@ async function waitForAutoMergeDependenciesInternal(opts, node, provider) {
|
|
|
581
620
|
? null
|
|
582
621
|
: opts.autoMergeTimeoutMinutes * 60_000;
|
|
583
622
|
const deadline = opts.autoMergeWait && timeoutMs !== null ? Date.now() + timeoutMs : null;
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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) {
|
|
591
652
|
return {
|
|
592
653
|
ok: false,
|
|
593
|
-
code:
|
|
594
|
-
message:
|
|
595
|
-
repairCommands: status.repairCommands,
|
|
596
|
-
command: status.command,
|
|
597
|
-
output: status.output,
|
|
654
|
+
code: "dependency_unavailable",
|
|
655
|
+
message: `${message}; rerun after it merges or enable auto-merge wait in ticket setup`,
|
|
598
656
|
};
|
|
599
657
|
}
|
|
600
|
-
if (!
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
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());
|
|
620
679
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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 });
|
|
627
686
|
}
|
|
628
687
|
}
|
|
629
688
|
function autoMergePollMs() {
|
|
@@ -636,6 +695,11 @@ function autoMergePollMs() {
|
|
|
636
695
|
function sleep(ms) {
|
|
637
696
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
638
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
|
+
}
|
|
639
703
|
function canonical(path) {
|
|
640
704
|
try {
|
|
641
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;
|