ai-foreman 1.7.11 → 1.7.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/activity.d.ts +5 -1
- package/dist/activity.js +1 -1
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +133 -7
- package/dist/adapters/codex.d.ts +5 -0
- package/dist/adapters/codex.js +121 -11
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +11 -1
- package/dist/adapters/types.d.ts +47 -1
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +8 -2
- package/dist/branch/resume.js +11 -1
- package/dist/branch/runner.d.ts +12 -3
- package/dist/branch/runner.js +131 -67
- package/dist/buildRuns.d.ts +6 -2
- package/dist/buildRuns.js +76 -32
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +84 -1
- package/dist/cli/manager.d.ts +17 -0
- package/dist/cli/manager.js +135 -0
- package/dist/cli/recovery.d.ts +4 -0
- package/dist/cli/recovery.js +80 -0
- package/dist/cli/start.js +446 -142
- package/dist/cli/status.js +13 -2
- package/dist/cli/tickets.js +27 -9
- package/dist/config.d.ts +4 -0
- package/dist/config.js +28 -1
- package/dist/continuity.js +14 -9
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.js +428 -0
- package/dist/foreman.d.ts +26 -6
- package/dist/foreman.js +147 -104
- package/dist/handoffs.d.ts +35 -3
- package/dist/handoffs.js +155 -17
- package/dist/index.js +6 -0
- package/dist/log.d.ts +3 -3
- package/dist/log.js +4 -1
- package/dist/managerPacket.d.ts +25 -0
- package/dist/managerPacket.js +83 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +4 -1
- package/dist/notify.d.ts +4 -0
- package/dist/notify.js +15 -0
- package/dist/observability.d.ts +248 -0
- package/dist/observability.js +1018 -0
- package/dist/planChanges.d.ts +4 -0
- package/dist/planChanges.js +16 -0
- package/dist/projectDiagnostics.d.ts +27 -0
- package/dist/projectDiagnostics.js +407 -0
- package/dist/qaRecovery.d.ts +140 -0
- package/dist/qaRecovery.js +595 -0
- package/dist/qaReview.d.ts +133 -14
- package/dist/qaReview.js +984 -59
- package/dist/qaSnapshot.d.ts +31 -5
- package/dist/qaSnapshot.js +209 -134
- package/dist/recoveryPolicy.d.ts +37 -0
- package/dist/recoveryPolicy.js +251 -0
- package/dist/roles.js +1 -0
- package/dist/supervisor.d.ts +40 -0
- package/dist/supervisor.js +102 -0
- package/dist/ticketHistoryReader.d.ts +19 -0
- package/dist/ticketHistoryReader.js +58 -0
- package/dist/tickets/details.d.ts +8 -0
- package/dist/tickets/details.js +59 -11
- package/dist/tickets/output.d.ts +5 -0
- package/dist/tickets/output.js +73 -0
- package/dist/tickets/stateDb.d.ts +1 -0
- package/dist/tickets/stateDb.js +5 -0
- package/dist/workflowDb.d.ts +31 -2
- package/dist/workflowDb.js +192 -23
- package/dist/workflowReader.d.ts +42 -0
- package/dist/workflowReader.js +171 -0
- package/foreman.yaml +13 -0
- package/package.json +51 -3
package/dist/cli/start.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { resolve, join, relative } from "node:path";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { select, text, isCancel } from "@clack/prompts";
|
|
7
7
|
import { loadConfig } from "../config.js";
|
|
8
8
|
import { Log } from "../log.js";
|
|
@@ -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,14 +39,37 @@ 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";
|
|
46
|
+
import { ObservabilityStore, RunObserver } from "../observability.js";
|
|
47
|
+
import { loadProjectAutonomyConfig, resolveAutonomyPolicy, resolveQaEnablement } from "../recoveryPolicy.js";
|
|
48
|
+
import { loadQaRecoveryPacket } from "../qaRecovery.js";
|
|
49
|
+
import { loadSkill } from "special-agents";
|
|
50
|
+
const FOREMAN_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
45
51
|
function fail(message) {
|
|
46
52
|
console.error(`foreman: ${message}`);
|
|
47
53
|
process.exit(1);
|
|
48
54
|
}
|
|
55
|
+
function pendingQaRecoveryPacket(projectDir, runId) {
|
|
56
|
+
const db = new WorkflowDb(projectDir);
|
|
57
|
+
try {
|
|
58
|
+
const pending = db.getRun(runId)?.state.qaReportRecovery;
|
|
59
|
+
if (!pending || typeof pending !== "object")
|
|
60
|
+
return undefined;
|
|
61
|
+
const state = pending;
|
|
62
|
+
if (state.pendingAction !== "operator-menu" || typeof state.packetPath !== "string" || typeof state.packetDigest !== "string")
|
|
63
|
+
return undefined;
|
|
64
|
+
const packet = loadQaRecoveryPacket(state.packetPath);
|
|
65
|
+
if (packet.manifest.packetDigest !== state.packetDigest || packet.manifest.runId !== runId)
|
|
66
|
+
throw new Error("saved QA recovery packet does not match its durable pending-decision state");
|
|
67
|
+
return packet;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
db.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
49
73
|
function findLastSessionId(dir) {
|
|
50
74
|
if (!existsSync(dir))
|
|
51
75
|
return undefined;
|
|
@@ -310,6 +334,9 @@ export function buildStartCommand() {
|
|
|
310
334
|
.option("--effort <level>", "reasoning effort level (low|medium|high|xhigh)")
|
|
311
335
|
.option("--fast", "fast mode — lower latency (maps to effort=low for codex)")
|
|
312
336
|
.option("--no-qa", "disable per-ticket QA review (enabled by default)")
|
|
337
|
+
.option("--autonomy <profile>", "autonomy profile (supervised | balanced | unattended)")
|
|
338
|
+
.option("--detach", "return after launching the durable supervisor")
|
|
339
|
+
.option("--no-supervisor", "run the compatibility worker directly without supervision")
|
|
313
340
|
.option("--branch-per-ticket", "run each selected structured ticket in an isolated git worktree and branch")
|
|
314
341
|
.option("--no-branch-per-ticket", "disable saved branch-per-ticket defaults for this run")
|
|
315
342
|
.option("--create-pr", "push each successful ticket branch and create a GitHub PR (implies --branch-per-ticket)")
|
|
@@ -374,6 +401,15 @@ export function buildStartCommand() {
|
|
|
374
401
|
const recoveryRecord = opts.recoverRun ? readBuildRuns(cwd).find((run) => run.runId === opts.recoverRun) : undefined;
|
|
375
402
|
if (opts.recoverRun && !recoveryRecord)
|
|
376
403
|
fail(`recoverable build run not found: ${opts.recoverRun}`);
|
|
404
|
+
const autonomyProfile = opts.autonomy;
|
|
405
|
+
if (autonomyProfile && !["supervised", "balanced", "unattended"].includes(autonomyProfile))
|
|
406
|
+
fail("--autonomy must be supervised, balanced, or unattended");
|
|
407
|
+
const frozenPolicy = recoveryRecord && "frozenPolicy" in recoveryRecord
|
|
408
|
+
? recoveryRecord.frozenPolicy
|
|
409
|
+
: resolveAutonomyPolicy(loadProjectAutonomyConfig(cwd), autonomyProfile);
|
|
410
|
+
const autoApprovePlanUpdates = recoveryRecord
|
|
411
|
+
? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
|
|
412
|
+
: Boolean(opts.yes);
|
|
377
413
|
if (opts.recoveryMode) {
|
|
378
414
|
const allowed = ["exact-session", "fresh-with-handoff", "fresh-recovery-only", "guided-recovery"];
|
|
379
415
|
if (!allowed.includes(String(opts.recoveryMode)))
|
|
@@ -603,7 +639,43 @@ export function buildStartCommand() {
|
|
|
603
639
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
604
640
|
const logPath = join(cwd, ".foreman", `${stamp}.jsonl`);
|
|
605
641
|
const log = new Log(logPath);
|
|
606
|
-
const
|
|
642
|
+
const qaResolution = resolveQaEnablement({
|
|
643
|
+
cli: command.getOptionValueSource("qa") === "cli" ? Boolean(opts.qa) : undefined,
|
|
644
|
+
frozen: recoveryRecord && "qaEnabled" in recoveryRecord ? Boolean(recoveryRecord.qaEnabled) : undefined,
|
|
645
|
+
projectDir: cwd,
|
|
646
|
+
legacyForeman: config.qa.enabled,
|
|
647
|
+
});
|
|
648
|
+
const qaEnabled = qaResolution.enabled;
|
|
649
|
+
if (qaResolution.deprecationWarning)
|
|
650
|
+
console.error(`rafi: warning: ${qaResolution.deprecationWarning}`);
|
|
651
|
+
const invocationRunId = recoveryRecord?.runId ?? randomUUID();
|
|
652
|
+
let observabilityStore;
|
|
653
|
+
if (config.observability.enabled) {
|
|
654
|
+
try {
|
|
655
|
+
observabilityStore = new ObservabilityStore(cwd, { config: config.observability });
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
console.error(`rafi: observability unavailable; build will continue: ${String(error).replace(/\s+/g, " ").slice(0, 300)}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
let activeObserver = observabilityStore ? new RunObserver(observabilityStore, invocationRunId) : undefined;
|
|
662
|
+
const finishObservationOnExit = () => {
|
|
663
|
+
if (!activeObserver)
|
|
664
|
+
return;
|
|
665
|
+
activeObserver.finish("interrupted");
|
|
666
|
+
observabilityStore?.closeLogFile(logPath, "interrupted");
|
|
667
|
+
observabilityStore?.close();
|
|
668
|
+
activeObserver = undefined;
|
|
669
|
+
};
|
|
670
|
+
process.once("exit", finishObservationOnExit);
|
|
671
|
+
activeObserver?.startResourceSampling();
|
|
672
|
+
observabilityStore?.recordCapabilities(invocationRunId, {
|
|
673
|
+
provider_turns: { source: "provider_turns", state: "available", observedAt: new Date().toISOString() },
|
|
674
|
+
tools: { source: "tools", state: "available", observedAt: new Date().toISOString() },
|
|
675
|
+
process_resources: { source: "process_resources", state: "available", observedAt: new Date().toISOString() },
|
|
676
|
+
child_process_resources: { source: "child_process_resources", state: "unavailable", observedAt: new Date().toISOString(), detail: "provider child-process attribution is unsupported" },
|
|
677
|
+
}, FOREMAN_VERSION);
|
|
678
|
+
observabilityStore?.indexLogFile(logPath, invocationRunId);
|
|
607
679
|
const createRawBuilder = async (builderCwd, sessionId, sessionRef) => {
|
|
608
680
|
const builderPolicy = new PermissionPolicy(config.permissions, builderCwd, { currentBranchWorkflow: !branchMode });
|
|
609
681
|
const roleBundle = loadRoleBundle("builder", { projectDir: builderCwd });
|
|
@@ -626,6 +698,7 @@ export function buildStartCommand() {
|
|
|
626
698
|
systemPromptAppend: roleBundle.system || undefined,
|
|
627
699
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
628
700
|
autoCompactThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
|
|
701
|
+
observer: activeObserver,
|
|
629
702
|
};
|
|
630
703
|
const adapter = agent === "codex"
|
|
631
704
|
? new CodexAdapter(adapterOpts)
|
|
@@ -676,6 +749,34 @@ export function buildStartCommand() {
|
|
|
676
749
|
settings_revision: revision,
|
|
677
750
|
};
|
|
678
751
|
};
|
|
752
|
+
const settingsForRequestedRuntime = (base, requestedRuntime) => requestedRuntime && requestedRuntime !== base.make
|
|
753
|
+
? { ...base, make: requestedRuntime, model: "default", source: "cli" }
|
|
754
|
+
: base;
|
|
755
|
+
const handoffRecoveryOptions = (role) => ({
|
|
756
|
+
allowProviderSwitch: true,
|
|
757
|
+
desktopNotifications: config.notifications.enabled,
|
|
758
|
+
terminalBell: config.notifications.terminal_bell,
|
|
759
|
+
onAccepted: (accepted) => {
|
|
760
|
+
if (role === "builder" && accepted.successor.agent !== agent) {
|
|
761
|
+
agent = accepted.successor.agent;
|
|
762
|
+
model = undefined;
|
|
763
|
+
agentExecutable = undefined;
|
|
764
|
+
}
|
|
765
|
+
if (role === "qa" && accepted.successor.agent !== qaAgent) {
|
|
766
|
+
qaAgent = accepted.successor.agent;
|
|
767
|
+
qaModel = undefined;
|
|
768
|
+
qaExecutable = undefined;
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
const acceptedRuntimeSettings = (current, make, acceptedModel) => current.make === make && current.model === (acceptedModel ?? "default")
|
|
773
|
+
? current
|
|
774
|
+
: { ...current, make, model: acceptedModel ?? "default", source: "cli" };
|
|
775
|
+
const withRunLocalAcceptedRuntimes = (run) => ({
|
|
776
|
+
...run,
|
|
777
|
+
...(run.builder ? { builder: { ...run.builder, settings: acceptedRuntimeSettings(run.builder.settings, agent, model) } } : {}),
|
|
778
|
+
...(run.qa ? { qa: { ...run.qa, settings: acceptedRuntimeSettings(run.qa.settings, qaAgent, qaModel) } } : {}),
|
|
779
|
+
});
|
|
679
780
|
const createBuilderForSettings = async (builderCwd, settings) => {
|
|
680
781
|
const ready = await ensureRuntimeReadyForCommand(builderCwd, settings.make, {
|
|
681
782
|
label: "live Builder settings",
|
|
@@ -700,6 +801,7 @@ export function buildStartCommand() {
|
|
|
700
801
|
systemPromptAppend: roleBundle.system || undefined,
|
|
701
802
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
702
803
|
autoCompactThresholdPercent: settings.auto_compact_threshold_percent,
|
|
804
|
+
observer: activeObserver,
|
|
703
805
|
};
|
|
704
806
|
const created = settings.make === "codex" ? new CodexAdapter(adapterOptions) : await ClaudeAdapter.create(adapterOptions);
|
|
705
807
|
await prepareNativeAutoCompaction(created);
|
|
@@ -728,6 +830,7 @@ export function buildStartCommand() {
|
|
|
728
830
|
systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
|
|
729
831
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
730
832
|
autoCompactThresholdPercent: settings.auto_compact_threshold_percent,
|
|
833
|
+
observer: activeObserver,
|
|
731
834
|
};
|
|
732
835
|
const created = settings.make === "codex" ? new CodexAdapter(adapterOptions) : await ClaudeAdapter.create(adapterOptions);
|
|
733
836
|
await prepareNativeAutoCompaction(created);
|
|
@@ -757,7 +860,8 @@ export function buildStartCommand() {
|
|
|
757
860
|
initial,
|
|
758
861
|
runtime: agent,
|
|
759
862
|
label: "builder turn",
|
|
760
|
-
|
|
863
|
+
observer: activeObserver,
|
|
864
|
+
enabled: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
761
865
|
allowSwitch: !(opts.resume || opts.continue),
|
|
762
866
|
recreate: async (nextRuntime, resumeSessionId, resumeRef) => {
|
|
763
867
|
const nextReady = await ensureRuntimeReadyForCommand(builderCwd, nextRuntime, {
|
|
@@ -806,7 +910,7 @@ export function buildStartCommand() {
|
|
|
806
910
|
} } : {}),
|
|
807
911
|
compactionCount, compactMaximum: effectiveSettings.compact_maximum,
|
|
808
912
|
resources: [{ label: "continuity-reconstruction", content: reconstruction, authoritative: true }],
|
|
809
|
-
}, () => createBuilderForSettings(builderCwd, effectiveSettings));
|
|
913
|
+
}, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(effectiveSettings, runtime)), handoffRecoveryOptions("builder"));
|
|
810
914
|
log.write("handoff-transfer", {
|
|
811
915
|
role: "builder", reason, recovery: "double-invalid-continuity", generation: transfer.manifest.generation,
|
|
812
916
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -854,7 +958,7 @@ export function buildStartCommand() {
|
|
|
854
958
|
compactionCount,
|
|
855
959
|
compactMaximum: effectiveSettings.compact_maximum,
|
|
856
960
|
resources: [{ label: "frozen-action", content: frozenAction, authoritative: true }],
|
|
857
|
-
}, () => createBuilderForSettings(builderCwd, effectiveSettings));
|
|
961
|
+
}, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(effectiveSettings, runtime)), handoffRecoveryOptions("builder"));
|
|
858
962
|
log.write("handoff-transfer", {
|
|
859
963
|
requestedByBuilder: true, reason: request.reason, generation: transfer.manifest.generation,
|
|
860
964
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -869,7 +973,8 @@ export function buildStartCommand() {
|
|
|
869
973
|
let adapter = await createRecoveringBuilder(builderCwd, effectiveSessionId, effectiveSessionId ? sessionRef : undefined);
|
|
870
974
|
if (pendingHandoffGeneration !== undefined && pendingHandoffRole === "builder" && activeContinuityRunId === recoveryRecord?.runId) {
|
|
871
975
|
const service = new HandoffService(cwd);
|
|
872
|
-
const
|
|
976
|
+
const staged = service.loadStaged(recoveryRecord.runId, pendingHandoffGeneration);
|
|
977
|
+
const accepted = await service.acceptStagedWithRecovery(staged, adapter, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(resolvedContinuitySettings("builder"), runtime)), handoffRecoveryOptions("builder"));
|
|
873
978
|
adapter = accepted.successor;
|
|
874
979
|
pendingHandoffGeneration = undefined;
|
|
875
980
|
log.write("recovery-handoff-accepted", { role: "builder", generation: accepted.manifest.generation, successorSessionId: accepted.successorSessionId, acceptanceCheckpointDigest: accepted.acceptanceCheckpointDigest });
|
|
@@ -904,7 +1009,7 @@ export function buildStartCommand() {
|
|
|
904
1009
|
} } : {}),
|
|
905
1010
|
compactionCount, compactMaximum: input.next.compact_maximum,
|
|
906
1011
|
resources: [{ label: "frozen-action", content: input.frozenAction, authoritative: true }],
|
|
907
|
-
}, () => createBuilderForSettings(builderCwd, input.next));
|
|
1012
|
+
}, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(input.next, runtime)), handoffRecoveryOptions("builder"));
|
|
908
1013
|
log.write("handoff-transfer", {
|
|
909
1014
|
role: "builder", reason, settingsRevision: input.next.settings_revision, generation: transfer.manifest.generation,
|
|
910
1015
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -947,6 +1052,7 @@ export function buildStartCommand() {
|
|
|
947
1052
|
systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
|
|
948
1053
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
949
1054
|
autoCompactThresholdPercent: qaSettings.auto_compact_threshold_percent,
|
|
1055
|
+
observer: activeObserver,
|
|
950
1056
|
};
|
|
951
1057
|
const created = qaSettings.make === "codex"
|
|
952
1058
|
? new CodexAdapter(adapterOpts)
|
|
@@ -960,7 +1066,8 @@ export function buildStartCommand() {
|
|
|
960
1066
|
initial,
|
|
961
1067
|
runtime: qaAgent,
|
|
962
1068
|
label: "QA turn",
|
|
963
|
-
|
|
1069
|
+
observer: activeObserver,
|
|
1070
|
+
enabled: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
964
1071
|
allowSwitch: !sessionRef,
|
|
965
1072
|
recreate: async (nextRuntime, resumeSessionId, resumeRef) => {
|
|
966
1073
|
const nextReady = await ensureRuntimeReadyForCommand(qaCwd, nextRuntime, {
|
|
@@ -1025,7 +1132,7 @@ export function buildStartCommand() {
|
|
|
1025
1132
|
} } : {}),
|
|
1026
1133
|
compactionCount, compactMaximum: effectiveSettings.compact_maximum,
|
|
1027
1134
|
resources: [{ label: "continuity-reconstruction", content: reconstruction, authoritative: true }],
|
|
1028
|
-
}, () => createQaForSettings(qaCwd, effectiveSettings));
|
|
1135
|
+
}, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(effectiveSettings, runtime)), handoffRecoveryOptions("qa"));
|
|
1029
1136
|
log.write("handoff-transfer", {
|
|
1030
1137
|
role: "qa", reason, recovery: "double-invalid-continuity", generation: transfer.manifest.generation,
|
|
1031
1138
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -1080,12 +1187,15 @@ export function buildStartCommand() {
|
|
|
1080
1187
|
void sessionId;
|
|
1081
1188
|
let adapter = await createRecoveringQa(qaCwd, effectiveSessionId);
|
|
1082
1189
|
let explicitRecoveryAccepted = false;
|
|
1190
|
+
let handoffReceipt = { unavailable: "no predecessor handoff was required" };
|
|
1083
1191
|
if (pendingHandoffGeneration !== undefined && pendingHandoffRole === "qa" && activeContinuityRunId === recoveryRecord?.runId) {
|
|
1084
1192
|
const service = new HandoffService(cwd);
|
|
1085
|
-
const
|
|
1193
|
+
const staged = service.loadStaged(recoveryRecord.runId, pendingHandoffGeneration);
|
|
1194
|
+
const accepted = await service.acceptStagedWithRecovery(staged, adapter, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(resolvedContinuitySettings("qa"), runtime)), handoffRecoveryOptions("qa"));
|
|
1086
1195
|
adapter = accepted.successor;
|
|
1087
1196
|
pendingHandoffGeneration = undefined;
|
|
1088
1197
|
explicitRecoveryAccepted = true;
|
|
1198
|
+
handoffReceipt = { generation: accepted.manifest.generation, successorSessionId: accepted.successorSessionId, acceptanceCheckpointDigest: accepted.acceptanceCheckpointDigest };
|
|
1089
1199
|
log.write("recovery-handoff-accepted", { role: "qa", generation: accepted.manifest.generation, successorSessionId: accepted.successorSessionId, acceptanceCheckpointDigest: accepted.acceptanceCheckpointDigest });
|
|
1090
1200
|
}
|
|
1091
1201
|
// A new disposable QA worktree always means a new provider session.
|
|
@@ -1116,8 +1226,9 @@ export function buildStartCommand() {
|
|
|
1116
1226
|
compactionCount,
|
|
1117
1227
|
compactMaximum: resolvedContinuitySettings("qa").compact_maximum,
|
|
1118
1228
|
});
|
|
1119
|
-
const accepted = await service.
|
|
1229
|
+
const accepted = await service.acceptStagedWithRecovery(staged, adapter, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(resolvedContinuitySettings("qa"), runtime)), handoffRecoveryOptions("qa"));
|
|
1120
1230
|
adapter = accepted.successor;
|
|
1231
|
+
handoffReceipt = { generation: accepted.manifest.generation, successorSessionId: accepted.successorSessionId, acceptanceCheckpointDigest: accepted.acceptanceCheckpointDigest };
|
|
1121
1232
|
log.write("handoff-transfer", {
|
|
1122
1233
|
role: "qa", reason: staged.manifest.reason, generation: accepted.manifest.generation,
|
|
1123
1234
|
predecessorSessionId: staged.manifest.predecessorSessionId, successorSessionId: accepted.successorSessionId,
|
|
@@ -1125,7 +1236,35 @@ export function buildStartCommand() {
|
|
|
1125
1236
|
});
|
|
1126
1237
|
}
|
|
1127
1238
|
}
|
|
1128
|
-
|
|
1239
|
+
const decorated = decorateQa(adapter, qaCwd, effectiveSessionId);
|
|
1240
|
+
const settings = resolvedContinuitySettings("qa");
|
|
1241
|
+
const bundle = loadRoleBundle("qa", { projectDir: qaCwd });
|
|
1242
|
+
const effectiveRoleInstructions = `${bundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`;
|
|
1243
|
+
return {
|
|
1244
|
+
adapter: decorated,
|
|
1245
|
+
sessionIdentity: decorated.sessionRef?.() ?? decorated.sessionId(),
|
|
1246
|
+
effectiveRoleInstructions,
|
|
1247
|
+
runtimeContext: settings,
|
|
1248
|
+
skills: bundle.skills.map((name) => {
|
|
1249
|
+
const path = [join(qaCwd, ".agents", "skills", name, "SKILL.md"), join(qaCwd, ".claude", "skills", name, "SKILL.md"), join(qaCwd, ".codex", "skills", name, "SKILL.md")].find(existsSync);
|
|
1250
|
+
if (path) {
|
|
1251
|
+
const dispatchedContent = `## ${name}\n${readFileSync(path, "utf8").trim()}`;
|
|
1252
|
+
return { name, path, digest: createHash("sha256").update(dispatchedContent).digest("hex") };
|
|
1253
|
+
}
|
|
1254
|
+
if (settings.make === "codex") {
|
|
1255
|
+
try {
|
|
1256
|
+
const bundled = loadSkill(name);
|
|
1257
|
+
if (bundled.body?.trim()) {
|
|
1258
|
+
const dispatchedContent = `## ${bundled.name}\n${bundled.body.trim()}`;
|
|
1259
|
+
return { name, path: `special-agents:${name}`, digest: createHash("sha256").update(dispatchedContent).digest("hex") };
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
catch { /* explicit unavailable result below */ }
|
|
1263
|
+
}
|
|
1264
|
+
return { name, digest: "unavailable", reason: "runtime skill name had no resolvable loaded skill content" };
|
|
1265
|
+
}),
|
|
1266
|
+
handoffReceipt,
|
|
1267
|
+
};
|
|
1129
1268
|
};
|
|
1130
1269
|
const applyQaSettingsBoundary = async (runId, qaCwd, input) => {
|
|
1131
1270
|
const context = await input.adapter.contextUsage?.().catch(() => undefined);
|
|
@@ -1155,7 +1294,7 @@ export function buildStartCommand() {
|
|
|
1155
1294
|
} } : {}),
|
|
1156
1295
|
compactionCount, compactMaximum: input.next.compact_maximum,
|
|
1157
1296
|
resources: [{ label: "frozen-qa-action", content: input.frozenAction, authoritative: true }],
|
|
1158
|
-
}, () => createQaForSettings(qaCwd, input.next));
|
|
1297
|
+
}, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(input.next, runtime)), handoffRecoveryOptions("qa"));
|
|
1159
1298
|
log.write("handoff-transfer", {
|
|
1160
1299
|
role: "qa", reason, settingsRevision: input.next.settings_revision, generation: transfer.manifest.generation,
|
|
1161
1300
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
@@ -1164,7 +1303,7 @@ export function buildStartCommand() {
|
|
|
1164
1303
|
await input.adapter.close().catch(() => { });
|
|
1165
1304
|
return decorateQa(transfer.successor, qaCwd, undefined, input.next);
|
|
1166
1305
|
};
|
|
1167
|
-
const qaSessionBoundary = async (adapter, frozenAction, strategy, qaCwd) => {
|
|
1306
|
+
const qaSessionBoundary = async (adapter, frozenAction, strategy, qaCwd, recovery) => {
|
|
1168
1307
|
if (!activeContinuityRunId)
|
|
1169
1308
|
throw new Error("QA session boundary requires an active durable run");
|
|
1170
1309
|
const qaSettings = resolvedContinuitySettings("qa");
|
|
@@ -1183,13 +1322,13 @@ export function buildStartCommand() {
|
|
|
1183
1322
|
}),
|
|
1184
1323
|
report: (event) => {
|
|
1185
1324
|
currentActivity()?.update(event.kind, event.detail, { provider: qaSettings.make, model: qaSettings.model });
|
|
1186
|
-
log.write("context-lifecycle", { role: "qa", ...event, sample: event.sample });
|
|
1187
1325
|
},
|
|
1188
1326
|
handoff: async ({ reason, adapter: predecessor, sample, settings, compactionCount }) => {
|
|
1189
1327
|
const nativeUsage = await predecessor.sessionUsage?.().catch(() => undefined);
|
|
1190
1328
|
const transfer = await new HandoffService(cwd).transfer({
|
|
1191
1329
|
runId: activeContinuityRunId, role: "qa", reason, predecessorSessionId: predecessor.sessionId(), predecessorSessionRef: predecessor.sessionRef?.(),
|
|
1192
|
-
roleState: { frozenActionDigest: createHash("sha256").update(frozenAction).digest("hex"), contextSample: sample
|
|
1330
|
+
roleState: { frozenActionDigest: createHash("sha256").update(frozenAction).digest("hex"), contextSample: sample,
|
|
1331
|
+
...(recovery ? { recoveryPacketDigest: recovery.packetDigest, reviewedStateDigest: recovery.reviewedStateDigest } : {}) },
|
|
1193
1332
|
...(nativeUsage ? { sessionUsage: {
|
|
1194
1333
|
version: 1, runId: activeContinuityRunId, role: "qa", provider: predecessor.agent,
|
|
1195
1334
|
providerSessionId: predecessor.sessionId(), observedAt: nativeUsage.observedAt, source: nativeUsage.source,
|
|
@@ -1197,8 +1336,11 @@ export function buildStartCommand() {
|
|
|
1197
1336
|
cumulativeTotalTokens: nativeUsage.totalTokens, authoritativeCostUsd: nativeUsage.authoritativeCostUsd,
|
|
1198
1337
|
} } : {}),
|
|
1199
1338
|
compactionCount, compactMaximum: settings.compact_maximum ?? 10,
|
|
1200
|
-
resources: [{ label: "frozen-qa-action", content: frozenAction, authoritative: true }
|
|
1201
|
-
|
|
1339
|
+
resources: [{ label: "frozen-qa-action", content: frozenAction, authoritative: true }, ...(recovery ? [
|
|
1340
|
+
{ label: "qa-recovery-packet", digest: recovery.packetDigest, authoritative: true, requiredForRecovery: true, mediaType: "application/vnd.rafi.qa-recovery+json", path: recovery.packetPath },
|
|
1341
|
+
{ label: "qa-reviewed-state", digest: recovery.reviewedStateDigest, authoritative: true, requiredForRecovery: true, mediaType: "application/vnd.rafi.reviewed-state", path: "reviewed-state/" },
|
|
1342
|
+
] : [])],
|
|
1343
|
+
}, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(settings, runtime)), handoffRecoveryOptions("qa"));
|
|
1202
1344
|
log.write("handoff-transfer", { role: "qa", generation: transfer.manifest.generation, predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId, acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest });
|
|
1203
1345
|
await predecessor.close().catch(() => { });
|
|
1204
1346
|
return decorateQa(transfer.successor, qaCwd, undefined, settings);
|
|
@@ -1219,7 +1361,8 @@ export function buildStartCommand() {
|
|
|
1219
1361
|
});
|
|
1220
1362
|
await controller.observeNativeCompactions(adapter);
|
|
1221
1363
|
};
|
|
1222
|
-
const qaNonconvergence = createQaNonconvergenceHandler(cwd,
|
|
1364
|
+
const qaNonconvergence = createQaNonconvergenceHandler(cwd, !process.stdin.isTTY || !process.stdout.isTTY, roleDefaults.planner, () => activeObserver);
|
|
1365
|
+
const qaReportRecovery = createQaReportRecoveryHandler(!process.stdin.isTTY || !process.stdout.isTTY, () => activeObserver);
|
|
1223
1366
|
if (branchMode) {
|
|
1224
1367
|
const ticketsConfig = loadTicketsConfig(cwd);
|
|
1225
1368
|
const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
|
|
@@ -1408,14 +1551,14 @@ export function buildStartCommand() {
|
|
|
1408
1551
|
console.log(` ! ${issue.ticket ?? "plan"}: ${issue.message}`);
|
|
1409
1552
|
}
|
|
1410
1553
|
console.log();
|
|
1411
|
-
if (!
|
|
1412
|
-
const action = await select({
|
|
1554
|
+
if (!autoApprovePlanUpdates && plan.issues.every((issue) => !issue.blocking)) {
|
|
1555
|
+
const action = await withObservedUserWait(activeObserver, "branch plan approval", () => select({
|
|
1413
1556
|
message: branchPresentation.prompt,
|
|
1414
1557
|
options: [
|
|
1415
1558
|
{ value: "proceed", label: "Proceed" },
|
|
1416
1559
|
{ value: "cancel", label: "Cancel" },
|
|
1417
1560
|
],
|
|
1418
|
-
});
|
|
1561
|
+
}));
|
|
1419
1562
|
if (isCancel(action) || action === "cancel") {
|
|
1420
1563
|
console.log("ai-foreman: cancelled");
|
|
1421
1564
|
process.exit(0);
|
|
@@ -1455,11 +1598,15 @@ export function buildStartCommand() {
|
|
|
1455
1598
|
};
|
|
1456
1599
|
const firstResumeRef = plan.nodes[0] ? resumeSessionByTicket.get(plan.nodes[0].ticket.id)?.sessionRef : undefined;
|
|
1457
1600
|
let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBranchBuilder, qa: capturedBranchQa, builderSessionId: firstResumeRef?.sessionId ?? null, builderSessionRef: firstResumeRef ?? null }) : createBuildRun({
|
|
1601
|
+
runId: invocationRunId,
|
|
1458
1602
|
tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
|
|
1459
1603
|
repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, baseRef: plan.baseRef, builder: capturedBranchBuilder, qa: capturedBranchQa,
|
|
1604
|
+
frozenPolicy, autonomyProfile, qaEnabled,
|
|
1460
1605
|
runDecisions: { workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 50, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
1461
1606
|
});
|
|
1462
1607
|
activeContinuityRunId = masterRun.runId;
|
|
1608
|
+
const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, masterRun.runId) : undefined;
|
|
1609
|
+
activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
|
|
1463
1610
|
const branchContextControllers = new Map();
|
|
1464
1611
|
const branchContextController = (worktreePath) => {
|
|
1465
1612
|
const existing = branchContextControllers.get(worktreePath);
|
|
@@ -1475,7 +1622,7 @@ export function buildStartCommand() {
|
|
|
1475
1622
|
adapter: active, settingsRevision: settings.settings_revision, displaySessionCost: settings.display_session_cost,
|
|
1476
1623
|
sessionTransition: "adopted live settings",
|
|
1477
1624
|
}),
|
|
1478
|
-
report: (event) => { currentActivity()?.update(event.kind, event.detail, { provider: capturedBranchBuilder.make, model: capturedBranchBuilder.model });
|
|
1625
|
+
report: (event) => { currentActivity()?.update(event.kind, event.detail, { provider: capturedBranchBuilder.make, model: capturedBranchBuilder.model }); },
|
|
1479
1626
|
handoff: async ({ reason, adapter, sample, settings, compactionCount, frozenAction }) => {
|
|
1480
1627
|
const nativeUsage = await adapter.sessionUsage?.().catch(() => undefined);
|
|
1481
1628
|
const transfer = await new HandoffService(cwd).transfer({
|
|
@@ -1484,7 +1631,7 @@ export function buildStartCommand() {
|
|
|
1484
1631
|
...(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
1632
|
compactionCount, compactMaximum: settings.compact_maximum ?? 10,
|
|
1486
1633
|
resources: [{ label: "frozen-action", content: frozenAction, authoritative: true }],
|
|
1487
|
-
}, () => createBuilderForSettings(worktreePath, settings));
|
|
1634
|
+
}, (_handoff, runtime) => createBuilderForSettings(worktreePath, settingsForRequestedRuntime(settings, runtime)), handoffRecoveryOptions("builder"));
|
|
1488
1635
|
log.write("handoff-transfer", { worktreePath, generation: transfer.manifest.generation, predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId, acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest });
|
|
1489
1636
|
if (adapter instanceof ContinuityAdapter) {
|
|
1490
1637
|
await adapter.adoptValidatedSuccessor(transfer.successor);
|
|
@@ -1500,93 +1647,109 @@ export function buildStartCommand() {
|
|
|
1500
1647
|
const branchHeartbeat = setInterval(() => { masterRun = heartbeatBuildRun(cwd, masterRun); }, 10_000);
|
|
1501
1648
|
branchHeartbeat.unref();
|
|
1502
1649
|
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
|
-
|
|
1650
|
+
let summaries;
|
|
1651
|
+
try {
|
|
1652
|
+
summaries = await runBranchPlan({
|
|
1653
|
+
projectDir: cwd,
|
|
1654
|
+
runId: masterRun.runId,
|
|
1655
|
+
plan,
|
|
1656
|
+
log,
|
|
1657
|
+
agent,
|
|
1658
|
+
model,
|
|
1659
|
+
effort: builderEffort,
|
|
1660
|
+
fast: builderFast,
|
|
1661
|
+
notificationsEnabled: config.notifications.enabled,
|
|
1662
|
+
terminalBellEnabled: config.notifications.terminal_bell,
|
|
1663
|
+
qaEnabled,
|
|
1664
|
+
qaRuntimeContext: capturedBranchQa,
|
|
1665
|
+
qaContinuityManaged: true,
|
|
1666
|
+
qaReportRecovery,
|
|
1667
|
+
qaResumedRecovery,
|
|
1668
|
+
qaMaxFixAttempts: frozenPolicy.limits.builderQaFixesPerTicket,
|
|
1669
|
+
observer: activeObserver,
|
|
1670
|
+
createPr: branchDefaults.createReview,
|
|
1671
|
+
completionMode: branchDefaults.completionMode,
|
|
1672
|
+
reviewProvider: branchDefaults.reviewProvider,
|
|
1673
|
+
prReady: branchDefaults.prReady,
|
|
1674
|
+
keepWorktrees: Boolean(opts.keepWorktrees) || selectedStacks.length > 0,
|
|
1675
|
+
cleanupBranches: branchDefaults.cleanupBranches,
|
|
1676
|
+
autoMergeWait: branchDefaults.autoMergeWait,
|
|
1677
|
+
autoMergeTimeoutMinutes: branchDefaults.autoMergeTimeoutMinutes,
|
|
1678
|
+
mergeMethod: branchDefaults.mergeMethod,
|
|
1679
|
+
allowedBaseDirtyPaths,
|
|
1680
|
+
baseWorktreePolicy,
|
|
1681
|
+
trackerPaths: {
|
|
1682
|
+
progressDoc: ticketsConfig.paths.progressDoc,
|
|
1683
|
+
archiveDoc: ticketsConfig.paths.archiveDoc,
|
|
1684
|
+
},
|
|
1685
|
+
resumeSessions: resumeSessionByTicket,
|
|
1686
|
+
createBuilder: (builderCwd, sessionId, sessionRef) => createBuilder(builderCwd, sessionId, sessionRef),
|
|
1687
|
+
recordBuilderSession: (session, ticketId) => { masterRun = persistBuildSession(cwd, { ...masterRun, currentTicket: ticketId }, "builder", session); },
|
|
1688
|
+
recordQaSession: (session, ticketId) => { masterRun = persistBuildSession(cwd, { ...masterRun, currentTicket: ticketId }, "qa", session); },
|
|
1689
|
+
onSessionUnavailable: (error) => { branchSessionFailure = error; },
|
|
1690
|
+
createQa: (qaCwd, sessionId) => createQa(qaCwd, sessionId),
|
|
1691
|
+
builderSessionStrategy: roleDefaults.builder?.session_strategy ?? "compact",
|
|
1692
|
+
qaSessionStrategy: roleDefaults.qa?.session_strategy ?? "compact",
|
|
1693
|
+
observeBuilder: async (builder) => {
|
|
1694
|
+
liveStatusReporter?.stop();
|
|
1695
|
+
activeBuilderForStatus = () => builder;
|
|
1696
|
+
const reporter = liveStatusReporter = new AgentStatusReporter({
|
|
1697
|
+
runId: masterRun.runId, role: "builder", provider: builder.agent, model: capturedBranchBuilder.model,
|
|
1698
|
+
reasoning: capturedBranchBuilder.reasoning, fast: capturedBranchBuilder.fast, step: Math.min(plan.nodes.length, branchContextControllers.size + 1), total: plan.nodes.length,
|
|
1699
|
+
phase: "builder ticket session", sessionTransition: builder.sessionId() ? "resumed session" : "initial session", adapter: builder,
|
|
1700
|
+
settingsRevision: capturedBranchBuilder.settings_revision, displaySessionCost: capturedBranchBuilder.display_session_cost ?? false,
|
|
1701
|
+
compactionCount: () => { const db = new WorkflowDb(cwd); try {
|
|
1702
|
+
return db.successfulCompactionCount(masterRun.runId, "builder", builder.sessionRef?.() ?? builder.sessionId());
|
|
1703
|
+
}
|
|
1704
|
+
finally {
|
|
1705
|
+
db.close();
|
|
1706
|
+
} },
|
|
1707
|
+
handoffGeneration: () => { const db = new WorkflowDb(cwd); try {
|
|
1708
|
+
return db.handoffs(masterRun.runId).at(-1)?.generation ?? 0;
|
|
1709
|
+
}
|
|
1710
|
+
finally {
|
|
1711
|
+
db.close();
|
|
1712
|
+
} },
|
|
1713
|
+
}, (line, snapshot) => {
|
|
1714
|
+
const activity = currentActivity();
|
|
1715
|
+
if (activity && process.stdout.isTTY)
|
|
1716
|
+
activity.setAgentStatus(line.replace(/^\[[^\]]+\]\s*/, ""));
|
|
1717
|
+
else
|
|
1718
|
+
console.log(line);
|
|
1719
|
+
activeObserver?.store.updateCurrentState({ runId: masterRun.runId, role: "builder", stream: "builder", executionId: activeObserver.executionId, ticketId: masterRun.currentTicket, providerSessionId: snapshot.contextSample.providerSessionId, phase: "builder ticket session", lastSignalAt: snapshot.contextSample.observedAt });
|
|
1720
|
+
if (snapshot.contextSample.used !== undefined)
|
|
1721
|
+
activeObserver?.store.recordMetric({ runId: masterRun.runId, executionId: activeObserver.executionId, role: "builder", stream: "builder", providerSessionId: snapshot.contextSample.providerSessionId }, "context_used_tokens", snapshot.contextSample.used, { unit: "tokens" });
|
|
1722
|
+
});
|
|
1723
|
+
reporter.start();
|
|
1565
1724
|
try {
|
|
1566
|
-
|
|
1567
|
-
db.recordContextSample(snapshot.contextSample);
|
|
1568
|
-
if (snapshot.sessionUsage)
|
|
1569
|
-
db.recordSessionUsage(snapshot.sessionUsage);
|
|
1725
|
+
await printEvents(builder.events());
|
|
1570
1726
|
}
|
|
1571
1727
|
finally {
|
|
1572
|
-
|
|
1728
|
+
reporter.stop();
|
|
1573
1729
|
}
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1730
|
+
},
|
|
1731
|
+
qaNonconvergence,
|
|
1732
|
+
beforeBuilderTurn: async (adapter, frozenAction, worktreePath) => (await branchContextController(worktreePath).atSafeBoundary(adapter, frozenAction)).adapter,
|
|
1733
|
+
observeBuilderNativeCompactions: async (adapter, worktreePath) => { await branchContextController(worktreePath).observeNativeCompactions(adapter); },
|
|
1734
|
+
builderSessionBoundary: async (adapter, frozenAction, strategy, worktreePath) => (await branchContextController(worktreePath).atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter,
|
|
1735
|
+
qaSessionBoundary,
|
|
1736
|
+
observeQaNativeCompactions,
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
catch (error) {
|
|
1740
|
+
if (!(error instanceof HandoffAcceptanceError) && !(error instanceof HandoffRecoveryPausedError))
|
|
1741
|
+
throw error;
|
|
1742
|
+
const summary = error.message.slice(0, 1000);
|
|
1743
|
+
masterRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, masterRun, "handoff-recovery-required", {
|
|
1744
|
+
status: "recoverable",
|
|
1745
|
+
failure: { category: "unknown", summary, at: new Date().toISOString() },
|
|
1746
|
+
}), "recoverable");
|
|
1747
|
+
clearInterval(branchHeartbeat);
|
|
1748
|
+
log.write("safe-boundary-paused", { runId: masterRun.runId, message: summary, handoff: true });
|
|
1749
|
+
console.error(`foreman: handoff paused at a recoverable checkpoint: ${summary}`);
|
|
1750
|
+
console.error(`foreman: resume this run with: rafi build:resume ${shellQuote(cwd)} --run ${shellQuote(masterRun.runId)}`);
|
|
1751
|
+
process.exit(2);
|
|
1752
|
+
}
|
|
1590
1753
|
if (branchSessionFailure) {
|
|
1591
1754
|
const failure = branchSessionFailure;
|
|
1592
1755
|
const guided = failure instanceof SessionUnavailableContinuityError ? failure.guidedRecovery : failure.failure.dispatchState === "unknown";
|
|
@@ -1624,9 +1787,18 @@ export function buildStartCommand() {
|
|
|
1624
1787
|
}
|
|
1625
1788
|
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
1789
|
}
|
|
1790
|
+
masterRun = withRunLocalAcceptedRuntimes(masterRun);
|
|
1627
1791
|
masterRun = summaries.every((row) => row.buildStatus === "done")
|
|
1628
1792
|
? completeBuildRun(cwd, masterRun)
|
|
1629
1793
|
: releaseBuildLease(cwd, checkpointBuildRun(cwd, masterRun, "branch-run-interrupted", { status: "recoverable" }), "recoverable");
|
|
1794
|
+
activeObserver?.finish(masterRun.status, observationSummaryMetadata(cwd, masterRun, qaEnabled, capturedBranchBuilder.make, summaries.find(row => row.pr?.url)));
|
|
1795
|
+
observabilityStore?.closeLogFile(logPath, masterRun.status);
|
|
1796
|
+
observabilityStore?.enforceLimits();
|
|
1797
|
+
observabilityStore?.maintainLogs(cwd);
|
|
1798
|
+
observabilityStore?.close();
|
|
1799
|
+
pruneRecoveryTelemetryAtCompletion(cwd, config.observability.detail_retention_days);
|
|
1800
|
+
activeObserver = undefined;
|
|
1801
|
+
process.off("exit", finishObservationOnExit);
|
|
1630
1802
|
console.log("foreman: branch run summary");
|
|
1631
1803
|
console.log("ticket\tbranch\tbase\tstatus\tcommit\tpush\tcompletion");
|
|
1632
1804
|
for (const row of summaries) {
|
|
@@ -1651,6 +1823,7 @@ export function buildStartCommand() {
|
|
|
1651
1823
|
if (masterRun.status === "completed")
|
|
1652
1824
|
printHandoffPruneCommand(masterRun.runId);
|
|
1653
1825
|
const failed = summaries.some((row) => row.buildStatus === "blocked" || row.buildStatus === "needs-human");
|
|
1826
|
+
fireTerminalBell(config.notifications.terminal_bell);
|
|
1654
1827
|
process.exit(failed ? 2 : 0);
|
|
1655
1828
|
}
|
|
1656
1829
|
const ready = await ensureRuntimeReadyForCommand(cwd, agent, {
|
|
@@ -1692,10 +1865,14 @@ export function buildStartCommand() {
|
|
|
1692
1865
|
};
|
|
1693
1866
|
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 ?? 50, compact_maximum: roleDefaults.qa?.compact_maximum ?? 10, settings_revision: settingsRevision };
|
|
1694
1867
|
let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBuilder, qa: capturedQa, builderSessionId: resumeSessionId ?? null, builderSessionRef: resumeSessionRef ?? null }) : createBuildRun({
|
|
1868
|
+
runId: invocationRunId,
|
|
1695
1869
|
tickets: [], repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
|
|
1870
|
+
frozenPolicy, autonomyProfile, qaEnabled,
|
|
1696
1871
|
runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 50, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
1697
1872
|
});
|
|
1873
|
+
activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
|
|
1698
1874
|
activeContinuityRunId = buildRun.runId;
|
|
1875
|
+
const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, buildRun.runId) : undefined;
|
|
1699
1876
|
const heartbeat = setInterval(() => { buildRun = heartbeatBuildRun(cwd, buildRun); }, 10_000);
|
|
1700
1877
|
heartbeat.unref();
|
|
1701
1878
|
const checkpointSignal = () => {
|
|
@@ -1739,7 +1916,6 @@ export function buildStartCommand() {
|
|
|
1739
1916
|
}),
|
|
1740
1917
|
report: (event) => {
|
|
1741
1918
|
currentActivity()?.update(event.kind, event.detail, { provider: capturedBuilder.make, model: capturedBuilder.model });
|
|
1742
|
-
log.write("context-lifecycle", { ...event, sample: event.sample });
|
|
1743
1919
|
},
|
|
1744
1920
|
handoff: async ({ reason, adapter, sample, settings, compactionCount, frozenAction }) => {
|
|
1745
1921
|
const nativeUsage = await adapter.sessionUsage?.().catch(() => undefined);
|
|
@@ -1759,7 +1935,7 @@ export function buildStartCommand() {
|
|
|
1759
1935
|
compactionCount,
|
|
1760
1936
|
compactMaximum: settings.compact_maximum ?? 10,
|
|
1761
1937
|
resources: [{ label: "frozen-action", content: frozenAction, authoritative: true }],
|
|
1762
|
-
}, () => createBuilderForSettings(cwd, settings));
|
|
1938
|
+
}, (_handoff, runtime) => createBuilderForSettings(cwd, settingsForRequestedRuntime(settings, runtime)), handoffRecoveryOptions("builder"));
|
|
1763
1939
|
log.write("handoff-transfer", { generation: transfer.manifest.generation, predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId, acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest });
|
|
1764
1940
|
if (adapter instanceof ContinuityAdapter) {
|
|
1765
1941
|
await adapter.adoptValidatedSuccessor(transfer.successor);
|
|
@@ -1769,7 +1945,7 @@ export function buildStartCommand() {
|
|
|
1769
1945
|
return continuousBuilder(transfer.successor, cwd, buildRun.runId, settings);
|
|
1770
1946
|
},
|
|
1771
1947
|
});
|
|
1772
|
-
const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled,
|
|
1948
|
+
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(adapter, frozenAction)).adapter, async (adapter, frozenAction, strategy) => (await contextController.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter, qaSessionBoundary, observeQaNativeCompactions, async (adapter) => { await contextController.observeNativeCompactions(adapter); }, activeObserver, capturedQa, true, qaReportRecovery, qaResumedRecovery);
|
|
1773
1949
|
activeBuilderForStatus = () => foreman.builderAdapter();
|
|
1774
1950
|
const statusReporter = liveStatusReporter = new AgentStatusReporter({
|
|
1775
1951
|
runId: buildRun.runId,
|
|
@@ -1796,17 +1972,9 @@ export function buildStartCommand() {
|
|
|
1796
1972
|
activity.setAgentStatus(line.replace(/^\[[^\]]+\]\s*/, ""));
|
|
1797
1973
|
else
|
|
1798
1974
|
console.log(line);
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
db.recordTelemetry(buildRun.runId, snapshot);
|
|
1803
|
-
db.recordContextSample(snapshot.contextSample);
|
|
1804
|
-
if (snapshot.sessionUsage)
|
|
1805
|
-
db.recordSessionUsage(snapshot.sessionUsage);
|
|
1806
|
-
}
|
|
1807
|
-
finally {
|
|
1808
|
-
db.close();
|
|
1809
|
-
}
|
|
1975
|
+
activeObserver?.store.updateCurrentState({ runId: buildRun.runId, role: "builder", stream: "builder", executionId: activeObserver.executionId, ticketId: buildRun.currentTicket, providerSessionId: snapshot.contextSample.providerSessionId, phase: "builder work session", lastSignalAt: snapshot.contextSample.observedAt });
|
|
1976
|
+
if (snapshot.contextSample.used !== undefined)
|
|
1977
|
+
activeObserver?.store.recordMetric({ runId: buildRun.runId, executionId: activeObserver.executionId, role: "builder", stream: "builder", providerSessionId: snapshot.contextSample.providerSessionId }, "context_used_tokens", snapshot.contextSample.used, { unit: "tokens" });
|
|
1810
1978
|
});
|
|
1811
1979
|
statusReporter.start();
|
|
1812
1980
|
const modifiers = [
|
|
@@ -1824,23 +1992,24 @@ export function buildStartCommand() {
|
|
|
1824
1992
|
try {
|
|
1825
1993
|
console.log("ai-foreman: asking builder to plan the next tickets or steps...\n");
|
|
1826
1994
|
buildRun = checkpointBuildRun(cwd, buildRun, "before-preflight");
|
|
1827
|
-
await foreman.runPreflight(steps, ticketsContent, preferredTicket);
|
|
1995
|
+
await (activeObserver ? activeObserver.span("preflight", "Builder preflight", () => foreman.runPreflight(steps, ticketsContent, preferredTicket)) : foreman.runPreflight(steps, ticketsContent, preferredTicket));
|
|
1996
|
+
buildRun = withRunLocalAcceptedRuntimes(buildRun);
|
|
1828
1997
|
if (foreman.builderSessionId())
|
|
1829
1998
|
buildRun = persistBuildSession(cwd, buildRun, "builder", foreman.builderAdapter().sessionRef?.() ?? foreman.builderSessionId());
|
|
1830
1999
|
if (foreman.qaSessionId())
|
|
1831
2000
|
buildRun = persistBuildSession(cwd, buildRun, "qa", foreman.qaSessionRef() ?? foreman.qaSessionId());
|
|
1832
2001
|
buildRun = checkpointBuildRun(cwd, buildRun, "preflight-complete");
|
|
1833
|
-
if (!
|
|
2002
|
+
if (!autoApprovePlanUpdates) {
|
|
1834
2003
|
while (true) {
|
|
1835
2004
|
console.log();
|
|
1836
|
-
const action = await select({
|
|
2005
|
+
const action = await withObservedUserWait(activeObserver, "build plan decision", () => select({
|
|
1837
2006
|
message: "How does this plan look?",
|
|
1838
2007
|
options: [
|
|
1839
2008
|
{ value: "proceed", label: "Proceed — start implementing" },
|
|
1840
2009
|
{ value: "feedback", label: "Give feedback — revise the plan" },
|
|
1841
2010
|
{ value: "cancel", label: "Cancel" },
|
|
1842
2011
|
],
|
|
1843
|
-
});
|
|
2012
|
+
}));
|
|
1844
2013
|
if (isCancel(action) || action === "cancel") {
|
|
1845
2014
|
console.log("ai-foreman: cancelled");
|
|
1846
2015
|
statusReporter.stop();
|
|
@@ -1852,10 +2021,10 @@ export function buildStartCommand() {
|
|
|
1852
2021
|
console.log();
|
|
1853
2022
|
break;
|
|
1854
2023
|
}
|
|
1855
|
-
const fb = await text({
|
|
2024
|
+
const fb = await withObservedUserWait(activeObserver, "build plan feedback", () => text({
|
|
1856
2025
|
message: "Your feedback:",
|
|
1857
2026
|
validate: (v) => (v?.trim() ? undefined : "Please enter some feedback"),
|
|
1858
|
-
});
|
|
2027
|
+
}));
|
|
1859
2028
|
if (isCancel(fb)) {
|
|
1860
2029
|
console.log("ai-foreman: cancelled");
|
|
1861
2030
|
statusReporter.stop();
|
|
@@ -1867,7 +2036,7 @@ export function buildStartCommand() {
|
|
|
1867
2036
|
await foreman.sendPreflightFeedback(String(fb));
|
|
1868
2037
|
}
|
|
1869
2038
|
}
|
|
1870
|
-
const result = await foreman.runBatch(steps, trackerRelPath, (ticketId) => {
|
|
2039
|
+
const result = await (activeObserver ? activeObserver.span("builder_work", "Builder and QA batch", () => foreman.runBatch(steps, trackerRelPath, (ticketId) => {
|
|
1871
2040
|
const tickets = buildRun.tickets.includes(ticketId)
|
|
1872
2041
|
? buildRun.tickets
|
|
1873
2042
|
: [...buildRun.tickets, ticketId];
|
|
@@ -1875,7 +2044,10 @@ export function buildStartCommand() {
|
|
|
1875
2044
|
tickets,
|
|
1876
2045
|
currentTicket: ticketId,
|
|
1877
2046
|
});
|
|
1878
|
-
}, preferredTicket)
|
|
2047
|
+
}, preferredTicket)) : foreman.runBatch(steps, trackerRelPath, (ticketId) => {
|
|
2048
|
+
const tickets = buildRun.tickets.includes(ticketId) ? buildRun.tickets : [...buildRun.tickets, ticketId];
|
|
2049
|
+
buildRun = checkpointBuildRun(cwd, buildRun, "ticket-selected", { tickets, currentTicket: ticketId });
|
|
2050
|
+
}, preferredTicket));
|
|
1879
2051
|
if (pendingHandoffGeneration !== undefined)
|
|
1880
2052
|
throw new Error(`recovery handoff generation ${pendingHandoffGeneration} for ${pendingHandoffRole} was not accepted; recovery mode was not substituted`);
|
|
1881
2053
|
if (foreman.builderSessionId())
|
|
@@ -1889,6 +2061,14 @@ export function buildStartCommand() {
|
|
|
1889
2061
|
statusReporter.stop();
|
|
1890
2062
|
await foreman.close();
|
|
1891
2063
|
await viewer;
|
|
2064
|
+
activeObserver?.finish(buildRun.status, observationSummaryMetadata(cwd, buildRun, qaEnabled, capturedBuilder.make));
|
|
2065
|
+
observabilityStore?.closeLogFile(logPath, buildRun.status);
|
|
2066
|
+
observabilityStore?.enforceLimits();
|
|
2067
|
+
observabilityStore?.maintainLogs(cwd);
|
|
2068
|
+
observabilityStore?.close();
|
|
2069
|
+
pruneRecoveryTelemetryAtCompletion(cwd, config.observability.detail_retention_days);
|
|
2070
|
+
activeObserver = undefined;
|
|
2071
|
+
process.off("exit", finishObservationOnExit);
|
|
1892
2072
|
console.log(`\nforeman: ${result.completed}/${result.requested} step(s) completed`);
|
|
1893
2073
|
console.log(`foreman: outcome — ${result.outcome}`);
|
|
1894
2074
|
if (result.detail)
|
|
@@ -1902,7 +2082,8 @@ export function buildStartCommand() {
|
|
|
1902
2082
|
console.log(line);
|
|
1903
2083
|
}
|
|
1904
2084
|
}
|
|
1905
|
-
|
|
2085
|
+
fireTerminalBell(config.notifications.terminal_bell);
|
|
2086
|
+
process.exit(result.outcome === "needs-human" || result.outcome === "blocked" ? 2 : 0);
|
|
1906
2087
|
}
|
|
1907
2088
|
catch (err) {
|
|
1908
2089
|
clearInterval(heartbeat);
|
|
@@ -1934,6 +2115,18 @@ export function buildStartCommand() {
|
|
|
1934
2115
|
: `foreman: inspect recovery with: rafi build:resume ${shellQuote(cwd)} --run ${shellQuote(buildRun.runId)} --inspect`);
|
|
1935
2116
|
process.exit(2);
|
|
1936
2117
|
}
|
|
2118
|
+
if (err instanceof HandoffAcceptanceError || err instanceof HandoffRecoveryPausedError) {
|
|
2119
|
+
const summary = err.message.slice(0, 1000);
|
|
2120
|
+
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "handoff-recovery-required", {
|
|
2121
|
+
status: "recoverable",
|
|
2122
|
+
failure: { category: "unknown", summary, at: new Date().toISOString() },
|
|
2123
|
+
}), "recoverable");
|
|
2124
|
+
await foreman.close().catch(() => { });
|
|
2125
|
+
log.write("safe-boundary-paused", { runId: buildRun.runId, message: summary, handoff: true });
|
|
2126
|
+
console.error(`foreman: handoff paused at a recoverable checkpoint: ${summary}`);
|
|
2127
|
+
console.error(`foreman: resume this run with: rafi build:resume ${shellQuote(cwd)} --run ${shellQuote(buildRun.runId)}`);
|
|
2128
|
+
process.exit(2);
|
|
2129
|
+
}
|
|
1937
2130
|
if (err instanceof HandoffLoopError || err instanceof ContinuityRecoveryRequiredError) {
|
|
1938
2131
|
const summary = err.message.slice(0, 1000);
|
|
1939
2132
|
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "guided-recovery-required", {
|
|
@@ -1953,6 +2146,74 @@ export function buildStartCommand() {
|
|
|
1953
2146
|
}
|
|
1954
2147
|
});
|
|
1955
2148
|
}
|
|
2149
|
+
function withObservedUserWait(observer, label, operation) {
|
|
2150
|
+
return observer ? observer.span("user_wait", label, operation) : operation();
|
|
2151
|
+
}
|
|
2152
|
+
function ticketSizeBucket(projectDir, ticketIds) {
|
|
2153
|
+
if (!ticketIds.length || !isTicketsInitialized(projectDir))
|
|
2154
|
+
return undefined;
|
|
2155
|
+
try {
|
|
2156
|
+
const paths = resolveTicketPaths(loadTicketsConfig(projectDir), projectDir);
|
|
2157
|
+
const ranks = new Map([["XS", 0], ["S", 1], ["M", 2], ["L", 3], ["XL", 4]]);
|
|
2158
|
+
return loadTickets(paths.tickets).filter(ticket => ticketIds.includes(ticket.id))
|
|
2159
|
+
.sort((left, right) => (ranks.get(right.size) ?? -1) - (ranks.get(left.size) ?? -1))[0]?.size;
|
|
2160
|
+
}
|
|
2161
|
+
catch {
|
|
2162
|
+
return undefined;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
function observationSummaryMetadata(projectDir, run, qaEnabled, provider, reviewRun) {
|
|
2166
|
+
const worktree = run.repository.worktree;
|
|
2167
|
+
const terminalHead = reviewRun?.commit ?? diagnosticGit(worktree, ["rev-parse", "HEAD"]);
|
|
2168
|
+
const base = run.repository.git.baselineHead ?? run.repository.baseHead;
|
|
2169
|
+
const commitCount = base ? Number(diagnosticGit(worktree, ["rev-list", "--count", `${base}..HEAD`])) : undefined;
|
|
2170
|
+
const status = diagnosticGit(worktree, ["status", "--porcelain=v1"]);
|
|
2171
|
+
const numstat = base ? diagnosticGit(worktree, ["diff", "--numstat", `${base}..HEAD`]) : undefined;
|
|
2172
|
+
let additions = 0;
|
|
2173
|
+
let deletions = 0;
|
|
2174
|
+
for (const line of numstat?.split("\n").filter(Boolean) ?? []) {
|
|
2175
|
+
const [added, deleted] = line.split("\t");
|
|
2176
|
+
if (/^\d+$/.test(added ?? ""))
|
|
2177
|
+
additions += Number(added);
|
|
2178
|
+
if (/^\d+$/.test(deleted ?? ""))
|
|
2179
|
+
deletions += Number(deleted);
|
|
2180
|
+
}
|
|
2181
|
+
const receipt = Object.entries(run.receipts).find(([key, value]) => /(?:pull|merge|pr|mr)/i.test(`${key} ${value.externalId ?? ""}`));
|
|
2182
|
+
const review = reviewRun?.pr;
|
|
2183
|
+
const reviewId = review?.url?.match(/\/(?:pull|merge_requests)\/(\d+)(?:\D|$)/)?.[1] ?? receipt?.[1].externalId;
|
|
2184
|
+
return {
|
|
2185
|
+
branchMode: run.branchMode, qaEnabled, primaryProvider: provider, model: run.builder?.settings.model,
|
|
2186
|
+
ticketCount: run.tickets.length, ticketIds: run.tickets, deliveryUnit: run.deliveryUnit, checkpoint: run.checkpoint,
|
|
2187
|
+
failureCategory: run.failure?.category ?? run.interruption?.category, ticketSizeBucket: ticketSizeBucket(projectDir, run.tickets),
|
|
2188
|
+
createdAt: run.createdAt, updatedAt: run.updatedAt, completedAt: run.completedAt,
|
|
2189
|
+
git: { branch: reviewRun?.branch ?? run.repository.git.branch ?? run.repository.branch, terminalHead, ...(Number.isFinite(commitCount) ? { commitCount } : {}),
|
|
2190
|
+
changedPathCount: status?.split("\n").filter(Boolean).length, ...(numstat !== undefined ? { additions, deletions } : {}),
|
|
2191
|
+
...(reviewId ? { pullRequest: { provider: /merge_requests\//.test(review?.url ?? "") || /mr|gitlab/i.test(receipt?.[0] ?? "") ? "gitlab" : "github", id: reviewId, state: review?.status, url: review?.url } } : {}),
|
|
2192
|
+
observedAt: new Date().toISOString(), historical: true },
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
function diagnosticGit(cwd, args) {
|
|
2196
|
+
try {
|
|
2197
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, maxBuffer: 1024 * 1024 }).trim();
|
|
2198
|
+
}
|
|
2199
|
+
catch {
|
|
2200
|
+
return undefined;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function pruneRecoveryTelemetryAtCompletion(projectDir, retentionDays) {
|
|
2204
|
+
try {
|
|
2205
|
+
const db = new WorkflowDb(projectDir);
|
|
2206
|
+
try {
|
|
2207
|
+
db.pruneTerminalTelemetry(retentionDays);
|
|
2208
|
+
}
|
|
2209
|
+
finally {
|
|
2210
|
+
db.close();
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
catch (error) {
|
|
2214
|
+
console.error(`rafi: recovery telemetry maintenance skipped: ${String(error).replace(/\s+/g, " ").slice(0, 300)}`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
1956
2217
|
async function prepareNativeAutoCompaction(adapter) {
|
|
1957
2218
|
if (!adapter.prepareAutoCompaction) {
|
|
1958
2219
|
await adapter.close().catch(() => { });
|
|
@@ -1971,25 +2232,25 @@ async function prepareNativeAutoCompaction(adapter) {
|
|
|
1971
2232
|
throw error;
|
|
1972
2233
|
}
|
|
1973
2234
|
}
|
|
1974
|
-
function createQaNonconvergenceHandler(projectDir, noninteractive, planner) {
|
|
2235
|
+
function createQaNonconvergenceHandler(projectDir, noninteractive, planner, observer) {
|
|
1975
2236
|
return async (context) => {
|
|
1976
2237
|
if (noninteractive || !process.stdin.isTTY || !process.stdout.isTTY)
|
|
1977
2238
|
return { action: "pause" };
|
|
1978
2239
|
while (true) {
|
|
1979
|
-
const choice = await select({ message: `QA did not converge for ${context.ticket.id}. What next?`, options: [
|
|
2240
|
+
const choice = await withObservedUserWait(observer?.(), "QA nonconvergence decision", () => select({ message: `QA did not converge for ${context.ticket.id}. What next?`, options: [
|
|
1980
2241
|
{ value: "retry", label: "Retry another Builder fix" },
|
|
1981
2242
|
{ value: "pause", label: "Pause at this checkpoint" },
|
|
1982
2243
|
{ value: "waive", label: "Explicitly waive QA" },
|
|
1983
2244
|
{ value: "planner", label: "Discuss with read-only Planner" },
|
|
1984
|
-
] });
|
|
2245
|
+
] }));
|
|
1985
2246
|
if (isCancel(choice) || choice === "pause")
|
|
1986
2247
|
return { action: "pause" };
|
|
1987
2248
|
if (choice === "retry")
|
|
1988
2249
|
return { action: "retry" };
|
|
1989
2250
|
if (choice === "waive") {
|
|
1990
|
-
const approved = await select({ message: "Waive QA and record validation_result=failed with all unresolved issues?", options: [
|
|
2251
|
+
const approved = await withObservedUserWait(observer?.(), "QA waiver confirmation", () => select({ message: "Waive QA and record validation_result=failed with all unresolved issues?", options: [
|
|
1991
2252
|
{ value: "no", label: "No (Recommended)" }, { value: "yes", label: "Yes, waive and continue" },
|
|
1992
|
-
] });
|
|
2253
|
+
] }));
|
|
1993
2254
|
if (approved === "yes") {
|
|
1994
2255
|
const db = new WorkflowDb(projectDir);
|
|
1995
2256
|
try {
|
|
@@ -2008,18 +2269,26 @@ function createQaNonconvergenceHandler(projectDir, noninteractive, planner) {
|
|
|
2008
2269
|
}
|
|
2009
2270
|
continue;
|
|
2010
2271
|
}
|
|
2011
|
-
const discussion = await text({ message: "What should the Planner focus on?", defaultValue: "Explain the QA failures and propose the safest concrete remediation." });
|
|
2272
|
+
const discussion = await withObservedUserWait(observer?.(), "QA remediation guidance", () => text({ message: "What should the Planner focus on?", defaultValue: "Explain the QA failures and propose the safest concrete remediation." }));
|
|
2012
2273
|
if (isCancel(discussion))
|
|
2013
2274
|
continue;
|
|
2014
|
-
const
|
|
2275
|
+
const rawDiff = execFileSync("git", ["-C", context.builderWorktree, "diff", "--binary", "HEAD"], { encoding: "buffer", maxBuffer: 128 * 1024 * 1024 });
|
|
2276
|
+
const diffLimit = 128 * 1024;
|
|
2277
|
+
const diffTruncated = rawDiff.length > diffLimit;
|
|
2278
|
+
const diff = rawDiff.subarray(0, diffLimit).toString("utf8");
|
|
2279
|
+
const changeSummary = execFileSync("git", ["-C", context.builderWorktree, "status", "--short", "--untracked-files=all"], { encoding: "utf8" }).split("\n").filter(Boolean).sort().join("\n");
|
|
2280
|
+
const untracked = execFileSync("git", ["-C", context.builderWorktree, "ls-files", "--others", "--exclude-standard"], { encoding: "utf8" }).split("\n").filter(Boolean).sort();
|
|
2015
2281
|
let instruction = [
|
|
2016
2282
|
"You are the read-only Planner mediating QA nonconvergence. Do not edit files or run mutating tools.",
|
|
2017
2283
|
`Ticket: ${context.ticket.id} — ${context.ticket.title}`,
|
|
2018
2284
|
`Requirements: ${context.ticket.acceptance.join("; ")}`,
|
|
2019
2285
|
`Required tests: ${context.ticket.required_tests.join("; ")}`,
|
|
2286
|
+
`Builder worktree: ${context.builderWorktree}`,
|
|
2287
|
+
`Deterministic change summary:\n${changeSummary || "(clean)"}`,
|
|
2288
|
+
`Untracked-file inventory:\n${untracked.join("\n") || "(none)"}`,
|
|
2020
2289
|
`User discussion: ${String(discussion)}`,
|
|
2021
2290
|
`Complete QA history:\n${JSON.stringify(context.history, null, 2)}`,
|
|
2022
|
-
|
|
2291
|
+
`${diffTruncated ? "Builder diff (TRUNCATED at 128 KiB; inspect the actual worktree before relying on it)" : "Full Builder diff"}:\n${diff || "(no tracked diff)"}`,
|
|
2023
2292
|
"Return RAFI_QA_REMEDIATION_START, then one JSON object {\"summary\":\"...\",\"fix_instructions\":[\"...\"]}, then RAFI_QA_REMEDIATION_END.",
|
|
2024
2293
|
"End with STEP_STATUS: plan_complete | summary=\"QA remediation proposed\"",
|
|
2025
2294
|
].join("\n\n");
|
|
@@ -2040,14 +2309,14 @@ function createQaNonconvergenceHandler(projectDir, noninteractive, planner) {
|
|
|
2040
2309
|
if (!proposal.summary || !Array.isArray(proposal.fix_instructions) || !proposal.fix_instructions.every((item) => typeof item === "string"))
|
|
2041
2310
|
throw new Error("Planner remediation proposal is malformed");
|
|
2042
2311
|
console.log(`\nPlanner remediation: ${proposal.summary}\n${proposal.fix_instructions.map((item) => `- ${item}`).join("\n")}`);
|
|
2043
|
-
const decision = await select({ message: "Use this remediation for a new Builder fix session?", options: [
|
|
2312
|
+
const decision = await withObservedUserWait(observer?.(), "QA remediation approval", () => select({ message: "Use this remediation for a new Builder fix session?", options: [
|
|
2044
2313
|
{ value: "approve", label: "Approve (Recommended)" }, { value: "revise", label: "Discuss/revise" }, { value: "cancel", label: "Cancel and return to choices" },
|
|
2045
|
-
] });
|
|
2314
|
+
] }));
|
|
2046
2315
|
if (decision === "approve")
|
|
2047
2316
|
return { action: "remediate", remediation: [proposal.summary, ...proposal.fix_instructions].join("\n") };
|
|
2048
2317
|
if (isCancel(decision) || decision === "cancel")
|
|
2049
2318
|
break;
|
|
2050
|
-
const feedback = await text({ message: "Planner revision feedback:" });
|
|
2319
|
+
const feedback = await withObservedUserWait(observer?.(), "QA Planner revision feedback", () => text({ message: "Planner revision feedback:" }));
|
|
2051
2320
|
if (isCancel(feedback))
|
|
2052
2321
|
break;
|
|
2053
2322
|
resumeSessionId = run.sessionId;
|
|
@@ -2056,6 +2325,41 @@ function createQaNonconvergenceHandler(projectDir, noninteractive, planner) {
|
|
|
2056
2325
|
}
|
|
2057
2326
|
};
|
|
2058
2327
|
}
|
|
2328
|
+
function createQaReportRecoveryHandler(noninteractive, observer) {
|
|
2329
|
+
return async ({ packet, originalIssues, liveSession, contextUsage }) => {
|
|
2330
|
+
if (noninteractive || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2331
|
+
console.error(`foreman: QA report recovery requires input; saved packet ${packet.directory}`);
|
|
2332
|
+
console.error(`foreman: resume with the run ID recorded in ${join(packet.directory, "manifest.json")}`);
|
|
2333
|
+
return { action: "pause" };
|
|
2334
|
+
}
|
|
2335
|
+
const choice = await withObservedUserWait(observer?.(), "QA report recovery decision", () => select({
|
|
2336
|
+
message: "QA report recovery:",
|
|
2337
|
+
options: [
|
|
2338
|
+
{ value: "fresh", label: "Try another fresh QA." },
|
|
2339
|
+
{ value: "plain", label: "Use plain issues fallback.", hint: originalIssues ? originalIssues.slice(0, 120) : "Unavailable: original issues synopsis was empty" },
|
|
2340
|
+
{ value: "manual", label: "Manually fix saved JSON.", hint: join(packet.directory, "report.json") },
|
|
2341
|
+
{ value: "guidance", label: "Give QA specific instructions." },
|
|
2342
|
+
{ value: "pause", label: "Pause." },
|
|
2343
|
+
],
|
|
2344
|
+
}));
|
|
2345
|
+
if (isCancel(choice) || choice === "pause")
|
|
2346
|
+
return { action: "pause" };
|
|
2347
|
+
if (choice === "fresh")
|
|
2348
|
+
return { action: "fresh" };
|
|
2349
|
+
if (choice === "plain")
|
|
2350
|
+
return { action: "plain" };
|
|
2351
|
+
if (choice === "manual")
|
|
2352
|
+
return { action: "manual" };
|
|
2353
|
+
console.log(`foreman: current QA context usage: ${JSON.stringify(contextUsage).slice(0, 500)}`);
|
|
2354
|
+
const guidance = await withObservedUserWait(observer?.(), "QA recovery guidance", () => text({ message: "Specific QA report-recovery instructions:" }));
|
|
2355
|
+
if (isCancel(guidance) || !String(guidance).trim())
|
|
2356
|
+
return { action: "pause" };
|
|
2357
|
+
const route = await withObservedUserWait(observer?.(), "QA recovery session choice", () => select({ message: "Use which QA session route?", options: liveSession ? [
|
|
2358
|
+
{ value: "current", label: "Current session" }, { value: "compact", label: "Compact current session" }, { value: "fresh", label: "Fresh validated handoff" },
|
|
2359
|
+
] : [{ value: "fresh", label: "Fresh validated handoff (resumed recovery)" }] }));
|
|
2360
|
+
return isCancel(route) ? { action: "pause" } : { action: "guidance", instructions: String(guidance), route };
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2059
2363
|
function readAgentDefaults(projectDir) {
|
|
2060
2364
|
const path = join(projectDir, "rafi-config.yaml");
|
|
2061
2365
|
if (!existsSync(path))
|