ai-foreman 1.7.25 → 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 +15 -0
- package/dist/buildAssignment.js +142 -5
- package/dist/buildRuns.d.ts +6 -0
- package/dist/buildRuns.js +73 -7
- package/dist/cli/start.d.ts +5 -0
- package/dist/cli/start.js +123 -33
- 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 +1 -1
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, isInterruptedBuilderRecovery } 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";
|
|
@@ -471,7 +472,7 @@ export function buildStartCommand(options = {}) {
|
|
|
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")
|
|
@@ -960,7 +961,10 @@ export function buildStartCommand(options = {}) {
|
|
|
960
961
|
let activeContinuityRunId;
|
|
961
962
|
let liveStatusReporter;
|
|
962
963
|
let activeBuilderForStatus;
|
|
963
|
-
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" ? {
|
|
964
968
|
role, source: roleDefaults.builder ? "project" : "provider", make: agent, model: model ?? "default",
|
|
965
969
|
reasoning: builderEffort ?? "default", fast: Boolean(builderFast), session_strategy: roleDefaults.builder?.session_strategy ?? "compact",
|
|
966
970
|
settings_revision: settingsRevision, display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
|
|
@@ -1258,6 +1262,35 @@ export function buildStartCommand(options = {}) {
|
|
|
1258
1262
|
await continuous.validateFreshRecovery();
|
|
1259
1263
|
return continuous;
|
|
1260
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
|
+
};
|
|
1261
1294
|
const applyBuilderSettingsBoundary = async (runId, builderCwd, input) => {
|
|
1262
1295
|
const context = await input.adapter.contextUsage?.().catch(() => undefined);
|
|
1263
1296
|
const nativeUsage = await input.adapter.sessionUsage?.().catch(() => undefined);
|
|
@@ -1454,7 +1487,7 @@ export function buildStartCommand(options = {}) {
|
|
|
1454
1487
|
db.close();
|
|
1455
1488
|
} },
|
|
1456
1489
|
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
1457
|
-
return activeContinuityRunId ? db.
|
|
1490
|
+
return activeContinuityRunId ? db.roleMutationLease(activeContinuityRunId, "qa")?.generation ?? 0 : 0;
|
|
1458
1491
|
}
|
|
1459
1492
|
finally {
|
|
1460
1493
|
db.close();
|
|
@@ -1473,7 +1506,7 @@ export function buildStartCommand(options = {}) {
|
|
|
1473
1506
|
db.close();
|
|
1474
1507
|
} },
|
|
1475
1508
|
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
1476
|
-
return activeContinuityRunId ? db.
|
|
1509
|
+
return activeContinuityRunId ? db.roleMutationLease(activeContinuityRunId, "builder")?.generation ?? 0 : 0;
|
|
1477
1510
|
}
|
|
1478
1511
|
finally {
|
|
1479
1512
|
db.close();
|
|
@@ -1548,8 +1581,16 @@ export function buildStartCommand(options = {}) {
|
|
|
1548
1581
|
const packet = pendingQaRecoveryPacket(cwd, recoveryRecord.runId, requestedTicketIds[0]);
|
|
1549
1582
|
if (!packet)
|
|
1550
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
|
+
}
|
|
1551
1592
|
const confinement = describeQaHandle(adapter, qaCwd, { kind: "initial" }).confinement;
|
|
1552
|
-
handoffReceipt = { kind: "accepted", receipt: qaV2HandoffReceipt(accepted.acceptanceReceipt, {
|
|
1593
|
+
handoffReceipt = afterOwnedRemediation ? { kind: "initial" } : { kind: "accepted", receipt: qaV2HandoffReceipt(accepted.acceptanceReceipt, {
|
|
1553
1594
|
runId: packet.manifest.runId, ticketId: packet.manifest.ticketId, packetDigest: packet.manifest.packetDigest,
|
|
1554
1595
|
reviewedStateDigest: packet.manifest.reviewedStateDigest,
|
|
1555
1596
|
}, confinement.digest) };
|
|
@@ -1758,7 +1799,8 @@ export function buildStartCommand(options = {}) {
|
|
|
1758
1799
|
if (branchDefaults.createReview && branchDefaults.reviewProvider) {
|
|
1759
1800
|
await ensureReviewProviderReady(cwd, branchDefaults.reviewProvider, log, Boolean(opts.yes));
|
|
1760
1801
|
}
|
|
1761
|
-
|
|
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.`);
|
|
1762
1804
|
const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
|
|
1763
1805
|
label: "start",
|
|
1764
1806
|
yes: Boolean(opts.yes),
|
|
@@ -1937,7 +1979,7 @@ export function buildStartCommand(options = {}) {
|
|
|
1937
1979
|
}
|
|
1938
1980
|
resumeSessionByTicket.set(ticketId, { ...session, sessionRef: availability.sessionRef });
|
|
1939
1981
|
}
|
|
1940
|
-
const capturedBranchBuilder = {
|
|
1982
|
+
const capturedBranchBuilder = savedQaBuilderSettings ? { ...savedQaBuilderSettings } : {
|
|
1941
1983
|
role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
|
|
1942
1984
|
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
1943
1985
|
session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
@@ -1953,7 +1995,9 @@ export function buildStartCommand(options = {}) {
|
|
|
1953
1995
|
compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
|
|
1954
1996
|
};
|
|
1955
1997
|
const firstResumeRef = plan.nodes[0] ? resumeSessionByTicket.get(plan.nodes[0].ticket.id)?.sessionRef : undefined;
|
|
1956
|
-
|
|
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({
|
|
1957
2001
|
runId: invocationRunId,
|
|
1958
2002
|
builderCapabilities, tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
|
|
1959
2003
|
repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, baseRef: plan.baseRef, builder: capturedBranchBuilder, qa: capturedBranchQa,
|
|
@@ -1961,6 +2005,7 @@ export function buildStartCommand(options = {}) {
|
|
|
1961
2005
|
runDecisions: { dirtyWork, workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
1962
2006
|
});
|
|
1963
2007
|
activeContinuityRunId = masterRun.runId;
|
|
2008
|
+
publishActivatedBuilderSession = ref => { masterRun = persistCurrentBuildSession(cwd, masterRun, "builder", ref); };
|
|
1964
2009
|
if (requiresBuildApproval(cwd, plan.nodes.map(node => node.ticket.id), autoApprovePlanUpdates, approvalConsequences) && plan.issues.every((issue) => !issue.blocking)) {
|
|
1965
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({
|
|
1966
2011
|
message: branchPresentation.prompt,
|
|
@@ -2125,7 +2170,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2125
2170
|
db.close();
|
|
2126
2171
|
} },
|
|
2127
2172
|
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
2128
|
-
return db.
|
|
2173
|
+
return db.roleMutationLease(masterRun.runId, "builder")?.generation ?? 0;
|
|
2129
2174
|
}
|
|
2130
2175
|
finally {
|
|
2131
2176
|
db.close();
|
|
@@ -2149,9 +2194,12 @@ export function buildStartCommand(options = {}) {
|
|
|
2149
2194
|
}
|
|
2150
2195
|
},
|
|
2151
2196
|
qaNonconvergence,
|
|
2152
|
-
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,
|
|
2153
2198
|
observeBuilderNativeCompactions: async (adapter, worktreePath) => { await branchContextController(worktreePath).observeNativeCompactions(adapter); },
|
|
2154
|
-
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
|
+
},
|
|
2155
2203
|
qaSessionBoundary,
|
|
2156
2204
|
observeQaNativeCompactions,
|
|
2157
2205
|
});
|
|
@@ -2256,7 +2304,8 @@ export function buildStartCommand(options = {}) {
|
|
|
2256
2304
|
state.close();
|
|
2257
2305
|
}
|
|
2258
2306
|
}
|
|
2259
|
-
|
|
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.`);
|
|
2260
2309
|
const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
|
|
2261
2310
|
label: "start",
|
|
2262
2311
|
yes: Boolean(opts.yes),
|
|
@@ -2286,7 +2335,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2286
2335
|
qaModel = qaReady.model;
|
|
2287
2336
|
qaExecutable = qaReady.executable;
|
|
2288
2337
|
}
|
|
2289
|
-
const capturedBuilder = {
|
|
2338
|
+
const capturedBuilder = savedQaBuilderSettings ? { ...savedQaBuilderSettings } : {
|
|
2290
2339
|
role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
|
|
2291
2340
|
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
2292
2341
|
session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
@@ -2295,7 +2344,11 @@ export function buildStartCommand(options = {}) {
|
|
|
2295
2344
|
compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
|
|
2296
2345
|
};
|
|
2297
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 };
|
|
2298
|
-
|
|
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({
|
|
2299
2352
|
runId: invocationRunId,
|
|
2300
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,
|
|
2301
2354
|
frozenPolicy, autonomyProfile, qaEnabled,
|
|
@@ -2304,6 +2357,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2304
2357
|
buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities });
|
|
2305
2358
|
activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
|
|
2306
2359
|
activeContinuityRunId = buildRun.runId;
|
|
2360
|
+
publishActivatedBuilderSession = ref => { buildRun = persistCurrentBuildSession(cwd, buildRun, "builder", ref); };
|
|
2307
2361
|
const qaResumedRecovery = recoveryRecord && !isInterruptedBuilderRecovery(recoveryRecord) ? pendingQaRecoveryPacket(cwd, buildRun.runId, requestedTicketIds[0]) : undefined;
|
|
2308
2362
|
let qaFinalizationTicket;
|
|
2309
2363
|
let qaProtocolResumeTicket;
|
|
@@ -2347,19 +2401,38 @@ export function buildStartCommand(options = {}) {
|
|
|
2347
2401
|
}
|
|
2348
2402
|
const heartbeat = setInterval(() => { buildRun = heartbeatBuildRun(cwd, buildRun); }, 10_000);
|
|
2349
2403
|
heartbeat.unref();
|
|
2404
|
+
let interruptionRequested = false;
|
|
2405
|
+
let stopActiveBuild;
|
|
2350
2406
|
const checkpointSignal = () => {
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
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) }));
|
|
2355
2416
|
};
|
|
2356
2417
|
process.once("SIGINT", checkpointSignal);
|
|
2357
2418
|
process.once("SIGTERM", checkpointSignal);
|
|
2358
2419
|
let builder;
|
|
2359
2420
|
try {
|
|
2421
|
+
if (recoveryRecord)
|
|
2422
|
+
assertInterruptedSourceSnapshot(cwd, recoveryRecord);
|
|
2360
2423
|
builder = await createBuilder(cwd, resumeSessionId, resumeSessionRef);
|
|
2424
|
+
stopActiveBuild = () => builder.close();
|
|
2425
|
+
if (interruptionRequested) {
|
|
2426
|
+
await builder.close();
|
|
2427
|
+
throw new BuildInterruptedError();
|
|
2428
|
+
}
|
|
2361
2429
|
}
|
|
2362
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
|
+
}
|
|
2363
2436
|
if (!(error instanceof SessionUnavailableError))
|
|
2364
2437
|
throw error;
|
|
2365
2438
|
const summary = error.message.slice(0, 1000);
|
|
@@ -2417,7 +2490,13 @@ export function buildStartCommand(options = {}) {
|
|
|
2417
2490
|
return continuousBuilder(transfer.successor, cwd, buildRun.runId, settings);
|
|
2418
2491
|
},
|
|
2419
2492
|
});
|
|
2420
|
-
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();
|
|
2421
2500
|
activeBuilderForStatus = () => foreman.builderAdapter();
|
|
2422
2501
|
const statusReporter = liveStatusReporter = new AgentStatusReporter({
|
|
2423
2502
|
runId: buildRun.runId,
|
|
@@ -2433,7 +2512,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2433
2512
|
db.close();
|
|
2434
2513
|
} },
|
|
2435
2514
|
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
2436
|
-
return db.
|
|
2515
|
+
return db.roleMutationLease(buildRun.runId, "builder")?.generation ?? 0;
|
|
2437
2516
|
}
|
|
2438
2517
|
finally {
|
|
2439
2518
|
db.close();
|
|
@@ -2455,7 +2534,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2455
2534
|
builderFast ? "fast" : null,
|
|
2456
2535
|
qaEnabled ? null : "qa=off",
|
|
2457
2536
|
].filter(Boolean).join(" ");
|
|
2458
|
-
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)}]` : ""}`);
|
|
2459
2538
|
console.log(`${accent("foreman:")} project ${sanitizeTerminalText(cwd)}`);
|
|
2460
2539
|
if (trackerRelPath)
|
|
2461
2540
|
console.log(`${accent("foreman:")} tracker ${sanitizeTerminalText(trackerRelPath)}`);
|
|
@@ -2501,8 +2580,6 @@ export function buildStartCommand(options = {}) {
|
|
|
2501
2580
|
: foreman.completePendingQaRecovery(ticketId));
|
|
2502
2581
|
if (pendingHandoffGeneration !== undefined)
|
|
2503
2582
|
throw new Error(`recovery handoff generation ${pendingHandoffGeneration} for ${pendingHandoffRole} was not accepted`);
|
|
2504
|
-
if (foreman.builderSessionId())
|
|
2505
|
-
buildRun = persistBuildSession(cwd, buildRun, "builder", foreman.builderAdapter().sessionRef?.() ?? foreman.builderSessionId());
|
|
2506
2583
|
if (foreman.qaSessionId())
|
|
2507
2584
|
buildRun = persistBuildSession(cwd, buildRun, "qa", foreman.qaSessionRef() ?? foreman.qaSessionId());
|
|
2508
2585
|
buildRun = qa.outcome === "passed" || qa.outcome === "waived"
|
|
@@ -2654,7 +2731,16 @@ export function buildStartCommand(options = {}) {
|
|
|
2654
2731
|
}
|
|
2655
2732
|
catch (err) {
|
|
2656
2733
|
clearInterval(heartbeat);
|
|
2734
|
+
process.off("SIGINT", checkpointSignal);
|
|
2735
|
+
process.off("SIGTERM", checkpointSignal);
|
|
2736
|
+
await foreman.close().catch(() => { });
|
|
2657
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
|
+
}
|
|
2658
2744
|
if (err instanceof ContextCapabilityError || err instanceof CurrentWorkflowChangedError) {
|
|
2659
2745
|
const summary = err.message.slice(0, 1000);
|
|
2660
2746
|
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "safe-boundary-paused", {
|
|
@@ -2721,7 +2807,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2721
2807
|
}
|
|
2722
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");
|
|
2723
2809
|
await foreman.close().catch(() => { });
|
|
2724
|
-
log.write("error", { message: String(err) });
|
|
2810
|
+
log.write("error", { message: String(err), ...(err instanceof Error ? { stack: err.stack } : {}) });
|
|
2725
2811
|
fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2726
2812
|
}
|
|
2727
2813
|
}
|
|
@@ -2805,6 +2891,7 @@ function pruneRecoveryTelemetryAtCompletion(projectDir, retentionDays) {
|
|
|
2805
2891
|
console.error(`rafi: recovery telemetry maintenance skipped: ${String(error).replace(/\s+/g, " ").slice(0, 300)}`);
|
|
2806
2892
|
}
|
|
2807
2893
|
}
|
|
2894
|
+
const reportedCompactionPolicies = new WeakMap();
|
|
2808
2895
|
async function prepareNativeAutoCompaction(adapter) {
|
|
2809
2896
|
if (!adapter.prepareAutoCompaction) {
|
|
2810
2897
|
await adapter.close().catch(() => { });
|
|
@@ -2812,11 +2899,14 @@ async function prepareNativeAutoCompaction(adapter) {
|
|
|
2812
2899
|
}
|
|
2813
2900
|
try {
|
|
2814
2901
|
const policy = await adapter.prepareAutoCompaction();
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
+
|
|
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.");
|
|
2819
2907
|
}
|
|
2908
|
+
if (noticeKey)
|
|
2909
|
+
reportedCompactionPolicies.set(adapter, noticeKey);
|
|
2820
2910
|
}
|
|
2821
2911
|
catch (error) {
|
|
2822
2912
|
await adapter.close().catch(() => { });
|
|
@@ -2979,10 +3069,10 @@ function readAgentDefaults(projectDir) {
|
|
|
2979
3069
|
function explicitDefaultValue(value) {
|
|
2980
3070
|
return value && value !== "default" ? value : undefined;
|
|
2981
3071
|
}
|
|
2982
|
-
function recoveryCompactionHistoryUncertain(projectDir, recovery, runId, role) {
|
|
3072
|
+
export function recoveryCompactionHistoryUncertain(projectDir, recovery, runId, role) {
|
|
2983
3073
|
if (!recovery)
|
|
2984
3074
|
return false;
|
|
2985
|
-
if (recovery.legacy || !recovery.runDecisions || !runId)
|
|
3075
|
+
if (recovery.legacy || (!recovery.runDecisions && recovery.version !== 3) || !runId)
|
|
2986
3076
|
return true;
|
|
2987
3077
|
const db = new WorkflowDb(projectDir);
|
|
2988
3078
|
try {
|
package/dist/continuity.js
CHANGED
|
@@ -102,6 +102,7 @@ export function continuityInstruction(role) {
|
|
|
102
102
|
return [
|
|
103
103
|
"Before your final STEP_STATUS line (or as the final line when STEP_STATUS is not requested), emit exactly one single-line continuity record:",
|
|
104
104
|
`${CONTINUITY_MARKER} {"version":1,"decisions":[],"constraints":[],"discoveries":[],"completedActions":[],"evidence":[],"failures":[],"blockers":[],"openWork":[],"nextAction":"specific next action"}`,
|
|
105
|
+
"The continuity record is JSON. A requested STEP_STATUS line is separate plain text: never add JSON braces, a trailing comma, or a code fence to that status line.",
|
|
105
106
|
...(role === "qa" ? ["QA protocol: this continuity record is required even when the review requests only a report or status. Put it before RAFI_QA_FAILURE_REPORT_START when returning a failure envelope, outside the report JSON. Rafi strips the record before validating the report, so keep the report envelope immediately followed by STEP_STATUS."] : []),
|
|
106
107
|
"Keep it concise and cumulative for facts learned this turn. Do not include credentials, hidden reasoning, or raw transcripts.",
|
|
107
108
|
].join("\n");
|
|
@@ -177,13 +178,6 @@ export class ContinuityAdapter {
|
|
|
177
178
|
return withGraphSessionAccess(this.options.projectDir, this.adapter, () => this.sendTurnOwned(instruction, policy));
|
|
178
179
|
}
|
|
179
180
|
async sendTurnOwned(instruction, policy) {
|
|
180
|
-
const db = new WorkflowDb(this.options.projectDir);
|
|
181
|
-
try {
|
|
182
|
-
db.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: "turn_started", payload: { role: this.options.role, instructionDigest: sha(instruction), instructionBytes: Buffer.byteLength(instruction) }, authoritativeStateRevision: this.revision() });
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
db.close();
|
|
186
|
-
}
|
|
187
181
|
const providerInstruction = `${instruction}\n\n${continuityInstruction(this.options.role)}`;
|
|
188
182
|
const journal = new WorkflowDb(this.options.projectDir);
|
|
189
183
|
const dispatchId = `dispatch:${this.options.runId}:${this.options.role}:${randomUUID()}`;
|
|
@@ -209,6 +203,7 @@ export class ContinuityAdapter {
|
|
|
209
203
|
throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "a previous dispatched turn has no durable completion; reconcile it before sending more work");
|
|
210
204
|
journal.planOperation({ runId: this.options.runId, idempotencyKey: dispatchId, kind: "provider-dispatch", intent: { role: this.options.role, sessionRef: this.adapter.sessionRef?.(), instructionDigest: sha(instruction), instructionBytes: Buffer.byteLength(instruction) } });
|
|
211
205
|
journal.updateOperation(dispatchId, "in_progress");
|
|
206
|
+
journal.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: "turn_started", payload: { role: this.options.role, dispatchId, instructionDigest: sha(instruction), instructionBytes: Buffer.byteLength(instruction) }, authoritativeStateRevision: this.revision() });
|
|
212
207
|
});
|
|
213
208
|
try {
|
|
214
209
|
original = await dispatchWithGraphAccess(this.options.projectDir, this.adapter, providerInstruction, policy, (text, p) => this.adapter.sendTurn(text, p));
|
|
@@ -218,7 +213,11 @@ export class ContinuityAdapter {
|
|
|
218
213
|
throw error;
|
|
219
214
|
}
|
|
220
215
|
const responseDigest = journal.putEvidence("handoff", original.rawResponse ?? original.text);
|
|
221
|
-
journal.
|
|
216
|
+
journal.atomic(() => {
|
|
217
|
+
journal.updateOperation(dispatchId, original.failure?.dispatchState === "unknown" ? "uncertain" : original.failure?.dispatchState === "not-sent" ? "failed" : "confirmed", { result: { responseDigest, isError: original.isError, failure: original.failure, turnId: original.turnId, sessionRef: this.adapter.sessionRef?.() } });
|
|
218
|
+
if (original.failure?.dispatchState === "not-sent")
|
|
219
|
+
journal.appendContinuityEvent({ runId: this.options.runId, role: this.options.role, kind: "turn_not_sent", payload: { dispatchId, role: this.options.role }, authoritativeStateRevision: this.revision() });
|
|
220
|
+
});
|
|
222
221
|
}
|
|
223
222
|
finally {
|
|
224
223
|
journal.close();
|
|
@@ -418,8 +417,10 @@ export class ContinuityAdapter {
|
|
|
418
417
|
const db = new WorkflowDb(this.options.projectDir);
|
|
419
418
|
try {
|
|
420
419
|
const sessionRef = this.adapter.sessionRef?.();
|
|
421
|
-
db.
|
|
422
|
-
|
|
420
|
+
return db.atomic(() => {
|
|
421
|
+
db.appendContinuityEvent({ runId: this.options.runId, role: this.options.role, kind, payload: { isError: result.isError, numTurns: result.numTurns, sessionId: this.adapter.sessionId(), sessionRef, delta }, authoritativeStateRevision: this.revision(), sessionRef });
|
|
422
|
+
return db.publishContinuityCheckpoint({ runId: this.options.runId, role: this.options.role, delta, authoritativeStateRevision: this.revision(), sessionRef });
|
|
423
|
+
});
|
|
423
424
|
}
|
|
424
425
|
finally {
|
|
425
426
|
db.close();
|
package/dist/foreman.d.ts
CHANGED
|
@@ -61,6 +61,9 @@ export interface ForemanNotificationOptions {
|
|
|
61
61
|
terminalBell: boolean;
|
|
62
62
|
}
|
|
63
63
|
/** Drives one builder through a batch of N steps via the STEP_STATUS protocol. */
|
|
64
|
+
export declare class BuildInterruptedError extends Error {
|
|
65
|
+
constructor();
|
|
66
|
+
}
|
|
64
67
|
export declare class Foreman {
|
|
65
68
|
private builder;
|
|
66
69
|
private readonly log;
|
|
@@ -88,6 +91,10 @@ export declare class Foreman {
|
|
|
88
91
|
private readonly continueIndependentTickets;
|
|
89
92
|
private readonly builderWorktree?;
|
|
90
93
|
private readonly decisionPrompt?;
|
|
94
|
+
private interruptionRequested;
|
|
95
|
+
private activeQaAdapter?;
|
|
96
|
+
private assertNotInterrupted;
|
|
97
|
+
private createOwnedQa;
|
|
91
98
|
private graphPurpose;
|
|
92
99
|
setGraphPurpose(purpose: GraphPurpose): void;
|
|
93
100
|
private readonly ticketsEnabled;
|
|
@@ -160,6 +167,7 @@ export declare class Foreman {
|
|
|
160
167
|
builderSessionId(): string | undefined;
|
|
161
168
|
builderAdapter(): BuilderAdapter;
|
|
162
169
|
close(): Promise<void>;
|
|
170
|
+
requestStop(): Promise<void>;
|
|
163
171
|
private ticketForQa;
|
|
164
172
|
runBatch(n: number, trackerPath?: string, onTicketStart?: (ticketId: string) => void | Promise<void>, preferredTicketId?: string, recoveryTickets?: readonly string[]): Promise<BatchResult>;
|
|
165
173
|
}
|