ai-foreman 1.7.26 → 1.7.28

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 (98) hide show
  1. package/dist/adapters/codex.d.ts +12 -0
  2. package/dist/adapters/codex.js +50 -1
  3. package/dist/adapters/recovering.d.ts +4 -0
  4. package/dist/adapters/recovering.js +10 -0
  5. package/dist/adapters/sessionFailure.d.ts +6 -0
  6. package/dist/adapters/sessionFailure.js +19 -0
  7. package/dist/adapters/types.d.ts +6 -0
  8. package/dist/branch/currentGuard.d.ts +8 -0
  9. package/dist/branch/currentGuard.js +5 -0
  10. package/dist/branch/runner.js +6 -2
  11. package/dist/buildApproval.js +2 -1
  12. package/dist/buildRuns.d.ts +12 -0
  13. package/dist/buildRuns.js +50 -0
  14. package/dist/buildWorkContext.js +1 -1
  15. package/dist/buildWorkMigration.d.ts +10 -0
  16. package/dist/buildWorkMigration.js +18 -2
  17. package/dist/cli/start.d.ts +10 -1
  18. package/dist/cli/start.js +186 -31
  19. package/dist/cli/state.js +39 -1
  20. package/dist/cli/tickets.d.ts +37 -0
  21. package/dist/cli/tickets.js +367 -12
  22. package/dist/config.js +14 -4
  23. package/dist/continuity.d.ts +1 -0
  24. package/dist/continuity.js +5 -0
  25. package/dist/foreman.js +89 -8
  26. package/dist/graph/preparation.js +6 -3
  27. package/dist/graph/semantic.js +2 -1
  28. package/dist/graph/turn.js +8 -1
  29. package/dist/handoffProtocol.js +2 -1
  30. package/dist/handoffs.js +7 -19
  31. package/dist/hostQaComparison.d.ts +50 -0
  32. package/dist/hostQaComparison.js +195 -0
  33. package/dist/log.d.ts +1 -1
  34. package/dist/managerPacket.js +1 -1
  35. package/dist/observability.d.ts +2 -0
  36. package/dist/observability.js +2 -1
  37. package/dist/permissions/policy.js +32 -0
  38. package/dist/planChanges.js +1 -1
  39. package/dist/projectDiagnostics.js +30 -0
  40. package/dist/providerQuestions.js +4 -24
  41. package/dist/qaBuildGate.d.ts +6 -1
  42. package/dist/qaBuildGate.js +76 -16
  43. package/dist/qaBuilderCoverage.d.ts +2 -1
  44. package/dist/qaBuilderCoverage.js +58 -7
  45. package/dist/qaBuilderEvidence.d.ts +18 -0
  46. package/dist/qaBuilderEvidence.js +60 -0
  47. package/dist/qaContractCoverage.d.ts +12 -1
  48. package/dist/qaContractCoverage.js +34 -2
  49. package/dist/qaContractDelivery.d.ts +2 -2
  50. package/dist/qaContractDelivery.js +88 -10
  51. package/dist/qaEffectiveConfig.js +2 -1
  52. package/dist/qaFailureDelivery.d.ts +23 -0
  53. package/dist/qaFailureDelivery.js +87 -6
  54. package/dist/qaHostFacts.d.ts +2 -0
  55. package/dist/qaHostFacts.js +11 -0
  56. package/dist/qaPreparation.d.ts +38 -1
  57. package/dist/qaPreparation.js +372 -31
  58. package/dist/qaPreparationInputs.js +5 -1
  59. package/dist/qaPreparationPolicy.d.ts +2 -1
  60. package/dist/qaPreparationPolicy.js +39 -9
  61. package/dist/qaPreparationStore.d.ts +4 -0
  62. package/dist/qaPreparationStore.js +8 -0
  63. package/dist/qaRecovery.d.ts +7 -0
  64. package/dist/qaRecovery.js +73 -1
  65. package/dist/qaReview.js +58 -9
  66. package/dist/qaReviewEvidence.d.ts +6 -0
  67. package/dist/qaReviewEvidence.js +37 -0
  68. package/dist/qaRollbackRehearsal.d.ts +2 -0
  69. package/dist/qaRollbackRehearsal.js +78 -0
  70. package/dist/qaSnapshot.d.ts +3 -1
  71. package/dist/qaSnapshot.js +3 -2
  72. package/dist/qaVerificationContract.d.ts +16 -0
  73. package/dist/qaVerificationContract.js +183 -17
  74. package/dist/sources/sourceRegistry.js +16 -5
  75. package/dist/stateTransfer.d.ts +14 -0
  76. package/dist/stateTransfer.js +51 -0
  77. package/dist/statusReporter.d.ts +8 -0
  78. package/dist/statusReporter.js +5 -0
  79. package/dist/supervisedStart.js +23 -0
  80. package/dist/ticketPlanning.d.ts +373 -0
  81. package/dist/ticketPlanning.js +34 -3
  82. package/dist/ticketPopulation.d.ts +28 -3
  83. package/dist/ticketPopulation.js +127 -15
  84. package/dist/tickets/details.d.ts +2 -1
  85. package/dist/tickets/details.js +7 -3
  86. package/dist/tickets/eligibility.d.ts +6 -0
  87. package/dist/tickets/eligibility.js +12 -1
  88. package/dist/tickets/riskWaivers.d.ts +3 -0
  89. package/dist/tickets/riskWaivers.js +12 -0
  90. package/dist/tickets/stateDb.d.ts +10 -0
  91. package/dist/tickets/stateDb.js +23 -0
  92. package/dist/tickets/ticketLoader.d.ts +9 -0
  93. package/dist/tickets/ticketLoader.js +15 -2
  94. package/dist/tickets/ticketSchema.d.ts +14 -1
  95. package/dist/tickets/ticketSchema.js +2 -1
  96. package/dist/tickets/validate.js +11 -1
  97. package/dist/workflowDb.js +24 -3
  98. package/package.json +9 -4
@@ -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. */
@@ -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
- return await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction, policy));
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
+ }
@@ -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() {
@@ -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
  }
@@ -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)))
@@ -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)
@@ -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
- if (identity.canonical_root !== realpathSync(projectDir))
11
- throw new Error("Build project identity requires explicit state-transfer rebinding");
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) {
@@ -1,9 +1,11 @@
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";
5
7
  import type { QaNonconvergenceContext, QaNonconvergenceDecision, QaReportRecoveryHandler } from "../qaReview.js";
6
- import { RunObserver } from "../observability.js";
8
+ import { RunObserver, type ObservabilityMode } from "../observability.js";
7
9
  export declare function formatStartResumeCommand(executable: "ai-foreman", projectDir: string, steps: number, sessionId: string): string;
8
10
  export declare function formatResumeGuidance(executable: "rafi" | "ai-foreman", projectDir: string, steps: number, sessionId?: string, run?: {
9
11
  runId: string;
@@ -20,6 +22,13 @@ 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
+ export declare function resolveAuditLoggingMode(cli: unknown, configured: unknown, environment?: string | undefined): ObservabilityMode;
26
+ /** Preserve the distinction between a staged handoff and an unattempted acceptance. */
27
+ export declare function recordHandoffAcceptanceNotAttempted(projectDir: string, runId: string, role: "builder" | "qa", generation: number, stage: string, detail: string): void;
28
+ /** Default start selection is deliberately the same eligibility model users see in the ticket queue. */
29
+ export declare function selectDefaultExecutionTickets(tickets: TicketDef[], states: Map<string, TicketState>, implementationLimit: number, steps: number, delivery?: DeliveryConfig): string[];
30
+ /** Fail before runtime/provider admission when canonical ticket data is unsafe. */
31
+ export declare function assertStartTicketPreflight(projectDir: string): void;
23
32
  /** Validate generated established recovery before reserving execution authority. */
24
33
  export declare function validateRecoveryArguments(args: readonly string[], projectDir: string): void;
25
34
  /** Validate persisted preparation argv without executing a command or provider. */