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
@@ -1,16 +1,126 @@
1
+ import { type GraphRecordKind } from "./graph/storage.js";
2
+ import { type ReadinessProcess } from "./readinessCleanup.js";
1
3
  import type { PendingHumanDecision, RecoveryAttemptReceipt, ResolvedAutonomyPolicy, SupervisorState, WorkflowIssue } from "rafi-spec";
2
4
  import { type OperationRecord, type RoleMutationLease, type ProjectLease, type WorkflowRunSnapshot } from "./workflowDb.js";
3
5
  import type { BranchResumeSession } from "./branch/resume.js";
4
6
  /** Inspection-only recovery access. It never creates `.rafi`, migrates, imports, checkpoints WAL, or updates timestamps. */
5
7
  export declare class WorkflowReader {
8
+ private readonly projectDir;
6
9
  readonly path: string;
7
10
  private readonly db?;
8
11
  constructor(projectDir: string, path?: string);
12
+ qaPreparation(runId: string): {
13
+ mode: string;
14
+ works: Array<{
15
+ workId: string;
16
+ state: string;
17
+ digest?: string;
18
+ depth?: number;
19
+ detail?: string;
20
+ }>;
21
+ };
22
+ qaPreparationMetrics(runId: string): {
23
+ withheldEvents?: number | undefined;
24
+ limitation?: string | undefined;
25
+ approved: number;
26
+ started: number;
27
+ comparableStarted: number;
28
+ excludedNonEquivalentReviews: number;
29
+ approvedNeverStarted: number;
30
+ preparationFailures: number;
31
+ firstReviews: number;
32
+ firstReviewPasses: number;
33
+ firstReviewPassRate: number | null;
34
+ endToEndFirstAttempt: number;
35
+ endToEndRate: number | null;
36
+ omissionWorks: number;
37
+ omissionRounds: number;
38
+ omissionIncidence: number | null;
39
+ omissionRoundsPerStarted: number | null;
40
+ causes: Record<import("./qaPreparationMetrics.js").FindingCause, number>;
41
+ statuses: Record<string, number>;
42
+ quality: {
43
+ day7: {
44
+ observed: number;
45
+ reopened: number;
46
+ escaped: number;
47
+ unknown: number;
48
+ };
49
+ day30: {
50
+ observed: number;
51
+ reopened: number;
52
+ escaped: number;
53
+ unknown: number;
54
+ };
55
+ };
56
+ phaseCosts: Record<string, {
57
+ knownUsd: number;
58
+ knownSamples: number;
59
+ unknownSamples: number;
60
+ durationMs: number;
61
+ }>;
62
+ outsideContractEvents: number;
63
+ formatRepairs: number;
64
+ providerRetries: number;
65
+ byMode: {
66
+ [k: string]: {
67
+ works: number;
68
+ started: number;
69
+ };
70
+ };
71
+ samples: {
72
+ eventId: string;
73
+ workKey: string;
74
+ mode: "legacy" | "shadow" | "enforce";
75
+ level: number | undefined;
76
+ cohort: string;
77
+ at: string;
78
+ costUsd: number | null;
79
+ durationMs: number | null;
80
+ evidenceRefs: string[];
81
+ contractDigest?: string | undefined;
82
+ reviewDigest?: string | undefined;
83
+ assessor?: string | undefined;
84
+ checkUsefulness?: {
85
+ checkId: string;
86
+ result: string;
87
+ duplicate?: boolean;
88
+ irrelevant?: boolean;
89
+ observation: string;
90
+ }[] | undefined;
91
+ excessiveInvestigation?: string;
92
+ coverageAdequate?: boolean | undefined;
93
+ }[];
94
+ cohortSizes: {
95
+ [k: string]: number;
96
+ };
97
+ };
98
+ qaPreparationEvents(runId: string): Array<{
99
+ eventId: string;
100
+ workId: string;
101
+ kind: string;
102
+ value: unknown;
103
+ }>;
104
+ graphHostProjectIdentity(): string | undefined;
105
+ graphEvidenceAllowed(digest: string): boolean;
9
106
  close(): void;
107
+ graphRecord<T>(kind: GraphRecordKind, id: string): {
108
+ revision: number;
109
+ value: T;
110
+ } | undefined;
111
+ graphReceiptSummaries(runId: string): unknown[];
112
+ graphRecentJobs(): Array<{
113
+ id: string;
114
+ state: string;
115
+ reason?: string;
116
+ generationId?: string;
117
+ }>;
118
+ graphGenerationForCorpus<T>(corpusDigest: string, policyDigest: string): T | undefined;
119
+ readinessProcesses(): ReadinessProcess[];
10
120
  available(): boolean;
11
121
  getRun(runId: string): WorkflowRunSnapshot | undefined;
12
122
  activeRuns(): WorkflowRunSnapshot[];
13
- buildRuns(): WorkflowRunSnapshot[];
123
+ buildRuns(strict?: boolean): WorkflowRunSnapshot[];
14
124
  events(runId: string): Array<{
15
125
  sequence: number;
16
126
  type: string;
@@ -19,8 +129,12 @@ export declare class WorkflowReader {
19
129
  at: string;
20
130
  }>;
21
131
  issues(runId: string): WorkflowIssue[];
22
- operations(runId: string): OperationRecord[];
132
+ operations(runId: string, strict?: boolean): OperationRecord[];
23
133
  currentLease(): ProjectLease | undefined;
134
+ pendingQaTicketIds(runId: string): string[];
135
+ preparationSuccessor(runId: string): string;
136
+ pendingBuildLaunches(runId: string): import("./buildAdmission.js").BuildLaunch[];
137
+ buildAdmission(): import("./buildAdmission.js").BuildAdmission | undefined;
24
138
  continuityHeads(runId: string): Array<{
25
139
  role: string;
26
140
  state: string;
@@ -1,19 +1,76 @@
1
+ import { bytesDigest } from "./graph/util.js";
2
+ import { measureQaPreparation } from "./qaPreparationMetrics.js";
3
+ import { graphDerivedAllowed } from "./graph/derived.js";
4
+ import { readGraphRecord } from "./graph/storage.js";
5
+ import { checkBuildOwnershipSchema, canonicalProject } from "./buildAdmission.js";
1
6
  import { existsSync } from "node:fs";
2
7
  import { join, resolve } from "node:path";
3
8
  import Database from "better-sqlite3";
4
9
  import { WORKFLOW_DB_FILE } from "./workflowDb.js";
5
10
  /** Inspection-only recovery access. It never creates `.rafi`, migrates, imports, checkpoints WAL, or updates timestamps. */
6
11
  export class WorkflowReader {
12
+ projectDir;
7
13
  path;
8
14
  db;
9
15
  constructor(projectDir, path = join(resolve(projectDir), WORKFLOW_DB_FILE)) {
16
+ this.projectDir = projectDir;
10
17
  this.path = path;
11
18
  if (!existsSync(path))
12
19
  return;
13
20
  this.db = new Database(path, { readonly: true, fileMustExist: true });
14
21
  this.db.pragma("query_only = ON");
22
+ try {
23
+ checkBuildOwnershipSchema(this.db);
24
+ }
25
+ catch (error) {
26
+ this.db.close();
27
+ throw error;
28
+ }
29
+ }
30
+ qaPreparation(runId) {
31
+ if (!this.db || !this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='qa_preparation_policy'").get())
32
+ return { mode: "legacy", works: [] };
33
+ const policy = this.db.prepare("SELECT record_json FROM qa_preparation_policy WHERE run_id=?").get(runId);
34
+ const rows = this.db.prepare("SELECT h.work_id,h.state,h.digest,h.detail,c.record_json FROM qa_contract_heads h LEFT JOIN qa_verification_contracts c ON c.digest=h.digest WHERE h.run_id=?").all(runId);
35
+ return { mode: policy ? JSON.parse(policy.record_json).mode : "legacy", works: rows.map(row => ({ workId: row.work_id, state: row.state, digest: row.digest, detail: row.detail && !this.graphEvidenceAllowed(bytesDigest(row.detail)) ? "Graph-derived preparation detail withheld; source-based recovery required" : row.detail, depth: row.record_json && this.graphEvidenceAllowed(bytesDigest(row.record_json)) ? JSON.parse(row.record_json).depthDecision?.level : undefined })) };
36
+ }
37
+ qaPreparationMetrics(runId) {
38
+ const events = this.qaPreparationEvents(runId), withheld = events.filter(event => event.kind === "evidence-unavailable").length;
39
+ return { ...measureQaPreparation(events.filter(event => event.kind === "metric").map(event => event.value)), ...(withheld ? { withheldEvents: withheld, limitation: "Metrics are incomplete: graph-derived events are unavailable" } : {}) };
15
40
  }
41
+ qaPreparationEvents(runId) {
42
+ if (!this.db || !this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='qa_preparation_events'").get())
43
+ return [];
44
+ return this.db.prepare("SELECT event_id,work_id,kind,record_json FROM qa_preparation_events WHERE run_id=? ORDER BY rowid").all(runId).map(row => ({ eventId: row.event_id, workId: row.work_id, kind: this.graphEvidenceAllowed(bytesDigest(row.record_json)) ? row.kind : "evidence-unavailable", value: this.graphEvidenceAllowed(bytesDigest(row.record_json)) ? JSON.parse(row.record_json) : { unavailable: "graph-access-revoked", originalDigest: bytesDigest(row.record_json) } }));
45
+ }
46
+ graphHostProjectIdentity() { if (!this.db || !this.db.prepare("SELECT 1 FROM sqlite_master WHERE name='build_project_identity'").get())
47
+ return; return this.db.prepare("SELECT project_id FROM build_project_identity WHERE singleton=1").get()?.project_id; }
48
+ graphEvidenceAllowed(digest) { return this.db ? graphDerivedAllowed(this.db, this.projectDir, digest) : true; }
16
49
  close() { this.db?.close(); }
50
+ graphRecord(kind, id) { return readGraphRecord(this.db, kind, id); }
51
+ graphReceiptSummaries(runId) {
52
+ if (!this.db || !this.db.prepare("SELECT 1 FROM sqlite_master WHERE name='graph_records'").get())
53
+ return [];
54
+ return this.db.prepare("SELECT id AS receiptId,json_extract(value,'$.purpose') AS purpose,json_extract(value,'$.decision') AS decision,json_extract(value,'$.deliveredAt') AS deliveredAt,json_extract(value,'$.generationIds') AS generationIds FROM graph_records WHERE kind='receipt' AND json_extract(value,'$.runId')=? ORDER BY rowid DESC LIMIT 20").all(runId);
55
+ }
56
+ graphRecentJobs() {
57
+ if (!this.db || !this.db.prepare("SELECT 1 FROM sqlite_master WHERE name='graph_records'").get())
58
+ return [];
59
+ return this.db.prepare("SELECT id,json_extract(value,'$.state') AS state,json_extract(value,'$.reason') AS reason,json_extract(value,'$.generationId') AS generationId FROM graph_records WHERE kind='job' ORDER BY rowid DESC LIMIT 15").all();
60
+ }
61
+ graphGenerationForCorpus(corpusDigest, policyDigest) {
62
+ if (!this.db || !this.db.prepare("SELECT 1 FROM sqlite_master WHERE name='graph_records'").get())
63
+ return;
64
+ const row = this.db.prepare("SELECT value FROM graph_records WHERE kind='generation' AND json_extract(value,'$.corpusDigest')=? AND json_extract(value,'$.policyDigest')=? ORDER BY json_extract(value,'$.createdAt') DESC LIMIT 1").get(corpusDigest, policyDigest);
65
+ return row ? JSON.parse(row.value) : undefined;
66
+ }
67
+ readinessProcesses() {
68
+ if (!this.db)
69
+ return [];
70
+ if (!this.db.prepare("SELECT 1 FROM sqlite_master WHERE name='build_owned_processes'").get())
71
+ return [];
72
+ return this.db.prepare("SELECT * FROM build_owned_processes WHERE state<>'quiescent'").all();
73
+ }
17
74
  available() { return Boolean(this.db); }
18
75
  getRun(runId) {
19
76
  if (!this.db)
@@ -36,13 +93,15 @@ export class WorkflowReader {
36
93
  return [];
37
94
  }
38
95
  }
39
- buildRuns() {
96
+ buildRuns(strict = false) {
40
97
  if (!this.db)
41
98
  return [];
42
99
  try {
43
100
  return this.db.prepare("SELECT * FROM workflow_runs WHERE kind='build' ORDER BY updated_at DESC").all().map(toRun);
44
101
  }
45
- catch {
102
+ catch (error) {
103
+ if (strict)
104
+ throw error;
46
105
  return [];
47
106
  }
48
107
  }
@@ -66,13 +125,15 @@ export class WorkflowReader {
66
125
  return [];
67
126
  }
68
127
  }
69
- operations(runId) {
128
+ operations(runId, strict = false) {
70
129
  if (!this.db)
71
130
  return [];
72
131
  try {
73
132
  return this.db.prepare("SELECT * FROM operation_journal WHERE run_id=? ORDER BY created_at,idempotency_key").all(runId).map(row => ({ idempotencyKey: String(row.idempotency_key), runId: String(row.run_id), kind: String(row.kind), status: String(row.status), intent: JSON.parse(String(row.intent_json)), ...(row.result_json ? { result: JSON.parse(String(row.result_json)) } : {}), ...(row.external_id ? { externalId: String(row.external_id) } : {}), ...(row.error ? { error: String(row.error) } : {}), createdAt: String(row.created_at), updatedAt: String(row.updated_at) }));
74
133
  }
75
- catch {
134
+ catch (error) {
135
+ if (strict)
136
+ throw error;
76
137
  return [];
77
138
  }
78
139
  }
@@ -87,6 +148,72 @@ export class WorkflowReader {
87
148
  return undefined;
88
149
  }
89
150
  }
151
+ pendingQaTicketIds(runId) {
152
+ if (!this.db)
153
+ return [];
154
+ const ids = [];
155
+ for (const query of ["SELECT ticket_id FROM qa_recovery_heads WHERE run_id=? AND pending_action<>'resolved' ORDER BY updated_at,ticket_id", "SELECT ticket_id FROM qa_ticket_heads WHERE run_id=? AND state NOT IN ('completed','waived') ORDER BY updated_at,ticket_id"]) {
156
+ try {
157
+ for (const row of this.db.prepare(query).all(runId))
158
+ if (!ids.includes(row.ticket_id))
159
+ ids.push(row.ticket_id);
160
+ }
161
+ catch (error) {
162
+ if (!String(error).includes("no such table"))
163
+ throw error;
164
+ }
165
+ }
166
+ return ids;
167
+ }
168
+ preparationSuccessor(runId) {
169
+ if (!this.db)
170
+ return runId;
171
+ const seen = new Set();
172
+ while (true) {
173
+ if (seen.has(runId))
174
+ throw new Error("Preparation retry lineage contains a cycle");
175
+ seen.add(runId);
176
+ let row;
177
+ try {
178
+ row = this.db.prepare("SELECT successor,project FROM build_retry_lineage WHERE predecessor=?").get(runId);
179
+ }
180
+ catch (error) {
181
+ if (String(error).includes("no such table"))
182
+ return runId;
183
+ throw error;
184
+ }
185
+ if (!row)
186
+ return runId;
187
+ if (canonicalProject(row.project) !== canonicalProject(this.projectDir))
188
+ throw new Error("Preparation lineage belongs to another project");
189
+ runId = row.successor;
190
+ }
191
+ }
192
+ pendingBuildLaunches(runId) {
193
+ if (!this.db)
194
+ return [];
195
+ try {
196
+ return this.db.prepare("SELECT record_json FROM build_launches WHERE run_id=? AND state IN ('reserved','dispatching')").all(runId).map(row => JSON.parse(row.record_json));
197
+ }
198
+ catch (error) {
199
+ if (!String(error).includes("no such table"))
200
+ throw error;
201
+ return [];
202
+ }
203
+ }
204
+ buildAdmission() {
205
+ if (!this.db)
206
+ return undefined;
207
+ try {
208
+ const row = this.db.prepare("SELECT record_json FROM build_admission WHERE singleton=1").get();
209
+ return row ? JSON.parse(row.record_json) : undefined;
210
+ }
211
+ catch (error) {
212
+ if (!String(error).includes("no such table"))
213
+ throw error;
214
+ return undefined;
215
+ }
216
+ }
90
217
  continuityHeads(runId) {
91
218
  if (!this.db)
92
219
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-foreman",
3
- "version": "1.7.19",
3
+ "version": "1.7.21",
4
4
  "description": "An orchestrator that keeps Codex CLI / Claude Code builders moving through their step list unattended. Part of Rafi.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,10 +17,38 @@
17
17
  "ai-foreman": "dist/index.js"
18
18
  },
19
19
  "exports": {
20
+ "./build-assignment.js": {
21
+ "types": "./dist/buildAssignment.d.ts",
22
+ "default": "./dist/buildAssignment.js"
23
+ },
24
+ "./build-ownership-reconciliation.js": {
25
+ "types": "./dist/buildOwnershipReconciliation.d.ts",
26
+ "default": "./dist/buildOwnershipReconciliation.js"
27
+ },
28
+ "./manager-evidence.js": {
29
+ "types": "./dist/managerEvidence.d.ts",
30
+ "default": "./dist/managerEvidence.js"
31
+ },
32
+ "./qa-evidence-reader.js": {
33
+ "types": "./dist/qaEvidenceReader.d.ts",
34
+ "default": "./dist/qaEvidenceReader.js"
35
+ },
36
+ "./manager-evidence-artifacts.js": {
37
+ "types": "./dist/managerEvidenceArtifacts.d.ts",
38
+ "default": "./dist/managerEvidenceArtifacts.js"
39
+ },
40
+ "./qa-timeline.js": {
41
+ "types": "./dist/qaTimeline.d.ts",
42
+ "default": "./dist/qaTimeline.js"
43
+ },
20
44
  "./activity.js": {
21
45
  "types": "./dist/activity.d.ts",
22
46
  "default": "./dist/activity.js"
23
47
  },
48
+ "./terminal-style.js": {
49
+ "types": "./dist/terminalStyle.d.ts",
50
+ "default": "./dist/terminalStyle.js"
51
+ },
24
52
  "./agent-run.js": {
25
53
  "types": "./dist/agentRun.d.ts",
26
54
  "default": "./dist/agentRun.js"
@@ -45,6 +73,10 @@
45
73
  "types": "./dist/roles.d.ts",
46
74
  "default": "./dist/roles.js"
47
75
  },
76
+ "./build-launch-gate.js": {
77
+ "types": "./dist/buildLaunchGate.d.ts",
78
+ "default": "./dist/buildLaunchGate.js"
79
+ },
48
80
  "./build-runs.js": {
49
81
  "types": "./dist/buildRuns.d.ts",
50
82
  "default": "./dist/buildRuns.js"
@@ -184,21 +216,69 @@
184
216
  "./state-transfer.js": {
185
217
  "types": "./dist/stateTransfer.d.ts",
186
218
  "default": "./dist/stateTransfer.js"
187
- }
219
+ },
220
+ "./human-decision.js": {
221
+ "types": "./dist/humanDecision.d.ts",
222
+ "default": "./dist/humanDecision.js"
223
+ },
224
+ "./build-approval.js": {
225
+ "types": "./dist/buildApproval.d.ts",
226
+ "default": "./dist/buildApproval.js"
227
+ },
228
+ "./build-work-admission.js": {
229
+ "types": "./dist/buildWorkAdmission.d.ts",
230
+ "default": "./dist/buildWorkAdmission.js"
231
+ },
232
+ "./build-work-migration.js": {
233
+ "types": "./dist/buildWorkMigration.d.ts",
234
+ "default": "./dist/buildWorkMigration.js"
235
+ },
236
+ "./build-interventions.js": {
237
+ "types": "./dist/buildInterventions.d.ts",
238
+ "default": "./dist/buildInterventions.js"
239
+ },
240
+ "./build-ownership-repair.js": {
241
+ "types": "./dist/buildOwnershipRepair.d.ts",
242
+ "default": "./dist/buildOwnershipRepair.js"
243
+ },
244
+ "./graph.js": {
245
+ "types": "./dist/graph/turn.d.ts",
246
+ "default": "./dist/graph/turn.js"
247
+ },
248
+ "./graph-maintenance.js": {
249
+ "types": "./dist/graph/maintenance.d.ts",
250
+ "default": "./dist/graph/maintenance.js"
251
+ },
252
+ "./cli/graph.js": {
253
+ "types": "./dist/cli/graph.d.ts",
254
+ "default": "./dist/cli/graph.js"
255
+ },
256
+ "./qa-preparation-policy.js": {
257
+ "types": "./dist/qaPreparationPolicy.d.ts",
258
+ "default": "./dist/qaPreparationPolicy.js"
259
+ },
260
+ "./qa-effective-config.js": {
261
+ "types": "./dist/qaEffectiveConfig.d.ts",
262
+ "default": "./dist/qaEffectiveConfig.js"
263
+ },
264
+ "./install-ownership.js": "./dist/installOwnership.js"
188
265
  },
189
266
  "files": [
190
267
  "dist",
191
268
  "README.md",
192
269
  "foreman.yaml",
193
- "LICENSE"
270
+ "LICENSE",
271
+ "scripts/copy-graph-assets.mjs",
272
+ "scripts/graph-evaluation.mjs",
273
+ "scripts/graph-benchmark.mjs"
194
274
  ],
195
275
  "scripts": {
196
- "build": "tsc -p tsconfig.build.json",
276
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-graph-assets.mjs",
197
277
  "dev": "tsx src/index.ts",
198
- "test": "tsx --test --test-concurrency=1 test/*.test.ts",
278
+ "test": "tsx --test --test-concurrency=1 test/*.test.ts test/*.test.mjs",
199
279
  "typecheck": "tsc --noEmit",
200
280
  "test:live-providers": "RAFI_LIVE_PROVIDER_SESSIONS=1 tsx --test test/providerSmoke.test.ts",
201
- "prepublishOnly": "tsc -p ../spec/tsconfig.build.json && tsc -p tsconfig.build.json && pnpm --filter ai-foreman test && tsc --noEmit"
281
+ "prepublishOnly": "tsc -p ../spec/tsconfig.build.json && pnpm build && pnpm --filter ai-foreman test && tsc --noEmit"
202
282
  },
203
283
  "keywords": [
204
284
  "claude",
@@ -218,16 +298,17 @@
218
298
  "@anthropic-ai/claude-agent-sdk": "^0.3.148"
219
299
  },
220
300
  "dependencies": {
221
- "special-agents": "0.8.11",
222
- "rafi-spec": "0.8.12",
223
301
  "@clack/prompts": "^1.4.0",
224
302
  "ajv": "^8.20.0",
225
303
  "ajv-formats": "^3.0.1",
226
304
  "better-sqlite3": "^12.10.0",
227
305
  "commander": "^12.1.0",
306
+ "ignore": "7.0.5",
228
307
  "luxon": "^3.7.2",
229
308
  "parse5": "7.2.1",
230
309
  "pdfjs-dist": "4.10.38",
310
+ "rafi-spec": "0.8.14",
311
+ "special-agents": "0.8.13",
231
312
  "yaml": "^2.5.1"
232
313
  },
233
314
  "devDependencies": {
@@ -0,0 +1,3 @@
1
+ import { copyFileSync, mkdirSync } from "node:fs";
2
+ mkdirSync(new URL("../dist/graph/", import.meta.url), { recursive: true });
3
+ copyFileSync(new URL("../src/graph/bridge.py", import.meta.url), new URL("../dist/graph/bridge.py", import.meta.url));
@@ -0,0 +1,20 @@
1
+ /** Offline deterministic bridge measurement. No agents, network or corpus scan. */
2
+ import { cpus, platform, release } from "node:os";
3
+ import { performance } from "node:perf_hooks";
4
+ import { resolveGraphPython, runBridge } from "../dist/graph/bridge.js";
5
+ const python=await resolveGraphPython();
6
+ const resourceControls=(await runBridge(python,{action:"probe"})).resourceControls;
7
+ const samples=[];
8
+ for(const count of [10000,100000]){
9
+ const graph={directed:true,multigraph:false,graph:{schema_version:1},nodes:Array.from({length:count},(_,i)=>({id:`n${i}`,label:`component${i}`,source_file:"fixture.py"})),links:Array.from({length:count-1},(_,i)=>({source:`n${i}`,target:`n${i+1}`,relation:"calls",confidence:"EXTRACTED",source_file:"fixture.py"}))};
10
+ const durations=[];let returned=0;
11
+ for(let sample=0;sample<5;sample++){
12
+ const start=performance.now();
13
+ const result=await runBridge(python,{action:"read",graph,operation:{operation:"neighbors",seeds:["n5000"],direction:"outgoing",depth:2}},{timeoutMs:15000});
14
+ durations.push(performance.now()-start);returned=result.nodes.length;
15
+ if(!result.nodes.some(n=>n.id==="n5002")||result.nodes.some(n=>n.id==="n4999"))throw new Error("Directed neighborhood correctness failed");
16
+ }
17
+ durations.sort((a,b)=>a-b);
18
+ samples.push({nodes:count,edges:count-1,samples:durations.length,medianMs:durations[2],largestObservedMs:durations[4],returnedNodes:returned,mode:"cold one-shot subprocess",warmLatency:"not measured",semanticCost:"not applicable"});
19
+ }
20
+ console.log(JSON.stringify({schema:1,measuredAt:new Date().toISOString(),resourceControls,environment:{platform:platform(),release:release(),cpu:cpus()[0]?.model,node:process.version},samples,limits:"Five samples establish a local fixture observation, not a production percentile or agent-quality evaluation."},null,2));
@@ -0,0 +1,85 @@
1
+ /** Offline evaluation manifest and matched-result validator. Never invokes a provider. */
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { resolve, join } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
7
+ const categories = ['missed-caller', 'undeclared-dependency', 'cross-layer-regression', 'existing-invariant', 'captured-requirement'];
8
+ export function evaluationManifest() {
9
+ const cases = categories.flatMap((category, index) => Array.from({ length: 4 }, (_, variant) => {
10
+ const name = `${category}-${variant + 1}`;
11
+ const files = {
12
+ 'requirements.md': `# Approved requirement\nReject negative values; preserve zero; return the normalized integer to every caller.\n`,
13
+ 'core.py': 'def normalize(value):\n if value < 0:\n raise ValueError("negative")\n return int(value)\n',
14
+ 'api.py': 'from core import normalize\ndef response(value):\n return {"value": normalize(value)}\n',
15
+ 'worker.py': 'from core import normalize\ndef process(value):\n return normalize(value)\n',
16
+ 'test_contract.py': 'from api import response\nfrom worker import process\ndef test_zero():\n assert response(0) == {"value": 0}\n assert process(0) == 0\n',
17
+ 'plugin.py': 'def dynamic(module, value):\n return getattr(module, "normalize")(value)\n',
18
+ };
19
+ const tasks = {
20
+ 'missed-caller': 'Assess changing normalize to accept a required precision argument. Identify every affected caller, including uncertain dynamic dispatch.',
21
+ 'undeclared-dependency': 'Plan a worker normalization change. Identify dependencies that must be reviewed before implementation.',
22
+ 'cross-layer-regression': 'Change integer conversion behavior while keeping API and worker contracts consistent. Identify regression tests.',
23
+ 'existing-invariant': 'Optimize normalization while preserving rejection of negative values and acceptance of zero.',
24
+ 'captured-requirement': 'Trace the approved negative-value and zero requirements to implementation and tests; identify missing test coverage.',
25
+ };
26
+ // Variants alter a source fact, not only the case identifier.
27
+ files['variant.py'] = `from ${variant % 2 ? 'api import response' : 'worker import process'}\ndef scheduled_${variant}(value):\n return ${variant % 2 ? 'response' : 'process'}(value)\n`;
28
+ return { id: name, category, files, baselineDigest: hash(files), task: tasks[category], relevantPaths: Object.keys(files),
29
+ sourceFacts: ['core.py rejects negative values', 'test_contract.py tests zero but omits negative-value coverage', 'plugin.py dispatch is dynamic and must be inspected in source'],
30
+ absentOrAmbiguousEdges: ['dynamic getattr dispatch is not assumed resolved', 'requirements-to-test semantic edges are navigation hints, not proof'],
31
+ mandatoryQa: ['inspect all affected callers', 'verify negative and zero behavior', 'run source tests', 'inspect dynamic dispatch'],
32
+ order: (index + variant) % 2 ? ['off', 'on'] : ['on', 'off'] };
33
+ }));
34
+ return { version: 1, kind: 'rafi_graph_evaluation', cases, negativeControl: { task: 'Correct a comment typo in core.py', expectedMaintenanceRuns: 0 },
35
+ controls: ['separate sessions and workspaces for each arm', 'identical approved scope, QA, depth, model, provider and baseline per pair', 'record cold and warm costs separately', 'retain blocked, cancelled, degraded and retried results', 'source inspection fallback is not graph retrieval'],
36
+ qualityTarget: { repeatedExplorationOrOmissionReduction: 0.2, coverageMustNotDecline: true },
37
+ limitation: 'Synthetic engineering fixtures validate machinery. Twenty matched fixture pairs do not establish representative production usefulness.' };
38
+ }
39
+ const metrics = ['usefulFindings', 'missedPaths', 'falseSuggestions', 'omissionRemediations', 'escapedDefects', 'repeatedExploration', 'preparationMs', 'buildMs', 'reviewMs', 'graphMs', 'extractionCostUsd', 'providerCostUsd', 'inputTokens', 'outputTokens'];
40
+ export function reportResults(manifest, rows) {
41
+ if (!Array.isArray(rows)) throw new Error('Results must be an array');
42
+ const keys = new Set(), sessions = new Set();
43
+ for (const row of rows) {
44
+ const fixture = manifest.cases.find(c => c.id === row.caseId);
45
+ if (!fixture || !['on', 'off'].includes(row.arm) || !['cold', 'warm'].includes(row.cache)) throw new Error('Unknown case, arm or cache classification');
46
+ const key = `${row.caseId}:${row.arm}:${row.cache}`;
47
+ if (keys.has(key)) throw new Error(`Duplicate outcome ${key}`);
48
+ keys.add(key);
49
+ if (!row.sessionId || sessions.has(row.sessionId)) throw new Error('Each outcome needs an isolated session');
50
+ sessions.add(row.sessionId);
51
+ if (row.baselineDigest !== fixture.baselineDigest || row.qaDigest !== hash(fixture.mandatoryQa)) throw new Error('Baseline or mandatory QA differs from manifest');
52
+ if (!['completed', 'blocked', 'cancelled', 'degraded', 'failed'].includes(row.status)) throw new Error('Missing outcome classification');
53
+ for (const field of ['provider', 'model', 'depth', 'executionMode', 'semanticMode', 'difficulty']) if (typeof row[field] !== 'string' || !row[field]) throw new Error(`Missing ${field}`);
54
+ if (!Number.isInteger(row.retries) || row.retries < 0) throw new Error('Retain retry accounting');
55
+ if (typeof row.sourceVerified !== 'boolean' || typeof row.coverageVerified !== 'boolean') throw new Error('Explicit source and QA coverage review required');
56
+ for (const metric of metrics) if (row[metric] !== null && !(Number.isFinite(row[metric]) && row[metric] >= 0)) throw new Error(`${metric} must be a nonnegative number or null (unmeasured)`);
57
+ }
58
+ const pairs = [], unmatched = [];
59
+ for (const fixture of manifest.cases) for (const cache of ['cold', 'warm']) {
60
+ const arms = rows.filter(r => r.caseId === fixture.id && r.cache === cache);
61
+ if (arms.length !== 2) { unmatched.push({ caseId: fixture.id, cache, recordedArms: arms.map(r => r.arm) }); continue; }
62
+ const on = arms.find(r => r.arm === 'on'), off = arms.find(r => r.arm === 'off');
63
+ for (const field of ['provider', 'model', 'depth', 'executionMode', 'semanticMode', 'difficulty']) if (on[field] !== off[field]) throw new Error(`Unmatched ${field} in ${fixture.id}`);
64
+ pairs.push({ caseId: fixture.id, cache, statuses: { on: on.status, off: off.status }, coverageVerified: on.coverageVerified && off.coverageVerified,
65
+ differencesOnMinusOff: Object.fromEntries(metrics.map(m => [m, on[m] === null || off[m] === null ? null : on[m] - off[m]])) });
66
+ }
67
+ return { version: 1, recordedOutcomes: rows.length, matchedPairs: pairs.length, pairs, unmatched, outcomes: rows,
68
+ qualityConclusion: 'No automatic benefit claim. Review source-backed findings and equal QA coverage, include all statuses and unknown costs, and disclose sample uncertainty.',
69
+ liveEvaluation: rows.length ? 'User-supplied observations; harness does not execute or certify providers' : 'Deferred by user; no billed runs performed' };
70
+ }
71
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
72
+ const [command, destination] = process.argv.slice(2);
73
+ const manifest = evaluationManifest();
74
+ if (command === 'prepare' && destination) {
75
+ mkdirSync(destination, { recursive: true });
76
+ for (const fixture of manifest.cases) {
77
+ const root = join(destination, fixture.id); mkdirSync(root, { recursive: true });
78
+ for (const [path, content] of Object.entries(fixture.files)) writeFileSync(join(root, path), content, { flag: 'wx' });
79
+ }
80
+ writeFileSync(join(destination, 'manifest.json'), JSON.stringify(manifest, null, 2), { flag: 'wx' });
81
+ console.log(`Prepared ${manifest.cases.length} offline cases in ${destination}. No providers invoked.`);
82
+ } else if (command === 'report') {
83
+ console.log(JSON.stringify(reportResults(manifest, destination ? JSON.parse(readFileSync(destination, 'utf8')) : []), null, 2));
84
+ } else throw new Error('Usage: node scripts/graph-evaluation.mjs prepare <new-directory> | report [results.json]');
85
+ }