ai-foreman 1.7.24 → 1.7.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/adapters/claude.d.ts +15 -0
- package/dist/adapters/claude.js +149 -28
- package/dist/adapters/codex.d.ts +9 -0
- package/dist/adapters/codex.js +77 -8
- package/dist/adapters/fastMode.d.ts +4 -0
- package/dist/adapters/fastMode.js +9 -0
- package/dist/adapters/types.d.ts +2 -0
- package/dist/agentRun.js +1 -0
- package/dist/branch/runner.js +46 -44
- package/dist/buildAssignment.d.ts +21 -1
- package/dist/buildAssignment.js +215 -2
- package/dist/buildRuns.d.ts +6 -0
- package/dist/buildRuns.js +73 -7
- package/dist/cli/start.d.ts +8 -1
- package/dist/cli/start.js +132 -38
- package/dist/cli/tickets.d.ts +1 -0
- package/dist/cli/tickets.js +20 -2
- package/dist/continuity.js +11 -10
- package/dist/foreman.d.ts +8 -0
- package/dist/foreman.js +69 -10
- package/dist/handoffProtocol.d.ts +4 -0
- package/dist/handoffProtocol.js +19 -0
- package/dist/handoffs.d.ts +1 -1
- package/dist/handoffs.js +23 -4
- package/dist/permissions/policy.js +18 -11
- package/dist/permissions/shellForms.d.ts +3 -0
- package/dist/permissions/shellForms.js +203 -0
- package/dist/qaBuilderActivation.d.ts +19 -0
- package/dist/qaBuilderActivation.js +112 -0
- package/dist/qaFailureDelivery.js +1 -1
- package/dist/qaRecovery.js +2 -1
- package/dist/qaReview.d.ts +4 -0
- package/dist/qaReview.js +37 -7
- package/dist/sessionLifecycle.js +1 -1
- package/dist/supervisedStart.js +23 -3
- package/dist/supervisor.js +33 -0
- package/dist/tickets/groupReset.js +100 -96
- package/dist/tickets/reset.js +80 -69
- package/dist/workflowDb.d.ts +16 -1
- package/dist/workflowDb.js +276 -8
- package/package.json +2 -2
package/dist/buildRuns.js
CHANGED
|
@@ -16,6 +16,7 @@ import { loadTickets } from "./tickets/ticketLoader.js";
|
|
|
16
16
|
import { loadProjectAutonomyConfig, resolveAutonomyPolicy } from "./recoveryPolicy.js";
|
|
17
17
|
import { classifyProcess, processStartIdentity } from "./processIdentity.js";
|
|
18
18
|
import { assertBuildAssignmentReconciled } from "./buildAssignment.js";
|
|
19
|
+
import { computeDisplayStatus, resolveBlockers } from "./tickets/blockers.js";
|
|
19
20
|
export const BUILD_RUN_VERSION = 3;
|
|
20
21
|
export const BUILD_RUN_DIRECTORY = ".foreman/runs";
|
|
21
22
|
export const BUILD_HEARTBEAT_MS = 10_000;
|
|
@@ -176,6 +177,11 @@ export function saveBuildRun(projectDir, run, now = new Date()) {
|
|
|
176
177
|
const existing = workflow.getRun(next.runId);
|
|
177
178
|
const priorProgress = existing?.state.progress;
|
|
178
179
|
next = { ...next, progress: { ...next.progress, completedTickets: [...new Set([...next.progress.completedTickets, ...(priorProgress?.completedTickets ?? [])])] } };
|
|
180
|
+
const newlyCompleted = next.progress.completedTickets.filter(ticket => !priorProgress?.completedTickets?.includes(ticket));
|
|
181
|
+
if (newlyCompleted.length || next.status === "completed")
|
|
182
|
+
next = { ...next, progress: { ...next.progress,
|
|
183
|
+
lastSuccessfulAction: next.status === "completed" ? "Build completed" : `Recorded completion of ${newlyCompleted.join(", ")}`,
|
|
184
|
+
} };
|
|
179
185
|
if (existing && ["completed", "cancelled", "superseded"].includes(existing.status) && workflowStatus(next.status) !== existing.status)
|
|
180
186
|
throw new Error(`cannot rewrite a ${existing.status} build run`);
|
|
181
187
|
if (!existing)
|
|
@@ -228,6 +234,21 @@ export function saveBuildRun(projectDir, run, now = new Date()) {
|
|
|
228
234
|
export function checkpointBuildRun(projectDir, run, checkpoint, patch = {}) {
|
|
229
235
|
return saveBuildRun(projectDir, { ...run, ...patch, checkpoint, receipts: patch.receipts ?? run.receipts });
|
|
230
236
|
}
|
|
237
|
+
/** Refresh a publication from durable state while retaining only the caller's
|
|
238
|
+
* original execution authority. A read-only projection never grants authority. */
|
|
239
|
+
export function persistCurrentBuildSession(projectDir, authorizedRun, role, session) {
|
|
240
|
+
const db = new WorkflowDb(projectDir);
|
|
241
|
+
try {
|
|
242
|
+
assertBuildAuthority(db, authorizedRun);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
db.close();
|
|
246
|
+
}
|
|
247
|
+
const latest = readBuildRuns(projectDir).find(run => run.runId === authorizedRun.runId);
|
|
248
|
+
if (!latest)
|
|
249
|
+
throw new Error("The authorized build no longer exists; inspect recovery before publishing a session");
|
|
250
|
+
return persistBuildSession(projectDir, { ...authorizedRun, ...latest }, role, session);
|
|
251
|
+
}
|
|
231
252
|
export function persistBuildSession(projectDir, run, role, session) {
|
|
232
253
|
const current = run[role];
|
|
233
254
|
if (!current)
|
|
@@ -329,6 +350,28 @@ export function finishRecoveredTicketScope(projectDir, run, checkpoint) {
|
|
|
329
350
|
progress: { ...run.progress, completedTickets: completed, remainingTickets: remaining, nextAction: `Resume ${remaining[0]}` },
|
|
330
351
|
}), "recoverable");
|
|
331
352
|
}
|
|
353
|
+
/** A recovered boundary cannot authorize work on a still-blocked remainder. */
|
|
354
|
+
export function hasRunnableBuildRemainder(projectDir, run) {
|
|
355
|
+
if (run.authorizedBatch && run.progress.completedTickets.length >= run.authorizedBatch.requestedSteps)
|
|
356
|
+
return false;
|
|
357
|
+
if (!isTicketsInitialized(projectDir))
|
|
358
|
+
return false;
|
|
359
|
+
const paths = resolveTicketPaths(loadTicketsConfig(projectDir), projectDir);
|
|
360
|
+
const db = new Database(paths.stateDb, { readonly: true, fileMustExist: true });
|
|
361
|
+
try {
|
|
362
|
+
const states = new Map(db.prepare("SELECT * FROM ticket_state").all().map(state => [state.ticket_id, state]));
|
|
363
|
+
return loadTickets(paths.tickets).some(ticket => {
|
|
364
|
+
if (!run.progress.remainingTickets.includes(ticket.id))
|
|
365
|
+
return false;
|
|
366
|
+
const blockedBy = resolveBlockers(ticket, states);
|
|
367
|
+
const status = computeDisplayStatus(states.get(ticket.id)?.status ?? "planned", blockedBy);
|
|
368
|
+
return blockedBy.length === 0 && ["next", "in_progress"].includes(status);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
db.close();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
332
375
|
export function readBuildRuns(projectDir) {
|
|
333
376
|
const directory = join(resolve(projectDir), BUILD_RUN_DIRECTORY);
|
|
334
377
|
const workflow = new WorkflowReader(projectDir);
|
|
@@ -363,9 +406,13 @@ export function readBuildRuns(projectDir) {
|
|
|
363
406
|
const authoritative = workflow?.getRun(projection.runId)?.state;
|
|
364
407
|
const run = authoritative?.runId ? authoritative : projection;
|
|
365
408
|
let upgraded = upgradeBuildRun(run, projectDir);
|
|
366
|
-
const
|
|
409
|
+
const storedRun = workflow.getRun(run.runId);
|
|
410
|
+
const dbStatus = storedRun?.status;
|
|
367
411
|
if (dbStatus === "superseded" || dbStatus === "completed" || dbStatus === "cancelled")
|
|
368
412
|
upgraded = { ...upgraded, status: dbStatus };
|
|
413
|
+
else if (storedRun && (dbStatus === "failed" || dbStatus === "paused" || dbStatus === "blocked")) {
|
|
414
|
+
upgraded = { ...upgraded, status: dbStatus === "failed" ? "failed" : "recoverable", checkpoint: storedRun.checkpoint, updatedAt: storedRun.updatedAt };
|
|
415
|
+
}
|
|
369
416
|
if (!["completed", "superseded", "cancelled"].includes(workflow?.getRun(run.runId)?.status ?? "")) {
|
|
370
417
|
for (const role of ["builder", "qa"]) {
|
|
371
418
|
const lease = workflow?.roleMutationLease(run.runId, role);
|
|
@@ -563,7 +610,8 @@ export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOv
|
|
|
563
610
|
unexpectedChanges.push({ path: recoveryWorktree, risk: "preserved worktree identity changed" });
|
|
564
611
|
if (branchChanged)
|
|
565
612
|
unexpectedChanges.push({ path: recoveryWorktree, risk: `branch changed from ${expectedBranch} to ${branchNow}` });
|
|
566
|
-
const completed = [...new Set(
|
|
613
|
+
const completed = [...new Set(progress?.completed ?? run.progress.completedTickets)];
|
|
614
|
+
const completedOperations = [...new Set([...run.progress.completedOperations, ...operationNames])];
|
|
567
615
|
const remaining = progress?.remaining ?? (run.progress.remainingTickets.length ? run.progress.remainingTickets : remainingTickets(run));
|
|
568
616
|
const interruption = run.interruption?.lastError ?? run.interruption?.summary ?? run.failure?.summary ?? issues.at(-1) ?? run.status;
|
|
569
617
|
const validation = run.progress.validation
|
|
@@ -580,8 +628,8 @@ export function projectBuildRecovery(projectDir, run, now = new Date(), ticketOv
|
|
|
580
628
|
run, ticketId, ticketTitle,
|
|
581
629
|
compactLabel: `${primary} — ${run.status}`,
|
|
582
630
|
compactHint: `${run.builder?.settings.make ?? "runtime unavailable"}; ${stateBranch ?? branchNow ?? run.repository.branch ?? "current branch"}; updated ${relativeTime(run.updatedAt, now)}`,
|
|
583
|
-
completed, remaining,
|
|
584
|
-
lastSuccess: run.progress.lastSuccessfulAction ??
|
|
631
|
+
completed, completedOperations, remaining,
|
|
632
|
+
lastSuccess: run.progress.lastSuccessfulAction ?? "none recorded",
|
|
585
633
|
interruption,
|
|
586
634
|
validation,
|
|
587
635
|
expectedChanges,
|
|
@@ -623,6 +671,7 @@ export function formatBuildRecoveryProjection(projection) {
|
|
|
623
671
|
`ticket ${projection.ticketId ?? "unavailable"}${projection.ticketTitle ? `: ${projection.ticketTitle}` : ""}`,
|
|
624
672
|
`status ${run.status}; checkpoint ${run.checkpoint}`,
|
|
625
673
|
`completed ${projection.completed.join(", ") || "none recorded"}`,
|
|
674
|
+
...(projection.completedOperations?.length ? [`verified operation receipts ${projection.completedOperations.join(", ")}`] : []),
|
|
626
675
|
`current ${run.progress.currentStep ?? projection.ticketId ?? "not recorded"}`,
|
|
627
676
|
`remaining ${projection.remaining.join(", ") || "none"}`,
|
|
628
677
|
`last successful action ${projection.lastSuccess}`,
|
|
@@ -680,7 +729,7 @@ function captureGitSnapshot(worktree, input) {
|
|
|
680
729
|
return undefined;
|
|
681
730
|
}
|
|
682
731
|
};
|
|
683
|
-
const statusPaths = (
|
|
732
|
+
const statusPaths = gitStatusPaths(worktree);
|
|
684
733
|
const head = input.startHead ?? git(["rev-parse", "HEAD"]);
|
|
685
734
|
const branch = input.branch ?? git(["branch", "--show-current"]);
|
|
686
735
|
return {
|
|
@@ -710,7 +759,24 @@ function gitValue(cwd, args) {
|
|
|
710
759
|
}
|
|
711
760
|
}
|
|
712
761
|
function gitStatusPaths(cwd) {
|
|
713
|
-
|
|
762
|
+
try {
|
|
763
|
+
// Porcelain status columns and pathname whitespace are significant. In -z
|
|
764
|
+
// mode a rename/copy has a second raw pathname, without status columns.
|
|
765
|
+
const records = execFileSync("git", ["status", "--porcelain=v1", "-z"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\0");
|
|
766
|
+
const paths = [];
|
|
767
|
+
for (let i = 0; i < records.length; i++) {
|
|
768
|
+
const record = records[i];
|
|
769
|
+
if (!record)
|
|
770
|
+
continue;
|
|
771
|
+
paths.push(record.slice(3));
|
|
772
|
+
if (/[RC]/.test(record.slice(0, 2)) && records[i + 1])
|
|
773
|
+
paths.push(records[++i]);
|
|
774
|
+
}
|
|
775
|
+
return [...new Set(paths.filter(Boolean))].sort();
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
714
780
|
}
|
|
715
781
|
function relativeTime(value, now) {
|
|
716
782
|
const seconds = Math.max(0, Math.floor((now.getTime() - new Date(value).getTime()) / 1000));
|
|
@@ -752,7 +818,7 @@ function upgradeBuildRun(run, projectDir = run.repository.root, now = new Date()
|
|
|
752
818
|
completedTickets: legacy.status === "completed" ? [...legacy.tickets] : [],
|
|
753
819
|
completedOperations: Object.keys(legacy.receipts).sort(),
|
|
754
820
|
remainingTickets: remaining,
|
|
755
|
-
lastSuccessfulAction:
|
|
821
|
+
...(legacy.status === "completed" ? { lastSuccessfulAction: "build completed" } : {}),
|
|
756
822
|
nextAction: remaining[0] ? `Resume ${remaining[0]}` : "Inspect legacy run",
|
|
757
823
|
},
|
|
758
824
|
frozenPolicy: policy,
|
package/dist/cli/start.d.ts
CHANGED
|
@@ -24,8 +24,15 @@ export declare function validateStartOptions(opts: Record<string, any>, requireC
|
|
|
24
24
|
export declare function validateRecoveryArguments(args: readonly string[], projectDir: string): void;
|
|
25
25
|
/** Validate persisted preparation argv without executing a command or provider. */
|
|
26
26
|
export declare function validatePreparationArguments(args: readonly string[], projectDir: string): string[];
|
|
27
|
-
export declare function buildStartCommand(
|
|
27
|
+
export declare function buildStartCommand(options?: {
|
|
28
|
+
beforeFreshStart?: (projectDir: string) => Promise<boolean>;
|
|
29
|
+
}): Command;
|
|
28
30
|
export declare function createQaNonconvergenceHandler(projectDir: string, noninteractive: boolean, planner?: AgentRoleDefaultsV1, observer?: () => RunObserver | undefined, activeRunId?: () => string): (context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>;
|
|
29
31
|
export declare function createQaReportRecoveryHandler(projectDir: string, noninteractive: boolean, observer?: () => RunObserver | undefined): QaReportRecoveryHandler;
|
|
32
|
+
export declare function recoveryCompactionHistoryUncertain(projectDir: string, recovery: {
|
|
33
|
+
legacy?: boolean;
|
|
34
|
+
version?: number;
|
|
35
|
+
runDecisions?: unknown;
|
|
36
|
+
} | undefined, runId: string | undefined, role: "builder" | "qa"): boolean;
|
|
30
37
|
/** Make the executable branch plan exactly match each explicit root-to-tip stack. */
|
|
31
38
|
export declare function applyExplicitStackTopology(plan: BranchPlan, stacks: DeliveryStack[], delivery: DeliveryConfig, branchPrefix: string): BranchPlan;
|
package/dist/cli/start.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { finalQaRoleInstructions, resolveEffectiveQaConfiguration } from "../qaEffectiveConfig.js";
|
|
2
|
+
import { activateQaRecoveryBuilder } from "../qaBuilderActivation.js";
|
|
2
3
|
import { graphContext, sendGraphTurn } from "../graph/turn.js";
|
|
3
4
|
import { initializeSyntheticWork } from "../buildSyntheticWork.js";
|
|
4
5
|
import { admitApprovedTicket } from "../buildWorkAuthorization.js";
|
|
@@ -8,7 +9,7 @@ import { formatRecoveryCommand, formatDecisionCommands, formatExactRunRecovery }
|
|
|
8
9
|
import { durableReadOnlyProposal } from "../readOnlyProposal.js";
|
|
9
10
|
import { captureFrozenQaSource } from "../qaSnapshot.js";
|
|
10
11
|
import { finishStartAdmission, superviseStart, supervisedRunId } from "../supervisedStart.js";
|
|
11
|
-
import { assertBuildAssignmentReconciled } from "../buildAssignment.js";
|
|
12
|
+
import { assertBuildAssignmentReconciled, isInterruptedBuilderRecovery, assertInterruptedSourceSnapshot } from "../buildAssignment.js";
|
|
12
13
|
import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions } from "../humanDecision.js";
|
|
13
14
|
import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate } from "../buildApproval.js";
|
|
14
15
|
import { Command, Option } from "commander";
|
|
@@ -24,7 +25,7 @@ import { PermissionPolicy } from "../permissions/policy.js";
|
|
|
24
25
|
import { ClaudeAdapter } from "../adapters/claude.js";
|
|
25
26
|
import { CodexAdapter } from "../adapters/codex.js";
|
|
26
27
|
import { RecoveringAdapter } from "../adapters/recovering.js";
|
|
27
|
-
import { Foreman, createPermissionHandler } from "../foreman.js";
|
|
28
|
+
import { Foreman, BuildInterruptedError, createPermissionHandler } from "../foreman.js";
|
|
28
29
|
import { printEvents } from "./events.js";
|
|
29
30
|
import { loadRoleBundle } from "../roles.js";
|
|
30
31
|
import { ensureBuildRuntimeReadyForCommand } from "./runtimeAuthPrompt.js";
|
|
@@ -39,7 +40,7 @@ import { formatGitHubFailure, preflightGh } from "../branch/github.js";
|
|
|
39
40
|
import { preflightGlab } from "../branch/gitlab.js";
|
|
40
41
|
import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
|
|
41
42
|
import { branchPlanLogMetadata, presentBranchPlan } from "../branch/presentation.js";
|
|
42
|
-
import { buildRunSessionBinding, checkpointBuildRun, completeBuildRun, createBuildRun, currentBranchRecoveryTicket, currentBranchTicketProgress, recoveryExecutionTickets, finishRecoveredTicketScope, heartbeatBuildRun, persistBuildSession, projectBuildRecovery, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
|
|
43
|
+
import { buildRunSessionBinding, checkpointBuildRun, completeBuildRun, createBuildRun, currentBranchRecoveryTicket, currentBranchTicketProgress, recoveryExecutionTickets, finishRecoveredTicketScope, heartbeatBuildRun, persistBuildSession, persistCurrentBuildSession, projectBuildRecovery, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
|
|
43
44
|
import { parse as parseYaml } from "yaml";
|
|
44
45
|
import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
|
|
45
46
|
import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
|
|
@@ -450,7 +451,7 @@ export function validatePreparationArguments(args, projectDir) {
|
|
|
450
451
|
throw new Error("Saved preparation contains stale internal recovery authority");
|
|
451
452
|
return savedStartArguments(command, projectDir, options);
|
|
452
453
|
}
|
|
453
|
-
export function buildStartCommand() {
|
|
454
|
+
export function buildStartCommand(options = {}) {
|
|
454
455
|
return new Command("start")
|
|
455
456
|
.description("Enlist a builder and drive it through a batch of N steps.")
|
|
456
457
|
.argument("<project>", "path to the project directory the builder works in")
|
|
@@ -471,7 +472,7 @@ export function buildStartCommand() {
|
|
|
471
472
|
.option("-t, --tickets <path>", "path to ticket file (.md, .txt, .yaml, …) — passed to the builder as context")
|
|
472
473
|
.option("-y, --yes", "skip pre-flight confirmation prompt")
|
|
473
474
|
.option("--effort <level>", "reasoning effort level (low|medium|high|xhigh)")
|
|
474
|
-
.option("--fast", "fast mode
|
|
475
|
+
.option("--fast", "use native fast mode when available; keep the model and reasoning settings")
|
|
475
476
|
.option("--no-qa", "disable per-ticket QA review (enabled by default)")
|
|
476
477
|
.option("--autonomy <profile>", "autonomy profile (supervised | balanced | unattended)")
|
|
477
478
|
.option("--detach", "return after launching the durable supervisor")
|
|
@@ -557,6 +558,10 @@ export function buildStartCommand() {
|
|
|
557
558
|
const cwd = resolve(project);
|
|
558
559
|
if (!existsSync(cwd))
|
|
559
560
|
fail(`project directory not found: ${cwd}`);
|
|
561
|
+
// Recovery commands launch a frozen continuation and must not recursively
|
|
562
|
+
// offer a different recovery path. Fresh launches may be redirected first.
|
|
563
|
+
if (!recoveryIntent && options.beforeFreshStart && !await options.beforeFreshStart(cwd))
|
|
564
|
+
return;
|
|
560
565
|
const recoveryRecord = opts.recoverRun ? readBuildRuns(cwd).find((run) => run.runId === opts.recoverRun) : undefined;
|
|
561
566
|
let dirtyWork = recoveryRecord?.runDecisions?.dirtyWork;
|
|
562
567
|
if (opts.recoverRun && !recoveryRecord)
|
|
@@ -956,7 +961,10 @@ export function buildStartCommand() {
|
|
|
956
961
|
let activeContinuityRunId;
|
|
957
962
|
let liveStatusReporter;
|
|
958
963
|
let activeBuilderForStatus;
|
|
959
|
-
const
|
|
964
|
+
const qaBuilderRecoverySelected = recoveryRecord?.recoveryDecision?.role === "qa";
|
|
965
|
+
const savedQaBuilderSettings = qaBuilderRecoverySelected ? recoveryRecord.builder?.settings : undefined;
|
|
966
|
+
let publishActivatedBuilderSession;
|
|
967
|
+
const resolvedContinuitySettings = (role) => role === "builder" && savedQaBuilderSettings ? { ...savedQaBuilderSettings } : role === "builder" ? {
|
|
960
968
|
role, source: roleDefaults.builder ? "project" : "provider", make: agent, model: model ?? "default",
|
|
961
969
|
reasoning: builderEffort ?? "default", fast: Boolean(builderFast), session_strategy: roleDefaults.builder?.session_strategy ?? "compact",
|
|
962
970
|
settings_revision: settingsRevision, display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
|
|
@@ -1254,6 +1262,35 @@ export function buildStartCommand() {
|
|
|
1254
1262
|
await continuous.validateFreshRecovery();
|
|
1255
1263
|
return continuous;
|
|
1256
1264
|
};
|
|
1265
|
+
// QA-only recovery leaves Builder ownership untouched until a real,
|
|
1266
|
+
// approved remediation/guidance boundary needs Builder work. The unused
|
|
1267
|
+
// adapter cannot bypass the old role owner or use QA's CLI model settings.
|
|
1268
|
+
const activateBuilderForQaRecovery = async (adapter, worktree, frozenAction) => {
|
|
1269
|
+
if (!qaBuilderRecoverySelected || !recoveryRecord)
|
|
1270
|
+
return adapter;
|
|
1271
|
+
const currentRun = readBuildRuns(cwd).find(run => run.runId === recoveryRecord.runId);
|
|
1272
|
+
// The saved recovery ticket is only the initial boundary. Later batch
|
|
1273
|
+
// tickets must bind their own durable selection before Builder work.
|
|
1274
|
+
const ticketId = currentRun?.currentTicket ?? recoveryRecord.recoveryDecision?.executionTickets?.[0] ?? requestedTicketIds[0];
|
|
1275
|
+
if (!ticketId)
|
|
1276
|
+
throw new Error("QA recovery has no approved Builder follow-up ticket; inspect this build before dispatch");
|
|
1277
|
+
const activated = await activateQaRecoveryBuilder({ projectDir: cwd, runId: recoveryRecord.runId, ticketId, worktree, adapter,
|
|
1278
|
+
settings: savedQaBuilderSettings ?? resolvedContinuitySettings("builder"), expectedCapabilities: builderCapabilities, frozenAction,
|
|
1279
|
+
createSuccessor: () => createBuilderForSettings(worktree, savedQaBuilderSettings ?? resolvedContinuitySettings("builder")),
|
|
1280
|
+
wrap: successor => continuousBuilder(successor, worktree, recoveryRecord.runId, savedQaBuilderSettings ?? resolvedContinuitySettings("builder")),
|
|
1281
|
+
recovery: { ...handoffRecoveryOptions("builder"), allowProviderSwitch: false },
|
|
1282
|
+
});
|
|
1283
|
+
if (activated !== adapter) {
|
|
1284
|
+
const current = readBuildRuns(cwd).find(run => run.runId === recoveryRecord.runId);
|
|
1285
|
+
const ref = activated.sessionRef?.();
|
|
1286
|
+
if (!current || !ref?.validatedAt)
|
|
1287
|
+
throw new Error("Accepted Builder activation lacks a durable scoped session; recovery is paused before implementation");
|
|
1288
|
+
if (!publishActivatedBuilderSession)
|
|
1289
|
+
throw new Error("Accepted Builder activation has no original execution authority; recovery is paused before implementation");
|
|
1290
|
+
publishActivatedBuilderSession(ref);
|
|
1291
|
+
}
|
|
1292
|
+
return activated;
|
|
1293
|
+
};
|
|
1257
1294
|
const applyBuilderSettingsBoundary = async (runId, builderCwd, input) => {
|
|
1258
1295
|
const context = await input.adapter.contextUsage?.().catch(() => undefined);
|
|
1259
1296
|
const nativeUsage = await input.adapter.sessionUsage?.().catch(() => undefined);
|
|
@@ -1450,7 +1487,7 @@ export function buildStartCommand() {
|
|
|
1450
1487
|
db.close();
|
|
1451
1488
|
} },
|
|
1452
1489
|
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
1453
|
-
return activeContinuityRunId ? db.
|
|
1490
|
+
return activeContinuityRunId ? db.roleMutationLease(activeContinuityRunId, "qa")?.generation ?? 0 : 0;
|
|
1454
1491
|
}
|
|
1455
1492
|
finally {
|
|
1456
1493
|
db.close();
|
|
@@ -1469,7 +1506,7 @@ export function buildStartCommand() {
|
|
|
1469
1506
|
db.close();
|
|
1470
1507
|
} },
|
|
1471
1508
|
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
1472
|
-
return activeContinuityRunId ? db.
|
|
1509
|
+
return activeContinuityRunId ? db.roleMutationLease(activeContinuityRunId, "builder")?.generation ?? 0 : 0;
|
|
1473
1510
|
}
|
|
1474
1511
|
finally {
|
|
1475
1512
|
db.close();
|
|
@@ -1544,8 +1581,16 @@ export function buildStartCommand() {
|
|
|
1544
1581
|
const packet = pendingQaRecoveryPacket(cwd, recoveryRecord.runId, requestedTicketIds[0]);
|
|
1545
1582
|
if (!packet)
|
|
1546
1583
|
throw new Error("accepted QA recovery handoff has no current durable packet");
|
|
1584
|
+
const boundaryDb = new WorkflowDb(cwd);
|
|
1585
|
+
let afterOwnedRemediation = false;
|
|
1586
|
+
try {
|
|
1587
|
+
afterOwnedRemediation = boundaryDb.hasConfirmedQaRemediationForRecheck(packet.manifest.runId, packet.manifest.ticketId);
|
|
1588
|
+
}
|
|
1589
|
+
finally {
|
|
1590
|
+
boundaryDb.close();
|
|
1591
|
+
}
|
|
1547
1592
|
const confinement = describeQaHandle(adapter, qaCwd, { kind: "initial" }).confinement;
|
|
1548
|
-
handoffReceipt = { kind: "accepted", receipt: qaV2HandoffReceipt(accepted.acceptanceReceipt, {
|
|
1593
|
+
handoffReceipt = afterOwnedRemediation ? { kind: "initial" } : { kind: "accepted", receipt: qaV2HandoffReceipt(accepted.acceptanceReceipt, {
|
|
1549
1594
|
runId: packet.manifest.runId, ticketId: packet.manifest.ticketId, packetDigest: packet.manifest.packetDigest,
|
|
1550
1595
|
reviewedStateDigest: packet.manifest.reviewedStateDigest,
|
|
1551
1596
|
}, confinement.digest) };
|
|
@@ -1754,7 +1799,8 @@ export function buildStartCommand() {
|
|
|
1754
1799
|
if (branchDefaults.createReview && branchDefaults.reviewProvider) {
|
|
1755
1800
|
await ensureReviewProviderReady(cwd, branchDefaults.reviewProvider, log, Boolean(opts.yes));
|
|
1756
1801
|
}
|
|
1757
|
-
|
|
1802
|
+
if (grantedNetwork || opts.builderApprovals)
|
|
1803
|
+
console.log(`foreman: requested Builder shell capabilities: network ${grantedNetwork ? "approved for this build" : "off"}; approvals ${opts.builderApprovals ? "per operation" : "never"}. Effective permissions will be verified with the provider.`);
|
|
1758
1804
|
const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
|
|
1759
1805
|
label: "start",
|
|
1760
1806
|
yes: Boolean(opts.yes),
|
|
@@ -1933,7 +1979,7 @@ export function buildStartCommand() {
|
|
|
1933
1979
|
}
|
|
1934
1980
|
resumeSessionByTicket.set(ticketId, { ...session, sessionRef: availability.sessionRef });
|
|
1935
1981
|
}
|
|
1936
|
-
const capturedBranchBuilder = {
|
|
1982
|
+
const capturedBranchBuilder = savedQaBuilderSettings ? { ...savedQaBuilderSettings } : {
|
|
1937
1983
|
role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
|
|
1938
1984
|
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
1939
1985
|
session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
@@ -1949,7 +1995,9 @@ export function buildStartCommand() {
|
|
|
1949
1995
|
compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
|
|
1950
1996
|
};
|
|
1951
1997
|
const firstResumeRef = plan.nodes[0] ? resumeSessionByTicket.get(plan.nodes[0].ticket.id)?.sessionRef : undefined;
|
|
1952
|
-
|
|
1998
|
+
const branchQaOnlyRecovery = Boolean(recoveryRecord && !isInterruptedBuilderRecovery(recoveryRecord)
|
|
1999
|
+
&& (opts.qaRevision !== undefined || pendingQaRecoveryPacket(cwd, recoveryRecord.runId, requestedTicketIds[0])));
|
|
2000
|
+
let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { ...(branchQaOnlyRecovery ? {} : { builder: capturedBranchBuilder, builderSessionId: firstResumeRef?.sessionId ?? null, builderSessionRef: firstResumeRef ?? null }), qa: capturedBranchQa, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
|
|
1953
2001
|
runId: invocationRunId,
|
|
1954
2002
|
builderCapabilities, tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
|
|
1955
2003
|
repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, baseRef: plan.baseRef, builder: capturedBranchBuilder, qa: capturedBranchQa,
|
|
@@ -1957,6 +2005,7 @@ export function buildStartCommand() {
|
|
|
1957
2005
|
runDecisions: { dirtyWork, workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
1958
2006
|
});
|
|
1959
2007
|
activeContinuityRunId = masterRun.runId;
|
|
2008
|
+
publishActivatedBuilderSession = ref => { masterRun = persistCurrentBuildSession(cwd, masterRun, "builder", ref); };
|
|
1960
2009
|
if (requiresBuildApproval(cwd, plan.nodes.map(node => node.ticket.id), autoApprovePlanUpdates, approvalConsequences) && plan.issues.every((issue) => !issue.blocking)) {
|
|
1961
2010
|
const action = await durableHumanDecision({ projectDir: cwd, runId: invocationRunId, key: `branch-plan:${buildScopeRevision(cwd, approvalConsequences)}`, prompt: branchPresentation.prompt, choices: [{ id: "proceed", label: "Proceed" }, { id: "cancel", label: "Cancel" }], operation: () => select({
|
|
1962
2011
|
message: branchPresentation.prompt,
|
|
@@ -1985,9 +2034,9 @@ export function buildStartCommand() {
|
|
|
1985
2034
|
for (const node of plan.nodes)
|
|
1986
2035
|
admitApprovedTicket(cwd, masterRun.runId, node.ticket.id, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved branch plan`);
|
|
1987
2036
|
masterRun = checkpointBuildRun(cwd, masterRun, "capabilities-resolved", { builderCapabilities });
|
|
1988
|
-
const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, masterRun.runId, requestedTicketIds[0]) : undefined;
|
|
2037
|
+
const qaResumedRecovery = recoveryRecord && !isInterruptedBuilderRecovery(recoveryRecord) ? pendingQaRecoveryPacket(cwd, masterRun.runId, requestedTicketIds[0]) : undefined;
|
|
1989
2038
|
let qaProtocolResumeTicket;
|
|
1990
|
-
if (recoveryRecord && !qaResumedRecovery) {
|
|
2039
|
+
if (recoveryRecord && !qaResumedRecovery && !isInterruptedBuilderRecovery(recoveryRecord)) {
|
|
1991
2040
|
const protocolDb = new WorkflowDb(cwd);
|
|
1992
2041
|
try {
|
|
1993
2042
|
const pendingHeads = protocolDb.pendingQaTicketHeads(masterRun.runId);
|
|
@@ -2121,7 +2170,7 @@ export function buildStartCommand() {
|
|
|
2121
2170
|
db.close();
|
|
2122
2171
|
} },
|
|
2123
2172
|
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
2124
|
-
return db.
|
|
2173
|
+
return db.roleMutationLease(masterRun.runId, "builder")?.generation ?? 0;
|
|
2125
2174
|
}
|
|
2126
2175
|
finally {
|
|
2127
2176
|
db.close();
|
|
@@ -2145,9 +2194,12 @@ export function buildStartCommand() {
|
|
|
2145
2194
|
}
|
|
2146
2195
|
},
|
|
2147
2196
|
qaNonconvergence,
|
|
2148
|
-
beforeBuilderTurn: async (adapter, frozenAction, worktreePath) => (await branchContextController(worktreePath).atSafeBoundary(adapter, frozenAction)).adapter,
|
|
2197
|
+
beforeBuilderTurn: async (adapter, frozenAction, worktreePath) => (await branchContextController(worktreePath).atSafeBoundary(await activateBuilderForQaRecovery(adapter, worktreePath, frozenAction), frozenAction)).adapter,
|
|
2149
2198
|
observeBuilderNativeCompactions: async (adapter, worktreePath) => { await branchContextController(worktreePath).observeNativeCompactions(adapter); },
|
|
2150
|
-
builderSessionBoundary: async (adapter, frozenAction, strategy, worktreePath) =>
|
|
2199
|
+
builderSessionBoundary: async (adapter, frozenAction, strategy, worktreePath) => {
|
|
2200
|
+
const activated = await activateBuilderForQaRecovery(adapter, worktreePath, frozenAction);
|
|
2201
|
+
return activated !== adapter ? activated : (await branchContextController(worktreePath).atWorkSessionBoundary(activated, frozenAction, strategy)).adapter;
|
|
2202
|
+
},
|
|
2151
2203
|
qaSessionBoundary,
|
|
2152
2204
|
observeQaNativeCompactions,
|
|
2153
2205
|
});
|
|
@@ -2252,7 +2304,8 @@ export function buildStartCommand() {
|
|
|
2252
2304
|
state.close();
|
|
2253
2305
|
}
|
|
2254
2306
|
}
|
|
2255
|
-
|
|
2307
|
+
if (grantedNetwork || opts.builderApprovals)
|
|
2308
|
+
console.log(`foreman: requested Builder shell capabilities: network ${grantedNetwork ? "approved for this build" : "off"}; approvals ${opts.builderApprovals ? "per operation" : "never"}. Effective permissions will be verified with the provider.`);
|
|
2256
2309
|
const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
|
|
2257
2310
|
label: "start",
|
|
2258
2311
|
yes: Boolean(opts.yes),
|
|
@@ -2282,7 +2335,7 @@ export function buildStartCommand() {
|
|
|
2282
2335
|
qaModel = qaReady.model;
|
|
2283
2336
|
qaExecutable = qaReady.executable;
|
|
2284
2337
|
}
|
|
2285
|
-
const capturedBuilder = {
|
|
2338
|
+
const capturedBuilder = savedQaBuilderSettings ? { ...savedQaBuilderSettings } : {
|
|
2286
2339
|
role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
|
|
2287
2340
|
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
2288
2341
|
session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
@@ -2291,7 +2344,11 @@ export function buildStartCommand() {
|
|
|
2291
2344
|
compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
|
|
2292
2345
|
};
|
|
2293
2346
|
const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false, auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 65, compact_maximum: roleDefaults.qa?.compact_maximum ?? 10, settings_revision: settingsRevision };
|
|
2294
|
-
|
|
2347
|
+
// QA recovery owns the QA session only. Its CLI model overrides and unused
|
|
2348
|
+
// Builder adapter must not replace the saved Builder that continues later.
|
|
2349
|
+
const qaOnlyRecovery = Boolean(recoveryRecord && !isInterruptedBuilderRecovery(recoveryRecord)
|
|
2350
|
+
&& (opts.qaRevision !== undefined || pendingQaRecoveryPacket(cwd, recoveryRecord.runId, requestedTicketIds[0])));
|
|
2351
|
+
let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { ...(qaOnlyRecovery ? {} : { builder: capturedBuilder, builderSessionId: resumeSessionId ?? null, builderSessionRef: resumeSessionRef ?? null }), qa: capturedQa, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
|
|
2295
2352
|
runId: invocationRunId,
|
|
2296
2353
|
tickets: [...(executionTickets ?? [])], builderCapabilities, authorizedBatch: { tickets: [...(executionTickets ?? [])], requestedSteps: steps, scopeRevision: buildScopeRevision(cwd, approvalConsequences), startedTickets: [] }, repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
|
|
2297
2354
|
frozenPolicy, autonomyProfile, qaEnabled,
|
|
@@ -2300,10 +2357,11 @@ export function buildStartCommand() {
|
|
|
2300
2357
|
buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities });
|
|
2301
2358
|
activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
|
|
2302
2359
|
activeContinuityRunId = buildRun.runId;
|
|
2303
|
-
|
|
2360
|
+
publishActivatedBuilderSession = ref => { buildRun = persistCurrentBuildSession(cwd, buildRun, "builder", ref); };
|
|
2361
|
+
const qaResumedRecovery = recoveryRecord && !isInterruptedBuilderRecovery(recoveryRecord) ? pendingQaRecoveryPacket(cwd, buildRun.runId, requestedTicketIds[0]) : undefined;
|
|
2304
2362
|
let qaFinalizationTicket;
|
|
2305
2363
|
let qaProtocolResumeTicket;
|
|
2306
|
-
if (recoveryRecord && !qaResumedRecovery) {
|
|
2364
|
+
if (recoveryRecord && !qaResumedRecovery && !isInterruptedBuilderRecovery(recoveryRecord)) {
|
|
2307
2365
|
const finalizationDb = new WorkflowDb(cwd);
|
|
2308
2366
|
try {
|
|
2309
2367
|
const pendingHeads = finalizationDb.pendingQaTicketHeads(buildRun.runId);
|
|
@@ -2343,19 +2401,38 @@ export function buildStartCommand() {
|
|
|
2343
2401
|
}
|
|
2344
2402
|
const heartbeat = setInterval(() => { buildRun = heartbeatBuildRun(cwd, buildRun); }, 10_000);
|
|
2345
2403
|
heartbeat.unref();
|
|
2404
|
+
let interruptionRequested = false;
|
|
2405
|
+
let stopActiveBuild;
|
|
2346
2406
|
const checkpointSignal = () => {
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2407
|
+
if (["completed", "cancelled", "superseded"].includes(buildRun.status))
|
|
2408
|
+
return;
|
|
2409
|
+
if (interruptionRequested)
|
|
2410
|
+
return;
|
|
2411
|
+
interruptionRequested = true;
|
|
2412
|
+
clearInterval(heartbeat);
|
|
2413
|
+
// Hold the lease until the cancelled provider turn has published its
|
|
2414
|
+
// outcome. Releasing here races with assignment and completion writes.
|
|
2415
|
+
void stopActiveBuild?.().catch(error => log.write("error", { phase: "interruption-cleanup", message: String(error) }));
|
|
2351
2416
|
};
|
|
2352
2417
|
process.once("SIGINT", checkpointSignal);
|
|
2353
2418
|
process.once("SIGTERM", checkpointSignal);
|
|
2354
2419
|
let builder;
|
|
2355
2420
|
try {
|
|
2421
|
+
if (recoveryRecord)
|
|
2422
|
+
assertInterruptedSourceSnapshot(cwd, recoveryRecord);
|
|
2356
2423
|
builder = await createBuilder(cwd, resumeSessionId, resumeSessionRef);
|
|
2424
|
+
stopActiveBuild = () => builder.close();
|
|
2425
|
+
if (interruptionRequested) {
|
|
2426
|
+
await builder.close();
|
|
2427
|
+
throw new BuildInterruptedError();
|
|
2428
|
+
}
|
|
2357
2429
|
}
|
|
2358
2430
|
catch (error) {
|
|
2431
|
+
if (error instanceof BuildInterruptedError) {
|
|
2432
|
+
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "interrupted", { status: "recoverable" }), "recoverable");
|
|
2433
|
+
console.error(`rafi: Build interrupted; current work is preserved. Continue with ${formatRecoveryCommand(cwd)}`);
|
|
2434
|
+
process.exit(2);
|
|
2435
|
+
}
|
|
2359
2436
|
if (!(error instanceof SessionUnavailableError))
|
|
2360
2437
|
throw error;
|
|
2361
2438
|
const summary = error.message.slice(0, 1000);
|
|
@@ -2413,7 +2490,13 @@ export function buildStartCommand() {
|
|
|
2413
2490
|
return continuousBuilder(transfer.successor, cwd, buildRun.runId, settings);
|
|
2414
2491
|
},
|
|
2415
2492
|
});
|
|
2416
|
-
const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, qaEnabled, frozenPolicy.limits.builderQaFixesPerTicket, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence, async (adapter, frozenAction) => (await contextController.atSafeBoundary(
|
|
2493
|
+
const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, qaEnabled, frozenPolicy.limits.builderQaFixesPerTicket, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence, async (adapter, frozenAction) => (await contextController.atSafeBoundary(await activateBuilderForQaRecovery(adapter, cwd, frozenAction), frozenAction)).adapter, async (adapter, frozenAction, strategy) => {
|
|
2494
|
+
const activated = await activateBuilderForQaRecovery(adapter, cwd, frozenAction);
|
|
2495
|
+
return activated !== adapter ? activated : (await contextController.atWorkSessionBoundary(activated, frozenAction, strategy)).adapter;
|
|
2496
|
+
}, qaSessionBoundary, observeQaNativeCompactions, async (adapter) => { await contextController.observeNativeCompactions(adapter); }, activeObserver, capturedQa, true, qaReportRecovery, qaResumedRecovery, buildRun.runId, frozenPolicy.continueIndependentTickets);
|
|
2497
|
+
stopActiveBuild = () => foreman.requestStop();
|
|
2498
|
+
if (interruptionRequested)
|
|
2499
|
+
await foreman.requestStop();
|
|
2417
2500
|
activeBuilderForStatus = () => foreman.builderAdapter();
|
|
2418
2501
|
const statusReporter = liveStatusReporter = new AgentStatusReporter({
|
|
2419
2502
|
runId: buildRun.runId,
|
|
@@ -2429,7 +2512,7 @@ export function buildStartCommand() {
|
|
|
2429
2512
|
db.close();
|
|
2430
2513
|
} },
|
|
2431
2514
|
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
2432
|
-
return db.
|
|
2515
|
+
return db.roleMutationLease(buildRun.runId, "builder")?.generation ?? 0;
|
|
2433
2516
|
}
|
|
2434
2517
|
finally {
|
|
2435
2518
|
db.close();
|
|
@@ -2451,7 +2534,7 @@ export function buildStartCommand() {
|
|
|
2451
2534
|
builderFast ? "fast" : null,
|
|
2452
2535
|
qaEnabled ? null : "qa=off",
|
|
2453
2536
|
].filter(Boolean).join(" ");
|
|
2454
|
-
console.log(`${accent("foreman:")}
|
|
2537
|
+
console.log(`${accent("foreman:")} requested batch budget ${steps} step(s); current admitted scope ${buildRun.tickets.join(", ") || "awaiting a validated work plan"}; ${sanitizeTerminalText(agent)} Builder${modifiers ? ` [${sanitizeTerminalText(modifiers)}]` : ""}`);
|
|
2455
2538
|
console.log(`${accent("foreman:")} project ${sanitizeTerminalText(cwd)}`);
|
|
2456
2539
|
if (trackerRelPath)
|
|
2457
2540
|
console.log(`${accent("foreman:")} tracker ${sanitizeTerminalText(trackerRelPath)}`);
|
|
@@ -2497,8 +2580,6 @@ export function buildStartCommand() {
|
|
|
2497
2580
|
: foreman.completePendingQaRecovery(ticketId));
|
|
2498
2581
|
if (pendingHandoffGeneration !== undefined)
|
|
2499
2582
|
throw new Error(`recovery handoff generation ${pendingHandoffGeneration} for ${pendingHandoffRole} was not accepted`);
|
|
2500
|
-
if (foreman.builderSessionId())
|
|
2501
|
-
buildRun = persistBuildSession(cwd, buildRun, "builder", foreman.builderAdapter().sessionRef?.() ?? foreman.builderSessionId());
|
|
2502
2583
|
if (foreman.qaSessionId())
|
|
2503
2584
|
buildRun = persistBuildSession(cwd, buildRun, "qa", foreman.qaSessionRef() ?? foreman.qaSessionId());
|
|
2504
2585
|
buildRun = qa.outcome === "passed" || qa.outcome === "waived"
|
|
@@ -2650,7 +2731,16 @@ export function buildStartCommand() {
|
|
|
2650
2731
|
}
|
|
2651
2732
|
catch (err) {
|
|
2652
2733
|
clearInterval(heartbeat);
|
|
2734
|
+
process.off("SIGINT", checkpointSignal);
|
|
2735
|
+
process.off("SIGTERM", checkpointSignal);
|
|
2736
|
+
await foreman.close().catch(() => { });
|
|
2653
2737
|
statusReporter.stop("interrupted");
|
|
2738
|
+
if (interruptionRequested || err instanceof BuildInterruptedError) {
|
|
2739
|
+
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "interrupted", { status: "recoverable" }), "recoverable");
|
|
2740
|
+
log.write("safe-boundary-paused", { runId: buildRun.runId, checkpoint: "interrupted", intentional: true });
|
|
2741
|
+
console.error(`rafi: Build interrupted; current work is preserved. Continue with ${formatRecoveryCommand(cwd)}`);
|
|
2742
|
+
process.exit(2);
|
|
2743
|
+
}
|
|
2654
2744
|
if (err instanceof ContextCapabilityError || err instanceof CurrentWorkflowChangedError) {
|
|
2655
2745
|
const summary = err.message.slice(0, 1000);
|
|
2656
2746
|
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "safe-boundary-paused", {
|
|
@@ -2717,7 +2807,7 @@ export function buildStartCommand() {
|
|
|
2717
2807
|
}
|
|
2718
2808
|
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "failed", { status: "failed", failure: { category: "unknown", summary: err instanceof Error ? err.message.slice(0, 1000) : String(err).slice(0, 1000), at: new Date().toISOString() } }), "failed");
|
|
2719
2809
|
await foreman.close().catch(() => { });
|
|
2720
|
-
log.write("error", { message: String(err) });
|
|
2810
|
+
log.write("error", { message: String(err), ...(err instanceof Error ? { stack: err.stack } : {}) });
|
|
2721
2811
|
fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2722
2812
|
}
|
|
2723
2813
|
}
|
|
@@ -2801,6 +2891,7 @@ function pruneRecoveryTelemetryAtCompletion(projectDir, retentionDays) {
|
|
|
2801
2891
|
console.error(`rafi: recovery telemetry maintenance skipped: ${String(error).replace(/\s+/g, " ").slice(0, 300)}`);
|
|
2802
2892
|
}
|
|
2803
2893
|
}
|
|
2894
|
+
const reportedCompactionPolicies = new WeakMap();
|
|
2804
2895
|
async function prepareNativeAutoCompaction(adapter) {
|
|
2805
2896
|
if (!adapter.prepareAutoCompaction) {
|
|
2806
2897
|
await adapter.close().catch(() => { });
|
|
@@ -2808,11 +2899,14 @@ async function prepareNativeAutoCompaction(adapter) {
|
|
|
2808
2899
|
}
|
|
2809
2900
|
try {
|
|
2810
2901
|
const policy = await adapter.prepareAutoCompaction();
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
+
|
|
2902
|
+
const noticeKey = policy ? `${policy.requestedThresholdPercent}:${policy.effectiveThresholdPercent}` : undefined;
|
|
2903
|
+
if (policy && policy.effectiveThresholdPercent !== policy.requestedThresholdPercent && reportedCompactionPolicies.get(adapter) !== noticeKey) {
|
|
2904
|
+
console.log(`foreman: ${adapter.agent} reports a native automatic-compaction trigger of ${policy.effectiveThresholdPercent}% `
|
|
2905
|
+
+ `(requested ${policy.requestedThresholdPercent}%). Configuration does not mean that compaction occurred. `
|
|
2906
|
+
+ "The provider's effective trigger may differ from the saved preference.");
|
|
2815
2907
|
}
|
|
2908
|
+
if (noticeKey)
|
|
2909
|
+
reportedCompactionPolicies.set(adapter, noticeKey);
|
|
2816
2910
|
}
|
|
2817
2911
|
catch (error) {
|
|
2818
2912
|
await adapter.close().catch(() => { });
|
|
@@ -2975,10 +3069,10 @@ function readAgentDefaults(projectDir) {
|
|
|
2975
3069
|
function explicitDefaultValue(value) {
|
|
2976
3070
|
return value && value !== "default" ? value : undefined;
|
|
2977
3071
|
}
|
|
2978
|
-
function recoveryCompactionHistoryUncertain(projectDir, recovery, runId, role) {
|
|
3072
|
+
export function recoveryCompactionHistoryUncertain(projectDir, recovery, runId, role) {
|
|
2979
3073
|
if (!recovery)
|
|
2980
3074
|
return false;
|
|
2981
|
-
if (recovery.legacy || !recovery.runDecisions || !runId)
|
|
3075
|
+
if (recovery.legacy || (!recovery.runDecisions && recovery.version !== 3) || !runId)
|
|
2982
3076
|
return true;
|
|
2983
3077
|
const db = new WorkflowDb(projectDir);
|
|
2984
3078
|
try {
|
package/dist/cli/tickets.d.ts
CHANGED
|
@@ -61,5 +61,6 @@ export declare function ticketOwnershipReceipts(projectDir: string, commandName:
|
|
|
61
61
|
export declare function buildTicketsCommand(options?: {
|
|
62
62
|
prepareOwnership?: (projectDir: string, receipts: TicketOwnershipReceipt[]) => void;
|
|
63
63
|
recordOwnership?: (projectDir: string, receipts: TicketOwnershipReceipt[]) => void;
|
|
64
|
+
restartBuild?: (projectDir: string, runId: string, tickets: string[]) => Promise<void>;
|
|
64
65
|
}): Command;
|
|
65
66
|
export {};
|