ai-foreman 1.7.26 → 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/dist/adapters/codex.d.ts +12 -0
- package/dist/adapters/codex.js +50 -1
- 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 +6 -0
- package/dist/branch/currentGuard.d.ts +8 -0
- package/dist/branch/currentGuard.js +5 -0
- package/dist/branch/runner.js +6 -2
- package/dist/buildApproval.js +2 -1
- package/dist/buildRuns.d.ts +12 -0
- package/dist/buildRuns.js +50 -0
- 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 +8 -0
- package/dist/cli/start.js +164 -27
- 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 +5 -0
- package/dist/foreman.js +89 -8
- package/dist/graph/preparation.js +6 -3
- package/dist/graph/semantic.js +2 -1
- package/dist/graph/turn.js +8 -1
- package/dist/handoffProtocol.js +2 -1
- package/dist/handoffs.js +7 -19
- package/dist/log.d.ts +1 -1
- package/dist/managerPacket.js +1 -1
- package/dist/permissions/policy.js +21 -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/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 +87 -6
- 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 +73 -1
- package/dist/qaReview.js +35 -8
- 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/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/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/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.js +11 -2
- package/package.json +9 -4
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -59,6 +59,10 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
59
59
|
private fastTier?;
|
|
60
60
|
private readonly fastUnavailableModels;
|
|
61
61
|
private turnSawTool;
|
|
62
|
+
/** Native compaction is non-blocking. The next host turn gets this reminder. */
|
|
63
|
+
private qaContractReminder?;
|
|
64
|
+
private qaContractReminderPending;
|
|
65
|
+
private qaContractReminderEvent?;
|
|
62
66
|
private disableFastMode;
|
|
63
67
|
private prepareFastMode;
|
|
64
68
|
constructor(opts: BuilderAdapterOptions);
|
|
@@ -66,6 +70,14 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
66
70
|
sameSessionAcceptance: boolean;
|
|
67
71
|
nativeCompactionBarrier: boolean;
|
|
68
72
|
};
|
|
73
|
+
contractCompactionSequence(): number;
|
|
74
|
+
configureQaContractReminder(reminder: string, onEvent?: (event: {
|
|
75
|
+
kind: "installed" | "compaction-observed" | "reminder-delivered" | "reminder-retained";
|
|
76
|
+
at: string;
|
|
77
|
+
sessionId?: string;
|
|
78
|
+
}) => void): void;
|
|
79
|
+
private qaReminderEvent;
|
|
80
|
+
private instructionWithPendingQaReminder;
|
|
69
81
|
buildInstruction(instruction: string): string;
|
|
70
82
|
buildAppServerArgs(): string[];
|
|
71
83
|
/** Deprecated fixture helper. Runtime execution uses only app-server. */
|
package/dist/adapters/codex.js
CHANGED
|
@@ -75,6 +75,10 @@ export class CodexAdapter {
|
|
|
75
75
|
fastTier;
|
|
76
76
|
fastUnavailableModels = new Set();
|
|
77
77
|
turnSawTool = false;
|
|
78
|
+
/** Native compaction is non-blocking. The next host turn gets this reminder. */
|
|
79
|
+
qaContractReminder;
|
|
80
|
+
qaContractReminderPending = false;
|
|
81
|
+
qaContractReminderEvent;
|
|
78
82
|
disableFastMode() {
|
|
79
83
|
const model = this.opts.model ?? this.effectiveModel ?? "default";
|
|
80
84
|
if (!this.fastUnavailableModels.has(model))
|
|
@@ -117,6 +121,22 @@ export class CodexAdapter {
|
|
|
117
121
|
this._sessionRef = opts.resumeSessionRef;
|
|
118
122
|
}
|
|
119
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
|
+
}
|
|
120
140
|
buildInstruction(instruction) {
|
|
121
141
|
return [this.opts.systemPromptAppend,
|
|
122
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,
|
|
@@ -143,12 +163,26 @@ export class CodexAdapter {
|
|
|
143
163
|
return args;
|
|
144
164
|
}
|
|
145
165
|
async sendTurn(instruction, policy) {
|
|
166
|
+
const prepared = this.instructionWithPendingQaReminder(instruction);
|
|
167
|
+
instruction = prepared.instruction;
|
|
146
168
|
const turnId = randomUUID();
|
|
147
169
|
this.activeProviderTurnId = turnId;
|
|
148
170
|
const observer = this.opts.observer;
|
|
149
171
|
if (!observer) {
|
|
150
172
|
try {
|
|
151
|
-
|
|
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;
|
|
152
186
|
}
|
|
153
187
|
finally {
|
|
154
188
|
this.activeProviderTurnId = undefined;
|
|
@@ -162,9 +196,17 @@ export class CodexAdapter {
|
|
|
162
196
|
try {
|
|
163
197
|
const result = await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction, policy));
|
|
164
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");
|
|
165
205
|
return result;
|
|
166
206
|
}
|
|
167
207
|
catch (error) {
|
|
208
|
+
if (prepared.includedReminder)
|
|
209
|
+
this.qaReminderEvent("reminder-retained");
|
|
168
210
|
observer.store.finishSpan(spanId, { outcome: "failed", attributes: { error: String(error).slice(0, 500) } });
|
|
169
211
|
throw error;
|
|
170
212
|
}
|
|
@@ -730,6 +772,13 @@ export class CodexAdapter {
|
|
|
730
772
|
provider: "codex",
|
|
731
773
|
usageRevision: this.usageRevision,
|
|
732
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
|
+
}
|
|
733
782
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted", detail: "provider-native automatic compaction" });
|
|
734
783
|
}
|
|
735
784
|
else {
|
|
@@ -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
|
@@ -299,6 +299,12 @@ export interface BuilderAdapter {
|
|
|
299
299
|
enableContractEnforcement?(): void;
|
|
300
300
|
contractCompactionSequence?(): number;
|
|
301
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;
|
|
302
308
|
/** Send one instruction; resolves when that turn completes. */
|
|
303
309
|
sendTurn(text: string, policy?: {
|
|
304
310
|
handback?: boolean;
|
|
@@ -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);
|
|
@@ -448,8 +448,12 @@ export async function runBranchPlan(opts) {
|
|
|
448
448
|
costUsd: result.costUsd,
|
|
449
449
|
isError: result.isError,
|
|
450
450
|
});
|
|
451
|
-
if (result.isError)
|
|
451
|
+
if (result.isError) {
|
|
452
|
+
const recoverable = recoverableBuilderTurnError(result);
|
|
453
|
+
if (recoverable)
|
|
454
|
+
throw recoverable;
|
|
452
455
|
throw new Error(`builder turn errored: ${result.text.slice(0, 200)}`);
|
|
456
|
+
}
|
|
453
457
|
if (status.branchDependency) {
|
|
454
458
|
throw new Error(`builder requested branch_dependency=${status.branchDependency}; retry is not available after ticket start in this run`);
|
|
455
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)))
|
package/dist/buildRuns.d.ts
CHANGED
|
@@ -54,6 +54,18 @@ export declare function releaseBuildLease(projectDir: string, run: BuildRunRecor
|
|
|
54
54
|
export declare function completeBuildRun(projectDir: string, run: BuildRunRecordV2, now?: Date): BuildRunRecordV2;
|
|
55
55
|
/** A recovered ticket boundary is complete only when the saved run scope is done. */
|
|
56
56
|
export declare function finishRecoveredTicketScope(projectDir: string, run: BuildRunRecordV2, checkpoint: string): BuildRunRecordV2;
|
|
57
|
+
/**
|
|
58
|
+
* Branch recovery may run one preserved ticket at a time. Its returned summary
|
|
59
|
+
* is therefore never proof that the original admitted run is complete. Use the
|
|
60
|
+
* tracker and (when enabled) each ticket's durable QA terminal state to derive
|
|
61
|
+
* the original scope before changing the run's terminal status.
|
|
62
|
+
*/
|
|
63
|
+
export declare function branchTicketScopeProgress(projectDir: string, run: BuildRunRecordV2): {
|
|
64
|
+
completed: string[];
|
|
65
|
+
remaining: string[];
|
|
66
|
+
} | undefined;
|
|
67
|
+
/** Complete an isolated-branch run only after its entire admitted scope is proven done. */
|
|
68
|
+
export declare function finishBranchTicketScope(projectDir: string, run: BuildRunRecordV2, checkpoint: string): BuildRunRecordV2;
|
|
57
69
|
/** A recovered boundary cannot authorize work on a still-blocked remainder. */
|
|
58
70
|
export declare function hasRunnableBuildRemainder(projectDir: string, run: BuildRunRecordV2): boolean;
|
|
59
71
|
export declare function readBuildRuns(projectDir: string): BuildRunRecordV2[];
|
package/dist/buildRuns.js
CHANGED
|
@@ -350,6 +350,56 @@ export function finishRecoveredTicketScope(projectDir, run, checkpoint) {
|
|
|
350
350
|
progress: { ...run.progress, completedTickets: completed, remainingTickets: remaining, nextAction: `Resume ${remaining[0]}` },
|
|
351
351
|
}), "recoverable");
|
|
352
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Branch recovery may run one preserved ticket at a time. Its returned summary
|
|
355
|
+
* is therefore never proof that the original admitted run is complete. Use the
|
|
356
|
+
* tracker and (when enabled) each ticket's durable QA terminal state to derive
|
|
357
|
+
* the original scope before changing the run's terminal status.
|
|
358
|
+
*/
|
|
359
|
+
export function branchTicketScopeProgress(projectDir, run) {
|
|
360
|
+
if (run.branchMode === "current" || !run.tickets.length || !isTicketsInitialized(projectDir))
|
|
361
|
+
return undefined;
|
|
362
|
+
if (run.status === "completed")
|
|
363
|
+
return { completed: [...run.tickets], remaining: [] };
|
|
364
|
+
let state;
|
|
365
|
+
let workflow;
|
|
366
|
+
try {
|
|
367
|
+
const paths = resolveTicketPaths(loadTicketsConfig(projectDir), projectDir);
|
|
368
|
+
state = new Database(paths.stateDb, { readonly: true, fileMustExist: true });
|
|
369
|
+
workflow = new WorkflowDb(projectDir);
|
|
370
|
+
const read = state.prepare("SELECT status FROM ticket_state WHERE ticket_id=?");
|
|
371
|
+
const qaEnabled = "qaEnabled" in run && run.qaEnabled === true;
|
|
372
|
+
const completed = run.tickets.filter((ticket) => {
|
|
373
|
+
if (read.get(ticket)?.status !== "done")
|
|
374
|
+
return false;
|
|
375
|
+
// A tracker row cannot replace a missing or unfinished QA verdict.
|
|
376
|
+
return !qaEnabled || ["completed", "waived"].includes(workflow.qaTicketHead(run.runId, ticket).state);
|
|
377
|
+
});
|
|
378
|
+
return { completed, remaining: run.tickets.filter(ticket => !completed.includes(ticket)) };
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// Missing tracker/QA evidence must retain a recoverable scope.
|
|
382
|
+
return { completed: [], remaining: [...run.tickets] };
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
workflow?.close();
|
|
386
|
+
state?.close();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** Complete an isolated-branch run only after its entire admitted scope is proven done. */
|
|
390
|
+
export function finishBranchTicketScope(projectDir, run, checkpoint) {
|
|
391
|
+
if (run.branchMode === "current")
|
|
392
|
+
throw new Error("Branch scope reconciliation requires an isolated branch run");
|
|
393
|
+
const progress = branchTicketScopeProgress(projectDir, run);
|
|
394
|
+
if (!progress)
|
|
395
|
+
throw new Error("Branch scope reconciliation requires initialized ticket evidence");
|
|
396
|
+
if (!progress.remaining.length)
|
|
397
|
+
return completeBuildRun(projectDir, run);
|
|
398
|
+
return releaseBuildLease(projectDir, checkpointBuildRun(projectDir, run, checkpoint, {
|
|
399
|
+
currentTicket: progress.remaining[0],
|
|
400
|
+
progress: { ...run.progress, completedTickets: progress.completed, remainingTickets: progress.remaining, nextAction: `Resume ${progress.remaining[0]}` },
|
|
401
|
+
}), "recoverable");
|
|
402
|
+
}
|
|
353
403
|
/** A recovered boundary cannot authorize work on a still-blocked remainder. */
|
|
354
404
|
export function hasRunnableBuildRemainder(projectDir, run) {
|
|
355
405
|
if (run.authorizedBatch && run.progress.completedTickets.length >= run.authorizedBatch.requestedSteps)
|
package/dist/buildWorkContext.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
export function renderBuildWorkContext(ticket, checklist) {
|
|
3
3
|
return ["Full assigned work definition (scope authority):", JSON.stringify(ticket, null, 2),
|
|
4
4
|
"Project validation checklist:", ...(checklist.length ? checklist.map((item, index) => `${index + 1}. ${item}`) : ["No additional project checklist configured."]),
|
|
5
|
-
"Implement this assigned work and provide verification evidence. Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation."].join("\n");
|
|
5
|
+
"Implement this assigned work and provide verification evidence. Explicitly map every acceptance item to a concrete result or truthful limitation in the final report. If the delivered QA contract requires a historical sequence (for example, a test-first red phase before an implementation edit), execute that sequence in order and retain bounded, truthful evidence of each required step in your final response, including the relevant command result and an ordering record. Do not claim a later passing test proves an earlier failing step. Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation."].join("\n");
|
|
6
6
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
2
|
export declare const BUILD_WORK_SCHEMA_VERSION = 4;
|
|
3
|
+
/**
|
|
4
|
+
* The recovery database is local state, rather than repository content. Keep
|
|
5
|
+
* the roots on the error so callers can give a copied checkout an actionable
|
|
6
|
+
* recovery path without weakening the ordinary writer guard.
|
|
7
|
+
*/
|
|
8
|
+
export declare class BuildProjectIdentityMismatchError extends Error {
|
|
9
|
+
readonly storedRoot: string;
|
|
10
|
+
readonly currentRoot: string;
|
|
11
|
+
constructor(storedRoot: string, currentRoot: string);
|
|
12
|
+
}
|
|
3
13
|
/** A copied database must be rebound by state transfer, never by an ordinary writer. */
|
|
4
14
|
export declare function assertBuildProject(db: Database.Database, projectDir: string): void;
|
|
5
15
|
/** Classify legacy records without converting observed progress into permission. */
|
|
@@ -4,11 +4,27 @@ import { join, relative } from "node:path";
|
|
|
4
4
|
import { classifyProcess } from "./processIdentity.js";
|
|
5
5
|
export const BUILD_WORK_SCHEMA_VERSION = 4;
|
|
6
6
|
const migration = "004_admitted_build_work";
|
|
7
|
+
/**
|
|
8
|
+
* The recovery database is local state, rather than repository content. Keep
|
|
9
|
+
* the roots on the error so callers can give a copied checkout an actionable
|
|
10
|
+
* recovery path without weakening the ordinary writer guard.
|
|
11
|
+
*/
|
|
12
|
+
export class BuildProjectIdentityMismatchError extends Error {
|
|
13
|
+
storedRoot;
|
|
14
|
+
currentRoot;
|
|
15
|
+
constructor(storedRoot, currentRoot) {
|
|
16
|
+
super(`Rafi local build state belongs to a different project path.\n Stored path: ${storedRoot}\n Current path: ${currentRoot}\n\nThis usually means the checkout was copied or moved with .rafi/recovery.sqlite3. Rafi will not write copied build history until you choose how to recover it.\n\nOptions:\n 1. Inspect: rafi state rebind .\n 2. Keep this copied local state: rafi state rebind . --yes\n 3. Transfer state from the original checkout: rafi state export <original-project> --output rafi-state.rafi.gz, then rafi state import . rafi-state.rafi.gz\n 4. Resolve it yourself: preserve or remove the copied .rafi/recovery.sqlite3, then rerun the command.`);
|
|
17
|
+
this.name = "BuildProjectIdentityMismatchError";
|
|
18
|
+
this.storedRoot = storedRoot;
|
|
19
|
+
this.currentRoot = currentRoot;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
7
22
|
/** A copied database must be rebound by state transfer, never by an ordinary writer. */
|
|
8
23
|
export function assertBuildProject(db, projectDir) {
|
|
9
24
|
const identity = db.prepare("SELECT canonical_root FROM build_project_identity WHERE singleton=1").get();
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
const currentRoot = realpathSync(projectDir);
|
|
26
|
+
if (identity.canonical_root !== currentRoot)
|
|
27
|
+
throw new BuildProjectIdentityMismatchError(identity.canonical_root, currentRoot);
|
|
12
28
|
}
|
|
13
29
|
/** Classify legacy records without converting observed progress into permission. */
|
|
14
30
|
export function migrateBuildWork(db, projectDir, faults) {
|
package/dist/cli/start.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import type { TicketState } from "../tickets/stateDb.js";
|
|
3
|
+
import type { TicketDef } from "../tickets/ticketSchema.js";
|
|
2
4
|
import type { AgentRoleDefaultsV1 } from "rafi-spec";
|
|
3
5
|
import type { BranchPlan } from "../branch/types.js";
|
|
4
6
|
import { type DeliveryConfig, type DeliveryStack } from "../tickets/delivery.js";
|
|
@@ -20,6 +22,12 @@ export declare function formatResumeGuidance(executable: "rafi" | "ai-foreman",
|
|
|
20
22
|
export declare function savedStartArguments(command: Command, projectDir: string, opts: Record<string, unknown>): string[];
|
|
21
23
|
/** Pure validation shared by interactive start and persisted preparation. */
|
|
22
24
|
export declare function validateStartOptions(opts: Record<string, any>, requireCount: boolean): void;
|
|
25
|
+
/** Preserve the distinction between a staged handoff and an unattempted acceptance. */
|
|
26
|
+
export declare function recordHandoffAcceptanceNotAttempted(projectDir: string, runId: string, role: "builder" | "qa", generation: number, stage: string, detail: string): void;
|
|
27
|
+
/** Default start selection is deliberately the same eligibility model users see in the ticket queue. */
|
|
28
|
+
export declare function selectDefaultExecutionTickets(tickets: TicketDef[], states: Map<string, TicketState>, implementationLimit: number, steps: number, delivery?: DeliveryConfig): string[];
|
|
29
|
+
/** Fail before runtime/provider admission when canonical ticket data is unsafe. */
|
|
30
|
+
export declare function assertStartTicketPreflight(projectDir: string): void;
|
|
23
31
|
/** Validate generated established recovery before reserving execution authority. */
|
|
24
32
|
export declare function validateRecoveryArguments(args: readonly string[], projectDir: string): void;
|
|
25
33
|
/** Validate persisted preparation argv without executing a command or provider. */
|