ai-foreman 1.7.24 → 1.7.25
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/buildAssignment.d.ts +6 -1
- package/dist/buildAssignment.js +76 -0
- package/dist/cli/start.d.ts +3 -1
- package/dist/cli/start.js +10 -6
- package/dist/cli/tickets.d.ts +1 -0
- package/dist/cli/tickets.js +20 -2
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import type { BuilderAdapter } from "./adapters/types.js";
|
|
|
2
2
|
import type { StepStatus } from "./foreman.js";
|
|
3
3
|
import type { TurnResult } from "./adapters/types.js";
|
|
4
4
|
import { type FrozenQaSourceState } from "./qaSnapshot.js";
|
|
5
|
-
import type { ProviderSessionRefV1 } from "rafi-spec";
|
|
5
|
+
import type { BuildRunRecordV2, ProviderSessionRefV1 } from "rafi-spec";
|
|
6
6
|
/** Work responses confirm an assignment; they never select the next ticket. */
|
|
7
7
|
export declare function validateBuildAssignment(ticketId: string, status: StepStatus, responseText?: string): string | undefined;
|
|
8
8
|
export interface BuildAssignmentCapture {
|
|
@@ -20,6 +20,8 @@ type BuildSourceState = Pick<FrozenQaSourceState, "digest" | "head" | "contentDi
|
|
|
20
20
|
};
|
|
21
21
|
export declare class BuildAssignmentRejected extends Error {
|
|
22
22
|
}
|
|
23
|
+
/** A frozen exact-session Builder decision may defer unrelated pending QA. */
|
|
24
|
+
export declare function isInterruptedBuilderRecovery(run: BuildRunRecordV2 | undefined): boolean;
|
|
23
25
|
/**
|
|
24
26
|
* Retire only the host-side records for a Builder turn that was interrupted
|
|
25
27
|
* before Rafi received a durable response. The next turn is sent to the
|
|
@@ -32,6 +34,9 @@ export declare function prepareExactSessionBuilderContinuation(projectDir: strin
|
|
|
32
34
|
dispatchIds: string[];
|
|
33
35
|
assignmentIds: string[];
|
|
34
36
|
};
|
|
37
|
+
/** Retire uncertain execution for an explicitly selected start-over, never claim
|
|
38
|
+
* that a provider did not run or that its preserved implementation passed QA. */
|
|
39
|
+
export declare function retireBuildExecutionForStartOver(projectDir: string, runId: string, operationId: string): void;
|
|
35
40
|
export interface BuilderGuidanceFollowup {
|
|
36
41
|
reviewAttemptId: string;
|
|
37
42
|
requirementsDigest: string;
|
package/dist/buildAssignment.js
CHANGED
|
@@ -26,6 +26,18 @@ export function validateBuildAssignment(ticketId, status, responseText = "") {
|
|
|
26
26
|
}
|
|
27
27
|
export class BuildAssignmentRejected extends Error {
|
|
28
28
|
}
|
|
29
|
+
/** A frozen exact-session Builder decision may defer unrelated pending QA. */
|
|
30
|
+
export function isInterruptedBuilderRecovery(run) {
|
|
31
|
+
if (!run)
|
|
32
|
+
return false;
|
|
33
|
+
const decision = run.recoveryDecision;
|
|
34
|
+
const continuation = run.interruptedBuilderContinuation;
|
|
35
|
+
return decision?.runId === run.runId && decision.role === "builder" && decision.mode === "exact-session"
|
|
36
|
+
&& decision.executionTickets?.length === 1
|
|
37
|
+
&& run.tickets.includes(decision.executionTickets[0])
|
|
38
|
+
&& Boolean(continuation?.ticketIds?.includes(decision.executionTickets[0]))
|
|
39
|
+
&& Boolean(continuation?.sessionRef && decision.predecessorSessionRef && sameSession(continuation.sessionRef, decision.predecessorSessionRef));
|
|
40
|
+
}
|
|
29
41
|
/**
|
|
30
42
|
* Retire only the host-side records for a Builder turn that was interrupted
|
|
31
43
|
* before Rafi received a durable response. The next turn is sent to the
|
|
@@ -78,12 +90,62 @@ export function prepareExactSessionBuilderContinuation(projectDir, runId, sessio
|
|
|
78
90
|
db.close();
|
|
79
91
|
}
|
|
80
92
|
}
|
|
93
|
+
/** Retire uncertain execution for an explicitly selected start-over, never claim
|
|
94
|
+
* that a provider did not run or that its preserved implementation passed QA. */
|
|
95
|
+
export function retireBuildExecutionForStartOver(projectDir, runId, operationId) {
|
|
96
|
+
const db = new WorkflowDb(projectDir);
|
|
97
|
+
let admission;
|
|
98
|
+
let lease;
|
|
99
|
+
try {
|
|
100
|
+
const operation = db.getRun(operationId);
|
|
101
|
+
const original = operation?.originalWork;
|
|
102
|
+
if (operation?.kind !== "recovery" || operation.state.operation !== "build-start-over" || original?.run?.runId !== runId)
|
|
103
|
+
throw new Error("Execution retirement requires a recorded start-over intent for this run");
|
|
104
|
+
admission = db.acquireBuildRecoveryAdmission(runId);
|
|
105
|
+
lease = db.acquireLease(runId);
|
|
106
|
+
if (db.readinessProcesses().length)
|
|
107
|
+
throw new Error("Owned process cleanup must be verified before starting over");
|
|
108
|
+
if (db.incompletePublications().some(item => {
|
|
109
|
+
if (item.runId !== runId)
|
|
110
|
+
return false;
|
|
111
|
+
const intent = item.intent;
|
|
112
|
+
return intent.operation !== "build-run-projection" || intent.target !== join(resolve(projectDir), ".foreman", "runs", `${runId}.json`);
|
|
113
|
+
}))
|
|
114
|
+
throw new Error("Finish pending publication recovery before starting over; preserved files may still be awaiting publication");
|
|
115
|
+
db.atomic(() => {
|
|
116
|
+
for (const dispatch of [...db.unresolvedRoleDispatches(runId, "builder"), ...db.unresolvedRoleDispatches(runId, "qa")]) {
|
|
117
|
+
db.updateOperation(dispatch.idempotencyKey, "failed", { result: { recovery: "start-over", operationId, outcomeUnknown: true, providerReplayAuthorized: false }, error: "Operator chose a fresh run from preserved source; historical execution outcome remains unknown" });
|
|
118
|
+
}
|
|
119
|
+
const run = db.getRun(runId);
|
|
120
|
+
if (!run)
|
|
121
|
+
throw new Error("Original run disappeared before start-over");
|
|
122
|
+
db.transition(runId, { checkpoint: "execution-retired-for-start-over", state: { ...run.state, executionRetiredForStartOver: operationId }, event: "execution_retired_for_start_over", payload: { operationId, providerReplayAuthorized: false } });
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
try {
|
|
127
|
+
if (lease)
|
|
128
|
+
db.releaseLease(lease);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
try {
|
|
132
|
+
if (admission)
|
|
133
|
+
db.releaseBuildAdmission(admission);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
db.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
81
141
|
function sameSession(value, session) {
|
|
82
142
|
if (!value || typeof value !== "object")
|
|
83
143
|
return false;
|
|
84
144
|
const candidate = value;
|
|
85
145
|
return candidate.provider === session.provider
|
|
86
146
|
&& candidate.sessionId === session.sessionId
|
|
147
|
+
&& candidate.generation === session.generation
|
|
148
|
+
&& session.role === "builder" && session.stream === "builder"
|
|
87
149
|
&& candidate.role === "builder"
|
|
88
150
|
&& candidate.stream === "builder"
|
|
89
151
|
&& candidate.cwd === session.cwd
|
|
@@ -93,9 +155,23 @@ export function assertBuildAssignmentReconciled(projectDir, runId, worktree = pr
|
|
|
93
155
|
const reader = new WorkflowReader(projectDir);
|
|
94
156
|
try {
|
|
95
157
|
const runs = [...new Set([runId, ...reader.buildRuns(true).map(run => run.runId)])];
|
|
158
|
+
const retiredRuns = new Set();
|
|
96
159
|
for (const operation of runs.flatMap(id => reader.operations(id, true))) {
|
|
97
160
|
if (operation.kind !== "build-assignment")
|
|
98
161
|
continue;
|
|
162
|
+
if (retiredRuns.has(operation.runId))
|
|
163
|
+
continue;
|
|
164
|
+
const owner = reader.getRun(operation.runId);
|
|
165
|
+
// An explicit start-over retires execution, while retaining assignments
|
|
166
|
+
// as historical evidence. It never authorizes their provider replay.
|
|
167
|
+
const retirement = typeof owner?.state.supersededBy === "string" ? reader.getRun(owner.state.supersededBy) : undefined;
|
|
168
|
+
if (owner?.status === "superseded" && retirement?.kind === "recovery" && retirement.state.operation === "build-start-over"
|
|
169
|
+
&& retirement.state.gitPreserved === true && typeof retirement.state.resetId === "string"
|
|
170
|
+
&& retirement.originalWork.run?.runId === operation.runId
|
|
171
|
+
&& reader.events(operation.runId).some(event => event.type === "execution_retired_for_start_over" && event.payload.operationId === retirement.runId)) {
|
|
172
|
+
retiredRuns.add(operation.runId);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
99
175
|
const result = operation.result;
|
|
100
176
|
if (result?.reconciliationId)
|
|
101
177
|
continue;
|
package/dist/cli/start.d.ts
CHANGED
|
@@ -24,7 +24,9 @@ export declare function validateStartOptions(opts: Record<string, any>, requireC
|
|
|
24
24
|
export declare function validateRecoveryArguments(args: readonly string[], projectDir: string): void;
|
|
25
25
|
/** Validate persisted preparation argv without executing a command or provider. */
|
|
26
26
|
export declare function validatePreparationArguments(args: readonly string[], projectDir: string): string[];
|
|
27
|
-
export declare function buildStartCommand(
|
|
27
|
+
export declare function buildStartCommand(options?: {
|
|
28
|
+
beforeFreshStart?: (projectDir: string) => Promise<boolean>;
|
|
29
|
+
}): Command;
|
|
28
30
|
export declare function createQaNonconvergenceHandler(projectDir: string, noninteractive: boolean, planner?: AgentRoleDefaultsV1, observer?: () => RunObserver | undefined, activeRunId?: () => string): (context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>;
|
|
29
31
|
export declare function createQaReportRecoveryHandler(projectDir: string, noninteractive: boolean, observer?: () => RunObserver | undefined): QaReportRecoveryHandler;
|
|
30
32
|
/** Make the executable branch plan exactly match each explicit root-to-tip stack. */
|
package/dist/cli/start.js
CHANGED
|
@@ -8,7 +8,7 @@ import { formatRecoveryCommand, formatDecisionCommands, formatExactRunRecovery }
|
|
|
8
8
|
import { durableReadOnlyProposal } from "../readOnlyProposal.js";
|
|
9
9
|
import { captureFrozenQaSource } from "../qaSnapshot.js";
|
|
10
10
|
import { finishStartAdmission, superviseStart, supervisedRunId } from "../supervisedStart.js";
|
|
11
|
-
import { assertBuildAssignmentReconciled } from "../buildAssignment.js";
|
|
11
|
+
import { assertBuildAssignmentReconciled, isInterruptedBuilderRecovery } from "../buildAssignment.js";
|
|
12
12
|
import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions } from "../humanDecision.js";
|
|
13
13
|
import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate } from "../buildApproval.js";
|
|
14
14
|
import { Command, Option } from "commander";
|
|
@@ -450,7 +450,7 @@ export function validatePreparationArguments(args, projectDir) {
|
|
|
450
450
|
throw new Error("Saved preparation contains stale internal recovery authority");
|
|
451
451
|
return savedStartArguments(command, projectDir, options);
|
|
452
452
|
}
|
|
453
|
-
export function buildStartCommand() {
|
|
453
|
+
export function buildStartCommand(options = {}) {
|
|
454
454
|
return new Command("start")
|
|
455
455
|
.description("Enlist a builder and drive it through a batch of N steps.")
|
|
456
456
|
.argument("<project>", "path to the project directory the builder works in")
|
|
@@ -557,6 +557,10 @@ export function buildStartCommand() {
|
|
|
557
557
|
const cwd = resolve(project);
|
|
558
558
|
if (!existsSync(cwd))
|
|
559
559
|
fail(`project directory not found: ${cwd}`);
|
|
560
|
+
// Recovery commands launch a frozen continuation and must not recursively
|
|
561
|
+
// offer a different recovery path. Fresh launches may be redirected first.
|
|
562
|
+
if (!recoveryIntent && options.beforeFreshStart && !await options.beforeFreshStart(cwd))
|
|
563
|
+
return;
|
|
560
564
|
const recoveryRecord = opts.recoverRun ? readBuildRuns(cwd).find((run) => run.runId === opts.recoverRun) : undefined;
|
|
561
565
|
let dirtyWork = recoveryRecord?.runDecisions?.dirtyWork;
|
|
562
566
|
if (opts.recoverRun && !recoveryRecord)
|
|
@@ -1985,9 +1989,9 @@ export function buildStartCommand() {
|
|
|
1985
1989
|
for (const node of plan.nodes)
|
|
1986
1990
|
admitApprovedTicket(cwd, masterRun.runId, node.ticket.id, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved branch plan`);
|
|
1987
1991
|
masterRun = checkpointBuildRun(cwd, masterRun, "capabilities-resolved", { builderCapabilities });
|
|
1988
|
-
const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, masterRun.runId, requestedTicketIds[0]) : undefined;
|
|
1992
|
+
const qaResumedRecovery = recoveryRecord && !isInterruptedBuilderRecovery(recoveryRecord) ? pendingQaRecoveryPacket(cwd, masterRun.runId, requestedTicketIds[0]) : undefined;
|
|
1989
1993
|
let qaProtocolResumeTicket;
|
|
1990
|
-
if (recoveryRecord && !qaResumedRecovery) {
|
|
1994
|
+
if (recoveryRecord && !qaResumedRecovery && !isInterruptedBuilderRecovery(recoveryRecord)) {
|
|
1991
1995
|
const protocolDb = new WorkflowDb(cwd);
|
|
1992
1996
|
try {
|
|
1993
1997
|
const pendingHeads = protocolDb.pendingQaTicketHeads(masterRun.runId);
|
|
@@ -2300,10 +2304,10 @@ export function buildStartCommand() {
|
|
|
2300
2304
|
buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities });
|
|
2301
2305
|
activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
|
|
2302
2306
|
activeContinuityRunId = buildRun.runId;
|
|
2303
|
-
const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, buildRun.runId, requestedTicketIds[0]) : undefined;
|
|
2307
|
+
const qaResumedRecovery = recoveryRecord && !isInterruptedBuilderRecovery(recoveryRecord) ? pendingQaRecoveryPacket(cwd, buildRun.runId, requestedTicketIds[0]) : undefined;
|
|
2304
2308
|
let qaFinalizationTicket;
|
|
2305
2309
|
let qaProtocolResumeTicket;
|
|
2306
|
-
if (recoveryRecord && !qaResumedRecovery) {
|
|
2310
|
+
if (recoveryRecord && !qaResumedRecovery && !isInterruptedBuilderRecovery(recoveryRecord)) {
|
|
2307
2311
|
const finalizationDb = new WorkflowDb(cwd);
|
|
2308
2312
|
try {
|
|
2309
2313
|
const pendingHeads = finalizationDb.pendingQaTicketHeads(buildRun.runId);
|
package/dist/cli/tickets.d.ts
CHANGED
|
@@ -61,5 +61,6 @@ export declare function ticketOwnershipReceipts(projectDir: string, commandName:
|
|
|
61
61
|
export declare function buildTicketsCommand(options?: {
|
|
62
62
|
prepareOwnership?: (projectDir: string, receipts: TicketOwnershipReceipt[]) => void;
|
|
63
63
|
recordOwnership?: (projectDir: string, receipts: TicketOwnershipReceipt[]) => void;
|
|
64
|
+
restartBuild?: (projectDir: string, runId: string, tickets: string[]) => Promise<void>;
|
|
64
65
|
}): Command;
|
|
65
66
|
export {};
|
package/dist/cli/tickets.js
CHANGED
|
@@ -1843,7 +1843,7 @@ export function buildTicketsCommand(options = {}) {
|
|
|
1843
1843
|
});
|
|
1844
1844
|
tickets
|
|
1845
1845
|
.command("reset [ticketId]")
|
|
1846
|
-
.description("Reset
|
|
1846
|
+
.description("Reset tracker state while retaining history; use build:start-over to restart interrupted execution.")
|
|
1847
1847
|
.option("-p, --project <dir>", "project directory (default: cwd)")
|
|
1848
1848
|
.option("--all", "open the interactive bulk reset scope chooser")
|
|
1849
1849
|
.option("--scope <scope>", "bulk scope (all | completed-and-unfinished | unfinished)")
|
|
@@ -1898,10 +1898,28 @@ export function buildTicketsCommand(options = {}) {
|
|
|
1898
1898
|
for (const row of preview.tickets)
|
|
1899
1899
|
console.log(` ${row.id}: ${row.title} [${row.status}] — clears ${row.cleared.join(", ")}`);
|
|
1900
1900
|
if (preview.relatedRuns.length) {
|
|
1901
|
-
console.log(" !
|
|
1901
|
+
console.log(" ! Ticket reset clears tracker state only. It cannot resume a Builder session or resolve pending QA/process recovery.");
|
|
1902
1902
|
for (const run of preview.relatedRuns)
|
|
1903
1903
|
console.log(` ${run.runId.slice(0, 8)} [${run.status}] ${run.tickets.join(", ")}`);
|
|
1904
1904
|
}
|
|
1905
|
+
if (preview.relatedRuns.length) {
|
|
1906
|
+
for (const run of preview.relatedRuns)
|
|
1907
|
+
console.log(` Keep files and restart execution: rafi build:start-over --run ${run.runId} --action restart-current`);
|
|
1908
|
+
if (options.restartBuild && process.stdin.isTTY && process.stdout.isTTY && !opts.yes) {
|
|
1909
|
+
const answer = await select({ message: "These tickets belong to interrupted builds. What should Rafi do?", options: [
|
|
1910
|
+
...preview.relatedRuns.map(run => ({ value: run.runId, label: `Restart execution from current files for build ${run.runId.slice(0, 8)} (keep implementation)` })),
|
|
1911
|
+
{ value: "tracker-only", label: "Reset tracker state only; leave build recovery unchanged" },
|
|
1912
|
+
{ value: "cancel", label: "Cancel" },
|
|
1913
|
+
] });
|
|
1914
|
+
if (isCancel(answer) || answer === "cancel")
|
|
1915
|
+
return;
|
|
1916
|
+
if (answer !== "tracker-only") {
|
|
1917
|
+
const run = preview.relatedRuns.find(item => item.runId === answer);
|
|
1918
|
+
await options.restartBuild(cwd(opts), run.runId, preview.tickets.map(item => item.id).filter(id => run.tickets.includes(id)));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1905
1923
|
if (!preview.tickets.length)
|
|
1906
1924
|
return;
|
|
1907
1925
|
const explicitlyConfirmed = Boolean(opts.yes && (ticketId || opts.scope));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-foreman",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.25",
|
|
4
4
|
"description": "An orchestrator that keeps Codex CLI / Claude Code builders moving through their step list unattended. Part of Rafi.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -308,7 +308,7 @@
|
|
|
308
308
|
"parse5": "7.2.1",
|
|
309
309
|
"pdfjs-dist": "4.10.38",
|
|
310
310
|
"rafi-spec": "0.8.15",
|
|
311
|
-
"special-agents": "0.8.
|
|
311
|
+
"special-agents": "0.8.15",
|
|
312
312
|
"yaml": "^2.5.1"
|
|
313
313
|
},
|
|
314
314
|
"devDependencies": {
|