ai-foreman 1.7.10 → 1.7.12
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 +3 -3
- package/dist/activity.d.ts +6 -1
- package/dist/activity.js +39 -2
- package/dist/agentRun.js +1 -1
- package/dist/branch/runner.d.ts +1 -0
- package/dist/branch/runner.js +18 -8
- package/dist/cli/start.js +170 -102
- package/dist/cli/tickets.js +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -1
- package/dist/foreman.d.ts +12 -2
- package/dist/foreman.js +82 -27
- package/dist/handoffs.d.ts +30 -2
- package/dist/handoffs.js +151 -16
- package/dist/log.d.ts +1 -1
- package/dist/notify.d.ts +4 -0
- package/dist/notify.js +15 -0
- package/dist/qaReview.d.ts +5 -0
- package/dist/qaReview.js +10 -2
- package/foreman.yaml +4 -0
- package/package.json +2 -2
package/dist/cli/start.js
CHANGED
|
@@ -31,6 +31,7 @@ import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig
|
|
|
31
31
|
import { loadDeliveryConfig, selectDeliveryUnitForRun, selectStacksForRun, updateStackDeliveryState } from "../tickets/delivery.js";
|
|
32
32
|
import { AgentStatusReporter, RoleStatusAdapter } from "../statusReporter.js";
|
|
33
33
|
import { currentActivity, withActivityPhase } from "../activity.js";
|
|
34
|
+
import { fireTerminalBell } from "../notify.js";
|
|
34
35
|
import { WorkflowDb, readCurrentWorkflowLease } from "../workflowDb.js";
|
|
35
36
|
import { makeLogPath as makeRoleLogPath, readOnlyPermissionConfig, runRoleInstruction } from "../agentRun.js";
|
|
36
37
|
import { eligibleTickets, evaluateTicketEligibility } from "../tickets/eligibility.js";
|
|
@@ -38,7 +39,7 @@ import { BUILTIN_BRANCH_PREFIX, validateBranchPrefix } from "../branch/prefix.js
|
|
|
38
39
|
import { captureCurrentWorkflowSessionIdentity, CurrentWorkflowChangedError, CurrentWorkflowGuardAdapter } from "../branch/currentGuard.js";
|
|
39
40
|
import { ContinuityAdapter, ContinuityRecoveryRequiredError, SessionUnavailableContinuityError } from "../continuity.js";
|
|
40
41
|
import { ContextCapabilityError, RoleSessionController, ThresholdCompactionController } from "../sessionLifecycle.js";
|
|
41
|
-
import { HandoffLoopError, HandoffService, parseBuilderHandoffRequest } from "../handoffs.js";
|
|
42
|
+
import { HandoffAcceptanceError, HandoffLoopError, HandoffRecoveryPausedError, HandoffService, parseBuilderHandoffRequest } from "../handoffs.js";
|
|
42
43
|
import { captureWorkspaceIdentity, createProviderSessionRef, resolveUniqueSessionBinding } from "../sessionIdentity.js";
|
|
43
44
|
import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
44
45
|
import { resolveProviderSessionAvailability } from "../sessionAvailability.js";
|
|
@@ -374,6 +375,9 @@ export function buildStartCommand() {
|
|
|
374
375
|
const recoveryRecord = opts.recoverRun ? readBuildRuns(cwd).find((run) => run.runId === opts.recoverRun) : undefined;
|
|
375
376
|
if (opts.recoverRun && !recoveryRecord)
|
|
376
377
|
fail(`recoverable build run not found: ${opts.recoverRun}`);
|
|
378
|
+
const autoApprovePlanUpdates = recoveryRecord
|
|
379
|
+
? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
|
|
380
|
+
: Boolean(opts.yes);
|
|
377
381
|
if (opts.recoveryMode) {
|
|
378
382
|
const allowed = ["exact-session", "fresh-with-handoff", "fresh-recovery-only", "guided-recovery"];
|
|
379
383
|
if (!allowed.includes(String(opts.recoveryMode)))
|
|
@@ -676,6 +680,34 @@ export function buildStartCommand() {
|
|
|
676
680
|
settings_revision: revision,
|
|
677
681
|
};
|
|
678
682
|
};
|
|
683
|
+
const settingsForRequestedRuntime = (base, requestedRuntime) => requestedRuntime && requestedRuntime !== base.make
|
|
684
|
+
? { ...base, make: requestedRuntime, model: "default", source: "cli" }
|
|
685
|
+
: base;
|
|
686
|
+
const handoffRecoveryOptions = (role) => ({
|
|
687
|
+
allowProviderSwitch: true,
|
|
688
|
+
desktopNotifications: config.notifications.enabled,
|
|
689
|
+
terminalBell: config.notifications.terminal_bell,
|
|
690
|
+
onAccepted: (accepted) => {
|
|
691
|
+
if (role === "builder" && accepted.successor.agent !== agent) {
|
|
692
|
+
agent = accepted.successor.agent;
|
|
693
|
+
model = undefined;
|
|
694
|
+
agentExecutable = undefined;
|
|
695
|
+
}
|
|
696
|
+
if (role === "qa" && accepted.successor.agent !== qaAgent) {
|
|
697
|
+
qaAgent = accepted.successor.agent;
|
|
698
|
+
qaModel = undefined;
|
|
699
|
+
qaExecutable = undefined;
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
const acceptedRuntimeSettings = (current, make, acceptedModel) => current.make === make && current.model === (acceptedModel ?? "default")
|
|
704
|
+
? current
|
|
705
|
+
: { ...current, make, model: acceptedModel ?? "default", source: "cli" };
|
|
706
|
+
const withRunLocalAcceptedRuntimes = (run) => ({
|
|
707
|
+
...run,
|
|
708
|
+
...(run.builder ? { builder: { ...run.builder, settings: acceptedRuntimeSettings(run.builder.settings, agent, model) } } : {}),
|
|
709
|
+
...(run.qa ? { qa: { ...run.qa, settings: acceptedRuntimeSettings(run.qa.settings, qaAgent, qaModel) } } : {}),
|
|
710
|
+
});
|
|
679
711
|
const createBuilderForSettings = async (builderCwd, settings) => {
|
|
680
712
|
const ready = await ensureRuntimeReadyForCommand(builderCwd, settings.make, {
|
|
681
713
|
label: "live Builder settings",
|
|
@@ -757,7 +789,7 @@ export function buildStartCommand() {
|
|
|
757
789
|
initial,
|
|
758
790
|
runtime: agent,
|
|
759
791
|
label: "builder turn",
|
|
760
|
-
enabled:
|
|
792
|
+
enabled: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
761
793
|
allowSwitch: !(opts.resume || opts.continue),
|
|
762
794
|
recreate: async (nextRuntime, resumeSessionId, resumeRef) => {
|
|
763
795
|
const nextReady = await ensureRuntimeReadyForCommand(builderCwd, nextRuntime, {
|
|
@@ -806,7 +838,7 @@ export function buildStartCommand() {
|
|
|
806
838
|
} } : {}),
|
|
807
839
|
compactionCount, compactMaximum: effectiveSettings.compact_maximum,
|
|
808
840
|
resources: [{ label: "continuity-reconstruction", content: reconstruction, authoritative: true }],
|
|
809
|
-
}, () => createBuilderForSettings(builderCwd, effectiveSettings));
|
|
841
|
+
}, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(effectiveSettings, runtime)), handoffRecoveryOptions("builder"));
|
|
810
842
|
log.write("handoff-transfer", {
|
|
811
843
|
role: "builder", reason, recovery: "double-invalid-continuity", generation: transfer.manifest.generation,
|
|
812
844
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -854,7 +886,7 @@ export function buildStartCommand() {
|
|
|
854
886
|
compactionCount,
|
|
855
887
|
compactMaximum: effectiveSettings.compact_maximum,
|
|
856
888
|
resources: [{ label: "frozen-action", content: frozenAction, authoritative: true }],
|
|
857
|
-
}, () => createBuilderForSettings(builderCwd, effectiveSettings));
|
|
889
|
+
}, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(effectiveSettings, runtime)), handoffRecoveryOptions("builder"));
|
|
858
890
|
log.write("handoff-transfer", {
|
|
859
891
|
requestedByBuilder: true, reason: request.reason, generation: transfer.manifest.generation,
|
|
860
892
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -869,7 +901,8 @@ export function buildStartCommand() {
|
|
|
869
901
|
let adapter = await createRecoveringBuilder(builderCwd, effectiveSessionId, effectiveSessionId ? sessionRef : undefined);
|
|
870
902
|
if (pendingHandoffGeneration !== undefined && pendingHandoffRole === "builder" && activeContinuityRunId === recoveryRecord?.runId) {
|
|
871
903
|
const service = new HandoffService(cwd);
|
|
872
|
-
const
|
|
904
|
+
const staged = service.loadStaged(recoveryRecord.runId, pendingHandoffGeneration);
|
|
905
|
+
const accepted = await service.acceptStagedWithRecovery(staged, adapter, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(resolvedContinuitySettings("builder"), runtime)), handoffRecoveryOptions("builder"));
|
|
873
906
|
adapter = accepted.successor;
|
|
874
907
|
pendingHandoffGeneration = undefined;
|
|
875
908
|
log.write("recovery-handoff-accepted", { role: "builder", generation: accepted.manifest.generation, successorSessionId: accepted.successorSessionId, acceptanceCheckpointDigest: accepted.acceptanceCheckpointDigest });
|
|
@@ -904,7 +937,7 @@ export function buildStartCommand() {
|
|
|
904
937
|
} } : {}),
|
|
905
938
|
compactionCount, compactMaximum: input.next.compact_maximum,
|
|
906
939
|
resources: [{ label: "frozen-action", content: input.frozenAction, authoritative: true }],
|
|
907
|
-
}, () => createBuilderForSettings(builderCwd, input.next));
|
|
940
|
+
}, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(input.next, runtime)), handoffRecoveryOptions("builder"));
|
|
908
941
|
log.write("handoff-transfer", {
|
|
909
942
|
role: "builder", reason, settingsRevision: input.next.settings_revision, generation: transfer.manifest.generation,
|
|
910
943
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -960,7 +993,7 @@ export function buildStartCommand() {
|
|
|
960
993
|
initial,
|
|
961
994
|
runtime: qaAgent,
|
|
962
995
|
label: "QA turn",
|
|
963
|
-
enabled:
|
|
996
|
+
enabled: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
964
997
|
allowSwitch: !sessionRef,
|
|
965
998
|
recreate: async (nextRuntime, resumeSessionId, resumeRef) => {
|
|
966
999
|
const nextReady = await ensureRuntimeReadyForCommand(qaCwd, nextRuntime, {
|
|
@@ -1025,7 +1058,7 @@ export function buildStartCommand() {
|
|
|
1025
1058
|
} } : {}),
|
|
1026
1059
|
compactionCount, compactMaximum: effectiveSettings.compact_maximum,
|
|
1027
1060
|
resources: [{ label: "continuity-reconstruction", content: reconstruction, authoritative: true }],
|
|
1028
|
-
}, () => createQaForSettings(qaCwd, effectiveSettings));
|
|
1061
|
+
}, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(effectiveSettings, runtime)), handoffRecoveryOptions("qa"));
|
|
1029
1062
|
log.write("handoff-transfer", {
|
|
1030
1063
|
role: "qa", reason, recovery: "double-invalid-continuity", generation: transfer.manifest.generation,
|
|
1031
1064
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -1082,7 +1115,8 @@ export function buildStartCommand() {
|
|
|
1082
1115
|
let explicitRecoveryAccepted = false;
|
|
1083
1116
|
if (pendingHandoffGeneration !== undefined && pendingHandoffRole === "qa" && activeContinuityRunId === recoveryRecord?.runId) {
|
|
1084
1117
|
const service = new HandoffService(cwd);
|
|
1085
|
-
const
|
|
1118
|
+
const staged = service.loadStaged(recoveryRecord.runId, pendingHandoffGeneration);
|
|
1119
|
+
const accepted = await service.acceptStagedWithRecovery(staged, adapter, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(resolvedContinuitySettings("qa"), runtime)), handoffRecoveryOptions("qa"));
|
|
1086
1120
|
adapter = accepted.successor;
|
|
1087
1121
|
pendingHandoffGeneration = undefined;
|
|
1088
1122
|
explicitRecoveryAccepted = true;
|
|
@@ -1116,7 +1150,7 @@ export function buildStartCommand() {
|
|
|
1116
1150
|
compactionCount,
|
|
1117
1151
|
compactMaximum: resolvedContinuitySettings("qa").compact_maximum,
|
|
1118
1152
|
});
|
|
1119
|
-
const accepted = await service.
|
|
1153
|
+
const accepted = await service.acceptStagedWithRecovery(staged, adapter, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(resolvedContinuitySettings("qa"), runtime)), handoffRecoveryOptions("qa"));
|
|
1120
1154
|
adapter = accepted.successor;
|
|
1121
1155
|
log.write("handoff-transfer", {
|
|
1122
1156
|
role: "qa", reason: staged.manifest.reason, generation: accepted.manifest.generation,
|
|
@@ -1155,7 +1189,7 @@ export function buildStartCommand() {
|
|
|
1155
1189
|
} } : {}),
|
|
1156
1190
|
compactionCount, compactMaximum: input.next.compact_maximum,
|
|
1157
1191
|
resources: [{ label: "frozen-qa-action", content: input.frozenAction, authoritative: true }],
|
|
1158
|
-
}, () => createQaForSettings(qaCwd, input.next));
|
|
1192
|
+
}, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(input.next, runtime)), handoffRecoveryOptions("qa"));
|
|
1159
1193
|
log.write("handoff-transfer", {
|
|
1160
1194
|
role: "qa", reason, settingsRevision: input.next.settings_revision, generation: transfer.manifest.generation,
|
|
1161
1195
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -1198,7 +1232,7 @@ export function buildStartCommand() {
|
|
|
1198
1232
|
} } : {}),
|
|
1199
1233
|
compactionCount, compactMaximum: settings.compact_maximum ?? 10,
|
|
1200
1234
|
resources: [{ label: "frozen-qa-action", content: frozenAction, authoritative: true }],
|
|
1201
|
-
}, () => createQaForSettings(qaCwd, settings));
|
|
1235
|
+
}, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(settings, runtime)), handoffRecoveryOptions("qa"));
|
|
1202
1236
|
log.write("handoff-transfer", { role: "qa", generation: transfer.manifest.generation, predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId, acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest });
|
|
1203
1237
|
await predecessor.close().catch(() => { });
|
|
1204
1238
|
return decorateQa(transfer.successor, qaCwd, undefined, settings);
|
|
@@ -1219,7 +1253,7 @@ export function buildStartCommand() {
|
|
|
1219
1253
|
});
|
|
1220
1254
|
await controller.observeNativeCompactions(adapter);
|
|
1221
1255
|
};
|
|
1222
|
-
const qaNonconvergence = createQaNonconvergenceHandler(cwd,
|
|
1256
|
+
const qaNonconvergence = createQaNonconvergenceHandler(cwd, !process.stdin.isTTY || !process.stdout.isTTY, roleDefaults.planner);
|
|
1223
1257
|
if (branchMode) {
|
|
1224
1258
|
const ticketsConfig = loadTicketsConfig(cwd);
|
|
1225
1259
|
const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
|
|
@@ -1408,7 +1442,7 @@ export function buildStartCommand() {
|
|
|
1408
1442
|
console.log(` ! ${issue.ticket ?? "plan"}: ${issue.message}`);
|
|
1409
1443
|
}
|
|
1410
1444
|
console.log();
|
|
1411
|
-
if (!
|
|
1445
|
+
if (!autoApprovePlanUpdates && plan.issues.every((issue) => !issue.blocking)) {
|
|
1412
1446
|
const action = await select({
|
|
1413
1447
|
message: branchPresentation.prompt,
|
|
1414
1448
|
options: [
|
|
@@ -1484,7 +1518,7 @@ export function buildStartCommand() {
|
|
|
1484
1518
|
...(nativeUsage ? { sessionUsage: { version: 1, runId: masterRun.runId, role: "builder", provider: adapter.agent, providerSessionId: adapter.sessionId(), observedAt: nativeUsage.observedAt, source: nativeUsage.source, cumulativeInputTokens: nativeUsage.inputTokens, cumulativeOutputTokens: nativeUsage.outputTokens, cumulativeTotalTokens: nativeUsage.totalTokens, authoritativeCostUsd: nativeUsage.authoritativeCostUsd } } : {}),
|
|
1485
1519
|
compactionCount, compactMaximum: settings.compact_maximum ?? 10,
|
|
1486
1520
|
resources: [{ label: "frozen-action", content: frozenAction, authoritative: true }],
|
|
1487
|
-
}, () => createBuilderForSettings(worktreePath, settings));
|
|
1521
|
+
}, (_handoff, runtime) => createBuilderForSettings(worktreePath, settingsForRequestedRuntime(settings, runtime)), handoffRecoveryOptions("builder"));
|
|
1488
1522
|
log.write("handoff-transfer", { worktreePath, generation: transfer.manifest.generation, predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId, acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest });
|
|
1489
1523
|
if (adapter instanceof ContinuityAdapter) {
|
|
1490
1524
|
await adapter.adoptValidatedSuccessor(transfer.successor);
|
|
@@ -1500,93 +1534,111 @@ export function buildStartCommand() {
|
|
|
1500
1534
|
const branchHeartbeat = setInterval(() => { masterRun = heartbeatBuildRun(cwd, masterRun); }, 10_000);
|
|
1501
1535
|
branchHeartbeat.unref();
|
|
1502
1536
|
let branchSessionFailure;
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1537
|
+
let summaries;
|
|
1538
|
+
try {
|
|
1539
|
+
summaries = await runBranchPlan({
|
|
1540
|
+
projectDir: cwd,
|
|
1541
|
+
runId: masterRun.runId,
|
|
1542
|
+
plan,
|
|
1543
|
+
log,
|
|
1544
|
+
agent,
|
|
1545
|
+
model,
|
|
1546
|
+
effort: builderEffort,
|
|
1547
|
+
fast: builderFast,
|
|
1548
|
+
notificationsEnabled: config.notifications.enabled,
|
|
1549
|
+
terminalBellEnabled: config.notifications.terminal_bell,
|
|
1550
|
+
qaEnabled,
|
|
1551
|
+
createPr: branchDefaults.createReview,
|
|
1552
|
+
completionMode: branchDefaults.completionMode,
|
|
1553
|
+
reviewProvider: branchDefaults.reviewProvider,
|
|
1554
|
+
prReady: branchDefaults.prReady,
|
|
1555
|
+
keepWorktrees: Boolean(opts.keepWorktrees) || selectedStacks.length > 0,
|
|
1556
|
+
cleanupBranches: branchDefaults.cleanupBranches,
|
|
1557
|
+
autoMergeWait: branchDefaults.autoMergeWait,
|
|
1558
|
+
autoMergeTimeoutMinutes: branchDefaults.autoMergeTimeoutMinutes,
|
|
1559
|
+
mergeMethod: branchDefaults.mergeMethod,
|
|
1560
|
+
allowedBaseDirtyPaths,
|
|
1561
|
+
baseWorktreePolicy,
|
|
1562
|
+
trackerPaths: {
|
|
1563
|
+
progressDoc: ticketsConfig.paths.progressDoc,
|
|
1564
|
+
archiveDoc: ticketsConfig.paths.archiveDoc,
|
|
1565
|
+
},
|
|
1566
|
+
resumeSessions: resumeSessionByTicket,
|
|
1567
|
+
createBuilder: (builderCwd, sessionId, sessionRef) => createBuilder(builderCwd, sessionId, sessionRef),
|
|
1568
|
+
recordBuilderSession: (session, ticketId) => { masterRun = persistBuildSession(cwd, { ...masterRun, currentTicket: ticketId }, "builder", session); },
|
|
1569
|
+
recordQaSession: (session, ticketId) => { masterRun = persistBuildSession(cwd, { ...masterRun, currentTicket: ticketId }, "qa", session); },
|
|
1570
|
+
onSessionUnavailable: (error) => { branchSessionFailure = error; },
|
|
1571
|
+
createQa: (qaCwd, sessionId) => createQa(qaCwd, sessionId),
|
|
1572
|
+
builderSessionStrategy: roleDefaults.builder?.session_strategy ?? "compact",
|
|
1573
|
+
qaSessionStrategy: roleDefaults.qa?.session_strategy ?? "compact",
|
|
1574
|
+
observeBuilder: async (builder) => {
|
|
1575
|
+
liveStatusReporter?.stop();
|
|
1576
|
+
activeBuilderForStatus = () => builder;
|
|
1577
|
+
const reporter = liveStatusReporter = new AgentStatusReporter({
|
|
1578
|
+
runId: masterRun.runId, role: "builder", provider: builder.agent, model: capturedBranchBuilder.model,
|
|
1579
|
+
reasoning: capturedBranchBuilder.reasoning, fast: capturedBranchBuilder.fast, step: Math.min(plan.nodes.length, branchContextControllers.size + 1), total: plan.nodes.length,
|
|
1580
|
+
phase: "builder ticket session", sessionTransition: builder.sessionId() ? "resumed session" : "initial session", adapter: builder,
|
|
1581
|
+
settingsRevision: capturedBranchBuilder.settings_revision, displaySessionCost: capturedBranchBuilder.display_session_cost ?? false,
|
|
1582
|
+
compactionCount: () => { const db = new WorkflowDb(cwd); try {
|
|
1583
|
+
return db.successfulCompactionCount(masterRun.runId, "builder", builder.sessionRef?.() ?? builder.sessionId());
|
|
1584
|
+
}
|
|
1585
|
+
finally {
|
|
1586
|
+
db.close();
|
|
1587
|
+
} },
|
|
1588
|
+
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
1589
|
+
return db.handoffs(masterRun.runId).at(-1)?.generation ?? 0;
|
|
1590
|
+
}
|
|
1591
|
+
finally {
|
|
1592
|
+
db.close();
|
|
1593
|
+
} },
|
|
1594
|
+
}, (line, snapshot) => {
|
|
1595
|
+
const activity = currentActivity();
|
|
1596
|
+
if (activity && process.stdout.isTTY)
|
|
1597
|
+
activity.setAgentStatus(line.replace(/^\[[^\]]+\]\s*/, ""));
|
|
1598
|
+
else
|
|
1599
|
+
console.log(line);
|
|
1600
|
+
log.write("agent-status", { ...snapshot });
|
|
1601
|
+
const db = new WorkflowDb(cwd);
|
|
1602
|
+
try {
|
|
1603
|
+
db.recordTelemetry(masterRun.runId, snapshot);
|
|
1604
|
+
db.recordContextSample(snapshot.contextSample);
|
|
1605
|
+
if (snapshot.sessionUsage)
|
|
1606
|
+
db.recordSessionUsage(snapshot.sessionUsage);
|
|
1607
|
+
}
|
|
1608
|
+
finally {
|
|
1609
|
+
db.close();
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
reporter.start();
|
|
1565
1613
|
try {
|
|
1566
|
-
|
|
1567
|
-
db.recordContextSample(snapshot.contextSample);
|
|
1568
|
-
if (snapshot.sessionUsage)
|
|
1569
|
-
db.recordSessionUsage(snapshot.sessionUsage);
|
|
1614
|
+
await printEvents(builder.events());
|
|
1570
1615
|
}
|
|
1571
1616
|
finally {
|
|
1572
|
-
|
|
1617
|
+
reporter.stop();
|
|
1573
1618
|
}
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1619
|
+
},
|
|
1620
|
+
qaNonconvergence,
|
|
1621
|
+
beforeBuilderTurn: async (adapter, frozenAction, worktreePath) => (await branchContextController(worktreePath).atSafeBoundary(adapter, frozenAction)).adapter,
|
|
1622
|
+
observeBuilderNativeCompactions: async (adapter, worktreePath) => { await branchContextController(worktreePath).observeNativeCompactions(adapter); },
|
|
1623
|
+
builderSessionBoundary: async (adapter, frozenAction, strategy, worktreePath) => (await branchContextController(worktreePath).atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter,
|
|
1624
|
+
qaSessionBoundary,
|
|
1625
|
+
observeQaNativeCompactions,
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
catch (error) {
|
|
1629
|
+
if (!(error instanceof HandoffAcceptanceError) && !(error instanceof HandoffRecoveryPausedError))
|
|
1630
|
+
throw error;
|
|
1631
|
+
const summary = error.message.slice(0, 1000);
|
|
1632
|
+
masterRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, masterRun, "handoff-recovery-required", {
|
|
1633
|
+
status: "recoverable",
|
|
1634
|
+
failure: { category: "unknown", summary, at: new Date().toISOString() },
|
|
1635
|
+
}), "recoverable");
|
|
1636
|
+
clearInterval(branchHeartbeat);
|
|
1637
|
+
log.write("safe-boundary-paused", { runId: masterRun.runId, message: summary, handoff: true });
|
|
1638
|
+
console.error(`foreman: handoff paused at a recoverable checkpoint: ${summary}`);
|
|
1639
|
+
console.error(`foreman: resume this run with: rafi build:resume ${shellQuote(cwd)} --run ${shellQuote(masterRun.runId)}`);
|
|
1640
|
+
process.exit(2);
|
|
1641
|
+
}
|
|
1590
1642
|
if (branchSessionFailure) {
|
|
1591
1643
|
const failure = branchSessionFailure;
|
|
1592
1644
|
const guided = failure instanceof SessionUnavailableContinuityError ? failure.guidedRecovery : failure.failure.dispatchState === "unknown";
|
|
@@ -1624,6 +1676,7 @@ export function buildStartCommand() {
|
|
|
1624
1676
|
}
|
|
1625
1677
|
console.log(allComplete ? "foreman: stack publication complete; delivery is awaiting_review (no merges were attempted)" : "foreman: partial stack prefix published; resume point is preserved");
|
|
1626
1678
|
}
|
|
1679
|
+
masterRun = withRunLocalAcceptedRuntimes(masterRun);
|
|
1627
1680
|
masterRun = summaries.every((row) => row.buildStatus === "done")
|
|
1628
1681
|
? completeBuildRun(cwd, masterRun)
|
|
1629
1682
|
: releaseBuildLease(cwd, checkpointBuildRun(cwd, masterRun, "branch-run-interrupted", { status: "recoverable" }), "recoverable");
|
|
@@ -1651,6 +1704,7 @@ export function buildStartCommand() {
|
|
|
1651
1704
|
if (masterRun.status === "completed")
|
|
1652
1705
|
printHandoffPruneCommand(masterRun.runId);
|
|
1653
1706
|
const failed = summaries.some((row) => row.buildStatus === "blocked" || row.buildStatus === "needs-human");
|
|
1707
|
+
fireTerminalBell(config.notifications.terminal_bell);
|
|
1654
1708
|
process.exit(failed ? 2 : 0);
|
|
1655
1709
|
}
|
|
1656
1710
|
const ready = await ensureRuntimeReadyForCommand(cwd, agent, {
|
|
@@ -1759,7 +1813,7 @@ export function buildStartCommand() {
|
|
|
1759
1813
|
compactionCount,
|
|
1760
1814
|
compactMaximum: settings.compact_maximum ?? 10,
|
|
1761
1815
|
resources: [{ label: "frozen-action", content: frozenAction, authoritative: true }],
|
|
1762
|
-
}, () => createBuilderForSettings(cwd, settings));
|
|
1816
|
+
}, (_handoff, runtime) => createBuilderForSettings(cwd, settingsForRequestedRuntime(settings, runtime)), handoffRecoveryOptions("builder"));
|
|
1763
1817
|
log.write("handoff-transfer", { generation: transfer.manifest.generation, predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId, acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest });
|
|
1764
1818
|
if (adapter instanceof ContinuityAdapter) {
|
|
1765
1819
|
await adapter.adoptValidatedSuccessor(transfer.successor);
|
|
@@ -1769,7 +1823,7 @@ export function buildStartCommand() {
|
|
|
1769
1823
|
return continuousBuilder(transfer.successor, cwd, buildRun.runId, settings);
|
|
1770
1824
|
},
|
|
1771
1825
|
});
|
|
1772
|
-
const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence, async (adapter, frozenAction) => (await contextController.atSafeBoundary(adapter, frozenAction)).adapter, async (adapter, frozenAction, strategy) => (await contextController.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter, qaSessionBoundary, observeQaNativeCompactions, async (adapter) => { await contextController.observeNativeCompactions(adapter); });
|
|
1826
|
+
const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, qaEnabled, 3, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence, async (adapter, frozenAction) => (await contextController.atSafeBoundary(adapter, frozenAction)).adapter, async (adapter, frozenAction, strategy) => (await contextController.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter, qaSessionBoundary, observeQaNativeCompactions, async (adapter) => { await contextController.observeNativeCompactions(adapter); });
|
|
1773
1827
|
activeBuilderForStatus = () => foreman.builderAdapter();
|
|
1774
1828
|
const statusReporter = liveStatusReporter = new AgentStatusReporter({
|
|
1775
1829
|
runId: buildRun.runId,
|
|
@@ -1825,12 +1879,13 @@ export function buildStartCommand() {
|
|
|
1825
1879
|
console.log("ai-foreman: asking builder to plan the next tickets or steps...\n");
|
|
1826
1880
|
buildRun = checkpointBuildRun(cwd, buildRun, "before-preflight");
|
|
1827
1881
|
await foreman.runPreflight(steps, ticketsContent, preferredTicket);
|
|
1882
|
+
buildRun = withRunLocalAcceptedRuntimes(buildRun);
|
|
1828
1883
|
if (foreman.builderSessionId())
|
|
1829
1884
|
buildRun = persistBuildSession(cwd, buildRun, "builder", foreman.builderAdapter().sessionRef?.() ?? foreman.builderSessionId());
|
|
1830
1885
|
if (foreman.qaSessionId())
|
|
1831
1886
|
buildRun = persistBuildSession(cwd, buildRun, "qa", foreman.qaSessionRef() ?? foreman.qaSessionId());
|
|
1832
1887
|
buildRun = checkpointBuildRun(cwd, buildRun, "preflight-complete");
|
|
1833
|
-
if (!
|
|
1888
|
+
if (!autoApprovePlanUpdates) {
|
|
1834
1889
|
while (true) {
|
|
1835
1890
|
console.log();
|
|
1836
1891
|
const action = await select({
|
|
@@ -1902,7 +1957,8 @@ export function buildStartCommand() {
|
|
|
1902
1957
|
console.log(line);
|
|
1903
1958
|
}
|
|
1904
1959
|
}
|
|
1905
|
-
|
|
1960
|
+
fireTerminalBell(config.notifications.terminal_bell);
|
|
1961
|
+
process.exit(result.outcome === "needs-human" || result.outcome === "blocked" ? 2 : 0);
|
|
1906
1962
|
}
|
|
1907
1963
|
catch (err) {
|
|
1908
1964
|
clearInterval(heartbeat);
|
|
@@ -1934,6 +1990,18 @@ export function buildStartCommand() {
|
|
|
1934
1990
|
: `foreman: inspect recovery with: rafi build:resume ${shellQuote(cwd)} --run ${shellQuote(buildRun.runId)} --inspect`);
|
|
1935
1991
|
process.exit(2);
|
|
1936
1992
|
}
|
|
1993
|
+
if (err instanceof HandoffAcceptanceError || err instanceof HandoffRecoveryPausedError) {
|
|
1994
|
+
const summary = err.message.slice(0, 1000);
|
|
1995
|
+
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "handoff-recovery-required", {
|
|
1996
|
+
status: "recoverable",
|
|
1997
|
+
failure: { category: "unknown", summary, at: new Date().toISOString() },
|
|
1998
|
+
}), "recoverable");
|
|
1999
|
+
await foreman.close().catch(() => { });
|
|
2000
|
+
log.write("safe-boundary-paused", { runId: buildRun.runId, message: summary, handoff: true });
|
|
2001
|
+
console.error(`foreman: handoff paused at a recoverable checkpoint: ${summary}`);
|
|
2002
|
+
console.error(`foreman: resume this run with: rafi build:resume ${shellQuote(cwd)} --run ${shellQuote(buildRun.runId)}`);
|
|
2003
|
+
process.exit(2);
|
|
2004
|
+
}
|
|
1937
2005
|
if (err instanceof HandoffLoopError || err instanceof ContinuityRecoveryRequiredError) {
|
|
1938
2006
|
const summary = err.message.slice(0, 1000);
|
|
1939
2007
|
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "guided-recovery-required", {
|
package/dist/cli/tickets.js
CHANGED
|
@@ -1026,7 +1026,7 @@ export async function cmdPopulateCli(opts) {
|
|
|
1026
1026
|
}));
|
|
1027
1027
|
builder = roleBuilder.builder;
|
|
1028
1028
|
viewer = printEvents(builder.events());
|
|
1029
|
-
const foreman = new Foreman(builder, log, config.notifications.enabled, false, 3, dir);
|
|
1029
|
+
const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, false, 3, dir);
|
|
1030
1030
|
console.log(`foreman tickets: populating tickets with ${roleBuilder.runtime}`);
|
|
1031
1031
|
console.log(`foreman tickets: project ${dir}`);
|
|
1032
1032
|
console.log(`foreman tickets: role ${TICKET_POPULATE_ROLE}`);
|
package/dist/config.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface PermissionConfig {
|
|
|
13
13
|
export interface NotificationsConfig {
|
|
14
14
|
/** Fire a desktop notification when the builder needs user input. Default: false. */
|
|
15
15
|
enabled: boolean;
|
|
16
|
+
/** Ring the terminal bell when user attention is required or a run finishes. Default: true. */
|
|
17
|
+
terminal_bell: boolean;
|
|
16
18
|
}
|
|
17
19
|
export interface QaConfig {
|
|
18
20
|
/** Run a QA review pass after each completed ticket or step. Default: true. */
|
package/dist/config.js
CHANGED
|
@@ -65,7 +65,7 @@ export const DEFAULT_CONFIG = {
|
|
|
65
65
|
],
|
|
66
66
|
escalateTools: ["WebFetch", "WebSearch"],
|
|
67
67
|
},
|
|
68
|
-
notifications: { enabled: false },
|
|
68
|
+
notifications: { enabled: false, terminal_bell: true },
|
|
69
69
|
qa: { enabled: true },
|
|
70
70
|
};
|
|
71
71
|
/** Load foreman.yaml if present and deep-merge it over the defaults. */
|
|
@@ -104,6 +104,10 @@ function validateConfig(raw, path) {
|
|
|
104
104
|
}
|
|
105
105
|
if (cfg.notifications !== undefined) {
|
|
106
106
|
validateBooleanObject(cfg.notifications, "notifications", path);
|
|
107
|
+
const terminalBell = cfg.notifications.terminal_bell;
|
|
108
|
+
if (terminalBell !== undefined && typeof terminalBell !== "boolean") {
|
|
109
|
+
throw new Error(`${path}: notifications.terminal_bell must be a boolean`);
|
|
110
|
+
}
|
|
107
111
|
}
|
|
108
112
|
if (cfg.qa !== undefined) {
|
|
109
113
|
validateBooleanObject(cfg.qa, "qa", path);
|
package/dist/foreman.d.ts
CHANGED
|
@@ -48,11 +48,14 @@ export interface BatchResult {
|
|
|
48
48
|
outcome: "all-done" | "plan-complete" | "blocked" | "needs-human";
|
|
49
49
|
detail?: string;
|
|
50
50
|
}
|
|
51
|
+
export interface ForemanNotificationOptions {
|
|
52
|
+
desktop: boolean;
|
|
53
|
+
terminalBell: boolean;
|
|
54
|
+
}
|
|
51
55
|
/** Drives one builder through a batch of N steps via the STEP_STATUS protocol. */
|
|
52
56
|
export declare class Foreman {
|
|
53
57
|
private builder;
|
|
54
58
|
private readonly log;
|
|
55
|
-
private readonly notificationsEnabled;
|
|
56
59
|
private readonly qaEnabled;
|
|
57
60
|
private readonly qaMaxCycles;
|
|
58
61
|
private readonly projectDir?;
|
|
@@ -69,9 +72,11 @@ export declare class Foreman {
|
|
|
69
72
|
/** Persist a Builder's provider-native event immediately after every turn. */
|
|
70
73
|
private readonly observeBuilderNativeCompactions?;
|
|
71
74
|
private readonly ticketsEnabled;
|
|
75
|
+
private readonly notificationsEnabled;
|
|
76
|
+
private readonly terminalBellEnabled;
|
|
72
77
|
private readonly qaStream;
|
|
73
78
|
private builderWorkSessions;
|
|
74
|
-
constructor(builder: BuilderAdapter, log: Log,
|
|
79
|
+
constructor(builder: BuilderAdapter, log: Log, notifications?: boolean | ForemanNotificationOptions, qaEnabled?: boolean, qaMaxCycles?: number, projectDir?: string | undefined, qaReviewer?: BuilderAdapter | undefined, qaFactory?: ((cwd: string, sessionId?: string) => Promise<BuilderAdapter>) | undefined, qaSessionStrategy?: SessionStrategy, builderFactory?: ((cwd: string, sessionId?: string) => Promise<BuilderAdapter>) | undefined, builderSessionStrategy?: SessionStrategy, qaNonconvergence?: ((context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>) | undefined, beforeBuilderTurn?: ((adapter: BuilderAdapter, frozenAction: string) => Promise<BuilderAdapter>) | undefined, builderSessionBoundary?: ((adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy) => Promise<BuilderAdapter>) | undefined, qaSessionBoundary?: ((adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>) | undefined, observeQaNativeCompactions?: ((adapter: BuilderAdapter) => Promise<void>) | undefined,
|
|
75
80
|
/** Persist a Builder's provider-native event immediately after every turn. */
|
|
76
81
|
observeBuilderNativeCompactions?: ((adapter: BuilderAdapter) => Promise<void>) | undefined);
|
|
77
82
|
/**
|
|
@@ -90,6 +95,11 @@ export declare class Foreman {
|
|
|
90
95
|
result: TurnResult;
|
|
91
96
|
status: StepStatus;
|
|
92
97
|
}>;
|
|
98
|
+
/** Ask an already-active role session to propose choices for its reported blocker. */
|
|
99
|
+
resolveBlocker(adapter: BuilderAdapter, reason: string): Promise<{
|
|
100
|
+
result: TurnResult;
|
|
101
|
+
status: StepStatus;
|
|
102
|
+
}>;
|
|
93
103
|
private observeBuilderNative;
|
|
94
104
|
/**
|
|
95
105
|
* Run a QA review pass on the ticket the builder just completed.
|