ai-foreman 1.7.19 → 1.7.21

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 (205) hide show
  1. package/README.md +29 -5
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +18 -6
  4. package/dist/adapters/claude.d.ts +16 -0
  5. package/dist/adapters/claude.js +28 -3
  6. package/dist/adapters/codex.d.ts +20 -0
  7. package/dist/adapters/codex.js +99 -5
  8. package/dist/adapters/recovering.d.ts +9 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/types.d.ts +15 -0
  11. package/dist/agentRun.d.ts +7 -0
  12. package/dist/agentRun.js +24 -13
  13. package/dist/branch/currentGuard.d.ts +1 -0
  14. package/dist/branch/currentGuard.js +1 -0
  15. package/dist/branch/finalization.js +6 -1
  16. package/dist/branch/git.d.ts +1 -0
  17. package/dist/branch/git.js +15 -1
  18. package/dist/branch/runner.d.ts +2 -0
  19. package/dist/branch/runner.js +92 -5
  20. package/dist/buildAdmission.d.ts +61 -0
  21. package/dist/buildAdmission.js +286 -0
  22. package/dist/buildApproval.d.ts +2 -0
  23. package/dist/buildApproval.js +17 -0
  24. package/dist/buildAssignment.d.ts +33 -0
  25. package/dist/buildAssignment.js +215 -0
  26. package/dist/buildInterventions.d.ts +90 -0
  27. package/dist/buildInterventions.js +334 -0
  28. package/dist/buildLaunchGate.d.ts +1 -0
  29. package/dist/buildLaunchGate.js +36 -0
  30. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  31. package/dist/buildOwnershipReconciliation.js +41 -0
  32. package/dist/buildOwnershipRepair.d.ts +33 -0
  33. package/dist/buildOwnershipRepair.js +111 -0
  34. package/dist/buildRuns.d.ts +24 -1
  35. package/dist/buildRuns.js +216 -70
  36. package/dist/buildSyntheticWork.d.ts +2 -0
  37. package/dist/buildSyntheticWork.js +30 -0
  38. package/dist/buildWorkAdmission.d.ts +17 -0
  39. package/dist/buildWorkAdmission.js +57 -0
  40. package/dist/buildWorkAuthorization.d.ts +2 -0
  41. package/dist/buildWorkAuthorization.js +23 -0
  42. package/dist/buildWorkContext.d.ts +3 -0
  43. package/dist/buildWorkContext.js +6 -0
  44. package/dist/buildWorkMigration.d.ts +9 -0
  45. package/dist/buildWorkMigration.js +206 -0
  46. package/dist/buildWorkReconciliation.d.ts +12 -0
  47. package/dist/buildWorkReconciliation.js +70 -0
  48. package/dist/builderGuidanceFollowup.d.ts +15 -0
  49. package/dist/builderGuidanceFollowup.js +34 -0
  50. package/dist/cli/doctor.js +3 -2
  51. package/dist/cli/graph.d.ts +2 -0
  52. package/dist/cli/graph.js +75 -0
  53. package/dist/cli/manager.d.ts +8 -0
  54. package/dist/cli/manager.js +234 -12
  55. package/dist/cli/recovery.js +3 -2
  56. package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
  57. package/dist/cli/runtimeAuthPrompt.js +41 -7
  58. package/dist/cli/start.d.ts +8 -0
  59. package/dist/cli/start.js +2091 -1849
  60. package/dist/cli/status.js +20 -5
  61. package/dist/cli/tickets.js +9 -7
  62. package/dist/continuity.d.ts +16 -1
  63. package/dist/continuity.js +68 -12
  64. package/dist/foreman.d.ts +12 -4
  65. package/dist/foreman.js +397 -60
  66. package/dist/graph/boundary.d.ts +15 -0
  67. package/dist/graph/boundary.js +50 -0
  68. package/dist/graph/bridge.d.ts +9 -0
  69. package/dist/graph/bridge.js +146 -0
  70. package/dist/graph/bridge.py +205 -0
  71. package/dist/graph/config.d.ts +11 -0
  72. package/dist/graph/config.js +40 -0
  73. package/dist/graph/corpus.d.ts +16 -0
  74. package/dist/graph/corpus.js +188 -0
  75. package/dist/graph/derived.d.ts +17 -0
  76. package/dist/graph/derived.js +73 -0
  77. package/dist/graph/host.d.ts +4 -0
  78. package/dist/graph/host.js +160 -0
  79. package/dist/graph/install.d.ts +3 -0
  80. package/dist/graph/install.js +50 -0
  81. package/dist/graph/lifecycle.d.ts +11 -0
  82. package/dist/graph/lifecycle.js +124 -0
  83. package/dist/graph/maintenance.d.ts +43 -0
  84. package/dist/graph/maintenance.js +301 -0
  85. package/dist/graph/manager.d.ts +9 -0
  86. package/dist/graph/manager.js +92 -0
  87. package/dist/graph/preparation.d.ts +41 -0
  88. package/dist/graph/preparation.js +43 -0
  89. package/dist/graph/read.d.ts +27 -0
  90. package/dist/graph/read.js +152 -0
  91. package/dist/graph/semantic.d.ts +17 -0
  92. package/dist/graph/semantic.js +27 -0
  93. package/dist/graph/session.d.ts +8 -0
  94. package/dist/graph/session.js +92 -0
  95. package/dist/graph/sources.d.ts +4 -0
  96. package/dist/graph/sources.js +11 -0
  97. package/dist/graph/storage.d.ts +38 -0
  98. package/dist/graph/storage.js +159 -0
  99. package/dist/graph/turn.d.ts +17 -0
  100. package/dist/graph/turn.js +360 -0
  101. package/dist/graph/util.d.ts +5 -0
  102. package/dist/graph/util.js +51 -0
  103. package/dist/handoffs.d.ts +1 -0
  104. package/dist/handoffs.js +5 -1
  105. package/dist/humanDecision.d.ts +21 -2
  106. package/dist/humanDecision.js +169 -10
  107. package/dist/index.js +4 -1
  108. package/dist/installOwnership.d.ts +46 -0
  109. package/dist/installOwnership.js +440 -0
  110. package/dist/linuxProbeInventory.d.ts +12 -0
  111. package/dist/linuxProbeInventory.js +48 -0
  112. package/dist/log.d.ts +1 -1
  113. package/dist/log.js +7 -1
  114. package/dist/managerEvidence.d.ts +25 -0
  115. package/dist/managerEvidence.js +326 -0
  116. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  117. package/dist/managerEvidenceArtifacts.js +118 -0
  118. package/dist/managerPacket.d.ts +2 -0
  119. package/dist/managerPacket.js +23 -0
  120. package/dist/markers.d.ts +1 -1
  121. package/dist/markers.js +2 -2
  122. package/dist/observability.js +1 -1
  123. package/dist/processIdentity.d.ts +21 -1
  124. package/dist/processIdentity.js +87 -5
  125. package/dist/providerPhase.d.ts +13 -0
  126. package/dist/providerPhase.js +26 -0
  127. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  128. package/dist/qaAuthorizedDisposition.js +26 -0
  129. package/dist/qaBuildGate.d.ts +4 -0
  130. package/dist/qaBuildGate.js +216 -0
  131. package/dist/qaBuilderCoverage.d.ts +5 -0
  132. package/dist/qaBuilderCoverage.js +78 -0
  133. package/dist/qaContractCoverage.d.ts +10 -0
  134. package/dist/qaContractCoverage.js +55 -0
  135. package/dist/qaContractDelivery.d.ts +23 -0
  136. package/dist/qaContractDelivery.js +145 -0
  137. package/dist/qaContractFreshness.d.ts +15 -0
  138. package/dist/qaContractFreshness.js +36 -0
  139. package/dist/qaDeliveryJournal.d.ts +10 -0
  140. package/dist/qaDeliveryJournal.js +16 -1
  141. package/dist/qaEffectiveConfig.d.ts +20 -0
  142. package/dist/qaEffectiveConfig.js +40 -0
  143. package/dist/qaEvidenceReader.d.ts +14 -0
  144. package/dist/qaEvidenceReader.js +176 -0
  145. package/dist/qaFailureDelivery.d.ts +2 -0
  146. package/dist/qaFailureDelivery.js +107 -10
  147. package/dist/qaHandbackMigration.d.ts +1 -1
  148. package/dist/qaHandbackMigration.js +3 -3
  149. package/dist/qaPreparation.d.ts +26 -0
  150. package/dist/qaPreparation.js +150 -0
  151. package/dist/qaPreparationInputs.d.ts +8 -0
  152. package/dist/qaPreparationInputs.js +47 -0
  153. package/dist/qaPreparationMetrics.d.ts +145 -0
  154. package/dist/qaPreparationMetrics.js +120 -0
  155. package/dist/qaPreparationPolicy.d.ts +5 -0
  156. package/dist/qaPreparationPolicy.js +51 -0
  157. package/dist/qaPreparationStore.d.ts +157 -0
  158. package/dist/qaPreparationStore.js +412 -0
  159. package/dist/qaProtocolV2.d.ts +28 -0
  160. package/dist/qaProtocolV2.js +12 -0
  161. package/dist/qaReview.d.ts +6 -0
  162. package/dist/qaReview.js +293 -41
  163. package/dist/qaTimeline.d.ts +18 -0
  164. package/dist/qaTimeline.js +123 -0
  165. package/dist/qaVerificationContract.d.ts +17 -0
  166. package/dist/qaVerificationContract.js +236 -0
  167. package/dist/readinessCleanup.d.ts +19 -0
  168. package/dist/readinessCleanup.js +122 -0
  169. package/dist/readinessGate.d.ts +1 -0
  170. package/dist/readinessGate.js +82 -0
  171. package/dist/recoveryGuidance.d.ts +17 -0
  172. package/dist/recoveryGuidance.js +38 -0
  173. package/dist/runtimeAuth.d.ts +1 -0
  174. package/dist/runtimeAuth.js +5 -1
  175. package/dist/runtimeReadiness.d.ts +13 -0
  176. package/dist/runtimeReadiness.js +206 -10
  177. package/dist/sessionIdentity.d.ts +2 -0
  178. package/dist/sessionIdentity.js +14 -0
  179. package/dist/sessionLifecycle.js +3 -3
  180. package/dist/sources/sourceRegistry.js +7 -0
  181. package/dist/stateTransfer.js +85 -8
  182. package/dist/statusReporter.d.ts +1 -0
  183. package/dist/statusReporter.js +1 -0
  184. package/dist/supervisedStart.d.ts +6 -0
  185. package/dist/supervisedStart.js +178 -62
  186. package/dist/supervisor.d.ts +2 -0
  187. package/dist/supervisor.js +26 -2
  188. package/dist/terminalStyle.d.ts +21 -0
  189. package/dist/terminalStyle.js +50 -0
  190. package/dist/ticketPopulation.d.ts +1 -0
  191. package/dist/ticketPopulation.js +5 -1
  192. package/dist/tickets/commands.js +2 -2
  193. package/dist/tickets/queue.js +1 -1
  194. package/dist/tickets/ticketSchema.d.ts +198 -0
  195. package/dist/tickets/ticketSchema.js +2 -0
  196. package/dist/windowsProbeJob.d.ts +12 -0
  197. package/dist/windowsProbeJob.js +144 -0
  198. package/dist/workflowDb.d.ts +121 -6
  199. package/dist/workflowDb.js +874 -30
  200. package/dist/workflowReader.d.ts +116 -2
  201. package/dist/workflowReader.js +131 -4
  202. package/package.json +89 -8
  203. package/scripts/copy-graph-assets.mjs +3 -0
  204. package/scripts/graph-benchmark.mjs +20 -0
  205. package/scripts/graph-evaluation.mjs +85 -0
@@ -0,0 +1,90 @@
1
+ import Database from "better-sqlite3";
2
+ import type { ManagerActionRequestV1 } from "rafi-spec";
3
+ export type InstructionRecipient = "builder" | "qa";
4
+ export type InstructionState = "queued" | "reserved" | "delivered" | "acknowledged" | "applied" | "verified" | "uncertain" | "rejected" | "superseded" | "withdrawn";
5
+ export interface BuildInstruction {
6
+ instructionId: string;
7
+ request: ManagerActionRequestV1;
8
+ sequence: number;
9
+ userTurn: string;
10
+ textDigest: string;
11
+ requirementsDigest: string;
12
+ createdAt: string;
13
+ basis: {
14
+ assignmentId: string;
15
+ scopeRevision: string;
16
+ reviewAttemptId?: string;
17
+ sourceDigest?: string;
18
+ };
19
+ deliveries: Array<{
20
+ recipient: InstructionRecipient;
21
+ state: InstructionState;
22
+ operationId?: string;
23
+ sourceDigest?: string;
24
+ instructionDigest?: string;
25
+ receipt?: unknown;
26
+ detail?: string;
27
+ }>;
28
+ }
29
+ export declare class BuildControlBoundary extends Error {
30
+ }
31
+ export declare function migrateBuildInterventions(db: Database.Database): void;
32
+ /** Questions belong to work through their ticket interruption or retained remediation stop. */
33
+ export declare function decisionBelongsToWork(decision: {
34
+ decisionId: string;
35
+ runId: string;
36
+ interruptionId: string;
37
+ }, runId: string, workId: string, stops: Array<{
38
+ runId: string;
39
+ workId: string;
40
+ operationId: string;
41
+ decisionId?: string;
42
+ }>): boolean;
43
+ export declare function decisionWorkId(db: Database.Database, decision: {
44
+ decisionId: string;
45
+ runId: string;
46
+ interruptionId: string;
47
+ }): string | undefined;
48
+ /** A proposal never authorizes itself. Only exact commands from the original user turn do. */
49
+ export declare function parseManagerAction(userTurn: string, requestId?: string, expectedRevision?: number): ManagerActionRequestV1 | undefined;
50
+ /** Narrow write capability coexists with a build lease and cannot dispatch or publish workflow state. */
51
+ export declare class BuildInterventionControl {
52
+ private readonly runId;
53
+ private readonly workId;
54
+ private readonly db;
55
+ constructor(projectDir: string, runId: string, workId: string);
56
+ close(): void;
57
+ revision(): number;
58
+ enqueue(request: ManagerActionRequestV1, userTurn: string): BuildInstruction;
59
+ }
60
+ export declare function instruction(db: Database.Database, id: string): BuildInstruction | undefined;
61
+ /** Restart consumes retained completion proof, never sends a replacement provider turn. */
62
+ export declare function reconcileGuidance(db: Database.Database, runId: string, workId: string): void;
63
+ export declare function queuedControls(db: Database.Database, runId: string, workId: string): BuildInstruction[];
64
+ export declare function hasQueuedBuilderGuidance(db: Database.Database, runId: string, workId: string): boolean;
65
+ export declare function completeControl(db: Database.Database, id: string, receipt: unknown, rejection?: string): void;
66
+ export declare function assertFinalizationControls(db: Database.Database, runId: string, workId: string): void;
67
+ export declare function builderVerificationContext(db: Database.Database, runId: string, workId: string, sourceDigest: string): {
68
+ text: string;
69
+ ids: string[];
70
+ };
71
+ export declare function verifyBuilderGuidance(db: Database.Database, ids: string[], certificate: {
72
+ certificateId: string;
73
+ sourceStateDigest: string;
74
+ reviewBasisDigest: string;
75
+ }, response: string, turnReceiptDigest: string): void;
76
+ /** Called only inside the owning process's existing dispatch transaction. */
77
+ export declare function reserveGuidance(db: Database.Database, runId: string, workId: string, recipient: InstructionRecipient, operationId: string, sourceDigest: string, instructionText: string, reserve?: boolean): {
78
+ text: string;
79
+ ids: string[];
80
+ };
81
+ export declare function finishGuidance(db: Database.Database, ids: string[], recipient: InstructionRecipient, result: {
82
+ submitted: boolean | undefined;
83
+ receipt?: unknown;
84
+ applied?: boolean;
85
+ }): void;
86
+ export declare function verifyGuidance(db: Database.Database, ids: string[], certificate: {
87
+ certificateId: string;
88
+ sourceStateDigest: string;
89
+ reviewBasisDigest: string;
90
+ }, response: string): void;
@@ -0,0 +1,334 @@
1
+ import Database from "better-sqlite3";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { realpathSync as realpath } from "node:fs";
5
+ import { validateManagerActionRequestV1 } from "rafi-spec";
6
+ import { registerHandbackWriter } from "./qaHandbackMigration.js";
7
+ import { assertAdmittedWork } from "./buildWorkAdmission.js";
8
+ export class BuildControlBoundary extends Error {
9
+ }
10
+ export function migrateBuildInterventions(db) {
11
+ if (db.prepare("SELECT 1 FROM recovery_schema_migrations WHERE migration='005_manager_controls'").get())
12
+ return;
13
+ db.transaction(() => {
14
+ db.exec(`CREATE TABLE build_instruction_streams(run_id TEXT NOT NULL,work_id TEXT NOT NULL,revision INTEGER NOT NULL,PRIMARY KEY(run_id,work_id),FOREIGN KEY(run_id,work_id) REFERENCES build_work_scope(run_id,work_id));
15
+ CREATE TABLE build_instructions(sequence INTEGER PRIMARY KEY AUTOINCREMENT,instruction_id TEXT NOT NULL UNIQUE,request_id TEXT NOT NULL UNIQUE,run_id TEXT NOT NULL,work_id TEXT NOT NULL,record_json TEXT NOT NULL,FOREIGN KEY(run_id,work_id) REFERENCES build_work_scope(run_id,work_id));
16
+ CREATE TABLE build_instruction_deliveries(instruction_id TEXT NOT NULL REFERENCES build_instructions(instruction_id),recipient TEXT NOT NULL CHECK(recipient IN ('builder','qa')),state TEXT NOT NULL,operation_id TEXT UNIQUE,record_json TEXT NOT NULL,PRIMARY KEY(instruction_id,recipient));
17
+ CREATE TABLE build_instruction_events(sequence INTEGER PRIMARY KEY AUTOINCREMENT,instruction_id TEXT NOT NULL REFERENCES build_instructions(instruction_id),recipient TEXT,event_json TEXT NOT NULL,created_at TEXT NOT NULL);`);
18
+ for (const table of ["build_instruction_streams", "build_instructions", "build_instruction_deliveries", "build_instruction_events"])
19
+ for (const action of ["INSERT", "UPDATE", "DELETE"])
20
+ db.exec(`CREATE TRIGGER control_protocol_${table}_${action} BEFORE ${action} ON ${table} BEGIN SELECT CASE WHEN rafi_writer_protocol()<>4 THEN RAISE(ABORT,'incompatible Manager control writer') END; END`);
21
+ for (const table of ["build_instructions", "build_instruction_events"])
22
+ for (const action of ["UPDATE", "DELETE"])
23
+ db.exec(`CREATE TRIGGER control_immutable_${table}_${action} BEFORE ${action} ON ${table} BEGIN SELECT RAISE(ABORT,'Instruction history is immutable'); END`);
24
+ for (const table of ["build_instructions", "build_instruction_streams"])
25
+ for (const action of ["INSERT", "UPDATE"])
26
+ db.exec(`CREATE TRIGGER control_admitted_${table}_${action} BEFORE ${action} ON ${table} WHEN NOT EXISTS(SELECT 1 FROM build_work_scope s WHERE s.run_id=NEW.run_id AND s.work_id=NEW.work_id AND s.state='admitted') BEGIN SELECT RAISE(ABORT,'Instruction work is not admitted'); END`);
27
+ db.exec(`CREATE TRIGGER control_delivery_insert BEFORE INSERT ON build_instruction_deliveries WHEN NEW.state NOT IN ('queued','withdrawn') BEGIN SELECT RAISE(ABORT,'New controls cannot claim provider execution'); END;
28
+ CREATE TRIGGER control_delivery_identity BEFORE UPDATE ON build_instruction_deliveries WHEN NEW.instruction_id<>OLD.instruction_id OR NEW.recipient<>OLD.recipient BEGIN SELECT RAISE(ABORT,'Instruction recipient identity is immutable'); END;`);
29
+ db.prepare("INSERT INTO recovery_schema_migrations VALUES('005_manager_controls',?)").run(new Date().toISOString());
30
+ }).immediate();
31
+ }
32
+ /** Questions belong to work through their ticket interruption or retained remediation stop. */
33
+ export function decisionBelongsToWork(decision, runId, workId, stops) {
34
+ return !!decision && decision.runId === runId && (decision.interruptionId === `ticket:${workId}` || stops.some(stop => stop.runId === runId && stop.workId === workId && stop.operationId === decision.interruptionId && stop.decisionId === decision.decisionId));
35
+ }
36
+ export function decisionWorkId(db, decision) {
37
+ if (decision.interruptionId.startsWith("ticket:"))
38
+ return decision.interruptionId.slice(7);
39
+ const rows = db.prepare("SELECT run_id,ticket_id,operation_id,record_json FROM qa_remediation_stops WHERE run_id=? AND operation_id=?").all(decision.runId, decision.interruptionId);
40
+ const matches = rows.filter(row => JSON.parse(row.record_json).decisionId === decision.decisionId);
41
+ return matches.length === 1 ? matches[0].ticket_id : undefined;
42
+ }
43
+ /** A proposal never authorizes itself. Only exact commands from the original user turn do. */
44
+ export function parseManagerAction(userTurn, requestId = randomUUID(), expectedRevision = 0) {
45
+ const natural = userTurn.trimStart().match(/^(?:guide|tell) (builder|qa|both) (?:for )?run ([A-Za-z0-9_.:-]+) (?:ticket|work) ([A-Za-z0-9_.:-]+):[ \t]?([\s\S]+)$/i);
46
+ const command = natural ? `/guide-${natural[1].toLowerCase()} ${natural[2]} ${natural[3]} ${natural[4]}` : userTurn.trimStart();
47
+ const match = command.match(/^\/(guide-builder|guide-qa|guide-both|pause|request-attempt|withdraw|supersede|answer-question)\s+(\S+)\s+(\S+)(?:[ \t]([\s\S]*))?$/);
48
+ if (!match)
49
+ return undefined;
50
+ const action = match[1].replaceAll("-", "_");
51
+ const tail = match[4] ?? "";
52
+ const request = { version: 1, requestId, runId: match[2], workId: match[3], action, text: tail, expectedRevision };
53
+ if (action === "withdraw" || action === "supersede") {
54
+ const target = tail.match(/^(\S+)(?:[ \t]([\s\S]*))?$/);
55
+ request.instructionId = target?.[1];
56
+ request.text = target?.[2] ?? "";
57
+ }
58
+ if (action === "answer_question") {
59
+ const answer = tail.match(/^(\S+)\s+(\S+)(?:[ \t]([\s\S]*))?$/);
60
+ request.decisionId = answer?.[1];
61
+ request.decisionRevision = answer?.[2];
62
+ request.text = answer?.[3] ?? "";
63
+ }
64
+ if (action === "pause") {
65
+ request.pauseScope = tail === "run" ? "run" : "work";
66
+ request.text = "Pause at the next safe boundary";
67
+ }
68
+ const validation = validateManagerActionRequestV1(request);
69
+ if (!validation.valid)
70
+ throw new Error(`Invalid Manager action: ${validation.errors.join("; ")}`);
71
+ return request;
72
+ }
73
+ /** Narrow write capability coexists with a build lease and cannot dispatch or publish workflow state. */
74
+ export class BuildInterventionControl {
75
+ runId;
76
+ workId;
77
+ db;
78
+ constructor(projectDir, runId, workId) {
79
+ this.runId = runId;
80
+ this.workId = workId;
81
+ this.db = new Database(join(projectDir, ".rafi/recovery.sqlite3"), { fileMustExist: true });
82
+ try {
83
+ registerHandbackWriter(this.db);
84
+ if (this.db.pragma("user_version", { simple: true }) !== 4)
85
+ throw new Error("Manager controls require the work-admission upgrade");
86
+ const project = this.db.prepare("SELECT canonical_root FROM build_project_identity").get();
87
+ if (project.canonical_root !== realpath(projectDir))
88
+ throw new Error("Control project identity mismatch");
89
+ assertAdmittedWork(this.db, runId, workId);
90
+ // Every existing table is denied except the scoped enqueue stream. No leases, tracker, QA or budget writes.
91
+ const quote = (value) => this.db.prepare("SELECT quote(?) AS q").get(value).q;
92
+ for (const { name } of this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all()) {
93
+ for (const action of ["INSERT", "UPDATE", "DELETE"]) {
94
+ let condition = "";
95
+ if (["build_instructions", "build_instruction_streams"].includes(name) && action !== "DELETE")
96
+ condition = `WHEN NEW.run_id<>${quote(runId)} OR NEW.work_id<>${quote(workId)}`;
97
+ else if (["build_instruction_deliveries", "build_instruction_events"].includes(name) && action === "INSERT")
98
+ condition = `WHEN NOT EXISTS(SELECT 1 FROM build_instructions i WHERE i.instruction_id=NEW.instruction_id AND i.run_id=${quote(runId)} AND i.work_id=${quote(workId)})`;
99
+ if (name === "build_instruction_deliveries" && action === "UPDATE")
100
+ condition = `WHEN OLD.state<>'queued' OR NEW.state NOT IN ('withdrawn','superseded') OR NEW.instruction_id<>OLD.instruction_id OR NEW.recipient<>OLD.recipient OR NEW.operation_id IS NOT NULL OR NOT EXISTS(SELECT 1 FROM build_instructions i WHERE i.instruction_id=NEW.instruction_id AND i.run_id=${quote(runId)} AND i.work_id=${quote(workId)})`;
101
+ this.db.exec(`CREATE TEMP TRIGGER enqueue_only_${name}_${action} BEFORE ${action} ON main."${name}" ${condition} BEGIN SELECT RAISE(ABORT,'Manager enqueue capability cannot mutate this record'); END`);
102
+ }
103
+ }
104
+ }
105
+ catch (error) {
106
+ this.db.close();
107
+ throw error;
108
+ }
109
+ }
110
+ close() { this.db.close(); }
111
+ revision() { return this.db.prepare("SELECT revision FROM build_instruction_streams WHERE run_id=? AND work_id=?").get(this.runId, this.workId)?.revision ?? 0; }
112
+ enqueue(request, userTurn) {
113
+ const parsed = parseManagerAction(userTurn, request.requestId, request.expectedRevision);
114
+ if (!parsed || JSON.stringify(parsed) !== JSON.stringify(request) || request.runId !== this.runId || request.workId !== this.workId)
115
+ throw new Error("Action requires the exact original user command and resolved scope");
116
+ return this.db.transaction(() => {
117
+ const previous = this.db.prepare("SELECT instruction_id,record_json FROM build_instructions WHERE request_id=?").get(request.requestId);
118
+ if (previous) {
119
+ const record = JSON.parse(previous.record_json);
120
+ if (JSON.stringify(record.request) !== JSON.stringify(request) || record.userTurn !== userTurn)
121
+ throw new Error("Idempotency key was reused with different authorization");
122
+ return instruction(this.db, previous.instruction_id);
123
+ }
124
+ if (this.revision() !== request.expectedRevision)
125
+ throw new Error("Instruction stream changed; refresh and revise the request");
126
+ const admission = assertAdmittedWork(this.db, this.runId, this.workId);
127
+ const run = this.db.prepare("SELECT status FROM workflow_runs WHERE run_id=?").get(this.runId);
128
+ if (["completed", "cancelled", "superseded"].includes(run.status))
129
+ throw new Error("Cannot enqueue instructions for terminal work");
130
+ const latest = this.db.prepare("SELECT attempt_id,source_digest,status FROM qa_review_attempts WHERE run_id=? AND ticket_id=? ORDER BY review_number DESC LIMIT 1").get(this.runId, this.workId);
131
+ if (request.action === "answer_question") {
132
+ const row = this.db.prepare("SELECT decision_json,status FROM human_decisions WHERE decision_id=? AND run_id=?").get(request.decisionId, this.runId);
133
+ const decision = row ? JSON.parse(row.decision_json) : undefined;
134
+ if (!row || row.status !== "pending" || decisionWorkId(this.db, decision) !== this.workId || hash(row.decision_json) !== request.decisionRevision)
135
+ throw new Error("Question is stale or belongs to another scope; refresh the pending decision");
136
+ }
137
+ if (request.action === "request_attempt" && latest?.status !== "failed" && !(latest?.status === "passed" && hasQueuedBuilderGuidance(this.db, this.runId, this.workId)))
138
+ throw new Error("An extra attempt requires a current failed review or waiting Builder guidance after a pass");
139
+ const state = this.db.prepare("SELECT state_json FROM workflow_runs WHERE run_id=?").get(this.runId);
140
+ if (JSON.parse(state.state_json).qaEnabled === false && ["guide_qa", "guide_both", "request_attempt"].includes(request.action))
141
+ throw new Error("QA is disabled for this run; QA guidance cannot be delivered");
142
+ let priorRecipients;
143
+ if (request.action === "withdraw" || request.action === "supersede") {
144
+ const prior = instruction(this.db, request.instructionId);
145
+ if (!prior || prior.request.runId !== this.runId || prior.request.workId !== this.workId)
146
+ throw new Error("Instruction belongs to another scope");
147
+ if (prior.deliveries.some(delivery => delivery.state !== "queued"))
148
+ throw new Error("Reserved or delivered guidance cannot be withdrawn; enqueue compensating guidance");
149
+ if (request.action === "supersede" && !["guide_builder", "guide_qa", "guide_both", "supersede"].includes(prior.request.action))
150
+ throw new Error("Only guidance may be superseded; withdraw this control explicitly");
151
+ priorRecipients = prior.deliveries.map(delivery => delivery.recipient);
152
+ for (const delivery of prior.deliveries)
153
+ setDelivery(this.db, prior.instructionId, { ...delivery, state: request.action === "withdraw" ? "withdrawn" : "superseded" });
154
+ }
155
+ const recipients = priorRecipients ?? (request.action === "guide_builder" ? ["builder"] : request.action === "guide_qa" ? ["qa"] : request.action === "guide_both" ? ["builder", "qa"] : ["builder"]);
156
+ const record = { instructionId: randomUUID(), request, sequence: this.db.prepare("SELECT COALESCE(MAX(sequence),0)+1 AS n FROM build_instructions").get().n, userTurn, textDigest: hash(request.text), requirementsDigest: admission.requirementsDigest, createdAt: new Date().toISOString(), basis: { assignmentId: admission.assignmentId, scopeRevision: admission.scopeRevision, ...(latest ? { reviewAttemptId: latest.attempt_id, sourceDigest: latest.source_digest } : {}) }, deliveries: recipients.map(recipient => ({ recipient, state: request.action === "withdraw" ? "withdrawn" : "queued" })) };
157
+ this.db.prepare("INSERT INTO build_instructions VALUES(?,?,?,?,?,?)").run(record.sequence, record.instructionId, request.requestId, this.runId, this.workId, JSON.stringify(record));
158
+ this.db.prepare("INSERT INTO build_instruction_streams VALUES(?,?,?) ON CONFLICT(run_id,work_id) DO UPDATE SET revision=excluded.revision").run(this.runId, this.workId, request.expectedRevision + 1);
159
+ for (const delivery of record.deliveries) {
160
+ this.db.prepare("INSERT INTO build_instruction_deliveries VALUES(?,?,?,NULL,?)").run(record.instructionId, delivery.recipient, delivery.state, JSON.stringify(delivery));
161
+ event(this.db, record.instructionId, delivery.recipient, delivery);
162
+ }
163
+ return record;
164
+ }).immediate();
165
+ }
166
+ }
167
+ const hash = (text) => createHash("sha256").update(text).digest("hex");
168
+ export function instruction(db, id) {
169
+ const row = db.prepare("SELECT record_json FROM build_instructions WHERE instruction_id=?").get(id);
170
+ if (!row)
171
+ return undefined;
172
+ return { ...JSON.parse(row.record_json), deliveries: db.prepare("SELECT record_json FROM build_instruction_deliveries WHERE instruction_id=? ORDER BY recipient").all(id).map(row => JSON.parse(row.record_json)) };
173
+ }
174
+ function event(db, id, recipient, value) { db.prepare("INSERT INTO build_instruction_events(instruction_id,recipient,event_json,created_at) VALUES(?,?,?,?)").run(id, recipient, JSON.stringify(value), new Date().toISOString()); }
175
+ function setDelivery(db, id, delivery) { db.prepare("UPDATE build_instruction_deliveries SET state=?,operation_id=?,record_json=? WHERE instruction_id=? AND recipient=?").run(delivery.state, delivery.operationId ?? null, JSON.stringify(delivery), id, delivery.recipient); event(db, id, delivery.recipient, delivery); }
176
+ /** Restart consumes retained completion proof, never sends a replacement provider turn. */
177
+ export function reconcileGuidance(db, runId, workId) {
178
+ const records = db.prepare("SELECT instruction_id FROM build_instructions WHERE run_id=? AND work_id=? ORDER BY sequence").all(runId, workId).map(row => instruction(db, row.instruction_id));
179
+ for (const record of records)
180
+ for (const delivery of record.deliveries) {
181
+ if (!["reserved", "uncertain"].includes(delivery.state) || !delivery.operationId)
182
+ continue;
183
+ const suffix = `:${record.instructionId}:${delivery.recipient}`;
184
+ if (!delivery.operationId.endsWith(suffix))
185
+ throw new Error("Instruction dispatch linkage is corrupt");
186
+ const operationId = delivery.operationId.slice(0, -suffix.length);
187
+ let receipt;
188
+ let state = "uncertain";
189
+ if (delivery.recipient === "qa") {
190
+ const turn = db.prepare("SELECT receipt_json FROM qa_turns WHERE operation_id=? AND run_id=? AND ticket_id=? AND source_state_digest=?").get(operationId, runId, workId, delivery.sourceDigest);
191
+ if (turn?.receipt_json) {
192
+ const retained = JSON.parse(turn.receipt_json);
193
+ if (retained.dispatch === "completed" && retained.terminalEventObserved && retained.providerTurnId) {
194
+ receipt = retained;
195
+ state = "applied";
196
+ }
197
+ else if (retained.dispatch === "not-dispatched") {
198
+ receipt = retained;
199
+ state = "rejected";
200
+ }
201
+ }
202
+ }
203
+ else {
204
+ const assignment = db.prepare("SELECT status,result_json FROM operation_journal WHERE idempotency_key=? AND run_id=? AND kind='build-assignment' AND json_extract(intent_json,'$.ticketId')=?").get(operationId, runId, workId);
205
+ if (assignment?.result_json) {
206
+ const retained = JSON.parse(assignment.result_json);
207
+ if (assignment.status === "confirmed" && retained.turnId && !retained.rejection && !retained.sourceError) {
208
+ receipt = { turnId: retained.turnId, responseDigest: retained.responseDigest, providerInstructionDigest: retained.providerInstructionDigest, postSourceDigest: retained.after?.digest };
209
+ state = "acknowledged";
210
+ }
211
+ else if (retained.dispatchState === "not-sent") {
212
+ receipt = retained;
213
+ state = "rejected";
214
+ }
215
+ }
216
+ else {
217
+ const turn = db.prepare("SELECT d.record_json FROM qa_delivery_turns d JOIN qa_reports r ON r.report_occurrence_id=d.report_occurrence_id WHERE d.operation_id=? AND d.kind='remediation' AND r.run_id=? AND r.ticket_id=?").get(operationId, runId, workId);
218
+ if (turn) {
219
+ const retained = JSON.parse(turn.record_json);
220
+ if (retained.status === "completed" && retained.providerTurnId && retained.terminalCount > 0) {
221
+ receipt = retained;
222
+ state = "acknowledged";
223
+ }
224
+ else if (retained.failure?.dispatchState === "not-sent") {
225
+ receipt = retained;
226
+ state = "rejected";
227
+ }
228
+ }
229
+ }
230
+ }
231
+ if (receipt && state !== "rejected") {
232
+ const digest = receipt.providerInstructionDigest;
233
+ const bytes = typeof digest === "string" ? db.prepare("SELECT content FROM content_refs WHERE digest=?").get(digest)?.content : undefined;
234
+ if (!bytes || hash(bytes.toString("utf8")) !== digest || !bytes.toString("utf8").includes(`Instruction ${record.instructionId} (${record.textDigest}):\n${record.request.text}`)) {
235
+ state = "uncertain";
236
+ receipt = undefined;
237
+ }
238
+ }
239
+ if (state !== delivery.state || receipt)
240
+ setDelivery(db, record.instructionId, { ...delivery, state, ...(receipt ? { receipt } : {}), detail: state === "uncertain" ? "No correlated durable completion proof; reconcile provider execution before retry" : state === "rejected" ? "Retained receipt proves this guidance was not submitted; a new explicit instruction may be queued" : "Recovered correlated completion receipt; no provider replay" });
241
+ }
242
+ }
243
+ export function queuedControls(db, runId, workId) {
244
+ return db.prepare("SELECT instruction_id FROM build_instructions WHERE run_id=? AND work_id=? ORDER BY sequence").all(runId, workId).map(row => instruction(db, row.instruction_id)).filter(record => ["answer_question", "request_attempt"].includes(record.request.action) && record.deliveries[0]?.state === "queued");
245
+ }
246
+ export function hasQueuedBuilderGuidance(db, runId, workId) {
247
+ return Boolean(db.prepare("SELECT 1 FROM build_instructions i JOIN build_instruction_deliveries d USING(instruction_id) WHERE i.run_id=? AND i.work_id=? AND d.recipient='builder' AND d.state='queued' AND json_extract(i.record_json,'$.request.action') IN ('guide_builder','guide_both','supersede') LIMIT 1").get(runId, workId));
248
+ }
249
+ export function completeControl(db, id, receipt, rejection) {
250
+ const record = instruction(db, id);
251
+ for (const delivery of record.deliveries)
252
+ setDelivery(db, id, { ...delivery, state: rejection ? "rejected" : "applied", receipt, detail: rejection });
253
+ }
254
+ export function assertFinalizationControls(db, runId, workId) {
255
+ const pending = db.prepare("SELECT i.record_json,d.state FROM build_instructions i JOIN build_instruction_deliveries d USING(instruction_id) WHERE i.run_id=? AND d.state IN ('queued','reserved','uncertain')").all(runId).map(row => ({ ...JSON.parse(row.record_json), state: row.state }));
256
+ if (pending.some(record => record.request.workId === workId || record.request.action === "pause" && record.request.pauseScope === "run"))
257
+ throw new BuildControlBoundary("Manager controls arrived before finalization; preserve this pass and resume at the authorized control boundary before further mutation");
258
+ }
259
+ export function builderVerificationContext(db, runId, workId, sourceDigest) {
260
+ const records = db.prepare("SELECT instruction_id FROM build_instructions WHERE run_id=? AND work_id=? ORDER BY sequence").all(runId, workId).map(row => instruction(db, row.instruction_id));
261
+ const eligible = records.filter(record => (record.request.action === "guide_builder" || record.request.action === "supersede" && !record.deliveries.some(delivery => delivery.recipient === "qa")) && record.deliveries.some(delivery => delivery.recipient === "builder" && ["acknowledged", "applied"].includes(delivery.state) && delivery.receipt?.postSourceDigest === sourceDigest));
262
+ return { ids: eligible.map(record => record.instructionId), text: eligible.length ? "\n\nBuilder guidance awaiting independent verification on this exact source:\n" + eligible.map(record => `Instruction ${record.instructionId}: ${record.request.text}`).join("\n") + "\nOnly for a relevant result independently established with supporting evidence, emit MANAGER_INSTRUCTION_VERIFIED: <instruction ID>. A general pass alone is insufficient." : "" };
263
+ }
264
+ export function verifyBuilderGuidance(db, ids, certificate, response, turnReceiptDigest) {
265
+ const verified = new Set([...response.matchAll(/^MANAGER_INSTRUCTION_VERIFIED:\s*([A-Za-z0-9_.:-]+)\s*$/gm)].map(match => match[1]));
266
+ for (const id of ids) {
267
+ if (!verified.has(id))
268
+ continue;
269
+ const record = instruction(db, id);
270
+ if (!db.prepare("SELECT 1 FROM qa_pass_certificates WHERE certificate_id=? AND run_id=? AND ticket_id=? AND source_state_digest=? AND review_basis_digest=? AND turn_receipt_digest=?").get(certificate.certificateId, record.request.runId, record.request.workId, certificate.sourceStateDigest, certificate.reviewBasisDigest, turnReceiptDigest))
271
+ throw new Error("Builder instruction verification certificate is not durably bound to this work and review turn");
272
+ const delivery = record.deliveries.find(delivery => delivery.recipient === "builder");
273
+ if (!delivery || !["acknowledged", "applied"].includes(delivery.state) || delivery.receipt?.postSourceDigest !== certificate.sourceStateDigest)
274
+ throw new Error("Builder instruction verification source mismatch");
275
+ setDelivery(db, id, { ...delivery, state: "verified", receipt: { deliveryReceipt: delivery.receipt, verification: certificate, independentQaTurnReceiptDigest: turnReceiptDigest } });
276
+ }
277
+ }
278
+ /** Called only inside the owning process's existing dispatch transaction. */
279
+ export function reserveGuidance(db, runId, workId, recipient, operationId, sourceDigest, instructionText, reserve = true) {
280
+ return db.transaction(() => {
281
+ const admission = assertAdmittedWork(db, runId, workId);
282
+ reconcileGuidance(db, runId, workId);
283
+ const records = db.prepare("SELECT instruction_id FROM build_instructions WHERE run_id=? ORDER BY sequence").all(runId).map(row => instruction(db, row.instruction_id));
284
+ const relevant = records.filter(record => record.request.workId === workId || record.request.action === "pause" && record.request.pauseScope === "run");
285
+ if (relevant.some(record => record.request.action === "pause" && record.deliveries.some(delivery => delivery.state === "queued" || delivery.state === "reserved")))
286
+ throw new BuildControlBoundary("Manager pause is active at this safe boundary; guidance remains queued");
287
+ if (relevant.some(record => record.deliveries.some(delivery => delivery.recipient === recipient && ["reserved", "uncertain"].includes(delivery.state))))
288
+ throw new BuildControlBoundary("Instruction dispatch is uncertain; reconcile before replay");
289
+ const eligible = relevant.filter(record => ["guide_builder", "guide_qa", "guide_both", "supersede"].includes(record.request.action) && record.deliveries.some(delivery => delivery.recipient === recipient && delivery.state === "queued"));
290
+ const selected = [];
291
+ for (const record of eligible) {
292
+ const delivery = record.deliveries.find(delivery => delivery.recipient === recipient);
293
+ if (record.requirementsDigest !== admission.requirementsDigest || record.basis.assignmentId !== admission.assignmentId || record.basis.scopeRevision !== admission.scopeRevision) {
294
+ setDelivery(db, record.instructionId, { ...delivery, state: "rejected", detail: "Requirements changed; renewed scope approval and guidance are required" });
295
+ continue;
296
+ }
297
+ if (recipient === "qa" && record.deliveries.some(delivery => delivery.recipient === "builder") && !record.deliveries.some(delivery => delivery.recipient === "builder" && ["acknowledged", "applied", "verified"].includes(delivery.state)))
298
+ throw new BuildControlBoundary("Both-agent guidance is waiting for Builder's scoped response before a fresh QA review");
299
+ selected.push(record);
300
+ }
301
+ const text = instructionText + (selected.length ? "\n\nAuthorized one-use Manager guidance (mandatory requirements and findings still apply):\n" + selected.map(record => `Instruction ${record.instructionId} (${record.textDigest}):\n${record.request.text}`).join("\n\n") + (recipient === "qa" ? "\nFor each instruction whose relevant result you independently established on this source, include MANAGER_INSTRUCTION_VERIFIED: <instruction ID> with the supporting verification evidence. A general passing review alone does not verify an unrelated instruction." : "\nReport each instruction ID whose requested action you applied, with evidence; delivery alone is not independent verification.") : "");
302
+ if (reserve)
303
+ for (const record of selected)
304
+ setDelivery(db, record.instructionId, { recipient, state: "reserved", operationId: `${operationId}:${record.instructionId}:${recipient}`, sourceDigest, instructionDigest: hash(text) });
305
+ return { text, ids: selected.map(record => record.instructionId) };
306
+ }).immediate();
307
+ }
308
+ export function finishGuidance(db, ids, recipient, result) {
309
+ db.transaction(() => { for (const id of ids) {
310
+ const record = instruction(db, id);
311
+ const delivery = record.deliveries.find(delivery => delivery.recipient === recipient);
312
+ if (delivery.state !== "reserved")
313
+ throw new Error("Instruction reservation changed");
314
+ setDelivery(db, id, { ...delivery, state: result.submitted === false ? "rejected" : result.submitted === undefined ? "uncertain" : result.applied ? "applied" : result.receipt ? "acknowledged" : "delivered", receipt: result.receipt });
315
+ } }).immediate();
316
+ }
317
+ export function verifyGuidance(db, ids, certificate, response) {
318
+ const verified = new Set([...response.matchAll(/^MANAGER_INSTRUCTION_VERIFIED:\s*([A-Za-z0-9_.:-]+)\s*$/gm)].map(match => match[1]));
319
+ for (const id of ids) {
320
+ if (!verified.has(id))
321
+ continue;
322
+ const record = instruction(db, id);
323
+ if (!db.prepare("SELECT 1 FROM qa_pass_certificates WHERE certificate_id=? AND run_id=? AND ticket_id=? AND source_state_digest=? AND review_basis_digest=?").get(certificate.certificateId, record.request.runId, record.request.workId, certificate.sourceStateDigest, certificate.reviewBasisDigest))
324
+ throw new Error("Instruction verification certificate is not durably bound to this work");
325
+ const qa = record.deliveries.find(delivery => delivery.recipient === "qa");
326
+ const receipt = qa?.receipt;
327
+ if (qa?.state !== "applied" || receipt?.sourceStateDigest !== certificate.sourceStateDigest || receipt.reviewBasisDigest !== certificate.reviewBasisDigest)
328
+ throw new Error("Instruction verification does not match its independent source-bound QA delivery");
329
+ setDelivery(db, id, { ...qa, state: "verified", receipt: { deliveryReceipt: receipt, verification: certificate } });
330
+ const builder = record.deliveries.find(delivery => delivery.recipient === "builder");
331
+ if (builder && ["applied", "acknowledged"].includes(builder.state) && record.deliveries.some(delivery => delivery.recipient === "qa"))
332
+ setDelivery(db, id, { ...builder, state: "verified", receipt: { deliveryReceipt: builder.receipt, verification: certificate } });
333
+ }
334
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ /** Minimal pre-CLI child. No providers or mutable workflow code before acknowledgement. */
2
+ import { WorkflowDb } from "./workflowDb.js";
3
+ import { pathToFileURL } from "node:url";
4
+ const [entry, project, token, ...args] = process.argv.slice(2);
5
+ if (!entry || !project || !token || !process.send || !process.connected)
6
+ throw new Error("Missing resume startup channel");
7
+ const db = new WorkflowDb(project);
8
+ try {
9
+ db.registerBuildLaunchChild(token);
10
+ }
11
+ finally {
12
+ db.close();
13
+ }
14
+ await new Promise((resolve, reject) => {
15
+ const timeout = setTimeout(() => finish(new Error("Resume startup acknowledgement timed out")), 10_000);
16
+ const disconnected = () => finish(new Error("Resume launcher exited before acknowledgement"));
17
+ const message = (value) => { if (value === "rafi-launch-ack")
18
+ finish(); };
19
+ function finish(error) {
20
+ clearTimeout(timeout);
21
+ process.off("disconnect", disconnected);
22
+ process.off("message", message);
23
+ if (error)
24
+ reject(error);
25
+ else
26
+ resolve();
27
+ }
28
+ process.once("disconnect", disconnected);
29
+ process.on("message", message);
30
+ process.send({ kind: "rafi-launch-registered", token });
31
+ });
32
+ // The durable claim still validates the token atomically after acknowledgement.
33
+ if (!process.env.RAFI_BUILD_WORKER_RUN)
34
+ process.disconnect();
35
+ process.argv = [process.execPath, entry, ...args];
36
+ await import(pathToFileURL(entry).href);
@@ -0,0 +1,19 @@
1
+ import { type QaEvidenceSnapshot } from "./qaEvidenceReader.js";
2
+ /** Inspection describes conflicts; it never expands saved scope or changes source. */
3
+ export declare function assessBuildOwnership(projectDir: string, runId: string, snapshot?: QaEvidenceSnapshot): {
4
+ runId: string;
5
+ revision: string;
6
+ savedScope: string[];
7
+ legacyTrackerHints: {
8
+ workIds: string[];
9
+ authority: boolean;
10
+ basis: string;
11
+ };
12
+ authoritativeAdmissions: import("./qaEvidenceReader.js").EvidenceRow[];
13
+ conflicts: Record<string, unknown>[];
14
+ availability: import("rafi-spec").ManagerEvidenceAvailability;
15
+ unavailableEvidence: string[];
16
+ allowedRecoveryChoices: string[];
17
+ };
18
+ export declare function ownershipRevision(snapshot: QaEvidenceSnapshot): string;
19
+ export declare function observedBuildWork(snapshot: QaEvidenceSnapshot): string[];
@@ -0,0 +1,41 @@
1
+ import { evidenceRecord, readQaEvidenceSnapshot } from "./qaEvidenceReader.js";
2
+ import { qaDigest } from "./qaProtocolV2.js";
3
+ import { legacyTrackerWorkHints } from "./buildRuns.js";
4
+ /** Inspection describes conflicts; it never expands saved scope or changes source. */
5
+ export function assessBuildOwnership(projectDir, runId, snapshot = readQaEvidenceSnapshot(projectDir, runId)) {
6
+ const state = snapshot.rows.workflow_runs?.[0] ? evidenceRecord(snapshot.rows.workflow_runs[0], "state_json") : undefined;
7
+ const savedScope = Array.isArray(state?.tickets) ? state.tickets.filter((id) => typeof id === "string") : [];
8
+ const observed = observedBuildWork(snapshot);
9
+ const conflicts = [];
10
+ const trackerHints = state?.version !== undefined ? legacyTrackerWorkHints(projectDir, state) : [];
11
+ if (!savedScope.length && trackerHints.length)
12
+ conflicts.push({ classification: "ownership_unestablished", reason: "Legacy tracker timestamps suggest work, but do not prove run-bound admission", workIds: trackerHints, resolution: "Inspect and supply approval/assignment provenance or authorize inspected mapped work" });
13
+ for (const row of snapshot.rows.build_ownership_conflicts ?? [])
14
+ if (row.status === "unresolved")
15
+ conflicts.push({ conflictId: row.conflict_id, workId: row.work_id, revision: row.revision, ...evidenceRecord(row) });
16
+ for (const id of observed)
17
+ if (!savedScope.includes(id))
18
+ conflicts.push({ workId: id, reason: "retained QA identity is outside saved membership", classification: "ownership_unestablished", resolution: "Supply run-bound approval and assignment provenance, explicitly authorize reconciled scope for fresh verification, or quarantine preserved work" });
19
+ for (const operation of snapshot.rows.operation_journal ?? []) {
20
+ if (operation.kind !== "build-assignment")
21
+ continue;
22
+ const result = evidenceRecord(operation, "result_json");
23
+ if (result?.reconciliationId)
24
+ continue;
25
+ if (!result?.rejection && !result?.sourceError && operation.status !== "in_progress" && operation.status !== "uncertain")
26
+ continue;
27
+ const intent = evidenceRecord(operation, "intent_json");
28
+ conflicts.push({ workId: intent?.ticketId, operationId: operation.idempotency_key, worktree: intent?.worktree, reason: result?.rejection ?? result?.sourceError ?? "provider dispatch outcome is unconfirmed", classification: "assignment_response_conflict", responseDigest: result?.responseDigest, before: intent?.before, after: result?.after,
29
+ resolution: "Preserve and inspect the assignment/source map; approve separation/restoration or explicitly reconciled scope, then run fresh QA. Merely adding the returned ticket is insufficient." });
30
+ }
31
+ return { runId, revision: ownershipRevision(snapshot), savedScope, legacyTrackerHints: { workIds: trackerHints, authority: false, basis: "timestamp correlation only" }, authoritativeAdmissions: snapshot.rows.build_work_admissions ?? [], conflicts, availability: snapshot.availability, unavailableEvidence: snapshot.gaps, allowedRecoveryChoices: conflicts.length ? ["inspect_preserved_evidence", "supply_authorization_provenance", "authorize_reconciled_scope", "quarantine_for_manual_reconciliation"] : [] };
32
+ }
33
+ export function ownershipRevision(snapshot) {
34
+ // Recovery's own lease acquisition cannot invalidate its inspected decision.
35
+ // Scope, source, instructions, QA and publication events remain in the CAS basis.
36
+ const rows = Object.fromEntries(Object.entries(snapshot.rows).filter(([table]) => table !== "project_lease").sort(([a], [b]) => a.localeCompare(b)).map(([table, records]) => [table, records.filter(row => table !== "workflow_events" || row.checkpoint !== "lease").map(row => table === "workflow_runs" ? Object.fromEntries(Object.entries(row).filter(([key]) => key !== "lease_generation")) : row).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))]));
37
+ return qaDigest("build-ownership-revision", { runId: snapshot.runId, rows });
38
+ }
39
+ export function observedBuildWork(snapshot) {
40
+ return [...new Set(Object.entries(snapshot.rows).filter(([table]) => table.startsWith("qa_") || table === "build_work_scope" || table === "build_work_admissions").flatMap(([, rows]) => rows.flatMap(row => typeof row.work_id === "string" ? [row.work_id] : typeof row.ticket_id === "string" ? [row.ticket_id] : [])))].sort();
41
+ }
@@ -0,0 +1,33 @@
1
+ export declare function prepareOwnershipRepair(projectDir: string, runId: string, workId: string): {
2
+ runId: string;
3
+ workId: string;
4
+ expectedRevision: string;
5
+ inspectedSourceDigest: string;
6
+ scopeRevision: string;
7
+ requiredPaths: string[];
8
+ worktree: string;
9
+ sourceInventory: import("./qaSnapshot.js").QaSourcePathState[];
10
+ ownership: {
11
+ runId: string;
12
+ revision: string;
13
+ savedScope: string[];
14
+ legacyTrackerHints: {
15
+ workIds: string[];
16
+ authority: boolean;
17
+ basis: string;
18
+ };
19
+ authoritativeAdmissions: import("./qaEvidenceReader.js").EvidenceRow[];
20
+ conflicts: Record<string, unknown>[];
21
+ availability: import("rafi-spec").ManagerEvidenceAvailability;
22
+ unavailableEvidence: string[];
23
+ allowedRecoveryChoices: string[];
24
+ };
25
+ sourceMutationAuthorized: boolean;
26
+ choices: string[];
27
+ };
28
+ export declare function repairOwnership(projectDir: string, value: unknown, userTurn: string, faults?: {
29
+ afterDecision?(): void;
30
+ afterMembership?(): void;
31
+ afterLocalCommit?(): void;
32
+ afterPublication?(): void;
33
+ }): Record<string, unknown>;