ai-foreman 1.7.25 → 1.7.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/adapters/claude.d.ts +15 -0
- package/dist/adapters/claude.js +149 -28
- package/dist/adapters/codex.d.ts +21 -0
- package/dist/adapters/codex.js +127 -9
- package/dist/adapters/fastMode.d.ts +4 -0
- package/dist/adapters/fastMode.js +9 -0
- package/dist/adapters/recovering.d.ts +4 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/sessionFailure.d.ts +6 -0
- package/dist/adapters/sessionFailure.js +19 -0
- package/dist/adapters/types.d.ts +8 -0
- package/dist/agentRun.js +1 -0
- package/dist/branch/currentGuard.d.ts +8 -0
- package/dist/branch/currentGuard.js +5 -0
- package/dist/branch/runner.js +52 -46
- package/dist/buildApproval.js +2 -1
- package/dist/buildAssignment.d.ts +15 -0
- package/dist/buildAssignment.js +142 -5
- package/dist/buildRuns.d.ts +18 -0
- package/dist/buildRuns.js +123 -7
- package/dist/buildWorkContext.js +1 -1
- package/dist/buildWorkMigration.d.ts +10 -0
- package/dist/buildWorkMigration.js +18 -2
- package/dist/cli/start.d.ts +13 -0
- package/dist/cli/start.js +286 -59
- package/dist/cli/state.js +39 -1
- package/dist/cli/tickets.d.ts +37 -0
- package/dist/cli/tickets.js +367 -12
- package/dist/config.js +1 -0
- package/dist/continuity.d.ts +1 -0
- package/dist/continuity.js +16 -10
- package/dist/foreman.d.ts +8 -0
- package/dist/foreman.js +158 -18
- package/dist/graph/preparation.js +6 -3
- package/dist/graph/semantic.js +2 -1
- package/dist/graph/turn.js +8 -1
- package/dist/handoffProtocol.d.ts +4 -0
- package/dist/handoffProtocol.js +20 -0
- package/dist/handoffs.d.ts +1 -1
- package/dist/handoffs.js +30 -23
- package/dist/log.d.ts +1 -1
- package/dist/managerPacket.js +1 -1
- package/dist/permissions/policy.js +39 -11
- package/dist/permissions/shellForms.d.ts +3 -0
- package/dist/permissions/shellForms.js +203 -0
- package/dist/planChanges.js +1 -1
- package/dist/projectDiagnostics.js +30 -0
- package/dist/providerQuestions.js +4 -24
- package/dist/qaBuildGate.d.ts +6 -1
- package/dist/qaBuildGate.js +76 -16
- package/dist/qaBuilderActivation.d.ts +19 -0
- package/dist/qaBuilderActivation.js +112 -0
- package/dist/qaBuilderCoverage.d.ts +2 -1
- package/dist/qaBuilderCoverage.js +47 -5
- package/dist/qaBuilderEvidence.d.ts +18 -0
- package/dist/qaBuilderEvidence.js +60 -0
- package/dist/qaContractCoverage.d.ts +12 -1
- package/dist/qaContractCoverage.js +34 -2
- package/dist/qaContractDelivery.d.ts +2 -2
- package/dist/qaContractDelivery.js +88 -10
- package/dist/qaEffectiveConfig.js +2 -1
- package/dist/qaFailureDelivery.d.ts +23 -0
- package/dist/qaFailureDelivery.js +88 -7
- package/dist/qaHostFacts.d.ts +2 -0
- package/dist/qaHostFacts.js +11 -0
- package/dist/qaPreparation.d.ts +38 -1
- package/dist/qaPreparation.js +372 -31
- package/dist/qaPreparationInputs.js +5 -1
- package/dist/qaPreparationPolicy.d.ts +2 -1
- package/dist/qaPreparationPolicy.js +39 -9
- package/dist/qaRecovery.d.ts +7 -0
- package/dist/qaRecovery.js +75 -2
- package/dist/qaReview.d.ts +4 -0
- package/dist/qaReview.js +72 -15
- package/dist/qaReviewEvidence.d.ts +6 -0
- package/dist/qaReviewEvidence.js +37 -0
- package/dist/qaRollbackRehearsal.d.ts +2 -0
- package/dist/qaRollbackRehearsal.js +78 -0
- package/dist/qaSnapshot.d.ts +3 -1
- package/dist/qaSnapshot.js +3 -2
- package/dist/qaVerificationContract.d.ts +9 -0
- package/dist/qaVerificationContract.js +143 -17
- package/dist/sessionLifecycle.js +1 -1
- package/dist/sources/sourceRegistry.js +16 -5
- package/dist/stateTransfer.d.ts +14 -0
- package/dist/stateTransfer.js +51 -0
- package/dist/statusReporter.d.ts +8 -0
- package/dist/statusReporter.js +5 -0
- package/dist/supervisedStart.js +23 -3
- package/dist/supervisor.js +33 -0
- package/dist/ticketPlanning.d.ts +373 -0
- package/dist/ticketPlanning.js +34 -3
- package/dist/ticketPopulation.d.ts +28 -3
- package/dist/ticketPopulation.js +125 -15
- package/dist/tickets/details.d.ts +2 -1
- package/dist/tickets/details.js +7 -3
- package/dist/tickets/eligibility.d.ts +6 -0
- package/dist/tickets/eligibility.js +12 -1
- package/dist/tickets/groupReset.js +100 -96
- package/dist/tickets/reset.js +80 -69
- package/dist/tickets/riskWaivers.d.ts +3 -0
- package/dist/tickets/riskWaivers.js +12 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +23 -0
- package/dist/tickets/ticketLoader.d.ts +9 -0
- package/dist/tickets/ticketLoader.js +15 -2
- package/dist/tickets/ticketSchema.d.ts +14 -1
- package/dist/tickets/ticketSchema.js +2 -1
- package/dist/tickets/validate.js +11 -1
- package/dist/workflowDb.d.ts +16 -1
- package/dist/workflowDb.js +287 -10
- package/package.json +9 -4
package/dist/adapters/codex.js
CHANGED
|
@@ -9,6 +9,8 @@ import { BuilderEventQueue, withActivityPhase } from "../activity.js";
|
|
|
9
9
|
import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
|
|
10
10
|
import { canonicalSessionPath, createProviderSessionRef, validateProviderSessionScope } from "../sessionIdentity.js";
|
|
11
11
|
import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
|
|
12
|
+
import { isFastModeUnavailable, reportFastModeUnavailable } from "./fastMode.js";
|
|
13
|
+
import { handoffSystemInstructions } from "../handoffProtocol.js";
|
|
12
14
|
/** Compatibility parser for recorded pre-app-server JSONL fixtures. */
|
|
13
15
|
export function parseCodexLine(raw) {
|
|
14
16
|
const type = raw.type;
|
|
@@ -42,6 +44,7 @@ export class CodexAdapter {
|
|
|
42
44
|
sessionAvailability;
|
|
43
45
|
initialized = false;
|
|
44
46
|
closed = false;
|
|
47
|
+
closePromise;
|
|
45
48
|
activeText = [];
|
|
46
49
|
usage;
|
|
47
50
|
usageRevision = 0;
|
|
@@ -67,12 +70,73 @@ export class CodexAdapter {
|
|
|
67
70
|
answeredApprovalRequests = new Set();
|
|
68
71
|
activeProviderTurnSpanId;
|
|
69
72
|
toolSpans = new Map();
|
|
73
|
+
effectiveModel;
|
|
74
|
+
fastPreparedModel;
|
|
75
|
+
fastTier;
|
|
76
|
+
fastUnavailableModels = new Set();
|
|
77
|
+
turnSawTool = false;
|
|
78
|
+
/** Native compaction is non-blocking. The next host turn gets this reminder. */
|
|
79
|
+
qaContractReminder;
|
|
80
|
+
qaContractReminderPending = false;
|
|
81
|
+
qaContractReminderEvent;
|
|
82
|
+
disableFastMode() {
|
|
83
|
+
const model = this.opts.model ?? this.effectiveModel ?? "default";
|
|
84
|
+
if (!this.fastUnavailableModels.has(model))
|
|
85
|
+
reportFastModeUnavailable(model);
|
|
86
|
+
this.fastUnavailableModels.add(model);
|
|
87
|
+
this.fastTier = undefined;
|
|
88
|
+
}
|
|
89
|
+
async prepareFastMode() {
|
|
90
|
+
const model = this.opts.model ?? this.effectiveModel ?? "default";
|
|
91
|
+
if (!this.opts.fast || this.fastPreparedModel === model)
|
|
92
|
+
return;
|
|
93
|
+
this.fastPreparedModel = model;
|
|
94
|
+
this.fastTier = undefined;
|
|
95
|
+
if (this.fastUnavailableModels.has(model))
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
let cursor;
|
|
99
|
+
for (let page = 0; page < 20; page++) {
|
|
100
|
+
const result = await this.request("model/list", { includeHidden: true, ...(cursor ? { cursor } : {}) });
|
|
101
|
+
const entry = result.data?.find(item => item.model === model || item.id === model);
|
|
102
|
+
if (entry) {
|
|
103
|
+
this.fastTier = entry.serviceTiers?.find(tier => tier.name?.toLowerCase() === "fast" || ["fast", "priority"].includes(tier.id))?.id;
|
|
104
|
+
// Older app-server versions publish only the legacy speed list.
|
|
105
|
+
if (entry.serviceTiers === undefined && entry.additionalSpeedTiers?.includes("fast"))
|
|
106
|
+
this.fastTier = "fast";
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (!result.nextCursor || result.nextCursor === cursor)
|
|
110
|
+
break;
|
|
111
|
+
cursor = result.nextCursor;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch { /* Optional capability discovery never blocks a standard turn. */ }
|
|
115
|
+
if (!this.fastTier)
|
|
116
|
+
this.disableFastMode();
|
|
117
|
+
}
|
|
70
118
|
constructor(opts) {
|
|
71
119
|
this.opts = opts;
|
|
72
120
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
73
121
|
this._sessionRef = opts.resumeSessionRef;
|
|
74
122
|
}
|
|
75
123
|
contractCapabilities() { return { sameSessionAcceptance: true, nativeCompactionBarrier: false }; }
|
|
124
|
+
contractCompactionSequence() { return this.nativeCompactionSequence; }
|
|
125
|
+
configureQaContractReminder(reminder, onEvent) {
|
|
126
|
+
this.qaContractReminder = reminder;
|
|
127
|
+
this.qaContractReminderEvent = onEvent;
|
|
128
|
+
onEvent?.({ kind: "installed", at: new Date().toISOString(), sessionId: this._sessionId });
|
|
129
|
+
}
|
|
130
|
+
qaReminderEvent(kind) {
|
|
131
|
+
this.qaContractReminderEvent?.({ kind, at: new Date().toISOString(), sessionId: this._sessionId });
|
|
132
|
+
}
|
|
133
|
+
instructionWithPendingQaReminder(instruction) {
|
|
134
|
+
if (!this.qaContractReminderPending || !this.qaContractReminder)
|
|
135
|
+
return { instruction, includedReminder: false };
|
|
136
|
+
// Do not clear this before the provider has completed a turn. A failed or
|
|
137
|
+
// disconnected dispatch is not evidence that Codex received the reminder.
|
|
138
|
+
return { instruction: `${this.qaContractReminder}\n\nContinue the assigned ticket now.\n\n${instruction}`, includedReminder: true };
|
|
139
|
+
}
|
|
76
140
|
buildInstruction(instruction) {
|
|
77
141
|
return [this.opts.systemPromptAppend,
|
|
78
142
|
this.opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${this.effectiveNetworkAccess() ? "explicitly approved for this build" : "disabled"}; sandbox escalation ${this.effectiveApprovalPolicy() === "on-request" ? "requires separate approval for each exact operation" : "disabled"}. Web tools do not establish shell network access. If acquisition needs unavailable capabilities, request input before running acquisition, or use a local source/dependency bundle with verified provenance. A ticket answer does not change runtime permissions.` : undefined,
|
|
@@ -93,20 +157,32 @@ export class CodexAdapter {
|
|
|
93
157
|
args.push("-m", this.opts.model);
|
|
94
158
|
if (this.opts.effort)
|
|
95
159
|
args.push("-c", `model_reasoning_effort=${this.opts.effort}`);
|
|
96
|
-
else if (this.opts.fast)
|
|
97
|
-
args.push("-c", "model_reasoning_effort=low");
|
|
98
160
|
if (this._sessionId)
|
|
99
161
|
args.push("resume", this._sessionId);
|
|
100
162
|
args.push(instruction);
|
|
101
163
|
return args;
|
|
102
164
|
}
|
|
103
165
|
async sendTurn(instruction, policy) {
|
|
166
|
+
const prepared = this.instructionWithPendingQaReminder(instruction);
|
|
167
|
+
instruction = prepared.instruction;
|
|
104
168
|
const turnId = randomUUID();
|
|
105
169
|
this.activeProviderTurnId = turnId;
|
|
106
170
|
const observer = this.opts.observer;
|
|
107
171
|
if (!observer) {
|
|
108
172
|
try {
|
|
109
|
-
|
|
173
|
+
const result = await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction, policy));
|
|
174
|
+
if (prepared.includedReminder && !result.isError && !result.failure) {
|
|
175
|
+
this.qaContractReminderPending = false;
|
|
176
|
+
this.qaReminderEvent("reminder-delivered");
|
|
177
|
+
}
|
|
178
|
+
else if (prepared.includedReminder)
|
|
179
|
+
this.qaReminderEvent("reminder-retained");
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (prepared.includedReminder)
|
|
184
|
+
this.qaReminderEvent("reminder-retained");
|
|
185
|
+
throw error;
|
|
110
186
|
}
|
|
111
187
|
finally {
|
|
112
188
|
this.activeProviderTurnId = undefined;
|
|
@@ -120,9 +196,17 @@ export class CodexAdapter {
|
|
|
120
196
|
try {
|
|
121
197
|
const result = await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction, policy));
|
|
122
198
|
observer.store.finishSpan(spanId, { outcome: result.isError ? "failed" : "completed", attributes: { usage: result.usage } });
|
|
199
|
+
if (prepared.includedReminder && !result.isError && !result.failure) {
|
|
200
|
+
this.qaContractReminderPending = false;
|
|
201
|
+
this.qaReminderEvent("reminder-delivered");
|
|
202
|
+
}
|
|
203
|
+
else if (prepared.includedReminder)
|
|
204
|
+
this.qaReminderEvent("reminder-retained");
|
|
123
205
|
return result;
|
|
124
206
|
}
|
|
125
207
|
catch (error) {
|
|
208
|
+
if (prepared.includedReminder)
|
|
209
|
+
this.qaReminderEvent("reminder-retained");
|
|
126
210
|
observer.store.finishSpan(spanId, { outcome: "failed", attributes: { error: String(error).slice(0, 500) } });
|
|
127
211
|
throw error;
|
|
128
212
|
}
|
|
@@ -144,23 +228,38 @@ export class CodexAdapter {
|
|
|
144
228
|
const providerInstruction = this.buildInstruction(instruction);
|
|
145
229
|
try {
|
|
146
230
|
await this.ensureThread();
|
|
231
|
+
if (this.opts.fast)
|
|
232
|
+
await this.prepareFastMode();
|
|
147
233
|
const toolsBefore = this.observedToolCalls;
|
|
234
|
+
this.turnSawTool = false;
|
|
148
235
|
this.activeText = [];
|
|
149
236
|
this.lastTurnTokens = undefined;
|
|
150
237
|
this.turnUsageBaseline = this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined;
|
|
151
238
|
const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
|
|
239
|
+
void completion.catch(() => { });
|
|
152
240
|
turnStartDispatched = true;
|
|
153
|
-
const
|
|
241
|
+
const startParams = {
|
|
154
242
|
threadId: this._sessionId,
|
|
155
243
|
input: [{ type: "text", text: providerInstruction, text_elements: [] }],
|
|
156
244
|
cwd: this.opts.cwd,
|
|
157
|
-
model: this.opts.model ?? null,
|
|
158
|
-
effort: this.opts.effort ??
|
|
245
|
+
model: this.opts.model ?? this.effectiveModel ?? null,
|
|
246
|
+
effort: this.opts.effort ?? null,
|
|
247
|
+
...(this.opts.fast ? { serviceTier: this.fastTier ?? "default" } : {}),
|
|
159
248
|
approvalPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" ? "never" : this.effectiveApprovalPolicy(),
|
|
160
249
|
sandboxPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" || this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa"
|
|
161
250
|
? { type: "readOnly", networkAccess: false }
|
|
162
251
|
: { type: "workspaceWrite", writableRoots: [this.opts.cwd], networkAccess: this.effectiveNetworkAccess() },
|
|
163
|
-
}
|
|
252
|
+
};
|
|
253
|
+
let started;
|
|
254
|
+
try {
|
|
255
|
+
started = await this.request("turn/start", startParams);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (!this.fastTier || !isFastModeUnavailable(String(error)) || this.turnSawTool || this.activeText.length)
|
|
259
|
+
throw error;
|
|
260
|
+
this.disableFastMode();
|
|
261
|
+
started = await this.request("turn/start", { ...startParams, serviceTier: "default" });
|
|
262
|
+
}
|
|
164
263
|
this.activeNativeTurnId = String(started.turn?.id ?? "") || undefined;
|
|
165
264
|
const params = await new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000).run(() => completion);
|
|
166
265
|
const turn = params.turn;
|
|
@@ -168,6 +267,10 @@ export class CodexAdapter {
|
|
|
168
267
|
const failed = turn?.status === "failed" || responseOnlyViolation;
|
|
169
268
|
const error = turn?.error;
|
|
170
269
|
const text = this.activeText.join("\n");
|
|
270
|
+
if (failed && !responseOnlyViolation && this.fastTier && !this.turnSawTool && !text && isFastModeUnavailable(String(error?.message ?? ""))) {
|
|
271
|
+
this.disableFastMode();
|
|
272
|
+
return this.sendTurnInternal(instruction, policy);
|
|
273
|
+
}
|
|
171
274
|
const result = {
|
|
172
275
|
text: failed ? normalizeRuntimeErrorText("codex", String(responseOnlyViolation ? "Response-only correction used tools; its result cannot authorize completion" : error?.message ?? text), null, "app-server turn") : text,
|
|
173
276
|
isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
|
|
@@ -381,6 +484,8 @@ export class CodexAdapter {
|
|
|
381
484
|
}
|
|
382
485
|
async sessionUsage() { return this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined; }
|
|
383
486
|
async switchSettings(settings) {
|
|
487
|
+
if (settings.model !== this.opts.model || settings.fast !== this.opts.fast)
|
|
488
|
+
this.fastPreparedModel = undefined;
|
|
384
489
|
this.opts.model = settings.model;
|
|
385
490
|
this.opts.effort = settings.effort;
|
|
386
491
|
this.opts.fast = settings.fast;
|
|
@@ -417,7 +522,8 @@ export class CodexAdapter {
|
|
|
417
522
|
}
|
|
418
523
|
observeEvents(listener) { return this.eventQueue.observe(listener); }
|
|
419
524
|
events() { return this.eventQueue; }
|
|
420
|
-
|
|
525
|
+
close() { return this.closePromise ??= this.closeInternal(); }
|
|
526
|
+
async closeInternal() {
|
|
421
527
|
if (this.closed)
|
|
422
528
|
return;
|
|
423
529
|
this.closed = true;
|
|
@@ -475,7 +581,7 @@ export class CodexAdapter {
|
|
|
475
581
|
model: this.opts.model ?? null, approvalPolicy: this.effectiveApprovalPolicy(),
|
|
476
582
|
config: { sandbox_workspace_write: { network_access: this.effectiveNetworkAccess() } },
|
|
477
583
|
sandbox: this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "read-only" : "workspace-write",
|
|
478
|
-
developerInstructions: this.opts.systemPromptAppend ?? null,
|
|
584
|
+
developerInstructions: handoffSystemInstructions(this.opts.sessionRole, this.opts.systemPromptAppend) ?? null,
|
|
479
585
|
});
|
|
480
586
|
}
|
|
481
587
|
catch (error) {
|
|
@@ -546,6 +652,7 @@ export class CodexAdapter {
|
|
|
546
652
|
}
|
|
547
653
|
this.sessionAvailability = { version: 1, status: "available", checkedAt: new Date().toISOString(), observedCwd: providerCwd ?? canonicalSessionPath(this.opts.cwd), sessionRef: this._sessionRef };
|
|
548
654
|
this.threadAttached = true;
|
|
655
|
+
this.effectiveModel = typeof result.model === "string" ? result.model : this.opts.model;
|
|
549
656
|
this.eventQueue.push({ kind: "session-transition", transition: method === "thread/resume" ? "resumed" : "started" });
|
|
550
657
|
}
|
|
551
658
|
threadAttached = false;
|
|
@@ -614,6 +721,7 @@ export class CodexAdapter {
|
|
|
614
721
|
if (item)
|
|
615
722
|
this.eventQueue.push({ kind: "provider-item", provider: "codex", lifecycle: "started", itemType: String(item.type ?? "unknown"), payload: exactProviderItem(item), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
616
723
|
if (isCodexToolItem(item)) {
|
|
724
|
+
this.turnSawTool = true;
|
|
617
725
|
this.observedToolCalls += 1;
|
|
618
726
|
const callId = codexItemId(item) ?? randomUUID();
|
|
619
727
|
const name = codexToolName(item);
|
|
@@ -633,6 +741,7 @@ export class CodexAdapter {
|
|
|
633
741
|
if (item)
|
|
634
742
|
this.eventQueue.push({ kind: "provider-item", provider: "codex", lifecycle: "completed", itemType: String(item.type ?? "unknown"), payload: exactProviderItem(item), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
635
743
|
if (isCodexToolItem(item)) {
|
|
744
|
+
this.turnSawTool = true;
|
|
636
745
|
const callId = codexItemId(item);
|
|
637
746
|
const spanId = callId ? this.toolSpans.get(callId) : undefined;
|
|
638
747
|
const status = typeof item?.status === "string" ? item.status : undefined;
|
|
@@ -663,6 +772,13 @@ export class CodexAdapter {
|
|
|
663
772
|
provider: "codex",
|
|
664
773
|
usageRevision: this.usageRevision,
|
|
665
774
|
});
|
|
775
|
+
// A native compaction does not invalidate completed work or QA handoff.
|
|
776
|
+
// The next Rafi-controlled turn re-supplies the full requirement
|
|
777
|
+
// contract and tells the Builder to check it before continuing.
|
|
778
|
+
if (this.qaContractReminder) {
|
|
779
|
+
this.qaContractReminderPending = true;
|
|
780
|
+
this.qaReminderEvent("compaction-observed");
|
|
781
|
+
}
|
|
666
782
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted", detail: "provider-native automatic compaction" });
|
|
667
783
|
}
|
|
668
784
|
else {
|
|
@@ -710,6 +826,8 @@ export class CodexAdapter {
|
|
|
710
826
|
else if (message.method === "error") {
|
|
711
827
|
const error = params.error;
|
|
712
828
|
const reason = String(error?.message ?? params.message ?? "Codex app-server error");
|
|
829
|
+
if (this.fastTier && isFastModeUnavailable(reason) && !this.activeText.length && !this.turnSawTool)
|
|
830
|
+
return;
|
|
713
831
|
if (params.willRetry === true) {
|
|
714
832
|
const retryId = randomUUID();
|
|
715
833
|
this.eventQueue.push(this.opts.observer
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Only explicit speed-option rejection permits a same-session retry. Auth,
|
|
2
|
+
* generic model failures, timeouts, and uncertain transport failures do not. */
|
|
3
|
+
export declare function isFastModeUnavailable(detail: string): boolean;
|
|
4
|
+
export declare function reportFastModeUnavailable(model?: string): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Only explicit speed-option rejection permits a same-session retry. Auth,
|
|
2
|
+
* generic model failures, timeouts, and uncertain transport failures do not. */
|
|
3
|
+
export function isFastModeUnavailable(detail) {
|
|
4
|
+
return /(?:fast(?:[ _-]mode)?|service[ _-]tier|speed\s*[=:]\s*["']?fast)[\s\S]{0,160}(?:not supported|unsupported|not available|unavailable|not enabled|disabled|not allowed|requires|invalid|unrecognized|unknown)|(?:not supported|unsupported|not available|unavailable|invalid|unrecognized|unknown)[\s\S]{0,100}(?:--fast|fast[ _-]mode|service[ _-]tier)/i.test(detail);
|
|
5
|
+
}
|
|
6
|
+
export function reportFastModeUnavailable(model) {
|
|
7
|
+
const label = (model ?? "the selected model").replace(/[\x00-\x1f\x7f-\x9f]/g, "").slice(0, 200);
|
|
8
|
+
console.error(`rafi: Fast mode not available with ${label}; continuing at standard speed.`);
|
|
9
|
+
}
|
|
@@ -24,6 +24,9 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
24
24
|
private eventPump;
|
|
25
25
|
private closed;
|
|
26
26
|
private contractEnforcing;
|
|
27
|
+
/** Reapply a Codex post-compaction reminder if recovery recreates its adapter. */
|
|
28
|
+
private qaContractReminder?;
|
|
29
|
+
private qaContractReminderEvent?;
|
|
27
30
|
constructor(opts: RecoveringAdapterOptions);
|
|
28
31
|
graphRuntimeSettings(): Pick<import("./types.js").BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
|
|
29
32
|
get agent(): "claude" | "codex";
|
|
@@ -34,6 +37,7 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
34
37
|
enableContractEnforcement(): void;
|
|
35
38
|
contractCompactionSequence(): number;
|
|
36
39
|
acceptContractDelivery(sequence: number): void;
|
|
40
|
+
configureQaContractReminder(reminder: string, onEvent?: Parameters<NonNullable<BuilderAdapter["configureQaContractReminder"]>>[1]): void;
|
|
37
41
|
sendTurn(text: string, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
|
|
38
42
|
sessionId(): string | undefined;
|
|
39
43
|
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
@@ -11,6 +11,9 @@ export class RecoveringAdapter {
|
|
|
11
11
|
eventPump;
|
|
12
12
|
closed = false;
|
|
13
13
|
contractEnforcing = false;
|
|
14
|
+
/** Reapply a Codex post-compaction reminder if recovery recreates its adapter. */
|
|
15
|
+
qaContractReminder;
|
|
16
|
+
qaContractReminderEvent;
|
|
14
17
|
constructor(opts) {
|
|
15
18
|
this.opts = opts;
|
|
16
19
|
this.adapter = opts.initial;
|
|
@@ -25,6 +28,11 @@ export class RecoveringAdapter {
|
|
|
25
28
|
enableContractEnforcement() { this.contractEnforcing = true; this.adapter.enableContractEnforcement?.(); }
|
|
26
29
|
contractCompactionSequence() { return this.adapter.contractCompactionSequence?.() ?? 0; }
|
|
27
30
|
acceptContractDelivery(sequence) { this.adapter.acceptContractDelivery?.(sequence); }
|
|
31
|
+
configureQaContractReminder(reminder, onEvent) {
|
|
32
|
+
this.qaContractReminder = reminder;
|
|
33
|
+
this.qaContractReminderEvent = onEvent;
|
|
34
|
+
this.adapter.configureQaContractReminder?.(reminder, onEvent);
|
|
35
|
+
}
|
|
28
36
|
async sendTurn(text, policy) {
|
|
29
37
|
while (true) {
|
|
30
38
|
const result = await this.adapter.sendTurn(text, policy);
|
|
@@ -123,6 +131,8 @@ export class RecoveringAdapter {
|
|
|
123
131
|
await this.adapter.close().catch(() => { });
|
|
124
132
|
await this.eventPump.catch(() => { });
|
|
125
133
|
this.adapter = await this.opts.recreate(runtime, resumeSessionId, resumeSessionRef);
|
|
134
|
+
if (this.qaContractReminder)
|
|
135
|
+
this.adapter.configureQaContractReminder?.(this.qaContractReminder, this.qaContractReminderEvent);
|
|
126
136
|
this.runtime = runtime;
|
|
127
137
|
this.opts.onRuntimeChange?.(runtime);
|
|
128
138
|
this.eventPump = this.forwardEvents(this.adapter);
|
|
@@ -16,3 +16,9 @@ export declare class SessionUnavailableError extends Error {
|
|
|
16
16
|
export declare function sessionUnavailableResult(error: SessionUnavailableError): TurnResult;
|
|
17
17
|
export declare function isSessionUnavailableFailure(value: unknown): boolean;
|
|
18
18
|
export declare function sessionUnavailableErrorFromFailure(failure: RuntimeFailure): SessionUnavailableError;
|
|
19
|
+
/**
|
|
20
|
+
* A Builder turn whose dispatch outcome is unknown must enter durable source
|
|
21
|
+
* recovery. It is not an ordinary ticket blocker: the provider may have
|
|
22
|
+
* applied part of the requested change before its stream failed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function recoverableBuilderTurnError(result: TurnResult): SessionUnavailableError | undefined;
|
|
@@ -49,3 +49,22 @@ export function sessionUnavailableErrorFromFailure(failure) {
|
|
|
49
49
|
availability: failure.availability,
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* A Builder turn whose dispatch outcome is unknown must enter durable source
|
|
54
|
+
* recovery. It is not an ordinary ticket blocker: the provider may have
|
|
55
|
+
* applied part of the requested change before its stream failed.
|
|
56
|
+
*/
|
|
57
|
+
export function recoverableBuilderTurnError(result) {
|
|
58
|
+
const failure = result.failure;
|
|
59
|
+
if (!failure || failure.dispatchState !== "unknown")
|
|
60
|
+
return undefined;
|
|
61
|
+
return new SessionUnavailableError({
|
|
62
|
+
runtime: failure.runtime,
|
|
63
|
+
phase: failure.phase === "preflight" || failure.phase === "attach" || failure.phase === "turn" ? failure.phase : "turn",
|
|
64
|
+
dispatchState: "unknown",
|
|
65
|
+
executable: failure.executable,
|
|
66
|
+
cwd: failure.cwd,
|
|
67
|
+
diagnostics: result.text || failure.diagnostics,
|
|
68
|
+
availability: failure.availability,
|
|
69
|
+
});
|
|
70
|
+
}
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { ConfigurableAgentRole, ProviderSessionRefV1, RuntimeProbeCategory,
|
|
|
6
6
|
import type { RunObserver } from "../observability.js";
|
|
7
7
|
/** A permission request surfaced by a builder before it runs a tool. */
|
|
8
8
|
export interface PermissionRequest {
|
|
9
|
+
/** Provider-observed tool cwd, distinct from immutable session launch scope. */
|
|
10
|
+
executionCwd?: string;
|
|
9
11
|
toolName: string;
|
|
10
12
|
input: Record<string, unknown>;
|
|
11
13
|
/** Human-readable prompt from the agent, when available. */
|
|
@@ -297,6 +299,12 @@ export interface BuilderAdapter {
|
|
|
297
299
|
enableContractEnforcement?(): void;
|
|
298
300
|
contractCompactionSequence?(): number;
|
|
299
301
|
acceptContractDelivery?(sequence: number): void;
|
|
302
|
+
/** Codex re-sends this exact contract reminder on its first host turn after native compaction. */
|
|
303
|
+
configureQaContractReminder?(reminder: string, onEvent?: (event: {
|
|
304
|
+
kind: "installed" | "compaction-observed" | "reminder-delivered" | "reminder-retained";
|
|
305
|
+
at: string;
|
|
306
|
+
sessionId?: string;
|
|
307
|
+
}) => void): void;
|
|
300
308
|
/** Send one instruction; resolves when that turn completes. */
|
|
301
309
|
sendTurn(text: string, policy?: {
|
|
302
310
|
handback?: boolean;
|
package/dist/agentRun.js
CHANGED
|
@@ -21,6 +21,14 @@ export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
|
|
|
21
21
|
readonly agent: BuilderAdapter["agent"];
|
|
22
22
|
constructor(adapter: BuilderAdapter, cwd: string, expected?: CurrentWorkflowIdentity);
|
|
23
23
|
sendTurn(text: string, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
|
|
24
|
+
contractCapabilities(): {
|
|
25
|
+
sameSessionAcceptance: boolean;
|
|
26
|
+
nativeCompactionBarrier: boolean;
|
|
27
|
+
};
|
|
28
|
+
enableContractEnforcement(): void;
|
|
29
|
+
contractCompactionSequence(): number;
|
|
30
|
+
acceptContractDelivery(sequence: number): void;
|
|
31
|
+
configureQaContractReminder(reminder: string, onEvent?: Parameters<NonNullable<BuilderAdapter["configureQaContractReminder"]>>[1]): void;
|
|
24
32
|
sessionId(): string | undefined;
|
|
25
33
|
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
26
34
|
prepareSession(): Promise<ProviderSessionRefV1>;
|
|
@@ -43,6 +43,11 @@ export class CurrentWorkflowGuardAdapter {
|
|
|
43
43
|
assertCurrentWorkflowIdentity(this.cwd, this.expected);
|
|
44
44
|
return result;
|
|
45
45
|
}
|
|
46
|
+
contractCapabilities() { return this.adapter.contractCapabilities?.() ?? { sameSessionAcceptance: false, nativeCompactionBarrier: false }; }
|
|
47
|
+
enableContractEnforcement() { this.adapter.enableContractEnforcement?.(); }
|
|
48
|
+
contractCompactionSequence() { return this.adapter.contractCompactionSequence?.() ?? 0; }
|
|
49
|
+
acceptContractDelivery(sequence) { this.adapter.acceptContractDelivery?.(sequence); }
|
|
50
|
+
configureQaContractReminder(reminder, onEvent) { this.adapter.configureQaContractReminder?.(reminder, onEvent); }
|
|
46
51
|
sessionId() { return this.adapter.sessionId(); }
|
|
47
52
|
sessionRef() { return this.adapter.sessionRef?.(); }
|
|
48
53
|
async prepareSession() {
|
package/dist/branch/runner.js
CHANGED
|
@@ -22,7 +22,7 @@ import { join, resolve } from "node:path";
|
|
|
22
22
|
import { WorkflowDb } from "../workflowDb.js";
|
|
23
23
|
import { assertBuildAssignmentReconciled, BuildAssignmentRejected } from "../buildAssignment.js";
|
|
24
24
|
import { currentActivity, withActivityPhase } from "../activity.js";
|
|
25
|
-
import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
25
|
+
import { SessionUnavailableError, recoverableBuilderTurnError } from "../adapters/sessionFailure.js";
|
|
26
26
|
import { SessionUnavailableContinuityError } from "../continuity.js";
|
|
27
27
|
export function readDeliveryUnitSession(projectDir, unitId) {
|
|
28
28
|
const path = deliverySessionPath(projectDir, unitId);
|
|
@@ -304,7 +304,7 @@ export async function runBranchPlan(opts) {
|
|
|
304
304
|
base: node.baseBranch,
|
|
305
305
|
dependencies: node.dependencies,
|
|
306
306
|
});
|
|
307
|
-
workflowCheckpoint(opts.projectDir, opts.runId, "builder-before", node.ticket.id, { branch: node.branch, worktree: node.worktreePath });
|
|
307
|
+
workflowCheckpoint(opts.projectDir, opts.runId, qaOnlyRecovery ? "qa-recovery-before" : "builder-before", node.ticket.id, { branch: node.branch, worktree: node.worktreePath });
|
|
308
308
|
const continuationDb = new WorkflowDb(opts.projectDir);
|
|
309
309
|
const continuationSession = continuations.length ? continuationDb.branchResumeSession(opts.runId, node.ticket.id) : undefined;
|
|
310
310
|
continuationDb.close();
|
|
@@ -391,50 +391,52 @@ export async function runBranchPlan(opts) {
|
|
|
391
391
|
? await opts.observer.withContext({ role: "builder", stream: "builder", ticketId: node.ticket.id, deliveryUnitId: node.deliveryUnitId }, turn)
|
|
392
392
|
: await turn();
|
|
393
393
|
builder = foreman.builderAdapter();
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (sessionId) {
|
|
404
|
-
const resumeSession = {
|
|
405
|
-
ticket: node.ticket.id,
|
|
406
|
-
branch: node.branch,
|
|
407
|
-
base: node.baseBranch,
|
|
408
|
-
worktreePath,
|
|
409
|
-
sessionId,
|
|
410
|
-
sessionRef: builder.sessionRef?.(),
|
|
411
|
-
agent: opts.agent,
|
|
412
|
-
model: opts.model,
|
|
413
|
-
effort: opts.effort,
|
|
414
|
-
fast: opts.fast,
|
|
415
|
-
qaEnabled: opts.qaEnabled,
|
|
416
|
-
createPr: createsReview,
|
|
417
|
-
completionMode,
|
|
418
|
-
reviewProvider,
|
|
419
|
-
prReady: opts.prReady,
|
|
420
|
-
keepWorktrees: opts.keepWorktrees,
|
|
421
|
-
deliveryUnitId: node.deliveryUnitId,
|
|
422
|
-
deliveryUnitFinal: node.deliveryUnitFinal,
|
|
423
|
-
logPath: "structured-recovery",
|
|
424
|
-
ts: new Date().toISOString(),
|
|
425
|
-
};
|
|
426
|
-
opts.log.write("branch-session", resumeSession);
|
|
427
|
-
const resumeDb = new WorkflowDb(opts.projectDir);
|
|
428
|
-
try {
|
|
429
|
-
resumeDb.recordBranchResumeSession(opts.runId, resumeSession);
|
|
430
|
-
}
|
|
431
|
-
finally {
|
|
432
|
-
resumeDb.close();
|
|
394
|
+
if (!qaOnlyRecovery) {
|
|
395
|
+
workflowCheckpoint(opts.projectDir, opts.runId, "builder-after", node.ticket.id, { status: status.kind, sessionId: builder.sessionId(), worktree: worktreePath });
|
|
396
|
+
const sessionId = builder.sessionId();
|
|
397
|
+
const sessionRef = builder.sessionRef?.();
|
|
398
|
+
if (sessionId)
|
|
399
|
+
builderStream = { sessionId, ...(sessionRef ? { sessionRef } : {}), worktreePath };
|
|
400
|
+
if (sessionId) {
|
|
401
|
+
opts.recordBuilderSession?.(sessionRef ?? sessionId, node.ticket.id, worktreePath);
|
|
402
|
+
workflowCheckpoint(opts.projectDir, opts.runId, "builder-session-scoped", node.ticket.id, { sessionId, sessionRef, worktree: worktreePath, branch: node.branch });
|
|
433
403
|
}
|
|
434
|
-
if (
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
404
|
+
if (sessionId) {
|
|
405
|
+
const resumeSession = {
|
|
406
|
+
ticket: node.ticket.id,
|
|
407
|
+
branch: node.branch,
|
|
408
|
+
base: node.baseBranch,
|
|
409
|
+
worktreePath,
|
|
410
|
+
sessionId,
|
|
411
|
+
sessionRef: builder.sessionRef?.(),
|
|
412
|
+
agent: opts.agent,
|
|
413
|
+
model: opts.model,
|
|
414
|
+
effort: opts.effort,
|
|
415
|
+
fast: opts.fast,
|
|
416
|
+
qaEnabled: opts.qaEnabled,
|
|
417
|
+
createPr: createsReview,
|
|
418
|
+
completionMode,
|
|
419
|
+
reviewProvider,
|
|
420
|
+
prReady: opts.prReady,
|
|
421
|
+
keepWorktrees: opts.keepWorktrees,
|
|
422
|
+
deliveryUnitId: node.deliveryUnitId,
|
|
423
|
+
deliveryUnitFinal: node.deliveryUnitFinal,
|
|
424
|
+
logPath: "structured-recovery",
|
|
425
|
+
ts: new Date().toISOString(),
|
|
426
|
+
};
|
|
427
|
+
opts.log.write("branch-session", resumeSession);
|
|
428
|
+
const resumeDb = new WorkflowDb(opts.projectDir);
|
|
429
|
+
try {
|
|
430
|
+
resumeDb.recordBranchResumeSession(opts.runId, resumeSession);
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
resumeDb.close();
|
|
434
|
+
}
|
|
435
|
+
if (node.deliveryUnitId) {
|
|
436
|
+
const path = deliverySessionPath(opts.projectDir, node.deliveryUnitId);
|
|
437
|
+
mkdirSync(join(opts.projectDir, ".foreman", "delivery-sessions"), { recursive: true });
|
|
438
|
+
writeFileSync(path, `${JSON.stringify({ runId: opts.runId, unitId: node.deliveryUnitId, branch: node.branch, worktreePath, sessionId, sessionRef: builder.sessionRef?.(), ticket: node.ticket.id }, null, 2)}\n`, "utf8");
|
|
439
|
+
}
|
|
438
440
|
}
|
|
439
441
|
}
|
|
440
442
|
opts.log.write("step", {
|
|
@@ -446,8 +448,12 @@ export async function runBranchPlan(opts) {
|
|
|
446
448
|
costUsd: result.costUsd,
|
|
447
449
|
isError: result.isError,
|
|
448
450
|
});
|
|
449
|
-
if (result.isError)
|
|
451
|
+
if (result.isError) {
|
|
452
|
+
const recoverable = recoverableBuilderTurnError(result);
|
|
453
|
+
if (recoverable)
|
|
454
|
+
throw recoverable;
|
|
450
455
|
throw new Error(`builder turn errored: ${result.text.slice(0, 200)}`);
|
|
456
|
+
}
|
|
451
457
|
if (status.branchDependency) {
|
|
452
458
|
throw new Error(`builder requested branch_dependency=${status.branchDependency}; retry is not available after ticket start in this run`);
|
|
453
459
|
}
|
package/dist/buildApproval.js
CHANGED
|
@@ -14,7 +14,7 @@ function canonical(value) {
|
|
|
14
14
|
}
|
|
15
15
|
function material(ticket) {
|
|
16
16
|
return { id: ticket.id, plan_ref: ticket.plan_ref, title: ticket.title, summary: ticket.summary, acceptance: ticket.acceptance,
|
|
17
|
-
required_tests: ticket.required_tests, depends_on: ticket.depends_on, likely_files: ticket.likely_files, rollback: ticket.rollback, source_refs: ticket.source_refs };
|
|
17
|
+
required_tests: ticket.required_tests, depends_on: ticket.depends_on, likely_files: ticket.likely_files, rollback: ticket.rollback, documentation: ticket.documentation ?? { required: false, targets: [], criteria: [] }, source_refs: ticket.source_refs };
|
|
18
18
|
}
|
|
19
19
|
/** Reuse only source-bound approval. Ambiguous legacy provenance still asks. */
|
|
20
20
|
export function approvedBuildScope(projectDir, ticketIds) {
|
|
@@ -52,6 +52,7 @@ export function approvedBuildScope(projectDir, ticketIds) {
|
|
|
52
52
|
const ids = new Map(all.filter(item => item.plan_ref?.plan_id === ref.plan_id && item.plan_ref.revision === ref.revision).map(item => [item.plan_ref.slice_ref, item.id]));
|
|
53
53
|
const legacy = slice && !ticket.rollback ? { ...ticket, title: slice.title, summary: slice.summary,
|
|
54
54
|
acceptance: slice.acceptance, required_tests: slice.required_tests, likely_files: slice.likely_files,
|
|
55
|
+
documentation: slice.documentation ?? { required: false, targets: [], criteria: [] },
|
|
55
56
|
depends_on: (slice.depends_on ?? []).map((id) => ids.get(id)),
|
|
56
57
|
source_refs: slice.source_refs?.map((source) => ({ source: source.source_id, item: source.item ?? "document", source_id: source.source_id, fingerprint: source.fingerprint, ...(source.url !== undefined ? { url: source.url } : {}), ...(source.note !== undefined ? { note: source.note } : {}) })) } : undefined;
|
|
57
58
|
if (canonical(material(approved ?? legacy ?? {})) !== canonical(material(ticket)))
|
|
@@ -22,6 +22,21 @@ export declare class BuildAssignmentRejected extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
/** A frozen exact-session Builder decision may defer unrelated pending QA. */
|
|
24
24
|
export declare function isInterruptedBuilderRecovery(run: BuildRunRecordV2 | undefined): boolean;
|
|
25
|
+
/** Recovery requires current source, assignment and admission proofs.
|
|
26
|
+
* A V3 or migrated record alone never grants continuation authority;
|
|
27
|
+
* full eligibility is verified before any unknown receipt is reconciled. */
|
|
28
|
+
export declare function canReconcileInterruptedBuilderSource(projectDir: string, runId: string): boolean;
|
|
29
|
+
/** Resume the existing approved build from observed local source, never replay
|
|
30
|
+
* the interrupted instruction or pretend its unknown outcome was successful.
|
|
31
|
+
* The caller must own recovery admission and the original run's workflow lease. */
|
|
32
|
+
export declare function prepareInterruptedBuilderSourceContinuation(projectDir: string, runId: string): {
|
|
33
|
+
ticketIds: string[];
|
|
34
|
+
reconciliationId: string;
|
|
35
|
+
sourceDigest: string;
|
|
36
|
+
contentDigest: string;
|
|
37
|
+
};
|
|
38
|
+
/** A launch may not consume a recovery snapshot after product source changes. */
|
|
39
|
+
export declare function assertInterruptedSourceSnapshot(projectDir: string, run: BuildRunRecordV2): void;
|
|
25
40
|
/**
|
|
26
41
|
* Retire only the host-side records for a Builder turn that was interrupted
|
|
27
42
|
* before Rafi received a durable response. The next turn is sent to the
|