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,188 @@
1
+ import ignore from "ignore";
2
+ import { readWorkspaceIdentity } from "../sessionIdentity.js";
3
+ import { WorkflowReader } from "../workflowReader.js";
4
+ import { execFileSync, spawnSync } from "node:child_process";
5
+ import { existsSync, lstatSync, readdirSync, realpathSync } from "node:fs";
6
+ import { extname, join } from "node:path";
7
+ import { loadSourceRegistry } from "../sources/sourceRegistry.js";
8
+ import { bytesDigest, confined, digest, readBounded } from "./util.js";
9
+ const forbidden = new Set([".git", ".rafi", ".foreman", ".tickets", "graphify-out", "node_modules", "dist", "build", "coverage", ".agents", ".codex", ".claude", "__pycache__", ".venv", "venv"]);
10
+ const code = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".c", ".h", ".cpp", ".cs", ".rb", ".swift", ".kt", ".vue", ".svelte", ".json", ".sql"]);
11
+ const semantic = new Set([".md", ".mdx", ".txt", ".rst", ".yaml", ".yml", ".toml", ".xml"]);
12
+ export function globMatch(path, pattern) {
13
+ pattern = pattern.replace(/^\//, "");
14
+ const directory = pattern.endsWith("/");
15
+ if (directory)
16
+ pattern = pattern.slice(0, -1);
17
+ let regex = "";
18
+ for (let i = 0; i < pattern.length; i++) {
19
+ const c = pattern[i];
20
+ if (c === "*" && pattern[i + 1] === "*") {
21
+ i++;
22
+ if (pattern[i + 1] === "/") {
23
+ i++;
24
+ regex += "(?:.*/)?";
25
+ }
26
+ else
27
+ regex += ".*";
28
+ }
29
+ else if (c === "*")
30
+ regex += "[^/]*";
31
+ else if (c === "?")
32
+ regex += "[^/]";
33
+ else
34
+ regex += c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35
+ }
36
+ return new RegExp(`^${pattern.includes("/") ? "" : "(?:.*/)?"}${regex}${directory ? "(?:/.*)?" : ""}$`).test(path);
37
+ }
38
+ export function graphExclusions(workspace, inputPaths = []) {
39
+ const path = join(workspace, ".graphifyignore");
40
+ const text = existsSync(path) ? readBounded(path, 128 * 1024).toString("utf8") : "";
41
+ const ignorePaths = new Set([".gitignore"]);
42
+ for (const input of inputPaths) {
43
+ const parts = input.split("/");
44
+ parts.pop();
45
+ for (let i = 1; i <= parts.length; i++)
46
+ ignorePaths.add(`${parts.slice(0, i).join("/")}/.gitignore`);
47
+ }
48
+ const gitIgnores = [...ignorePaths].sort().filter(p => existsSync(join(workspace, p))).map(p => ({ path: p, text: readBounded(confined(workspace, p), 128 * 1024).toString("utf8") }));
49
+ return { text: JSON.stringify({ graphify: text, gitIgnores }), patterns: text.split(/\r?\n/) };
50
+ }
51
+ export function graphPathAllowed(path, config, patterns) {
52
+ if (!path || path.startsWith("/") || path.includes("\\") || path.split("/").some(p => p === ".." || forbidden.has(p)))
53
+ return false;
54
+ const base = path.split("/").at(-1);
55
+ if (/^(\.env(?:\.|$)|credentials(?:\.|$)|id_(rsa|ed25519)|\.npmrc$)|\.(pem|key|p12|pfx)$/i.test(base))
56
+ return false;
57
+ if (!config.config?.include.some(p => globMatch(path, p)))
58
+ return false;
59
+ // Preserve escaping, anchored patterns, character classes and excluded-parent
60
+ // semantics. Trimming lines before parsing changes Git's meaning.
61
+ return !ignore({ ignorecase: false }).add(patterns).ignores(path)
62
+ && !config.config.exclude.some(p => globMatch(path, p));
63
+ }
64
+ export function captureGraphCorpus(workspace, configRoot, config, sourceRef = "unknown") {
65
+ if (!config.enabled)
66
+ throw new Error(`Graph is ${config.reason}`);
67
+ const root = realpathSync(workspace), started = Date.now();
68
+ let exclusions = graphExclusions(root);
69
+ const check = () => {
70
+ if (Date.now() - started > config.limits.inventoryMs)
71
+ throw new Error("Graph inventory deadline exceeded");
72
+ };
73
+ let paths;
74
+ let gitRepository = true;
75
+ try {
76
+ paths = execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, timeout: config.limits.inventoryMs, maxBuffer: 16 * 1024 * 1024, encoding: "utf8" }).split("\0").filter(Boolean);
77
+ }
78
+ catch (error) {
79
+ if (!String(error.stderr ?? error).includes("not a git repository"))
80
+ throw error;
81
+ gitRepository = false;
82
+ paths = [];
83
+ const walk = (prefix, rules = []) => {
84
+ const ignorePath = join(root, prefix, ".gitignore");
85
+ if (existsSync(ignorePath))
86
+ rules = [...rules, { prefix, matcher: ignore({ ignorecase: false }).add(readBounded(confined(root, prefix ? `${prefix}/.gitignore` : ".gitignore"), 128 * 1024).toString("utf8")) }];
87
+ check();
88
+ for (const e of readdirSync(join(root, prefix), { withFileTypes: true })) {
89
+ const p = prefix ? `${prefix}/${e.name}` : e.name;
90
+ if (forbidden.has(e.name) || e.isSymbolicLink())
91
+ continue;
92
+ let excluded = false;
93
+ for (const rule of rules) {
94
+ const result = rule.matcher.test((rule.prefix ? p.slice(rule.prefix.length + 1) : p) + (e.isDirectory() ? "/" : ""));
95
+ if (result.ignored)
96
+ excluded = true;
97
+ else if (result.unignored)
98
+ excluded = false;
99
+ }
100
+ if (excluded)
101
+ continue;
102
+ if (e.isDirectory())
103
+ walk(p, rules);
104
+ else if (e.isFile())
105
+ paths.push(p);
106
+ if (paths.length > config.limits.maxFiles * 4)
107
+ throw new Error("Graph scope-decision-required: inventory too large");
108
+ }
109
+ };
110
+ walk("");
111
+ }
112
+ exclusions = graphExclusions(root, paths.filter(p => graphPathAllowed(p, config, exclusions.patterns)));
113
+ if (gitRepository) {
114
+ const ignored = spawnSync("git", ["check-ignore", "--no-index", "-z", "--stdin"], { cwd: root, input: paths.join("\0") + (paths.length ? "\0" : ""), encoding: "utf8", timeout: Math.max(1, config.limits.inventoryMs - (Date.now() - started)), maxBuffer: 16 * 1024 * 1024 });
115
+ if (ignored.error)
116
+ throw ignored.error;
117
+ if (ignored.status === 0) {
118
+ const excluded = new Set(ignored.stdout.split("\0"));
119
+ paths = paths.filter(p => !excluded.has(p));
120
+ }
121
+ else if (ignored.status !== 1 && !String(ignored.stderr).includes("not a git repository"))
122
+ throw new Error(`Git exclusion check failed: ${ignored.stderr}`);
123
+ }
124
+ const inputs = [], bytes = new Map();
125
+ let total = 0, words = 0;
126
+ const add = (path, content, mode, kind, extra = {}) => {
127
+ total += content.length;
128
+ // Incrementally count without allocating an array proportional to the input.
129
+ for (const _ of content.toString("utf8").matchAll(/\S+/gu)) {
130
+ if (++words > config.limits.maxWords)
131
+ throw new Error("Graph scope-decision-required: adopted word limit exceeded");
132
+ }
133
+ if (inputs.length >= config.limits.maxFiles || total > config.limits.maxInputBytes)
134
+ throw new Error("Graph scope-decision-required: adopted inventory limits exceeded");
135
+ inputs.push({ path, digest: bytesDigest(content), bytes: content.length, mode, kind, ...extra });
136
+ bytes.set(path, content);
137
+ };
138
+ for (const path of [...new Set(paths)].sort()) {
139
+ check();
140
+ if (!graphPathAllowed(path, config, exclusions.patterns))
141
+ continue;
142
+ const extension = extname(path).toLowerCase();
143
+ if (!code.has(extension) && !semantic.has(extension))
144
+ continue;
145
+ const full = join(root, path);
146
+ if (!existsSync(full))
147
+ continue; // tracked deletion
148
+ if (lstatSync(full).isSymbolicLink())
149
+ throw new Error(`Graph symlink input is unsupported: ${path}`);
150
+ const safe = confined(root, path), stat = lstatSync(safe);
151
+ if (!stat.isFile())
152
+ continue;
153
+ const content = readBounded(safe, Math.min(config.limits.maxInputBytes, 8 * 1024 * 1024));
154
+ if (content.includes(0))
155
+ continue;
156
+ add(path, content, stat.mode & 0o777, code.has(extension) ? "code" : "semantic");
157
+ }
158
+ const registry = loadSourceRegistry(configRoot).registry;
159
+ for (const id of config.config.sourceIds) {
160
+ check();
161
+ const entry = registry.entries.find(e => e.id === id);
162
+ if (!entry || !entry.active)
163
+ throw new Error(`Registered graph source is absent or revoked: ${id}`);
164
+ const pin = config.config.sourceVersions?.[id];
165
+ const version = pin ? entry.versions.find(v => v.fingerprint === pin) : undefined;
166
+ if (!version)
167
+ throw new Error(`Registered graph source has no captured version: ${id}`);
168
+ const content = readBounded(confined(configRoot, version.snapshot_path), 8 * 1024 * 1024);
169
+ if (bytesDigest(content) !== version.fingerprint)
170
+ throw new Error(`Registered graph source capture checksum mismatch: ${id}@${version.fingerprint}`);
171
+ const key = `registered-sources/${digest("source-key", id)}.md`;
172
+ add(key, content, 0o444, "semantic", { sourceId: id, sourceVersion: version.fingerprint, provenance: { classification: "registered-reference", authority: "reference-only; approval not inferred", label: entry.label, sourceType: entry.type, capturedAt: version.captured_at } });
173
+ }
174
+ inputs.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
175
+ const exclusionsDigest = digest("exclusions", graphExclusions(root, inputs.filter(i => !i.sourceId).map(i => i.path)).text);
176
+ const corpusDigest = digest("corpus", { inputs, policy: config.policyDigest, exclusionsDigest, package: "0.9.82", bridge: 1 });
177
+ const stat = lstatSync(root);
178
+ const identityReader = new WorkflowReader(configRoot);
179
+ let hostProjectRef;
180
+ try {
181
+ hostProjectRef = identityReader.graphHostProjectIdentity();
182
+ }
183
+ finally {
184
+ identityReader.close();
185
+ }
186
+ const binding = { version: 1, projectRef: config.projectRef, hostProjectRef, hostWorkspaceRef: readWorkspaceIdentity(root), workspaceRef: digest("workspace", { root, dev: stat.dev, ino: stat.ino, birthtime: stat.birthtimeMs }), sourceRef, corpusDigest, policyDigest: config.policyDigest };
187
+ return { inputs, bytes, corpusDigest, binding, exclusionsDigest };
188
+ }
@@ -0,0 +1,17 @@
1
+ import type Database from "better-sqlite3";
2
+ export interface GraphDerivedAccess {
3
+ policyDigest: string;
4
+ workspace: string;
5
+ exclusionsDigest: string;
6
+ /** Independent canonical-project exclusion baseline for worktree evidence. */
7
+ projectExclusionsDigest?: string;
8
+ sourceVersions: Record<string, string>;
9
+ paths: string[];
10
+ }
11
+ /** Carries provenance through asynchronous provider callbacks without tainting concurrent turns. */
12
+ export declare function withGraphDerivedAccess<T>(access: GraphDerivedAccess | GraphDerivedAccess[] | undefined, action: () => T): T;
13
+ export declare function currentGraphDerivedAccess(): GraphDerivedAccess | GraphDerivedAccess[] | undefined;
14
+ /** Expand an owning operation's provenance as it consumes retained evidence. */
15
+ export declare function inheritGraphDerivedAccess(access: GraphDerivedAccess[]): void;
16
+ export declare function registerGraphDerived(db: Database.Database, bytes: string | Buffer, access: GraphDerivedAccess | GraphDerivedAccess[]): void;
17
+ export declare function graphDerivedAllowed(db: Database.Database, root: string, contentDigest: string): boolean;
@@ -0,0 +1,73 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { existsSync, realpathSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { parse } from "yaml";
5
+ import { bytesDigest, digest, readBounded, canonical } from "./util.js";
6
+ import { graphExclusions } from "./corpus.js";
7
+ import { readGraphRecord } from "./storage.js";
8
+ const deliveryAccess = new AsyncLocalStorage();
9
+ /** Carries provenance through asynchronous provider callbacks without tainting concurrent turns. */
10
+ export function withGraphDerivedAccess(access, action) {
11
+ return access ? deliveryAccess.run(access, action) : action();
12
+ }
13
+ export function currentGraphDerivedAccess() {
14
+ const access = deliveryAccess.getStore();
15
+ return Array.isArray(access) && access.length === 0 ? undefined : access;
16
+ }
17
+ /** Expand an owning operation's provenance as it consumes retained evidence. */
18
+ export function inheritGraphDerivedAccess(access) {
19
+ const current = deliveryAccess.getStore();
20
+ if (!Array.isArray(current))
21
+ return;
22
+ for (const grant of access)
23
+ if (!current.some(item => canonical(item) === canonical(grant)))
24
+ current.push(grant);
25
+ }
26
+ export function registerGraphDerived(db, bytes, access) {
27
+ if (Array.isArray(access) && access.length === 0)
28
+ return;
29
+ db.exec("CREATE TABLE IF NOT EXISTS graph_derived_refs(digest TEXT PRIMARY KEY,access_json TEXT NOT NULL)");
30
+ const id = bytesDigest(bytes);
31
+ const previous = db.prepare("SELECT access_json FROM graph_derived_refs WHERE digest=?").get(id);
32
+ const parsed = previous ? JSON.parse(previous.access_json) : [];
33
+ const grants = Array.isArray(parsed) ? parsed : [parsed];
34
+ for (const item of Array.isArray(access) ? access : [access])
35
+ if (!grants.some(grant => canonical(grant) === canonical(item)))
36
+ grants.push(item);
37
+ db.prepare("INSERT INTO graph_derived_refs(digest,access_json) VALUES(?,?) ON CONFLICT(digest) DO UPDATE SET access_json=excluded.access_json").run(id, JSON.stringify(grants));
38
+ }
39
+ export function graphDerivedAllowed(db, root, contentDigest) {
40
+ if (!db.prepare("SELECT 1 FROM sqlite_master WHERE name='graph_derived_refs'").get())
41
+ return true;
42
+ const row = db.prepare("SELECT access_json FROM graph_derived_refs WHERE digest=?").get(contentDigest);
43
+ if (!row)
44
+ return true;
45
+ try {
46
+ const decoded = JSON.parse(row.access_json);
47
+ if (decoded?.transferWithheld === true)
48
+ return false;
49
+ const parsed = decoded;
50
+ const grants = Array.isArray(parsed) ? parsed : [parsed];
51
+ return grants.length > 0 && grants.every(access => {
52
+ const path = ["rafi-config.yaml", "project.yaml"].map(p => join(root, p)).find(existsSync);
53
+ const raw = path ? parse(readBounded(path, 4 * 1024 * 1024).toString("utf8")) : undefined;
54
+ const adoption = readGraphRecord(db, "adoption", "project")?.value;
55
+ const config = raw?.graph ?? adoption?.config;
56
+ if (!config?.enabled || !adoption || adoption.projectRef !== digest("project", realpathSync(root)) || digest("policy", config) !== access.policyDigest || adoption.policyDigest !== access.policyDigest)
57
+ return false;
58
+ if (!existsSync(access.workspace))
59
+ return false;
60
+ if (digest("exclusions", graphExclusions(access.workspace, access.paths).text) !== access.exclusionsDigest)
61
+ return false;
62
+ if (access.projectExclusionsDigest && digest("exclusions", graphExclusions(root, access.paths).text) !== access.projectExclusionsDigest)
63
+ return false;
64
+ for (const [id, version] of Object.entries(access.sourceVersions))
65
+ if (!raw?.sources?.entries?.some((e) => e.id === id && e.active && e.versions.some(v => v.fingerprint === version)))
66
+ return false;
67
+ return true;
68
+ });
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
@@ -0,0 +1,4 @@
1
+ import type { BuilderAdapter, BuilderAdapterOptions } from "../adapters/types.js";
2
+ import type { GraphSemanticHost } from "./maintenance.js";
3
+ /** Fresh packet-only worker, using the selected host's exact runtime settings. */
4
+ export declare function graphSemanticHost(root: string, host: BuilderAdapter, createWorker?: (options: BuilderAdapterOptions) => Promise<BuilderAdapter>): GraphSemanticHost | undefined;
@@ -0,0 +1,160 @@
1
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ClaudeAdapter } from "../adapters/claude.js";
4
+ import { CodexAdapter } from "../adapters/codex.js";
5
+ import { WorkflowDb } from "../workflowDb.js";
6
+ import { parseSemanticResult, semanticExchange, SEMANTIC_INSTRUCTIONS } from "./semantic.js";
7
+ import { canonical, confined, digest, bytesDigest } from "./util.js";
8
+ /** Fresh packet-only worker, using the selected host's exact runtime settings. */
9
+ export function graphSemanticHost(root, host, createWorker = async (options) => host.agent === "codex" ? new CodexAdapter(options) : ClaudeAdapter.create(options)) {
10
+ const settings = host.graphRuntimeSettings?.();
11
+ if (!settings)
12
+ return undefined;
13
+ return async (chunks, operationId, budget) => {
14
+ const result = { nodes: [], edges: [], origin: { provider: host.agent, model: settings.model, operationId } };
15
+ const deadline = budget?.deadline ?? Date.now() + 300000;
16
+ const check = () => {
17
+ if (budget?.signal?.aborted)
18
+ throw new Error("Semantic extraction cancelled");
19
+ if (Date.now() >= deadline)
20
+ throw new Error("Semantic extraction deadline exceeded");
21
+ };
22
+ let outputBytes = 0;
23
+ const accountOutput = (text) => {
24
+ outputBytes += Buffer.byteLength(text);
25
+ if (outputBytes > 120000)
26
+ throw new Error("Semantic output budget exceeded");
27
+ if (Buffer.byteLength(text) > 16000)
28
+ throw new Error("Semantic chunk output budget exceeded");
29
+ };
30
+ if (chunks.reduce((n, c) => n + Buffer.byteLength(c.text), 0) > 1000000)
31
+ throw new Error("Semantic input budget exceeds 250k conservatively estimated tokens");
32
+ const groups = [];
33
+ for (const chunk of chunks) {
34
+ const size = Buffer.byteLength(chunk.text);
35
+ if (size > 64000)
36
+ throw new Error("scope-decision-required: semantic file exceeds chunk budget");
37
+ const last = groups.at(-1);
38
+ if (last && last.reduce((n, c) => n + Buffer.byteLength(c.text), 0) + size <= 64000)
39
+ last.push(chunk);
40
+ else
41
+ groups.push([chunk]);
42
+ }
43
+ for (let index = 0; index < groups.length; index++) {
44
+ check();
45
+ // Whole input files remain attributable; oversize files require explicit
46
+ // narrower adoption instead of silently truncating their semantic input.
47
+ const request = semanticExchange(groups[index], `${operationId}:chunk:${index}`);
48
+ const id = digest("semantic-dispatch", request);
49
+ const db = new WorkflowDb(root);
50
+ try {
51
+ const store = db.graphStore(), prior = store.get("job", id)?.value;
52
+ if (prior) {
53
+ if (prior.state !== "completed" || !prior.result)
54
+ throw new Error("Uncertain semantic dispatch requires explicit reconciliation; refusing replay");
55
+ outputBytes += prior.outputBytes ?? Buffer.byteLength(canonical(prior.result));
56
+ if (outputBytes > 120000)
57
+ throw new Error("Semantic output budget exceeded");
58
+ result.nodes.push(...prior.result.nodes);
59
+ result.edges.push(...prior.result.edges);
60
+ continue;
61
+ }
62
+ const scratchRoot = join(root, "graphify-out", "rafi", "semantic-sessions");
63
+ mkdirSync(scratchRoot, { recursive: true });
64
+ confined(root, "graphify-out/rafi/semantic-sessions");
65
+ const scratch = mkdtempSync(join(scratchRoot, "worker-"));
66
+ let worker;
67
+ let closing;
68
+ const close = () => {
69
+ // Do not memoize an empty close while asynchronous creation is pending.
70
+ // A worker arriving after the deadline still needs its own teardown.
71
+ if (!worker)
72
+ return Promise.resolve();
73
+ const target = worker;
74
+ return closing ??= Promise.resolve().then(() => target.close()).then(() => undefined).catch(() => undefined);
75
+ };
76
+ let stop;
77
+ const stopped = new Promise((_, reject) => { stop = reject; });
78
+ // The provider may fail to settle its turn even after close. The host's
79
+ // absolute deadline independently bounds the awaited dispatch.
80
+ const cancel = () => {
81
+ stop(new Error(budget?.signal?.aborted ? "Semantic extraction cancelled" : "Semantic extraction deadline exceeded"));
82
+ if (worker)
83
+ void close();
84
+ };
85
+ const timer = setTimeout(cancel, Math.max(1, deadline - Date.now()));
86
+ budget?.signal?.addEventListener("abort", cancel, { once: true });
87
+ try {
88
+ const opts = { ...settings, cwd: scratch, configRoot: root, sessionRole: "planner", sessionStream: "graph-semantic", sandboxMode: "read-only", approvalPolicy: "never", networkAccess: false, systemPromptAppend: SEMANTIC_INSTRUCTIONS, permission: async () => ({ behavior: "deny", message: "Graph semantic extraction accepts only the captured packet; all tools denied" }), turnDeadlineMs: Math.max(1, deadline - Date.now()) };
89
+ worker = await Promise.race([createWorker(opts).then(created => {
90
+ worker = created;
91
+ if (budget?.signal?.aborted || Date.now() >= deadline)
92
+ void close();
93
+ return created;
94
+ }), stopped]);
95
+ check();
96
+ store.put("job", id, { version: 1, state: "dispatch-intended", operationId, requestId: request.operationId, inputDigest: request.inputDigest, provider: host.agent, model: settings.model });
97
+ const chunkOutputStart = outputBytes;
98
+ const observations = [];
99
+ const recordObservation = (turn) => {
100
+ observations.push(turn);
101
+ store.put("job", id, { version: 1, state: "returned-unvalidated", operationId, requestId: request.operationId,
102
+ inputDigest: request.inputDigest, sessionId: worker?.sessionId(),
103
+ observations: observations.map(t => ({ turnId: t.turnId, usage: t.usage, costUsd: t.costAuthoritative ? t.costUsd : undefined,
104
+ responseDigest: bytesDigest(t.rawResponse ?? t.text), failed: Boolean(t.isError || t.failure) })) });
105
+ };
106
+ const turn = await Promise.race([worker.sendTurn(canonical(request), { responseOnly: true, handback: true, logicalActionId: request.operationId }), stopped]);
107
+ recordObservation(turn);
108
+ check();
109
+ accountOutput(turn.cleanedResponse ?? turn.text);
110
+ if (turn.isError || turn.failure)
111
+ throw new Error(`Semantic provider failed: ${turn.text.slice(0, 500)}`);
112
+ let parsed;
113
+ try {
114
+ parsed = parseSemanticResult(turn.cleanedResponse ?? turn.text, request);
115
+ }
116
+ catch (error) {
117
+ check();
118
+ store.put("job", id, { version: 1, state: "repair-intended", operationId, requestId: request.operationId, inputDigest: request.inputDigest, initialResponseDigest: bytesDigest(turn.rawResponse ?? turn.text), observations: observations.map(t => ({ turnId: t.turnId, usage: t.usage, costUsd: t.costAuthoritative ? t.costUsd : undefined })) });
119
+ const repaired = await Promise.race([worker.sendTurn(`Format correction only. Preserve your source observations; no tools or new extraction. Return strict JSON matching ${canonical({ operationId: request.operationId, inputDigest: request.inputDigest })}. Validation: ${String(error)}`, { responseOnly: true, handback: true, logicalActionId: request.operationId }), stopped]);
120
+ recordObservation(repaired);
121
+ check();
122
+ accountOutput(repaired.cleanedResponse ?? repaired.text);
123
+ if (repaired.isError || repaired.failure)
124
+ throw new Error("Semantic format correction failed; reconciliation required");
125
+ parsed = parseSemanticResult(repaired.cleanedResponse ?? repaired.text, request);
126
+ }
127
+ const known = observations.every(t => t.usage?.scope === "turn-delta" && t.usage.inputTokens !== undefined && t.usage.outputTokens !== undefined);
128
+ const authoritative = observations.every(t => t.costAuthoritative === true);
129
+ store.put("job", id, { version: 1, state: "completed", operationId, sessionId: worker.sessionId(),
130
+ turnIds: observations.map(t => t.turnId), result: parsed, outputBytes: outputBytes - chunkOutputStart,
131
+ usage: { inputTokens: known ? observations.reduce((n, t) => n + t.usage.inputTokens, 0) : undefined,
132
+ outputTokens: known ? observations.reduce((n, t) => n + t.usage.outputTokens, 0) : undefined,
133
+ costUsd: authoritative ? observations.reduce((n, t) => n + t.costUsd, 0) : undefined,
134
+ observations: observations.map(t => ({ turnId: t.turnId, usage: t.usage, costUsd: t.costAuthoritative ? t.costUsd : undefined })) } });
135
+ result.nodes.push(...parsed.nodes);
136
+ result.edges.push(...parsed.edges);
137
+ }
138
+ finally {
139
+ clearTimeout(timer);
140
+ budget?.signal?.removeEventListener("abort", cancel);
141
+ let closeTimer;
142
+ try {
143
+ await Promise.race([close(), new Promise(resolve => { closeTimer = setTimeout(resolve, 1000); })]);
144
+ }
145
+ finally {
146
+ if (closeTimer)
147
+ clearTimeout(closeTimer);
148
+ }
149
+ rmSync(scratch, { recursive: true, force: true });
150
+ }
151
+ }
152
+ finally {
153
+ db.close();
154
+ }
155
+ if (Date.now() > deadline)
156
+ throw new Error("Semantic extraction deadline exceeded");
157
+ }
158
+ return result;
159
+ };
160
+ }
@@ -0,0 +1,3 @@
1
+ export declare function ownedGraphPython(root: string): string;
2
+ /** Installation is called only after an explicit adoption/setup grant. */
3
+ export declare function ensureGraphInstallation(root: string): Promise<string>;
@@ -0,0 +1,50 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { GRAPHIFY_VERSION } from "rafi-spec";
6
+ import { resolveGraphPython, runBridge } from "./bridge.js";
7
+ import { confined } from "./util.js";
8
+ function run(executable, args, deadline) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(executable, args, { stdio: ["ignore", "ignore", "pipe"], windowsHide: true, env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot, WINDIR: process.env.WINDIR, TEMP: process.env.TEMP, TMP: process.env.TMP } });
11
+ let diagnostics = "";
12
+ const timer = setTimeout(() => { child.kill(); reject(new Error("Graph installation deadline exceeded")); }, Math.max(1, deadline - Date.now()));
13
+ child.stderr.on("data", chunk => {
14
+ if (diagnostics.length < 8000)
15
+ diagnostics += String(chunk).slice(0, 8000 - diagnostics.length);
16
+ });
17
+ child.on("error", error => { clearTimeout(timer); reject(error); });
18
+ child.on("close", code => { clearTimeout(timer); code === 0 ? resolve() : reject(new Error(`Graph environment setup failed (${code}): ${diagnostics}`)); });
19
+ });
20
+ }
21
+ export function ownedGraphPython(root) { return join(root, "graphify-out", "rafi", "runtime", process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python"); }
22
+ /** Installation is called only after an explicit adoption/setup grant. */
23
+ export async function ensureGraphInstallation(root) {
24
+ try {
25
+ return await resolveGraphPython();
26
+ }
27
+ catch { /* Preserve incompatible/shared installations. */ }
28
+ const existing = ownedGraphPython(root);
29
+ if (existsSync(existing)) {
30
+ await runBridge(existing, { action: "probe" }, { timeoutMs: 5000 });
31
+ return existing;
32
+ }
33
+ const base = join(root, "graphify-out", "rafi");
34
+ mkdirSync(base, { recursive: true });
35
+ confined(root, "graphify-out/rafi");
36
+ const temporary = join(base, `runtime-${randomUUID()}`), deadline = Date.now() + 120000;
37
+ const python = process.platform === "win32" ? "python" : "python3";
38
+ try {
39
+ await run(python, ["-I", "-m", "venv", temporary], deadline);
40
+ const isolated = join(temporary, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
41
+ await run(isolated, ["-I", "-m", "pip", "--isolated", "install", "--disable-pip-version-check", "--no-input", `graphifyy==${GRAPHIFY_VERSION}`], deadline);
42
+ await runBridge(isolated, { action: "probe" }, { timeoutMs: 5000 });
43
+ renameSync(temporary, join(base, "runtime"));
44
+ return existing;
45
+ }
46
+ catch (error) {
47
+ rmSync(temporary, { recursive: true, force: true });
48
+ throw new Error(`Graphify enabled-unavailable. Install a supported Python with venv/pip, then retry graph adopt. ${String(error)}`);
49
+ }
50
+ }
@@ -0,0 +1,11 @@
1
+ /** Loaded readers own in-memory bytes; durable consumers explicitly retain IDs. */
2
+ export declare function retainGraphGeneration(root: string, generationId: string, owner: string): void;
3
+ export declare function pruneGraph(root: string): {
4
+ removed: string[];
5
+ retainedBytes: number;
6
+ limited: boolean;
7
+ };
8
+ export declare function graphStorageBytes(root: string): number;
9
+ /** A checkout boundary invalidates its mutable head, never its immutable history.
10
+ * This is metadata-only: a merge or recreation does not itself authorize extraction. */
11
+ export declare function invalidateGraphWorkspace(root: string, workspace: string, reason: "merge-or-rebase" | "worktree-created" | "worktree-removed"): void;
@@ -0,0 +1,124 @@
1
+ import { graphScope } from "./read.js";
2
+ import { existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { WorkflowDb } from "../workflowDb.js";
5
+ import { loadGraphConfig } from "./config.js";
6
+ import { confined } from "./util.js";
7
+ /** Loaded readers own in-memory bytes; durable consumers explicitly retain IDs. */
8
+ export function retainGraphGeneration(root, generationId, owner) {
9
+ const db = new WorkflowDb(root);
10
+ try {
11
+ db.graphStore().put("retention", owner, { generationId, owner, createdAt: new Date().toISOString() });
12
+ }
13
+ finally {
14
+ db.close();
15
+ }
16
+ }
17
+ export function pruneGraph(root) {
18
+ const config = loadGraphConfig(root), base = join(root, "graphify-out", "rafi");
19
+ if (!existsSync(base))
20
+ return { removed: [], retainedBytes: 0, limited: false };
21
+ confined(root, "graphify-out/rafi");
22
+ const db = new WorkflowDb(root);
23
+ try {
24
+ const store = db.graphStore();
25
+ const protectedIds = new Set([...store.list("head"), ...store.list("retention")].map(x => x.value.generationId));
26
+ // Active/recoverable run receipts pin their navigation generations. A graph
27
+ // cannot become required QA authority, but pruning must respect live users.
28
+ for (const receipt of store.list("receipt")) {
29
+ const run = receipt.value.runId ? db.getRun(receipt.value.runId) : undefined;
30
+ if (run && !["completed", "cancelled"].includes(run.status))
31
+ for (const id of receipt.value.generationIds ?? [])
32
+ protectedIds.add(id);
33
+ }
34
+ const staging = join(base, "staging");
35
+ if (existsSync(staging)) {
36
+ confined(root, "graphify-out/rafi/staging");
37
+ for (const name of readdirSync(staging)) {
38
+ const match = /^([a-f0-9]{64})-(\d+)$/.exec(name);
39
+ if (!match)
40
+ continue;
41
+ const job = store.get("job", match[1])?.value;
42
+ if (job?.scope && Date.now() - lstatSync(join(staging, name)).mtimeMs >= 86400000 && !store.ownsLease(job.scope, match[1], Number(match[2]), Date.now()))
43
+ rmSync(confined(staging, name), { recursive: true });
44
+ }
45
+ }
46
+ const dir = join(base, "generations"), removed = [];
47
+ let retainedBytes = 0;
48
+ if (!existsSync(dir))
49
+ return { removed, retainedBytes, limited: false };
50
+ confined(root, "graphify-out/rafi/generations");
51
+ let visited = 0;
52
+ const inventoryStarted = Date.now();
53
+ const size = (path) => {
54
+ if (++visited > 200000 || Date.now() - inventoryStarted > 5000)
55
+ throw new Error("Graph cleanup inventory limit exceeded");
56
+ const st = lstatSync(path);
57
+ if (st.isSymbolicLink())
58
+ throw new Error("Refusing graph cleanup through symbolic link");
59
+ return st.isDirectory() ? readdirSync(path).reduce((n, name) => n + size(join(path, name)), 0) : st.size;
60
+ };
61
+ const generations = store.list("generation").sort((a, b) => b.value.createdAt.localeCompare(a.value.createdAt));
62
+ // Only known immutable Rafi generations can be deleted. Never follow an
63
+ // arbitrary user-provided cache path or remove shared Python installations.
64
+ for (const row of generations.filter(r => protectedIds.has(r.id))) {
65
+ if (existsSync(join(dir, row.id)))
66
+ retainedBytes += size(confined(dir, row.id));
67
+ }
68
+ const counts = new Map();
69
+ for (const row of generations.filter(r => !protectedIds.has(r.id))) {
70
+ if (!/^[a-f0-9]{64}$/.test(row.id) || !existsSync(join(dir, row.id)))
71
+ continue;
72
+ const path = confined(dir, row.id), bytes = size(path);
73
+ const scope = row.value.binding.workspaceRef, count = counts.get(scope) ?? 0;
74
+ counts.set(scope, count + 1);
75
+ if (count < 3 && Date.now() - Date.parse(row.value.createdAt) < 7 * 86400000 && retainedBytes + bytes <= config.limits.storageBytes) {
76
+ retainedBytes += bytes;
77
+ continue;
78
+ }
79
+ rmSync(path, { recursive: true });
80
+ removed.push(row.id);
81
+ store.put("revocation", `cache:${row.id}`, { generationId: row.id, reason: "cache-pruned", at: new Date().toISOString() });
82
+ }
83
+ return { removed, retainedBytes, limited: retainedBytes > config.limits.storageBytes };
84
+ }
85
+ finally {
86
+ db.close();
87
+ }
88
+ }
89
+ export function graphStorageBytes(root) {
90
+ const base = join(root, "graphify-out", "rafi");
91
+ if (!existsSync(base))
92
+ return 0;
93
+ confined(root, "graphify-out/rafi");
94
+ let entries = 0;
95
+ const started = Date.now();
96
+ const walk = (path) => {
97
+ if (++entries > 200000 || Date.now() - started > 5000)
98
+ throw new Error("Graph storage inventory limit exceeded");
99
+ const st = lstatSync(path);
100
+ if (st.isSymbolicLink())
101
+ throw new Error("Graph storage contains an unsupported symbolic link");
102
+ return st.isDirectory() ? readdirSync(path).reduce((n, name) => n + walk(join(path, name)), 0) : st.size;
103
+ };
104
+ return ["generations", "staging", "semantic-sessions"].reduce((n, name) => n + (existsSync(join(base, name)) ? walk(join(base, name)) : 0), 0);
105
+ }
106
+ /** A checkout boundary invalidates its mutable head, never its immutable history.
107
+ * This is metadata-only: a merge or recreation does not itself authorize extraction. */
108
+ export function invalidateGraphWorkspace(root, workspace, reason) {
109
+ const config = loadGraphConfig(root);
110
+ if (!config.enabled || !existsSync(workspace))
111
+ return;
112
+ const scope = graphScope(workspace);
113
+ const db = new WorkflowDb(root);
114
+ try {
115
+ const store = db.graphStore(), head = store.get("head", scope);
116
+ if (!head)
117
+ return;
118
+ store.remove("head", scope);
119
+ store.put("revocation", `workspace:${scope}`, { reason, generationId: head.value.generationId, at: new Date().toISOString(), limitation: "Checkout changed; capture and rebind before current-source reuse" });
120
+ }
121
+ finally {
122
+ db.close();
123
+ }
124
+ }