ai-foreman 1.7.13 → 1.7.14

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.
Files changed (47) hide show
  1. package/README.md +33 -3
  2. package/dist/adapters/claude.d.ts +5 -0
  3. package/dist/adapters/claude.js +122 -16
  4. package/dist/adapters/codex.d.ts +2 -0
  5. package/dist/adapters/codex.js +30 -8
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +6 -0
  8. package/dist/adapters/types.d.ts +22 -0
  9. package/dist/agentRun.js +21 -3
  10. package/dist/branch/currentGuard.d.ts +2 -0
  11. package/dist/branch/currentGuard.js +9 -0
  12. package/dist/branch/finalization.d.ts +22 -0
  13. package/dist/branch/finalization.js +120 -0
  14. package/dist/branch/git.js +7 -2
  15. package/dist/branch/runner.d.ts +3 -2
  16. package/dist/branch/runner.js +277 -75
  17. package/dist/buildRuns.d.ts +1 -0
  18. package/dist/buildRuns.js +33 -3
  19. package/dist/cli/start.js +314 -86
  20. package/dist/cli/state.d.ts +4 -0
  21. package/dist/cli/state.js +69 -0
  22. package/dist/continuity.d.ts +4 -0
  23. package/dist/continuity.js +26 -3
  24. package/dist/foreman.d.ts +22 -4
  25. package/dist/foreman.js +105 -19
  26. package/dist/handoffs.d.ts +5 -2
  27. package/dist/handoffs.js +93 -12
  28. package/dist/index.js +2 -0
  29. package/dist/qaFailureDelivery.d.ts +100 -0
  30. package/dist/qaFailureDelivery.js +570 -0
  31. package/dist/qaProtocolV2.d.ts +233 -0
  32. package/dist/qaProtocolV2.js +114 -0
  33. package/dist/qaRecovery.d.ts +15 -3
  34. package/dist/qaRecovery.js +477 -83
  35. package/dist/qaReview.d.ts +42 -17
  36. package/dist/qaReview.js +1216 -457
  37. package/dist/qaRuntime.d.ts +14 -0
  38. package/dist/qaRuntime.js +44 -0
  39. package/dist/qaSnapshot.d.ts +34 -0
  40. package/dist/qaSnapshot.js +302 -34
  41. package/dist/stateTransfer.d.ts +57 -0
  42. package/dist/stateTransfer.js +644 -0
  43. package/dist/statusReporter.d.ts +2 -0
  44. package/dist/statusReporter.js +6 -0
  45. package/dist/workflowDb.d.ts +245 -2
  46. package/dist/workflowDb.js +647 -6
  47. package/package.json +15 -3
package/dist/handoffs.js CHANGED
@@ -9,6 +9,34 @@ export const HANDOFF_CACHE_RETENTION_DAYS = 30;
9
9
  export const HANDOFF_ACCEPTED = "HANDOFF_ACCEPTED";
10
10
  export const HANDOFF_REQUEST_START = "RAFI_HANDOFF_REQUEST_START";
11
11
  export const HANDOFF_REQUEST_END = "RAFI_HANDOFF_REQUEST_END";
12
+ function handoffSessionIdentity(ref) {
13
+ return JSON.stringify([ref.version, ref.provider, ref.sessionId, ref.role, ref.stream, ref.generation, resolve(ref.cwd), resolve(ref.configRoot), ref.workspaceIdentity, ref.ticketId, ref.deliveryUnitId]);
14
+ }
15
+ async function collectHandoffTerminalEvents(adapter, turnId) {
16
+ const events = [];
17
+ let timer;
18
+ const collect = (async () => {
19
+ for await (const event of adapter.events()) {
20
+ events.push(event);
21
+ if (event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === turnId)
22
+ break;
23
+ }
24
+ })();
25
+ try {
26
+ await Promise.race([collect, new Promise((resolve) => { timer = setTimeout(resolve, 1000); })]);
27
+ }
28
+ catch (error) {
29
+ events.push({ kind: "error", message: `handoff event stream failed: ${error instanceof Error ? error.message : String(error)}` });
30
+ }
31
+ finally {
32
+ if (timer)
33
+ clearTimeout(timer);
34
+ }
35
+ // A missing terminal event makes acceptance fail below and closes the
36
+ // successor, ending any pending iterator read before it can perform work.
37
+ void collect.catch(() => { });
38
+ return events;
39
+ }
12
40
  export function parseBuilderHandoffRequest(text) {
13
41
  const starts = [...text.matchAll(/RAFI_HANDOFF_REQUEST_START/g)];
14
42
  const ends = [...text.matchAll(/RAFI_HANDOFF_REQUEST_END/g)];
@@ -113,11 +141,13 @@ export class HandoffService {
113
141
  label: resource.label,
114
142
  digest: resource.digest && /^[a-f0-9]{64}$/.test(resource.digest) ? resource.digest : digest(resource.content ?? ""),
115
143
  authoritative: resource.authoritative,
116
- ...(resource.requiredForRecovery !== undefined ? { requiredForRecovery: resource.requiredForRecovery } : {}),
117
- ...(resource.mediaType ? { mediaType: resource.mediaType } : {}),
118
- ...(resource.path ? { path: resource.path } : {}),
144
+ requiredForRecovery: resource.requiredForRecovery ?? false,
145
+ mediaType: resource.mediaType ?? "application/octet-stream",
146
+ path: resource.path ?? `embedded:${resource.label}`,
147
+ purpose: resource.purpose ?? `Declared ${input.role} handoff resource: ${resource.label}`,
148
+ bytes: resource.bytes ?? (resource.content !== undefined ? Buffer.byteLength(resource.content) : 0),
119
149
  }));
120
- resources.unshift({ label: "continuity-checkpoint", digest: head.digest, authoritative: true }, { label: "authoritative-run-state", digest: runHead.digest, authoritative: true });
150
+ resources.unshift({ label: "continuity-checkpoint", digest: head.digest, authoritative: true, requiredForRecovery: false, mediaType: "application/vnd.rafi.digest", path: "db:continuity-checkpoint", purpose: "Digest reference to the durable role continuity checkpoint", bytes: Buffer.byteLength(head.digest) }, { label: "authoritative-run-state", digest: runHead.digest, authoritative: true, requiredForRecovery: false, mediaType: "application/vnd.rafi.digest", path: "db:authoritative-run-state", purpose: "Digest reference to the durable run continuity state", bytes: Buffer.byteLength(runHead.digest) });
121
151
  const manifest = {
122
152
  version: 1,
123
153
  runId: input.runId,
@@ -212,6 +242,47 @@ export class HandoffService {
212
242
  async acceptStaged(staged, successor, options = {}) {
213
243
  const db = new WorkflowDb(this.projectDir);
214
244
  try {
245
+ const qaBoundary = staged.manifest.role === "qa";
246
+ const preparedRef = successor.sessionRef?.();
247
+ if (qaBoundary && (!preparedRef || preparedRef.version !== 1 || preparedRef.source !== "observed"
248
+ || preparedRef.role !== "qa" || preparedRef.stream !== "qa" || preparedRef.provider !== successor.agent
249
+ || !preparedRef.sessionId.trim() || /^(?:unknown|unavailable)$/i.test(preparedRef.sessionId.trim())
250
+ || !preparedRef.cwd || !preparedRef.configRoot || !Number.isSafeInteger(preparedRef.generation) || preparedRef.generation < 0
251
+ || !preparedRef.validatedAt || Number.isNaN(Date.parse(preparedRef.validatedAt)) || Number.isNaN(Date.parse(preparedRef.createdAt))
252
+ || preparedRef.sessionId !== successor.sessionId())) {
253
+ throw new HandoffAcceptanceError("missing-scoped-successor-session", staged.manifest.runId, staged.manifest.generation, "QA acceptance requires a prepared, validated scoped successor");
254
+ }
255
+ const acceptanceAttempts = [];
256
+ const sendAcceptance = async (prompt) => {
257
+ const hostPromptDigest = db.putEvidence("handoff", Buffer.from(prompt));
258
+ db.appendContinuityEvent({ runId: staged.manifest.runId, role: "host", kind: "handoff_acceptance_intended", payload: { generation: staged.manifest.generation, attempt: acceptanceAttempts.length + 1, hostPromptDigest, preparedRef }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0 });
259
+ const response = await successor.sendTurn(prompt);
260
+ const events = qaBoundary && response.turnId ? await collectHandoffTerminalEvents(successor, response.turnId) : [];
261
+ const attempt = {
262
+ hostPromptDigest,
263
+ providerPromptDigest: db.putEvidence("handoff", Buffer.from(response.providerInstruction ?? response.hostInstruction ?? prompt)),
264
+ rawResponseDigest: db.putEvidence("handoff", Buffer.from(response.rawResponse ?? response.text)),
265
+ cleanedResponseDigest: db.putEvidence("handoff", Buffer.from(response.cleanedResponse ?? response.text)),
266
+ ...(response.turnId ? { providerTurnId: response.turnId } : {}),
267
+ ...(qaBoundary ? { eventStreamDigest: db.putEvidence("handoff", Buffer.from(JSON.stringify(events))) } : {}),
268
+ };
269
+ acceptanceAttempts.push(attempt);
270
+ db.appendContinuityEvent({ runId: staged.manifest.runId, role: "host", kind: "handoff_acceptance_observed", payload: { generation: staged.manifest.generation, attempt, isError: response.isError, failure: response.failure, providerMetadata: response.providerMetadata, activeSession: successor.sessionRef?.() }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0 });
271
+ if (response.isError || response.failure)
272
+ throw new HandoffAcceptanceError("provider-turn-failed", staged.manifest.runId, staged.manifest.generation, "provider rejected or failed the acknowledgement turn");
273
+ const activeRef = successor.sessionRef?.();
274
+ const reportedRef = response.providerMetadata?.sessionRef;
275
+ if ((preparedRef && (!activeRef || handoffSessionIdentity(activeRef) !== handoffSessionIdentity(preparedRef)))
276
+ || (qaBoundary && (!response.providerMetadata || !reportedRef || response.providerMetadata.provider !== preparedRef.provider
277
+ || response.providerMetadata.sessionId !== preparedRef.sessionId || successor.sessionId() !== preparedRef.sessionId
278
+ || successor.agent !== preparedRef.provider || handoffSessionIdentity(reportedRef) !== handoffSessionIdentity(preparedRef)))) {
279
+ throw new HandoffAcceptanceError("successor-identity-mismatch", staged.manifest.runId, staged.manifest.generation, "acknowledgement did not come from the prepared successor identity");
280
+ }
281
+ if (qaBoundary && (!response.turnId || !events.some((event) => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === response.turnId))) {
282
+ throw new HandoffAcceptanceError("missing-terminal-event", staged.manifest.runId, staged.manifest.generation, "QA acknowledgement has no correlated terminal provider event");
283
+ }
284
+ return response;
285
+ };
215
286
  const acceptancePrompt = [
216
287
  "Accept this validated Rafi handoff. Do not repeat completed work and reconcile host receipts before side effects.",
217
288
  staged.markdown,
@@ -219,14 +290,15 @@ export class HandoffService {
219
290
  ...(options.guidance ? [`Human recovery guidance: ${options.guidance}`] : []),
220
291
  `Reply with ${HANDOFF_ACCEPTED} on the first line, then ${continuityInstruction()}`,
221
292
  ].join("\n\n");
222
- let response = await successor.sendTurn(acceptancePrompt);
293
+ let response = await sendAcceptance(acceptancePrompt);
223
294
  let validation = validateHandoffAcceptance(staged, successor, response.text);
224
295
  if (validation && (validation.code === "missing-acknowledgement" || validation.code === "invalid-continuity-delta")) {
225
- response = await successor.sendTurn([
296
+ const repairPrompt = [
226
297
  `Your handoff acknowledgement was rejected: ${validation.message}.`,
227
298
  "Correction only: do not use tools, repeat completed work, or perform implementation.",
228
299
  `Reply with ${HANDOFF_ACCEPTED} on the first line, then ${continuityInstruction()}`,
229
- ].join("\n\n"));
300
+ ].join("\n\n");
301
+ response = await sendAcceptance(repairPrompt);
230
302
  validation = validateHandoffAcceptance(staged, successor, response.text);
231
303
  }
232
304
  if (validation)
@@ -237,12 +309,20 @@ export class HandoffService {
237
309
  const successorRef = observedSuccessorRef ? { ...observedSuccessorRef, generation: staged.manifest.generation, validatedAt: new Date().toISOString() } : undefined;
238
310
  if (!parsed.delta || !sessionId)
239
311
  throw new Error("validated handoff acceptance lost its parsed continuity state");
312
+ if (staged.manifest.resources.some((resource) => resource.requiredForRecovery)
313
+ && (!successorRef || staged.manifest.resources.some((resource) => resource.requiredForRecovery && (!resource.purpose || !Number.isSafeInteger(resource.bytes) || resource.bytes < 0)))) {
314
+ throw new HandoffAcceptanceError("missing-scoped-successor-session", staged.manifest.runId, staged.manifest.generation, "recovery handoffs require a scoped successor and complete resource purpose/byte metadata");
315
+ }
240
316
  if (successorRef)
241
317
  successor.adoptSessionRef?.(successorRef);
242
- db.appendContinuityEvent({ runId: staged.manifest.runId, role: staged.manifest.role, kind: "handoff_successor_accepted", payload: { generation: staged.manifest.generation, sessionId, sessionRef: successorRef, delta: parsed.delta }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: successorRef });
318
+ db.appendContinuityEvent({ runId: staged.manifest.runId, role: staged.manifest.role, kind: "handoff_successor_accepted", payload: { generation: staged.manifest.generation, sessionId, sessionRef: successorRef, delta: parsed.delta, acceptanceAttempts }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: successorRef });
243
319
  const checkpoint = db.publishContinuityCheckpoint({ runId: staged.manifest.runId, role: staged.manifest.role, delta: parsed.delta, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: successorRef });
244
- const lineage = db.acceptHandoff(staged.manifest.runId, staged.manifest.generation, successorRef ?? sessionId);
245
- return { ...staged, lineage, successor, successorSessionId: sessionId, acceptanceCheckpointDigest: checkpoint.digest };
320
+ const receipt = successorRef ? { version: 1, runId: staged.manifest.runId, generation: staged.manifest.generation, role: staged.manifest.role,
321
+ manifestDigest: staged.lineage.manifestDigest, continuityCheckpointDigest: staged.manifest.continuityCheckpointDigest, acceptanceCheckpointDigest: checkpoint.digest,
322
+ ...(staged.manifest.predecessorSessionRef ? { predecessorSessionRef: staged.manifest.predecessorSessionRef } : {}), successorSessionRef: successorRef,
323
+ resources: staged.manifest.resources, acceptedAt: new Date().toISOString() } : undefined;
324
+ const lineage = db.acceptHandoff(staged.manifest.runId, staged.manifest.generation, successorRef ?? sessionId, undefined, receipt);
325
+ return { ...staged, lineage, successor, successorSessionId: sessionId, acceptanceCheckpointDigest: checkpoint.digest, ...(receipt ? { acceptanceReceipt: receipt } : {}) };
246
326
  }
247
327
  catch (error) {
248
328
  if (error instanceof HandoffAcceptanceError)
@@ -368,8 +448,9 @@ export class HandoffService {
368
448
  }
369
449
  }
370
450
  function validateHandoffAcceptance(staged, successor, text) {
371
- if (!text.trimStart().startsWith(HANDOFF_ACCEPTED)) {
372
- return { code: "missing-acknowledgement", message: `the first non-whitespace line must begin with ${HANDOFF_ACCEPTED}` };
451
+ const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
452
+ if (firstLine !== HANDOFF_ACCEPTED) {
453
+ return { code: "missing-acknowledgement", message: `the first non-whitespace line must equal ${HANDOFF_ACCEPTED}` };
373
454
  }
374
455
  const parsed = parseContinuityDelta(text);
375
456
  if (!parsed.delta) {
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { buildStartCommand } from "./cli/start.js";
6
6
  import { buildStatusCommand } from "./cli/status.js";
7
7
  import { buildDoctorCommand } from "./cli/doctor.js";
8
8
  import { buildManagerCommand } from "./cli/manager.js";
9
+ import { buildStateCommand } from "./cli/state.js";
9
10
  import { buildAttachCommand, buildDecideCommand, buildStopCommand } from "./cli/recovery.js";
10
11
  import { withActivityContext } from "./activity.js";
11
12
  const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))?.version;
@@ -19,6 +20,7 @@ program.addCommand(buildStartCommand());
19
20
  program.addCommand(buildStatusCommand());
20
21
  program.addCommand(buildDoctorCommand());
21
22
  program.addCommand(buildManagerCommand({ requireProject: true }));
23
+ program.addCommand(buildStateCommand());
22
24
  program.addCommand(buildAttachCommand());
23
25
  program.addCommand(buildDecideCommand());
24
26
  program.addCommand(buildStopCommand());
@@ -0,0 +1,100 @@
1
+ import { type ProviderSessionRefV1, type QaFailureReportV1, type QaFindingRefV2, type SessionStrategy } from "rafi-spec";
2
+ import type { BuilderAdapter, BuilderEvent } from "./adapters/types.js";
3
+ import type { TicketDef } from "./tickets/ticketSchema.js";
4
+ export interface QaFailureDeliveryInput {
5
+ projectDir: string;
6
+ runId: string;
7
+ ticket: TicketDef;
8
+ builderWorktree: string;
9
+ report: QaFailureReportV1;
10
+ reportDigest: string;
11
+ reviewAttemptId: string;
12
+ reviewNumber: number;
13
+ reviewedSourceStateDigest: string;
14
+ reviewBasisDigest: string;
15
+ remediationGeneration: number;
16
+ latestBuilderResult: string;
17
+ history: unknown[];
18
+ plannerRemediation?: string;
19
+ }
20
+ export interface QaFailureDeliveryController {
21
+ adapter(): BuilderAdapter | undefined;
22
+ setAdapter?(adapter: BuilderAdapter): void;
23
+ sessionStrategy: SessionStrategy;
24
+ prepareBoundary?(adapter: BuilderAdapter, instruction: string, strategy: SessionStrategy, worktree: string): Promise<BuilderAdapter>;
25
+ beforeTurn?(adapter: BuilderAdapter, instruction: string, worktree: string): Promise<BuilderAdapter>;
26
+ recordSession?(session: string | ProviderSessionRefV1, ticketId: string, worktree: string): void;
27
+ events?: BuilderEvent[];
28
+ }
29
+ export interface QaFailureDeliveryResult {
30
+ ok: boolean;
31
+ detail?: string;
32
+ response?: string;
33
+ summary?: string;
34
+ providerTurnId?: string;
35
+ handoffId?: string;
36
+ operationId?: string;
37
+ }
38
+ interface QaFailureHandoffV2 {
39
+ version: 2;
40
+ handoffId: string;
41
+ scope: {
42
+ runId: string;
43
+ ticketId: string;
44
+ reviewNumber: number;
45
+ reviewAttemptId: string;
46
+ remediationGeneration: number;
47
+ };
48
+ ticket: {
49
+ digest: string;
50
+ definition: TicketDef;
51
+ };
52
+ source: {
53
+ reviewedContentDigest: string;
54
+ reviewBasisDigest: string;
55
+ capturedAt: string;
56
+ worktreePath: string;
57
+ gitDir: string;
58
+ changeSummary: string;
59
+ affectedPaths: string[];
60
+ };
61
+ report: {
62
+ digest: string;
63
+ rawEvidenceDigest: string;
64
+ value: QaFailureReportV1;
65
+ findingRefs: QaFindingRefV2[];
66
+ };
67
+ latestBuilderResult: {
68
+ rawResponseDigest: string;
69
+ summaryDigest: string;
70
+ summary: string;
71
+ summaryTruncated: boolean;
72
+ originalByteCount: number;
73
+ };
74
+ history: unknown[];
75
+ plannerRemediation?: {
76
+ supplementId: string;
77
+ digest: string;
78
+ reviewedContentDigest: string;
79
+ summary: string;
80
+ fixInstructions: string[];
81
+ advisoryOnly: true;
82
+ };
83
+ responseContract: {
84
+ schemaDigest: string;
85
+ };
86
+ createdAt: string;
87
+ }
88
+ export declare class QaFailureDeliveryService {
89
+ deliver(input: QaFailureDeliveryInput, controller: QaFailureDeliveryController): Promise<QaFailureDeliveryResult>;
90
+ private buildHandoff;
91
+ private commitIntent;
92
+ private tryCorrectResponse;
93
+ private finishSuccess;
94
+ private finishInvalidOrBlocked;
95
+ private finishUncertain;
96
+ private transitionHandoff;
97
+ private supersedeForDrift;
98
+ }
99
+ export declare function renderQaFailureHandoff(handoff: QaFailureHandoffV2): string;
100
+ export {};