ai-foreman 1.7.20 → 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 (189) hide show
  1. package/README.md +12 -2
  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 +90 -4
  20. package/dist/buildAdmission.js +2 -0
  21. package/dist/buildAssignment.d.ts +33 -0
  22. package/dist/buildAssignment.js +215 -0
  23. package/dist/buildInterventions.d.ts +90 -0
  24. package/dist/buildInterventions.js +334 -0
  25. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  26. package/dist/buildOwnershipReconciliation.js +41 -0
  27. package/dist/buildOwnershipRepair.d.ts +33 -0
  28. package/dist/buildOwnershipRepair.js +111 -0
  29. package/dist/buildRuns.d.ts +6 -0
  30. package/dist/buildRuns.js +23 -17
  31. package/dist/buildSyntheticWork.d.ts +2 -0
  32. package/dist/buildSyntheticWork.js +30 -0
  33. package/dist/buildWorkAdmission.d.ts +17 -0
  34. package/dist/buildWorkAdmission.js +57 -0
  35. package/dist/buildWorkAuthorization.d.ts +2 -0
  36. package/dist/buildWorkAuthorization.js +23 -0
  37. package/dist/buildWorkContext.d.ts +3 -0
  38. package/dist/buildWorkContext.js +6 -0
  39. package/dist/buildWorkMigration.d.ts +9 -0
  40. package/dist/buildWorkMigration.js +206 -0
  41. package/dist/buildWorkReconciliation.d.ts +12 -0
  42. package/dist/buildWorkReconciliation.js +70 -0
  43. package/dist/builderGuidanceFollowup.d.ts +15 -0
  44. package/dist/builderGuidanceFollowup.js +34 -0
  45. package/dist/cli/doctor.js +3 -2
  46. package/dist/cli/graph.d.ts +2 -0
  47. package/dist/cli/graph.js +75 -0
  48. package/dist/cli/manager.d.ts +8 -0
  49. package/dist/cli/manager.js +234 -12
  50. package/dist/cli/recovery.js +3 -2
  51. package/dist/cli/runtimeAuthPrompt.js +28 -8
  52. package/dist/cli/start.js +152 -48
  53. package/dist/cli/status.js +20 -5
  54. package/dist/cli/tickets.js +9 -7
  55. package/dist/continuity.d.ts +11 -0
  56. package/dist/continuity.js +22 -6
  57. package/dist/foreman.d.ts +10 -2
  58. package/dist/foreman.js +330 -39
  59. package/dist/graph/boundary.d.ts +15 -0
  60. package/dist/graph/boundary.js +50 -0
  61. package/dist/graph/bridge.d.ts +9 -0
  62. package/dist/graph/bridge.js +146 -0
  63. package/dist/graph/bridge.py +205 -0
  64. package/dist/graph/config.d.ts +11 -0
  65. package/dist/graph/config.js +40 -0
  66. package/dist/graph/corpus.d.ts +16 -0
  67. package/dist/graph/corpus.js +188 -0
  68. package/dist/graph/derived.d.ts +17 -0
  69. package/dist/graph/derived.js +73 -0
  70. package/dist/graph/host.d.ts +4 -0
  71. package/dist/graph/host.js +160 -0
  72. package/dist/graph/install.d.ts +3 -0
  73. package/dist/graph/install.js +50 -0
  74. package/dist/graph/lifecycle.d.ts +11 -0
  75. package/dist/graph/lifecycle.js +124 -0
  76. package/dist/graph/maintenance.d.ts +43 -0
  77. package/dist/graph/maintenance.js +301 -0
  78. package/dist/graph/manager.d.ts +9 -0
  79. package/dist/graph/manager.js +92 -0
  80. package/dist/graph/preparation.d.ts +41 -0
  81. package/dist/graph/preparation.js +43 -0
  82. package/dist/graph/read.d.ts +27 -0
  83. package/dist/graph/read.js +152 -0
  84. package/dist/graph/semantic.d.ts +17 -0
  85. package/dist/graph/semantic.js +27 -0
  86. package/dist/graph/session.d.ts +8 -0
  87. package/dist/graph/session.js +92 -0
  88. package/dist/graph/sources.d.ts +4 -0
  89. package/dist/graph/sources.js +11 -0
  90. package/dist/graph/storage.d.ts +38 -0
  91. package/dist/graph/storage.js +159 -0
  92. package/dist/graph/turn.d.ts +17 -0
  93. package/dist/graph/turn.js +360 -0
  94. package/dist/graph/util.d.ts +5 -0
  95. package/dist/graph/util.js +51 -0
  96. package/dist/handoffs.d.ts +1 -0
  97. package/dist/handoffs.js +5 -1
  98. package/dist/humanDecision.d.ts +21 -2
  99. package/dist/humanDecision.js +169 -11
  100. package/dist/index.js +2 -0
  101. package/dist/installOwnership.d.ts +46 -0
  102. package/dist/installOwnership.js +440 -0
  103. package/dist/linuxProbeInventory.d.ts +12 -0
  104. package/dist/linuxProbeInventory.js +48 -0
  105. package/dist/log.d.ts +1 -1
  106. package/dist/log.js +7 -1
  107. package/dist/managerEvidence.d.ts +25 -0
  108. package/dist/managerEvidence.js +326 -0
  109. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  110. package/dist/managerEvidenceArtifacts.js +118 -0
  111. package/dist/managerPacket.d.ts +2 -0
  112. package/dist/managerPacket.js +23 -0
  113. package/dist/markers.d.ts +1 -1
  114. package/dist/markers.js +2 -2
  115. package/dist/observability.js +1 -1
  116. package/dist/processIdentity.js +7 -0
  117. package/dist/providerPhase.d.ts +13 -0
  118. package/dist/providerPhase.js +26 -0
  119. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  120. package/dist/qaAuthorizedDisposition.js +26 -0
  121. package/dist/qaBuildGate.d.ts +4 -0
  122. package/dist/qaBuildGate.js +216 -0
  123. package/dist/qaBuilderCoverage.d.ts +5 -0
  124. package/dist/qaBuilderCoverage.js +78 -0
  125. package/dist/qaContractCoverage.d.ts +10 -0
  126. package/dist/qaContractCoverage.js +55 -0
  127. package/dist/qaContractDelivery.d.ts +23 -0
  128. package/dist/qaContractDelivery.js +145 -0
  129. package/dist/qaContractFreshness.d.ts +15 -0
  130. package/dist/qaContractFreshness.js +36 -0
  131. package/dist/qaDeliveryJournal.d.ts +10 -0
  132. package/dist/qaDeliveryJournal.js +16 -1
  133. package/dist/qaEffectiveConfig.d.ts +20 -0
  134. package/dist/qaEffectiveConfig.js +40 -0
  135. package/dist/qaEvidenceReader.d.ts +14 -0
  136. package/dist/qaEvidenceReader.js +176 -0
  137. package/dist/qaFailureDelivery.d.ts +2 -0
  138. package/dist/qaFailureDelivery.js +107 -10
  139. package/dist/qaHandbackMigration.d.ts +1 -1
  140. package/dist/qaHandbackMigration.js +3 -3
  141. package/dist/qaPreparation.d.ts +26 -0
  142. package/dist/qaPreparation.js +150 -0
  143. package/dist/qaPreparationInputs.d.ts +8 -0
  144. package/dist/qaPreparationInputs.js +47 -0
  145. package/dist/qaPreparationMetrics.d.ts +145 -0
  146. package/dist/qaPreparationMetrics.js +120 -0
  147. package/dist/qaPreparationPolicy.d.ts +5 -0
  148. package/dist/qaPreparationPolicy.js +51 -0
  149. package/dist/qaPreparationStore.d.ts +157 -0
  150. package/dist/qaPreparationStore.js +412 -0
  151. package/dist/qaProtocolV2.d.ts +28 -0
  152. package/dist/qaProtocolV2.js +12 -0
  153. package/dist/qaReview.d.ts +6 -0
  154. package/dist/qaReview.js +234 -24
  155. package/dist/qaTimeline.d.ts +18 -0
  156. package/dist/qaTimeline.js +123 -0
  157. package/dist/qaVerificationContract.d.ts +17 -0
  158. package/dist/qaVerificationContract.js +236 -0
  159. package/dist/readinessCleanup.js +12 -5
  160. package/dist/recoveryGuidance.d.ts +9 -0
  161. package/dist/recoveryGuidance.js +13 -0
  162. package/dist/runtimeAuth.d.ts +1 -0
  163. package/dist/runtimeAuth.js +5 -1
  164. package/dist/runtimeReadiness.d.ts +1 -0
  165. package/dist/runtimeReadiness.js +13 -1
  166. package/dist/sessionIdentity.d.ts +2 -0
  167. package/dist/sessionIdentity.js +14 -0
  168. package/dist/sessionLifecycle.js +3 -3
  169. package/dist/sources/sourceRegistry.js +7 -0
  170. package/dist/stateTransfer.js +50 -1
  171. package/dist/statusReporter.d.ts +1 -0
  172. package/dist/statusReporter.js +1 -0
  173. package/dist/supervisedStart.js +18 -8
  174. package/dist/terminalStyle.d.ts +21 -0
  175. package/dist/terminalStyle.js +50 -0
  176. package/dist/ticketPopulation.d.ts +1 -0
  177. package/dist/ticketPopulation.js +5 -1
  178. package/dist/tickets/commands.js +2 -2
  179. package/dist/tickets/queue.js +1 -1
  180. package/dist/tickets/ticketSchema.d.ts +198 -0
  181. package/dist/tickets/ticketSchema.js +2 -0
  182. package/dist/workflowDb.d.ts +51 -5
  183. package/dist/workflowDb.js +338 -7
  184. package/dist/workflowReader.d.ts +109 -2
  185. package/dist/workflowReader.js +48 -4
  186. package/package.json +84 -7
  187. package/scripts/copy-graph-assets.mjs +3 -0
  188. package/scripts/graph-benchmark.mjs +20 -0
  189. package/scripts/graph-evaluation.mjs +85 -0
@@ -0,0 +1,43 @@
1
+ export { graphSemanticHost } from "./host.js";
2
+ export { ensureGraphInstallation } from "./install.js";
3
+ import { type GraphAdoptionV1, type GraphConfigV1 } from "rafi-spec";
4
+ export interface SemanticChunk {
5
+ path: string;
6
+ digest: string;
7
+ text: string;
8
+ }
9
+ export interface SemanticExtraction {
10
+ nodes: Array<Record<string, unknown>>;
11
+ edges: Array<Record<string, unknown>>;
12
+ origin: {
13
+ provider: string;
14
+ model?: string;
15
+ operationId: string;
16
+ };
17
+ inputTokens?: number;
18
+ outputTokens?: number;
19
+ }
20
+ export type GraphSemanticHost = (chunks: SemanticChunk[], operationId: string, budget?: {
21
+ deadline: number;
22
+ signal?: AbortSignal;
23
+ }) => Promise<SemanticExtraction>;
24
+ export interface GraphMaintenanceOutcome {
25
+ operationId: string;
26
+ state: "published" | "failed" | "deferred";
27
+ generationId?: string;
28
+ reason?: string;
29
+ }
30
+ /** Called only by an accepted setup or explicit adoption, never by a read/factory. */
31
+ export declare function adoptGraph(configRoot: string, input: {
32
+ config?: GraphConfigV1;
33
+ authorization: "setup" | "explicit";
34
+ }): GraphAdoptionV1;
35
+ export declare function disableGraph(configRoot: string): void;
36
+ /** Foreground, fenced publication. Semantic failure never replaces a mixed graph. */
37
+ export declare function refreshGraph(configRoot: string, workspace: string, logicalTaskId: string, options?: {
38
+ semanticHost?: GraphSemanticHost;
39
+ sourceRef?: string;
40
+ python?: string;
41
+ signal?: AbortSignal;
42
+ }): Promise<GraphMaintenanceOutcome>;
43
+ export { loadGraphConfig } from "./config.js";
@@ -0,0 +1,301 @@
1
+ export { graphSemanticHost } from "./host.js";
2
+ import { capturePreimage, finalizeOwnedWrite, registerOwnedFile, readInstallManifest } from "../installOwnership.js";
3
+ import { graphStorageBytes } from "./lifecycle.js";
4
+ import { ownedGraphPython } from "./install.js";
5
+ export { ensureGraphInstallation } from "./install.js";
6
+ import { randomUUID } from "node:crypto";
7
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, openSync, closeSync, fsyncSync } from "node:fs";
8
+ import { dirname, join, relative } from "node:path";
9
+ import { parseDocument } from "yaml";
10
+ import { loadSourceRegistry } from "../sources/sourceRegistry.js";
11
+ import { assertGraphConfig, DEFAULT_GRAPH_CONFIG, GRAPHIFY_VERSION } from "rafi-spec";
12
+ import { WorkflowDb } from "../workflowDb.js";
13
+ import { loadGraphConfig } from "./config.js";
14
+ import { captureGraphCorpus } from "./corpus.js";
15
+ import { resolveGraphPython, runBridge } from "./bridge.js";
16
+ import { graphScope } from "./read.js";
17
+ import { bytesDigest, canonical, digest, readBounded, confined } from "./util.js";
18
+ /** Called only by an accepted setup or explicit adoption, never by a read/factory. */
19
+ export function adoptGraph(configRoot, input) {
20
+ const existing = loadGraphConfig(configRoot);
21
+ const config = structuredClone(input.config ?? existing.config ?? DEFAULT_GRAPH_CONFIG);
22
+ config.enabled = true;
23
+ if (config.sourceSelection === "active-captured") {
24
+ config.sourceVersions = { ...(existing.config?.sourceSelection === "active-captured" ? existing.config.sourceVersions : {}), ...config.sourceVersions };
25
+ const registry = loadSourceRegistry(configRoot).registry;
26
+ config.sourceIds = registry.entries.filter(entry => entry.active && entry.versions.length).map(entry => entry.id).sort();
27
+ config.sourceVersions = Object.fromEntries(config.sourceIds.flatMap(id => config.sourceVersions?.[id] ? [[id, config.sourceVersions[id]]] : []));
28
+ }
29
+ if (config.sourceIds.length) {
30
+ const registry = loadSourceRegistry(configRoot).registry;
31
+ config.sourceVersions = { ...config.sourceVersions };
32
+ for (const id of config.sourceIds) {
33
+ const entry = registry.entries.find(e => e.id === id && e.active);
34
+ const version = config.sourceVersions[id] ?? entry?.versions.at(-1)?.fingerprint;
35
+ if (!version || !entry?.versions.some(v => v.fingerprint === version))
36
+ throw new Error(`Registered source ${id} requires an approved captured version`);
37
+ config.sourceVersions[id] = version;
38
+ }
39
+ }
40
+ assertGraphConfig(config);
41
+ if (existing.enabled && existing.policyDigest === digest("policy", config) && existing.adoption)
42
+ return existing.adoption;
43
+ const adoption = { version: 1, projectRef: existing.projectRef, config, policyDigest: digest("policy", config), acceptedAt: new Date().toISOString(), authorization: input.authorization, initialOperationId: randomUUID() };
44
+ // Update only the graph setting; preserve other project fields and user files.
45
+ const configFile = ["rafi-config.yaml", "project.yaml"].map(name => join(configRoot, name)).find(existsSync);
46
+ if (configFile) {
47
+ const ownedConfig = capturePreimage(configRoot, relative(configRoot, configFile), "graph-adoption", "config");
48
+ const raw = parseDocument(readFileSync(configFile, "utf8"));
49
+ if (!raw || typeof raw !== "object")
50
+ throw new Error("Invalid project configuration");
51
+ raw.set("graph", config);
52
+ const temporary = `${configFile}.${randomUUID()}.tmp`;
53
+ writeFileSync(temporary, raw.toString());
54
+ renameSync(temporary, configFile);
55
+ finalizeOwnedWrite(configRoot, ownedConfig);
56
+ }
57
+ const db = new WorkflowDb(configRoot);
58
+ try {
59
+ db.graphStore().put("adoption", "project", adoption);
60
+ }
61
+ finally {
62
+ db.close();
63
+ }
64
+ const ignore = join(configRoot, ".gitignore");
65
+ const prior = existsSync(ignore) ? readFileSync(ignore, "utf8") : "";
66
+ if (!prior.split(/\r?\n/).includes("/graphify-out/")) {
67
+ const recorded = readInstallManifest(configRoot)?.files.find(entry => entry.path === ".gitignore");
68
+ const ownedIgnore = capturePreimage(configRoot, ".gitignore", "graph-adoption", "managed-gitignore");
69
+ const [start, end] = recorded?.marker?.split("..") ?? [];
70
+ if (recorded?.mode === "managed-block" && start && end && prior.includes(start) && prior.includes(end)) {
71
+ // Ownership has one entry per path: extend its existing managed block.
72
+ writeFileSync(ignore, prior.replace(end, `/graphify-out/\n${end}`));
73
+ finalizeOwnedWrite(configRoot, recorded);
74
+ }
75
+ else {
76
+ writeFileSync(ignore, `${prior}${prior && !prior.endsWith("\n") ? "\n" : ""}# rafi-graph:start\n/graphify-out/\n# rafi-graph:end\n`);
77
+ finalizeOwnedWrite(configRoot, ownedIgnore.mode === "created" || ownedIgnore.mode === "generated" ? ownedIgnore : { ...ownedIgnore, mode: "managed-block", marker: "# rafi-graph:start..# rafi-graph:end" });
78
+ }
79
+ }
80
+ const ownedRoot = join(configRoot, "graphify-out", "rafi");
81
+ if (!existsSync(ownedRoot)) {
82
+ mkdirSync(ownedRoot, { recursive: true });
83
+ registerOwnedFile(configRoot, "graphify-out/rafi", { mode: "generated", origin: "graph-adoption", category: "runtime-state" });
84
+ }
85
+ return adoption;
86
+ }
87
+ export function disableGraph(configRoot) {
88
+ const effective = loadGraphConfig(configRoot);
89
+ if (!effective.config || !effective.adoption)
90
+ return;
91
+ const config = { ...effective.config, enabled: false };
92
+ const configFile = ["rafi-config.yaml", "project.yaml"].map(name => join(configRoot, name)).find(existsSync);
93
+ if (configFile) {
94
+ const raw = parseDocument(readFileSync(configFile, "utf8"));
95
+ raw.set("graph", config);
96
+ const temp = `${configFile}.${randomUUID()}.tmp`;
97
+ writeFileSync(temp, raw.toString());
98
+ renameSync(temp, configFile);
99
+ }
100
+ const db = new WorkflowDb(configRoot);
101
+ try {
102
+ db.graphStore().put("adoption", "project", { ...effective.adoption, config, policyDigest: digest("policy", config) });
103
+ }
104
+ finally {
105
+ db.close();
106
+ }
107
+ }
108
+ function validateSemantics(result, corpus, expected) {
109
+ if (Buffer.byteLength(canonical(result)) > 120000)
110
+ throw new Error("Semantic output budget exceeded");
111
+ if (!Array.isArray(result.nodes) || !Array.isArray(result.edges) || !result.origin?.operationId || !result.origin.provider)
112
+ throw new Error("Invalid semantic extraction protocol");
113
+ const files = new Set(expected.map(x => x.path));
114
+ const ids = new Set();
115
+ for (const node of result.nodes) {
116
+ if (typeof node.id !== "string" || ids.has(node.id) || typeof node.source_file !== "string" || !files.has(node.source_file) || !corpus.bytes.has(node.source_file))
117
+ throw new Error("Semantic node has invalid identity/provenance");
118
+ ids.add(node.id);
119
+ }
120
+ for (const edge of result.edges)
121
+ if (typeof edge.source_file !== "string" || !files.has(edge.source_file) || !ids.has(String(edge.source)) || !ids.has(String(edge.target)) || !["EXTRACTED", "INFERRED", "AMBIGUOUS"].includes(String(edge.confidence)))
122
+ throw new Error("Semantic edge has invalid endpoint/evidence class");
123
+ for (const file of files)
124
+ if (!result.nodes.some(n => n.source_file === file))
125
+ throw new Error(`Semantic extraction omitted ${file}; input remains unstamped`);
126
+ }
127
+ /** Foreground, fenced publication. Semantic failure never replaces a mixed graph. */
128
+ export async function refreshGraph(configRoot, workspace, logicalTaskId, options = {}) {
129
+ const effective = loadGraphConfig(configRoot);
130
+ if (!effective.enabled)
131
+ throw new Error(`Graph ${effective.reason}; explicit adoption required`);
132
+ const scope = graphScope(workspace), operationId = digest("maintenance", { scope, logicalTaskId, policy: effective.policyDigest });
133
+ const db = new WorkflowDb(configRoot), store = db.graphStore();
134
+ const previous = store.get("job", operationId);
135
+ if (previous?.value.state === "published") {
136
+ db.close();
137
+ return previous.value;
138
+ }
139
+ const owner = operationId;
140
+ let fence;
141
+ try {
142
+ fence = store.lease(scope, owner, Date.now(), effective.limits.maintenanceMs);
143
+ }
144
+ catch (error) {
145
+ db.close();
146
+ return { operationId, state: "deferred", reason: String(error) };
147
+ }
148
+ const stage = join(configRoot, "graphify-out", "rafi", "staging", `${operationId}-${fence}`);
149
+ const started = Date.now();
150
+ const check = () => {
151
+ if (options.signal?.aborted)
152
+ throw new Error("Graph maintenance cancelled");
153
+ if (Date.now() - started > effective.limits.maintenanceMs)
154
+ throw new Error("Graph maintenance deadline exceeded");
155
+ if (!store.ownsLease(scope, owner, fence, Date.now()))
156
+ throw new Error("Graph maintenance lease lost");
157
+ };
158
+ try {
159
+ const prior = store.get("head", scope);
160
+ store.put("job", operationId, { version: 1, operationId, state: "capturing", scope, fence, startedAt: new Date().toISOString() });
161
+ const corpus = captureGraphCorpus(workspace, configRoot, effective, options.sourceRef);
162
+ check();
163
+ const parent = prior ? store.get("generation", prior.value.generationId)?.value : undefined;
164
+ if (parent) {
165
+ const { id, ...metadata } = parent;
166
+ if (!/^[a-f0-9]{64}$/.test(id) || digest("generation", metadata) !== id)
167
+ throw new Error("Parent generation metadata checksum mismatch");
168
+ }
169
+ let reusable;
170
+ if (parent?.policyDigest === effective.policyDigest) {
171
+ try {
172
+ const bytes = readBounded(confined(configRoot, `graphify-out/rafi/generations/${parent.id}/semantic.json`), effective.limits.maxGraphBytes);
173
+ if (parent.files["semantic.json"] !== bytesDigest(bytes))
174
+ throw new Error("Semantic cache checksum mismatch");
175
+ reusable = JSON.parse(bytes.toString("utf8"));
176
+ }
177
+ catch { /* Unknown legacy semantic provenance is never silently reused. */ }
178
+ }
179
+ const unchanged = new Set(corpus.inputs.filter(i => i.kind === "semantic" && parent?.inputs.some(p => p.path === i.path && p.digest === i.digest)).map(i => i.path));
180
+ const preservedNodes = reusable?.nodes.filter(n => unchanged.has(String(n.source_file))) ?? [];
181
+ const preservedIds = new Set(preservedNodes.map(n => String(n.id)));
182
+ const preservedEdges = reusable?.edges.filter(e => preservedIds.has(String(e.source)) && preservedIds.has(String(e.target)) && unchanged.has(String(e.source_file))) ?? [];
183
+ const semanticInputs = effective.config.mode === "mixed" ? corpus.inputs.filter(i => i.kind === "semantic" && (!unchanged.has(i.path) || !preservedNodes.some(n => n.source_file === i.path))) : [];
184
+ if (semanticInputs.length > effective.limits.semanticInputs)
185
+ throw new Error("scope-decision-required: semantic input budget exceeded");
186
+ if (semanticInputs.length && !options.semanticHost)
187
+ throw new Error("semantic-runtime-unavailable: mixed extraction needs an authorized host exchange");
188
+ store.reserveStorage(`${operationId}:${fence}`, corpus.inputs.reduce((n, i) => n + i.bytes, 0) + effective.limits.maxGraphBytes * 4, graphStorageBytes(configRoot), effective.limits.storageBytes, { scope, owner, fence });
189
+ mkdirSync(join(stage, "input"), { recursive: true });
190
+ confined(configRoot, `graphify-out/rafi/staging/${operationId}-${fence}`);
191
+ for (const input of corpus.inputs) {
192
+ const path = join(stage, "input", input.path);
193
+ mkdirSync(dirname(path), { recursive: true });
194
+ writeFileSync(path, corpus.bytes.get(input.path), { mode: 0o444 });
195
+ }
196
+ let semantic;
197
+ if (semanticInputs.length) {
198
+ const chunks = semanticInputs.map(i => ({ path: i.path, digest: i.digest, text: corpus.bytes.get(i.path).toString("utf8") }));
199
+ if (chunks.reduce((n, c) => n + c.text.length, 0) > 750000)
200
+ throw new Error("Semantic input budget exceeded; choose an explicit larger scoped policy");
201
+ semantic = await options.semanticHost(chunks, operationId, { deadline: started + effective.limits.maintenanceMs, signal: options.signal });
202
+ validateSemantics(semantic, corpus, chunks);
203
+ }
204
+ if (preservedNodes.length) {
205
+ semantic = { nodes: [...preservedNodes, ...(semantic?.nodes ?? [])], edges: [...preservedEdges, ...(semantic?.edges ?? [])], origin: semantic?.origin ?? reusable.origin, inputTokens: semantic?.inputTokens, outputTokens: semantic?.outputTokens };
206
+ }
207
+ check();
208
+ const python = options.python ?? (existsSync(ownedGraphPython(configRoot)) ? ownedGraphPython(configRoot) : await resolveGraphPython());
209
+ store.put("capability", "machine", { version: 1, python, packageVersion: GRAPHIFY_VERSION, checkedAt: new Date().toISOString(), scope: "local-machine; not portable" });
210
+ store.put("job", operationId, { version: 1, operationId, state: "extracting", scope, fence });
211
+ const extracted = await runBridge(python, { action: "extract", staging: stage, ...(parent?.policyDigest === effective.policyDigest ? { parentCache: confined(configRoot, `graphify-out/rafi/generations/${parent.id}`), cacheChecksums: parent.files } : {}), paths: corpus.inputs.filter(i => i.kind === "code").map(i => i.path), semantic: semantic ?? { nodes: [], edges: [] } }, { timeoutMs: Math.max(1, effective.limits.maintenanceMs - (Date.now() - started)), maxOutputBytes: effective.limits.maxGraphBytes, cwd: stage, signal: options.signal });
212
+ check();
213
+ if (Array.isArray(extracted.extraction.failed_sources) && extracted.extraction.failed_sources.length)
214
+ throw new Error(`AST extraction failed for ${extracted.extraction.failed_sources.length} inputs; prior generation retained`);
215
+ if (parent?.policyDigest === effective.policyDigest) {
216
+ try {
217
+ const parentBytes = readBounded(confined(configRoot, `graphify-out/rafi/generations/${parent.id}/graph.json`), effective.limits.maxGraphBytes);
218
+ if (bytesDigest(parentBytes) !== parent.graphDigest)
219
+ throw new Error("Parent graph checksum mismatch");
220
+ const old = JSON.parse(parentBytes.toString("utf8"));
221
+ const stable = new Set(corpus.inputs.filter(i => parent.inputs.some(p => p.path === i.path && p.digest === i.digest)).map(i => i.path));
222
+ const nextIds = new Set(extracted.graph.nodes.map(n => n.id));
223
+ const lost = old.nodes.filter(n => n.source_file && stable.has(n.source_file) && !nextIds.has(n.id));
224
+ if (lost.length)
225
+ throw new Error(`unexplained-shrinkage: ${lost.length} unchanged-input nodes disappeared`);
226
+ const stableIds = new Set(old.nodes.filter(n => n.source_file && stable.has(n.source_file)).map(n => n.id));
227
+ const edgeKey = (edge) => canonical({ source: edge.source, target: edge.target, relation: edge.relation, source_file: edge.source_file });
228
+ const nextEdges = new Set((extracted.graph.links ?? extracted.graph.edges ?? []).map(edgeKey));
229
+ const lostEdges = (old.links ?? old.edges ?? []).filter(edge => stableIds.has(String(edge.source)) && stableIds.has(String(edge.target))
230
+ && typeof edge.source_file === "string" && stable.has(edge.source_file) && !nextEdges.has(edgeKey(edge)));
231
+ if (lostEdges.length)
232
+ throw new Error(`unexplained-shrinkage: ${lostEdges.length} unchanged-input relationships disappeared`);
233
+ }
234
+ catch (error) {
235
+ if (String(error).includes("unexplained-shrinkage"))
236
+ throw error;
237
+ throw new Error(`Parent graph validation unavailable: ${String(error)}`);
238
+ }
239
+ }
240
+ const graphBytes = Buffer.from(canonical(extracted.graph));
241
+ const limitations = effective.config.mode === "code-only" && corpus.inputs.some(i => i.kind === "semantic") ? [{ reason: "code-only", detail: "Documentation and requirement semantics were deliberately not extracted" }] : [];
242
+ const sidecars = { "extraction.json": canonical(extracted.extraction), ...(semantic ? { "semantic.json": canonical(semantic), "semantic-origin.json": canonical(semantic.origin) } : {}) };
243
+ const sidecarDigests = {};
244
+ for (const [name, text] of Object.entries(sidecars)) {
245
+ writeFileSync(join(stage, name), text);
246
+ sidecarDigests[name] = bytesDigest(text);
247
+ }
248
+ sidecarDigests["graph.html"] = bytesDigest(readBounded(join(stage, "graph.html"), effective.limits.maxGraphBytes * 2));
249
+ const metadata = { version: 1, corpusDigest: corpus.corpusDigest, policyDigest: effective.policyDigest, graphDigest: bytesDigest(graphBytes), binding: corpus.binding, inputs: corpus.inputs, createdAt: new Date().toISOString(), operationId, packageVersion: GRAPHIFY_VERSION, graphSchema: 1, parent: prior?.value.generationId, limitations, files: { ...extracted.cacheFiles, ...sidecarDigests, "exclusions-digest": corpus.exclusionsDigest } };
250
+ const generation = { ...metadata, id: digest("generation", metadata) };
251
+ writeFileSync(join(stage, "graph.json"), graphBytes);
252
+ writeFileSync(join(stage, "manifest.json"), canonical(generation));
253
+ const report = `# Rafi Graphify generation\n\nGeneration: ${generation.id}\nCorpus: ${corpus.corpusDigest}\nInputs: ${corpus.inputs.length}\nReused AST inputs: ${extracted.reusedAstInputs}\nGraphify: ${GRAPHIFY_VERSION}\n\nUsage: ${semantic?.inputTokens ?? "unknown"} input tokens; ${semantic?.outputTokens ?? "unknown"} output tokens.\n\nGraph evidence requires independent source verification.\n${limitations.map(l => l.detail).join("\n")}\n`;
254
+ writeFileSync(join(stage, "GRAPH_REPORT.md"), report);
255
+ if (!existsSync(join(stage, "graph.html")))
256
+ throw new Error("Required graph visualization missing");
257
+ const current = loadGraphConfig(configRoot);
258
+ if (!current.enabled || current.policyDigest !== effective.policyDigest)
259
+ throw new Error("Graph policy revoked during extraction");
260
+ if (captureGraphCorpus(workspace, configRoot, current, options.sourceRef).corpusDigest !== corpus.corpusDigest)
261
+ throw new Error("Source drift during extraction; publication deferred");
262
+ check();
263
+ rmSync(join(stage, "input"), { recursive: true, force: true });
264
+ const directory = join(configRoot, "graphify-out", "rafi", "generations", generation.id);
265
+ mkdirSync(dirname(directory), { recursive: true });
266
+ for (const name of ["graph.json", "manifest.json", "extraction.json", "GRAPH_REPORT.md", "graph.html", ...(semantic ? ["semantic.json", "semantic-origin.json"] : []), ...Object.keys(extracted.cacheFiles)]) {
267
+ const fd = openSync(join(stage, name), "r");
268
+ try {
269
+ fsyncSync(fd);
270
+ }
271
+ finally {
272
+ closeSync(fd);
273
+ }
274
+ }
275
+ renameSync(stage, directory);
276
+ if (process.platform !== "win32") {
277
+ const fd = openSync(dirname(directory), "r");
278
+ try {
279
+ fsyncSync(fd);
280
+ }
281
+ finally {
282
+ closeSync(fd);
283
+ }
284
+ }
285
+ const outcome = { operationId, state: "published", generationId: generation.id };
286
+ store.publish(scope, owner, fence, Date.now(), prior?.revision ?? 0, generation, outcome);
287
+ return outcome;
288
+ }
289
+ catch (error) {
290
+ const outcome = { operationId, state: "failed", reason: String(error) };
291
+ if (store.ownsLease(scope, owner, fence, Date.now()))
292
+ store.put("job", operationId, outcome);
293
+ return outcome;
294
+ }
295
+ finally {
296
+ store.remove("storage-reservation", `${operationId}:${fence}`);
297
+ store.release(scope, owner, fence);
298
+ db.close();
299
+ }
300
+ }
301
+ export { loadGraphConfig } from "./config.js";
@@ -0,0 +1,9 @@
1
+ import { type ManagerGraphRequestV3 } from "rafi-spec";
2
+ import type { GraphDerivedAccess } from "./derived.js";
3
+ export declare const MANAGER_GRAPH_GUIDANCE = "For substantial architecture/dependency questions, request graph evidence through this additional host envelope: {\"kind\":\"manager_graph_evidence_request\",\"version\":3,\"requestId\":\"unique-id\",\"operations\":[{\"operation\":\"query\",\"query\":\"specific source concepts\"}]}. Do not mix envelopes. This shares your existing evidence lookup budget. Only the host chooses project/source scope. An optional receiptId can select previously delivered evidence belonging to the host-selected run; unavailable historical caches stay unavailable. Current-source graph evidence cannot establish a historical run's implementation or cause. Graph output never authorizes controls.";
4
+ export declare function parseManagerGraphRequest(text: string): ManagerGraphRequestV3 | undefined;
5
+ export declare function createManagerGraphEvidence(configRoot: string, runId?: string): {
6
+ guidance: string;
7
+ access: () => GraphDerivedAccess[];
8
+ execute: (request: ManagerGraphRequestV3) => Promise<string>;
9
+ };
@@ -0,0 +1,92 @@
1
+ import { WorkflowReader } from "../workflowReader.js";
2
+ import { assertManagerGraphRequest, parseStrictJson } from "rafi-spec";
3
+ import { loadGraphConfig } from "./config.js";
4
+ import { captureGraphCorpus } from "./corpus.js";
5
+ import { acquireGraphView, graphUnavailable, readGraph } from "./read.js";
6
+ import { canonical } from "./util.js";
7
+ export const MANAGER_GRAPH_GUIDANCE = `For substantial architecture/dependency questions, request graph evidence through this additional host envelope: {"kind":"manager_graph_evidence_request","version":3,"requestId":"unique-id","operations":[{"operation":"query","query":"specific source concepts"}]}. Do not mix envelopes. This shares your existing evidence lookup budget. Only the host chooses project/source scope. An optional receiptId can select previously delivered evidence belonging to the host-selected run; unavailable historical caches stay unavailable. Current-source graph evidence cannot establish a historical run's implementation or cause. Graph output never authorizes controls.`;
8
+ export function parseManagerGraphRequest(text) {
9
+ if (Buffer.byteLength(text) > 8192 || !text.trim().startsWith("{"))
10
+ return undefined;
11
+ let value;
12
+ try {
13
+ value = parseStrictJson(text);
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ if (!value || typeof value !== "object" || value.kind !== "manager_graph_evidence_request")
19
+ return undefined;
20
+ if (value.version !== 3)
21
+ throw new Error("Unsupported Manager graph protocol");
22
+ assertManagerGraphRequest(value);
23
+ return value;
24
+ }
25
+ export function createManagerGraphEvidence(configRoot, runId) {
26
+ const config = loadGraphConfig(configRoot);
27
+ let deliveredAccess;
28
+ let initialized = false, view;
29
+ return {
30
+ access: () => deliveredAccess ? [deliveredAccess] : [],
31
+ guidance: config.enabled ? MANAGER_GRAPH_GUIDANCE : "", execute: async (request) => {
32
+ let unavailable = graphUnavailable(config.reason, "Graph unavailable; use source/runtime evidence");
33
+ if (config.enabled && !initialized) {
34
+ initialized = true;
35
+ try {
36
+ const captured = captureGraphCorpus(configRoot, configRoot, config);
37
+ const acquired = acquireGraphView(configRoot, configRoot, captured);
38
+ if ("graph" in acquired)
39
+ view = acquired;
40
+ else
41
+ unavailable = acquired;
42
+ }
43
+ catch (error) {
44
+ unavailable = graphUnavailable("freshness-check-failed", String(error));
45
+ }
46
+ }
47
+ let selected = view;
48
+ if (request.receiptId) {
49
+ const reader = new WorkflowReader(configRoot);
50
+ try {
51
+ const receipt = reader.graphRecord("receipt", request.receiptId)?.value;
52
+ if (!runId || receipt?.runId !== runId || !receipt.generationIds.length) {
53
+ selected = undefined;
54
+ unavailable = graphUnavailable("invalid-scope", "Receipt is unavailable in the host-selected run");
55
+ }
56
+ else {
57
+ const acquired = acquireGraphView(configRoot, configRoot, undefined, receipt.generationIds[0]);
58
+ if ("graph" in acquired)
59
+ selected = acquired;
60
+ else {
61
+ selected = undefined;
62
+ unavailable = acquired;
63
+ }
64
+ }
65
+ }
66
+ finally {
67
+ reader.close();
68
+ }
69
+ }
70
+ const results = [];
71
+ for (const operation of request.operations)
72
+ results.push(selected ? await readGraph(selected, operation) : unavailable);
73
+ const reader = new WorkflowReader(configRoot);
74
+ let receipts = [];
75
+ try {
76
+ if (runId && request.operations.some(o => o.operation === "status"))
77
+ receipts = reader.graphReceiptSummaries(runId);
78
+ }
79
+ finally {
80
+ reader.close();
81
+ }
82
+ let packet = canonical({ kind: "manager_graph_evidence", version: 3, requestId: request.requestId, sourceTime: request.receiptId ? "historical receipt-bound navigation; not proof of executed work" : "current-checkout observation; not historical run evidence", results, receipts });
83
+ if (Buffer.byteLength(packet) > 24 * 1024)
84
+ packet = canonical({ kind: "manager_graph_evidence", version: 3, requestId: request.requestId, results: [graphUnavailable("packet-limit", "Narrow the request to one focused operation")] });
85
+ if (selected)
86
+ deliveredAccess = { policyDigest: config.policyDigest, workspace: configRoot,
87
+ exclusionsDigest: selected.generation.files["exclusions-digest"], sourceVersions: config.config?.sourceVersions ?? {},
88
+ paths: selected.generation.inputs.filter(i => !i.sourceId).map(i => i.path) };
89
+ return packet;
90
+ }
91
+ };
92
+ }
@@ -0,0 +1,41 @@
1
+ import type { BuilderAdapter } from "../adapters/types.js";
2
+ /** One reserved preparation operation owns all optional evidence subturns. */
3
+ export declare function sendPreparationGraphTurn(adapter: BuilderAdapter, instruction: string, input: {
4
+ phase: "planner" | "prepare" | "assess" | "challenge" | "repair";
5
+ projectDir: string;
6
+ workspace: string;
7
+ accessWorkspace?: string;
8
+ sourceRef: string;
9
+ runId: string;
10
+ workId: string;
11
+ operationId: string;
12
+ remainingMs: number;
13
+ }): Promise<{
14
+ graphReceiptRefs: string[];
15
+ text: string;
16
+ isError: boolean;
17
+ numTurns: number;
18
+ costUsd: number;
19
+ costAuthoritative?: boolean;
20
+ inputTokens?: number;
21
+ outputTokens?: number;
22
+ usage?: {
23
+ scope: "turn-delta" | "session-cumulative";
24
+ inputTokens?: number;
25
+ outputTokens?: number;
26
+ totalTokens?: number;
27
+ costUsd?: number;
28
+ };
29
+ failure?: import("../adapters/types.js").RuntimeFailure;
30
+ turnId?: string;
31
+ continuityErrors?: string[];
32
+ hostInstruction?: string;
33
+ providerInstruction?: string;
34
+ rawResponse?: string;
35
+ cleanedResponse?: string;
36
+ providerMetadata?: {
37
+ provider: "claude" | "codex";
38
+ sessionId?: string;
39
+ sessionRef?: import("rafi-spec").ProviderSessionRefV1;
40
+ };
41
+ }>;
@@ -0,0 +1,43 @@
1
+ import { OperationDeadline } from "../util/deadline.js";
2
+ import { graphContext, sendGraphTurn } from "./turn.js";
3
+ import { WorkflowReader } from "../workflowReader.js";
4
+ import { inheritGraphDerivedAccess } from "./derived.js";
5
+ import { digest } from "./util.js";
6
+ /** One reserved preparation operation owns all optional evidence subturns. */
7
+ export async function sendPreparationGraphTurn(adapter, instruction, input) {
8
+ const purpose = input.phase === "planner" ? "planning" : input.phase === "assess" ? "preparation-assessment"
9
+ : input.phase === "challenge" ? "preparation-challenge" : "qa-preparation";
10
+ const context = graphContext(input.projectDir, input.workspace, purpose, instruction, {
11
+ runId: input.runId, workId: input.workId, operationId: input.operationId,
12
+ logicalTaskId: input.operationId, sourceRef: input.sourceRef, accessWorkspace: input.accessWorkspace,
13
+ });
14
+ const policy = { purpose: input.phase === "repair" ? "response-repair"
15
+ : input.phase === "planner" ? "planning" : "preparation",
16
+ responseOnly: input.phase === "repair" };
17
+ const deadline = new OperationDeadline("QA preparation graph exchange", input.remainingMs);
18
+ let expired = false;
19
+ const result = await deadline.run(() => sendGraphTurn(adapter, instruction, context, policy, (text, turnPolicy) => {
20
+ // A read already in flight may finish after timeout. It must not dispatch again.
21
+ if (expired || deadline.remaining() <= 0)
22
+ throw new Error("Preparation deadline expired; graph continuation cannot dispatch");
23
+ return adapter.sendTurn(text, turnPolicy);
24
+ }), () => { expired = true; void adapter.close().catch(() => { }); });
25
+ const reader = new WorkflowReader(input.projectDir);
26
+ try {
27
+ const session = adapter.sessionId();
28
+ if (session)
29
+ inheritGraphDerivedAccess(reader.graphRecord("session-access", digest("session-access", { provider: adapter.agent, session }))?.value.grants ?? []);
30
+ const exchange = reader.graphRecord("exchange", input.operationId)?.value;
31
+ const graphReceiptRefs = [];
32
+ if (exchange)
33
+ for (let round = 0; round <= exchange.rounds; round++) {
34
+ const id = `${input.operationId}:${round}`;
35
+ if (reader.graphRecord("receipt", id))
36
+ graphReceiptRefs.push(id);
37
+ }
38
+ return { ...result, graphReceiptRefs };
39
+ }
40
+ finally {
41
+ reader.close();
42
+ }
43
+ }
@@ -0,0 +1,27 @@
1
+ import type { GraphGenerationV1, GraphReadOperationV1, GraphReadResultV1 } from "rafi-spec";
2
+ import { WorkflowReader } from "../workflowReader.js";
3
+ import { type EffectiveGraphConfig } from "./config.js";
4
+ import { type CapturedGraphCorpus } from "./corpus.js";
5
+ export declare function graphScope(workspace: string): string;
6
+ export declare function graphUnavailable(reason: string, detail: string): GraphReadResultV1;
7
+ export declare function sealResult(result: Omit<GraphReadResultV1, "digest">): GraphReadResultV1;
8
+ export declare function graphStatus(configRoot: string, workspace?: string): {
9
+ enabled: boolean;
10
+ reason: string;
11
+ policyDigest: string;
12
+ generation?: GraphGenerationV1;
13
+ capability?: unknown;
14
+ jobs?: ReturnType<WorkflowReader["graphRecentJobs"]>;
15
+ };
16
+ export interface GraphReadView {
17
+ configRoot: string;
18
+ workspace: string;
19
+ config: EffectiveGraphConfig;
20
+ generation: GraphGenerationV1;
21
+ graph: Record<string, unknown>;
22
+ corpus?: CapturedGraphCorpus;
23
+ python?: string;
24
+ historical?: boolean;
25
+ }
26
+ export declare function acquireGraphView(configRoot: string, workspace: string, corpus?: CapturedGraphCorpus, historicalGenerationId?: string): GraphReadView | GraphReadResultV1;
27
+ export declare function readGraph(view: GraphReadView, operation: GraphReadOperationV1): Promise<GraphReadResultV1>;