ai-foreman 1.7.19 → 1.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +29 -5
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +18 -6
  4. package/dist/adapters/claude.d.ts +16 -0
  5. package/dist/adapters/claude.js +28 -3
  6. package/dist/adapters/codex.d.ts +20 -0
  7. package/dist/adapters/codex.js +99 -5
  8. package/dist/adapters/recovering.d.ts +9 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/types.d.ts +15 -0
  11. package/dist/agentRun.d.ts +7 -0
  12. package/dist/agentRun.js +24 -13
  13. package/dist/branch/currentGuard.d.ts +1 -0
  14. package/dist/branch/currentGuard.js +1 -0
  15. package/dist/branch/finalization.js +6 -1
  16. package/dist/branch/git.d.ts +1 -0
  17. package/dist/branch/git.js +15 -1
  18. package/dist/branch/runner.d.ts +2 -0
  19. package/dist/branch/runner.js +92 -5
  20. package/dist/buildAdmission.d.ts +61 -0
  21. package/dist/buildAdmission.js +286 -0
  22. package/dist/buildApproval.d.ts +2 -0
  23. package/dist/buildApproval.js +17 -0
  24. package/dist/buildAssignment.d.ts +33 -0
  25. package/dist/buildAssignment.js +215 -0
  26. package/dist/buildInterventions.d.ts +90 -0
  27. package/dist/buildInterventions.js +334 -0
  28. package/dist/buildLaunchGate.d.ts +1 -0
  29. package/dist/buildLaunchGate.js +36 -0
  30. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  31. package/dist/buildOwnershipReconciliation.js +41 -0
  32. package/dist/buildOwnershipRepair.d.ts +33 -0
  33. package/dist/buildOwnershipRepair.js +111 -0
  34. package/dist/buildRuns.d.ts +24 -1
  35. package/dist/buildRuns.js +216 -70
  36. package/dist/buildSyntheticWork.d.ts +2 -0
  37. package/dist/buildSyntheticWork.js +30 -0
  38. package/dist/buildWorkAdmission.d.ts +17 -0
  39. package/dist/buildWorkAdmission.js +57 -0
  40. package/dist/buildWorkAuthorization.d.ts +2 -0
  41. package/dist/buildWorkAuthorization.js +23 -0
  42. package/dist/buildWorkContext.d.ts +3 -0
  43. package/dist/buildWorkContext.js +6 -0
  44. package/dist/buildWorkMigration.d.ts +9 -0
  45. package/dist/buildWorkMigration.js +206 -0
  46. package/dist/buildWorkReconciliation.d.ts +12 -0
  47. package/dist/buildWorkReconciliation.js +70 -0
  48. package/dist/builderGuidanceFollowup.d.ts +15 -0
  49. package/dist/builderGuidanceFollowup.js +34 -0
  50. package/dist/cli/doctor.js +3 -2
  51. package/dist/cli/graph.d.ts +2 -0
  52. package/dist/cli/graph.js +75 -0
  53. package/dist/cli/manager.d.ts +8 -0
  54. package/dist/cli/manager.js +234 -12
  55. package/dist/cli/recovery.js +3 -2
  56. package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
  57. package/dist/cli/runtimeAuthPrompt.js +41 -7
  58. package/dist/cli/start.d.ts +8 -0
  59. package/dist/cli/start.js +2091 -1849
  60. package/dist/cli/status.js +20 -5
  61. package/dist/cli/tickets.js +9 -7
  62. package/dist/continuity.d.ts +16 -1
  63. package/dist/continuity.js +68 -12
  64. package/dist/foreman.d.ts +12 -4
  65. package/dist/foreman.js +397 -60
  66. package/dist/graph/boundary.d.ts +15 -0
  67. package/dist/graph/boundary.js +50 -0
  68. package/dist/graph/bridge.d.ts +9 -0
  69. package/dist/graph/bridge.js +146 -0
  70. package/dist/graph/bridge.py +205 -0
  71. package/dist/graph/config.d.ts +11 -0
  72. package/dist/graph/config.js +40 -0
  73. package/dist/graph/corpus.d.ts +16 -0
  74. package/dist/graph/corpus.js +188 -0
  75. package/dist/graph/derived.d.ts +17 -0
  76. package/dist/graph/derived.js +73 -0
  77. package/dist/graph/host.d.ts +4 -0
  78. package/dist/graph/host.js +160 -0
  79. package/dist/graph/install.d.ts +3 -0
  80. package/dist/graph/install.js +50 -0
  81. package/dist/graph/lifecycle.d.ts +11 -0
  82. package/dist/graph/lifecycle.js +124 -0
  83. package/dist/graph/maintenance.d.ts +43 -0
  84. package/dist/graph/maintenance.js +301 -0
  85. package/dist/graph/manager.d.ts +9 -0
  86. package/dist/graph/manager.js +92 -0
  87. package/dist/graph/preparation.d.ts +41 -0
  88. package/dist/graph/preparation.js +43 -0
  89. package/dist/graph/read.d.ts +27 -0
  90. package/dist/graph/read.js +152 -0
  91. package/dist/graph/semantic.d.ts +17 -0
  92. package/dist/graph/semantic.js +27 -0
  93. package/dist/graph/session.d.ts +8 -0
  94. package/dist/graph/session.js +92 -0
  95. package/dist/graph/sources.d.ts +4 -0
  96. package/dist/graph/sources.js +11 -0
  97. package/dist/graph/storage.d.ts +38 -0
  98. package/dist/graph/storage.js +159 -0
  99. package/dist/graph/turn.d.ts +17 -0
  100. package/dist/graph/turn.js +360 -0
  101. package/dist/graph/util.d.ts +5 -0
  102. package/dist/graph/util.js +51 -0
  103. package/dist/handoffs.d.ts +1 -0
  104. package/dist/handoffs.js +5 -1
  105. package/dist/humanDecision.d.ts +21 -2
  106. package/dist/humanDecision.js +169 -10
  107. package/dist/index.js +4 -1
  108. package/dist/installOwnership.d.ts +46 -0
  109. package/dist/installOwnership.js +440 -0
  110. package/dist/linuxProbeInventory.d.ts +12 -0
  111. package/dist/linuxProbeInventory.js +48 -0
  112. package/dist/log.d.ts +1 -1
  113. package/dist/log.js +7 -1
  114. package/dist/managerEvidence.d.ts +25 -0
  115. package/dist/managerEvidence.js +326 -0
  116. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  117. package/dist/managerEvidenceArtifacts.js +118 -0
  118. package/dist/managerPacket.d.ts +2 -0
  119. package/dist/managerPacket.js +23 -0
  120. package/dist/markers.d.ts +1 -1
  121. package/dist/markers.js +2 -2
  122. package/dist/observability.js +1 -1
  123. package/dist/processIdentity.d.ts +21 -1
  124. package/dist/processIdentity.js +87 -5
  125. package/dist/providerPhase.d.ts +13 -0
  126. package/dist/providerPhase.js +26 -0
  127. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  128. package/dist/qaAuthorizedDisposition.js +26 -0
  129. package/dist/qaBuildGate.d.ts +4 -0
  130. package/dist/qaBuildGate.js +216 -0
  131. package/dist/qaBuilderCoverage.d.ts +5 -0
  132. package/dist/qaBuilderCoverage.js +78 -0
  133. package/dist/qaContractCoverage.d.ts +10 -0
  134. package/dist/qaContractCoverage.js +55 -0
  135. package/dist/qaContractDelivery.d.ts +23 -0
  136. package/dist/qaContractDelivery.js +145 -0
  137. package/dist/qaContractFreshness.d.ts +15 -0
  138. package/dist/qaContractFreshness.js +36 -0
  139. package/dist/qaDeliveryJournal.d.ts +10 -0
  140. package/dist/qaDeliveryJournal.js +16 -1
  141. package/dist/qaEffectiveConfig.d.ts +20 -0
  142. package/dist/qaEffectiveConfig.js +40 -0
  143. package/dist/qaEvidenceReader.d.ts +14 -0
  144. package/dist/qaEvidenceReader.js +176 -0
  145. package/dist/qaFailureDelivery.d.ts +2 -0
  146. package/dist/qaFailureDelivery.js +107 -10
  147. package/dist/qaHandbackMigration.d.ts +1 -1
  148. package/dist/qaHandbackMigration.js +3 -3
  149. package/dist/qaPreparation.d.ts +26 -0
  150. package/dist/qaPreparation.js +150 -0
  151. package/dist/qaPreparationInputs.d.ts +8 -0
  152. package/dist/qaPreparationInputs.js +47 -0
  153. package/dist/qaPreparationMetrics.d.ts +145 -0
  154. package/dist/qaPreparationMetrics.js +120 -0
  155. package/dist/qaPreparationPolicy.d.ts +5 -0
  156. package/dist/qaPreparationPolicy.js +51 -0
  157. package/dist/qaPreparationStore.d.ts +157 -0
  158. package/dist/qaPreparationStore.js +412 -0
  159. package/dist/qaProtocolV2.d.ts +28 -0
  160. package/dist/qaProtocolV2.js +12 -0
  161. package/dist/qaReview.d.ts +6 -0
  162. package/dist/qaReview.js +293 -41
  163. package/dist/qaTimeline.d.ts +18 -0
  164. package/dist/qaTimeline.js +123 -0
  165. package/dist/qaVerificationContract.d.ts +17 -0
  166. package/dist/qaVerificationContract.js +236 -0
  167. package/dist/readinessCleanup.d.ts +19 -0
  168. package/dist/readinessCleanup.js +122 -0
  169. package/dist/readinessGate.d.ts +1 -0
  170. package/dist/readinessGate.js +82 -0
  171. package/dist/recoveryGuidance.d.ts +17 -0
  172. package/dist/recoveryGuidance.js +38 -0
  173. package/dist/runtimeAuth.d.ts +1 -0
  174. package/dist/runtimeAuth.js +5 -1
  175. package/dist/runtimeReadiness.d.ts +13 -0
  176. package/dist/runtimeReadiness.js +206 -10
  177. package/dist/sessionIdentity.d.ts +2 -0
  178. package/dist/sessionIdentity.js +14 -0
  179. package/dist/sessionLifecycle.js +3 -3
  180. package/dist/sources/sourceRegistry.js +7 -0
  181. package/dist/stateTransfer.js +85 -8
  182. package/dist/statusReporter.d.ts +1 -0
  183. package/dist/statusReporter.js +1 -0
  184. package/dist/supervisedStart.d.ts +6 -0
  185. package/dist/supervisedStart.js +178 -62
  186. package/dist/supervisor.d.ts +2 -0
  187. package/dist/supervisor.js +26 -2
  188. package/dist/terminalStyle.d.ts +21 -0
  189. package/dist/terminalStyle.js +50 -0
  190. package/dist/ticketPopulation.d.ts +1 -0
  191. package/dist/ticketPopulation.js +5 -1
  192. package/dist/tickets/commands.js +2 -2
  193. package/dist/tickets/queue.js +1 -1
  194. package/dist/tickets/ticketSchema.d.ts +198 -0
  195. package/dist/tickets/ticketSchema.js +2 -0
  196. package/dist/windowsProbeJob.d.ts +12 -0
  197. package/dist/windowsProbeJob.js +144 -0
  198. package/dist/workflowDb.d.ts +121 -6
  199. package/dist/workflowDb.js +874 -30
  200. package/dist/workflowReader.d.ts +116 -2
  201. package/dist/workflowReader.js +131 -4
  202. package/package.json +89 -8
  203. package/scripts/copy-graph-assets.mjs +3 -0
  204. package/scripts/graph-benchmark.mjs +20 -0
  205. package/scripts/graph-evaluation.mjs +85 -0
@@ -0,0 +1,440 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmodSync, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ export const INSTALL_MANIFEST = ".rafi/install-manifest.json";
6
+ export const OWNERSHIP_BACKUPS = ".rafi/ownership-backups";
7
+ export function initializeInstallManifest(projectDir, dirtyChoice) {
8
+ const existing = readInstallManifest(projectDir);
9
+ if (existing)
10
+ return existing;
11
+ const manifest = emptyManifest(projectDir);
12
+ manifest.repository.dirtyChoice = dirtyChoice;
13
+ return writeManifest(projectDir, manifest);
14
+ }
15
+ export function readInstallManifest(projectDir) {
16
+ const path = join(resolve(projectDir), INSTALL_MANIFEST);
17
+ if (!existsSync(path))
18
+ return undefined;
19
+ const value = JSON.parse(readFileSync(path, "utf8"));
20
+ validateInstallManifest(projectDir, value);
21
+ return upgradeManifest(projectDir, value);
22
+ }
23
+ export function registerOwnedFile(projectDir, relativePath, input) {
24
+ const root = resolve(projectDir);
25
+ const safe = validateOwnedPath(root, relativePath);
26
+ const current = readInstallManifest(root) ?? emptyManifest(root);
27
+ const sha256 = input.sha256 ?? fingerprint(join(root, safe));
28
+ const prior = current.files.find((item) => item.path === safe);
29
+ const entry = {
30
+ ...prior, path: safe, sha256, ...input,
31
+ category: input.category ?? categorizeOwnedPath(safe, input.mode),
32
+ installedSha256: sha256,
33
+ lastRafiWriteAt: new Date().toISOString(),
34
+ };
35
+ const files = [...current.files.filter((item) => item.path !== safe), entry].sort((a, b) => a.path.localeCompare(b.path));
36
+ return writeManifest(root, { ...current, files, updatedAt: new Date().toISOString() });
37
+ }
38
+ export function prepareOwnedWrite(projectDir, relativePath, origin, category) {
39
+ const entry = capturePreimage(projectDir, relativePath, origin, category);
40
+ return registerOwnedFile(projectDir, entry.path, entry);
41
+ }
42
+ export function finalizePreparedOwnedWrite(projectDir, relativePath, origin, category) {
43
+ const existing = readInstallManifest(projectDir)?.files.find((entry) => entry.path === relativePath);
44
+ if (existing)
45
+ return finalizeOwnedWrite(projectDir, { ...existing, origin, category });
46
+ return registerOwnedFile(projectDir, relativePath, { mode: "generated", origin, category });
47
+ }
48
+ /** Capture an exact pre-Rafi file before a managed write. */
49
+ export function capturePreimage(projectDir, relativePath, origin, category) {
50
+ const root = resolve(projectDir);
51
+ const safe = validateOwnedPath(root, relativePath);
52
+ const recorded = readInstallManifest(root)?.files.find((entry) => entry.path === safe);
53
+ if (recorded?.mode === "created" || recorded?.mode === "generated")
54
+ return { ...recorded };
55
+ const absolute = join(root, safe);
56
+ if (!existsSync(absolute))
57
+ return { path: safe, sha256: null, mode: "created", origin, category: category ?? categorizeOwnedPath(safe, "created"), preimageSha256: null };
58
+ if (!lstatSync(absolute).isFile())
59
+ throw new Error(`ownership target is not a regular file: ${safe}`);
60
+ const backupDir = join(root, OWNERSHIP_BACKUPS);
61
+ mkdirSync(backupDir, { recursive: true, mode: 0o700 });
62
+ chmodSync(backupDir, 0o700);
63
+ const backup = `${createHash("sha256").update(safe).digest("hex")}.original`;
64
+ const backupPath = join(backupDir, backup);
65
+ if (!existsSync(backupPath))
66
+ copyFileSync(absolute, backupPath);
67
+ const preimageSha256 = fingerprint(absolute);
68
+ return { path: safe, sha256: preimageSha256, mode: "modified", origin, backup: `${OWNERSHIP_BACKUPS}/${backup}`, category: category ?? categorizeOwnedPath(safe, "modified"), preimageSha256 };
69
+ }
70
+ export function finalizeOwnedWrite(projectDir, entry) {
71
+ return registerOwnedFile(projectDir, entry.path, { ...entry, sha256: fingerprint(join(resolve(projectDir), entry.path)) });
72
+ }
73
+ export function registerDependency(projectDir, dependency) {
74
+ const current = readInstallManifest(projectDir) ?? emptyManifest(projectDir);
75
+ const dependencies = [...current.dependencies.filter((item) => !(item.manager === dependency.manager && item.package === dependency.package)), dependency];
76
+ return writeManifest(projectDir, { ...current, dependencies, updatedAt: new Date().toISOString() });
77
+ }
78
+ export function validateInstallManifest(projectDir, manifest) {
79
+ if (![1, 2].includes(manifest.version) || !Array.isArray(manifest.files) || !Array.isArray(manifest.dependencies))
80
+ throw new Error("invalid install manifest");
81
+ const seen = new Set();
82
+ for (const entry of manifest.files) {
83
+ const path = validateOwnedPath(projectDir, entry.path);
84
+ if (seen.has(path))
85
+ throw new Error(`duplicate ownership path: ${path}`);
86
+ seen.add(path);
87
+ if (entry.backup)
88
+ validateOwnedPath(projectDir, entry.backup);
89
+ }
90
+ for (const dependency of manifest.dependencies) {
91
+ if (!dependency.package || /[\s;&|`$]/.test(dependency.package))
92
+ throw new Error(`unsafe dependency name: ${dependency.package}`);
93
+ dependency.manifests.forEach((path) => validateOwnedPath(projectDir, path));
94
+ }
95
+ }
96
+ export function validateOwnedPath(projectDir, value) {
97
+ if (!value || isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.includes("\0"))
98
+ throw new Error(`ownership path must be repository-relative: ${value}`);
99
+ const safe = normalize(value.replace(/\\/g, "/")).replace(/\\/g, "/").replace(/^\.\//, "");
100
+ if (safe === "." || safe === ".." || safe.startsWith("../") || safe.split("/").includes(".."))
101
+ throw new Error(`ownership path escapes project: ${value}`);
102
+ if (safe === ".git" || safe.startsWith(".git/"))
103
+ throw new Error(`ordinary ownership may not target Git metadata: ${value}`);
104
+ const root = resolve(projectDir);
105
+ const absolute = resolve(root, safe);
106
+ const rel = relative(root, absolute);
107
+ if (!rel || rel.startsWith("..") || isAbsolute(rel))
108
+ throw new Error(`ownership path escapes project: ${value}`);
109
+ assertNoSymlinkEscape(root, safe);
110
+ return safe;
111
+ }
112
+ function assertNoSymlinkEscape(root, safe) {
113
+ const rootReal = realpathSync(root);
114
+ let current = root;
115
+ for (const part of safe.split("/")) {
116
+ current = join(current, part);
117
+ if (!existsSync(current))
118
+ break;
119
+ const real = realpathSync(current);
120
+ const rel = relative(rootReal, real);
121
+ if (rel.startsWith("..") || isAbsolute(rel))
122
+ throw new Error(`ownership path resolves outside project: ${safe}`);
123
+ }
124
+ }
125
+ function emptyManifest(projectDir, now = new Date()) {
126
+ const stamp = now.toISOString();
127
+ const root = resolve(projectDir);
128
+ const status = gitValue(root, ["status", "--porcelain=v1", "-z"]);
129
+ return {
130
+ version: 2, createdAt: stamp, updatedAt: stamp,
131
+ repository: {
132
+ rootIdentity: createHash("sha256").update(realpathSync(root)).digest("hex"),
133
+ preInstallHead: gitValue(root, ["rev-parse", "HEAD"]),
134
+ initialBranch: gitValue(root, ["branch", "--show-current"]),
135
+ initialDirtyDigest: status ? createHash("sha256").update(status).digest("hex") : undefined,
136
+ dirtyChoice: status ? "snapshot-and-continue" : "clean",
137
+ baselineComplete: Boolean(gitValue(root, ["rev-parse", "HEAD"])),
138
+ },
139
+ files: [], dependencies: [],
140
+ };
141
+ }
142
+ function writeManifest(projectDir, manifest) {
143
+ validateInstallManifest(projectDir, manifest);
144
+ const path = join(resolve(projectDir), INSTALL_MANIFEST);
145
+ mkdirSync(dirname(path), { recursive: true });
146
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
147
+ writeFileSync(temp, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
148
+ renameSync(temp, path);
149
+ return manifest;
150
+ }
151
+ function fingerprint(path) {
152
+ if (!existsSync(path) || !statSync(path).isFile())
153
+ return null;
154
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
155
+ }
156
+ export function listOwnedTree(projectDir, relativeDirectory) {
157
+ const root = resolve(projectDir);
158
+ const safe = validateOwnedPath(root, relativeDirectory);
159
+ const start = join(root, safe);
160
+ if (!existsSync(start))
161
+ return [];
162
+ const output = [];
163
+ const visit = (directory) => {
164
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
165
+ const path = join(directory, entry.name);
166
+ if (entry.isSymbolicLink())
167
+ throw new Error(`refusing recursive ownership through symlink: ${relative(root, path)}`);
168
+ if (entry.isDirectory())
169
+ visit(path);
170
+ else if (entry.isFile())
171
+ output.push(relative(root, path).replace(/\\/g, "/"));
172
+ }
173
+ };
174
+ if (lstatSync(start).isDirectory())
175
+ visit(start);
176
+ else
177
+ output.push(safe);
178
+ return output.sort();
179
+ }
180
+ export function removeOwnedPathsTransaction(projectDir, paths) {
181
+ const root = resolve(projectDir);
182
+ const safePaths = [...new Set(paths.map((path) => validateOwnedPath(root, path)))].sort((a, b) => b.length - a.length);
183
+ const runId = randomUUID();
184
+ const transactionRoot = join(root, ".rafi-uninstall", runId);
185
+ const payload = join(transactionRoot, "payload");
186
+ mkdirSync(payload, { recursive: true, mode: 0o700 });
187
+ const manifest = readInstallManifest(root);
188
+ const journal = { version: 2, runId, recoveryId: runId, startedAt: new Date().toISOString(), status: "in_progress", manifest, operations: [] };
189
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
190
+ const moved = [];
191
+ try {
192
+ for (const safe of safePaths) {
193
+ const source = join(root, safe);
194
+ if (!existsSync(source))
195
+ continue;
196
+ const destination = join(payload, safe);
197
+ mkdirSync(dirname(destination), { recursive: true });
198
+ renameSync(source, destination);
199
+ moved.push({ source, destination });
200
+ journal.operations.push({ path: safe, status: "preserved", sha256: fingerprint(destination) });
201
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
202
+ }
203
+ journal.status = "complete";
204
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
205
+ return { runId, recoveryId: runId, removed: journal.operations.map((operation) => operation.path) };
206
+ }
207
+ catch (error) {
208
+ for (const item of moved.reverse()) {
209
+ if (existsSync(item.destination) && !existsSync(item.source)) {
210
+ mkdirSync(dirname(item.source), { recursive: true });
211
+ renameSync(item.destination, item.source);
212
+ }
213
+ }
214
+ throw new Error(`uninstall transaction ${runId} rolled back: ${error instanceof Error ? error.message : String(error)}`);
215
+ }
216
+ }
217
+ export function restoreOwnedPreimagesTransaction(projectDir, entries) {
218
+ const root = resolve(projectDir);
219
+ const runId = randomUUID();
220
+ const transactionRoot = join(root, ".rafi-uninstall", runId);
221
+ const payload = join(transactionRoot, "payload");
222
+ mkdirSync(payload, { recursive: true, mode: 0o700 });
223
+ const journal = { version: 2, runId, recoveryId: runId, startedAt: new Date().toISOString(), status: "in_progress", kind: "preimage-restore", operations: [] };
224
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
225
+ const restored = [];
226
+ try {
227
+ for (const entry of entries) {
228
+ const safe = validateOwnedPath(root, entry.path);
229
+ if (!entry.backup)
230
+ throw new Error(`no pre-Rafi backup is recorded for ${safe}`);
231
+ const preimage = join(root, validateOwnedPath(root, entry.backup));
232
+ if (!existsSync(preimage))
233
+ throw new Error(`pre-Rafi backup is missing for ${safe}`);
234
+ const current = join(root, safe);
235
+ const displaced = join(payload, "displaced-current", safe);
236
+ if (existsSync(current)) {
237
+ mkdirSync(dirname(displaced), { recursive: true });
238
+ renameSync(current, displaced);
239
+ }
240
+ mkdirSync(dirname(current), { recursive: true });
241
+ copyFileSync(preimage, current);
242
+ restored.push(safe);
243
+ journal.operations.push({ path: safe, backup: relative(root, displaced).replace(/\\/g, "/"), preimage: entry.backup, status: "restored" });
244
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
245
+ }
246
+ journal.status = "complete";
247
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
248
+ return { runId, recoveryId: runId, restored };
249
+ }
250
+ catch (error) {
251
+ for (const entry of entries.slice(0, restored.length).reverse()) {
252
+ const current = join(root, entry.path);
253
+ const displaced = join(payload, "displaced-current", entry.path);
254
+ if (existsSync(current))
255
+ rmSync(current, { force: true });
256
+ if (existsSync(displaced)) {
257
+ mkdirSync(dirname(current), { recursive: true });
258
+ renameSync(displaced, current);
259
+ }
260
+ }
261
+ throw new Error(`preimage restore ${runId} rolled back: ${error instanceof Error ? error.message : String(error)}`);
262
+ }
263
+ }
264
+ /** Preserve recorded pre-Rafi bytes for a file the user chose to keep today. */
265
+ export function preservePreimagesForLaterRestore(projectDir, entries) {
266
+ const root = resolve(projectDir);
267
+ const restorable = entries.filter((entry) => entry.backup && existsSync(join(root, validateOwnedPath(root, entry.backup))));
268
+ if (!restorable.length)
269
+ return undefined;
270
+ const recoveryId = randomUUID();
271
+ const transactionRoot = join(root, ".rafi-uninstall", recoveryId);
272
+ const payload = join(transactionRoot, "payload");
273
+ mkdirSync(payload, { recursive: true, mode: 0o700 });
274
+ const operations = [];
275
+ for (const entry of restorable) {
276
+ const safe = validateOwnedPath(root, entry.path);
277
+ const source = join(root, validateOwnedPath(root, entry.backup));
278
+ const destination = join(payload, safe);
279
+ mkdirSync(dirname(destination), { recursive: true });
280
+ copyFileSync(source, destination);
281
+ operations.push({ path: safe, status: "preimage-preserved-for-later-restore", sha256: fingerprint(destination) });
282
+ }
283
+ const journal = { version: 2, recoveryId, runId: recoveryId, kind: "kept-preimage", status: "complete", createdAt: new Date().toISOString(), operations };
284
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
285
+ return { recoveryId, preserved: operations.map((operation) => operation.path) };
286
+ }
287
+ export function removeManagedBlocksTransaction(projectDir, entries) {
288
+ const root = resolve(projectDir);
289
+ const runId = randomUUID();
290
+ const transactionRoot = join(root, ".rafi-uninstall", runId);
291
+ const payload = join(transactionRoot, "payload");
292
+ mkdirSync(payload, { recursive: true, mode: 0o700 });
293
+ const edited = [];
294
+ const journal = { version: 2, runId, recoveryId: runId, status: "in_progress", kind: "managed-block-removal", operations: [] };
295
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
296
+ try {
297
+ for (const entry of entries) {
298
+ const safe = validateOwnedPath(root, entry.path);
299
+ const current = join(root, safe);
300
+ if (!existsSync(current) || !entry.marker)
301
+ continue;
302
+ const [start, end] = entry.marker.split("..");
303
+ if (!start || !end)
304
+ throw new Error(`invalid managed block marker for ${safe}`);
305
+ const original = readFileSync(current, "utf8");
306
+ const pattern = new RegExp(`${escapeRegExp(start)}\\r?\\n[\\s\\S]*?${escapeRegExp(end)}\\r?\\n?`, "g");
307
+ if (!pattern.test(original))
308
+ throw new Error(`managed block markers are missing from ${safe}; choose keep or full preimage restore`);
309
+ const backup = join(payload, safe);
310
+ mkdirSync(dirname(backup), { recursive: true });
311
+ copyFileSync(current, backup);
312
+ journal.operations.push({ path: safe, status: "intent-recorded", sha256: fingerprint(backup) });
313
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
314
+ writeFileSync(current, original.replace(pattern, "").replace(/^\s+$/, ""), "utf8");
315
+ edited.push(safe);
316
+ journal.operations[journal.operations.length - 1].status = "managed-block-removed";
317
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
318
+ }
319
+ journal.status = "complete";
320
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
321
+ return { runId, recoveryId: runId, edited };
322
+ }
323
+ catch (error) {
324
+ for (const safe of edited.reverse()) {
325
+ const backup = join(payload, safe);
326
+ const current = join(root, safe);
327
+ if (existsSync(backup)) {
328
+ mkdirSync(dirname(current), { recursive: true });
329
+ copyFileSync(backup, current);
330
+ }
331
+ }
332
+ journal.status = "rolled_back";
333
+ writeFileSync(join(transactionRoot, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
334
+ throw new Error(`managed-block removal ${runId} rolled back: ${error instanceof Error ? error.message : String(error)}`);
335
+ }
336
+ }
337
+ export function restoreUninstallRecovery(projectDir, recoveryId, selectedPaths, overwrite = false) {
338
+ const root = resolve(projectDir);
339
+ const recoveryRoot = join(root, ".rafi-uninstall", validateRecoveryId(recoveryId));
340
+ const journalPath = join(recoveryRoot, "journal.json");
341
+ if (!existsSync(journalPath))
342
+ throw new Error(`uninstall recovery not found: ${recoveryId}`);
343
+ const journal = JSON.parse(readFileSync(journalPath, "utf8"));
344
+ const allowed = new Set((journal.operations ?? []).map((operation) => operation.path));
345
+ const wanted = selectedPaths?.length ? selectedPaths.map((path) => validateOwnedPath(root, path)) : [...allowed];
346
+ const unknown = wanted.filter((path) => !allowed.has(path));
347
+ if (unknown.length)
348
+ throw new Error(`paths are not in recovery ${recoveryId}: ${unknown.join(", ")}`);
349
+ const collisions = wanted.filter((path) => existsSync(join(root, path)));
350
+ if (collisions.length && !overwrite)
351
+ return { restored: [], collisions };
352
+ const backupId = collisions.length ? `restore-backup-${randomUUID()}` : undefined;
353
+ if (backupId) {
354
+ const backupOperations = [];
355
+ for (const safe of collisions) {
356
+ const backup = join(root, ".rafi-uninstall", backupId, "payload", safe);
357
+ mkdirSync(dirname(backup), { recursive: true });
358
+ renameSync(join(root, safe), backup);
359
+ backupOperations.push({ path: safe, status: "displaced-before-restore", sha256: fingerprint(backup) });
360
+ }
361
+ const backupJournal = { version: 2, recoveryId: backupId, runId: backupId, kind: "restore-collision-backup", status: "complete", createdAt: new Date().toISOString(), operations: backupOperations };
362
+ writeFileSync(join(root, ".rafi-uninstall", backupId, "journal.json"), `${JSON.stringify(backupJournal, null, 2)}\n`, "utf8");
363
+ }
364
+ const restored = [];
365
+ for (const safe of wanted) {
366
+ const source = join(recoveryRoot, "payload", safe);
367
+ if (!existsSync(source))
368
+ continue;
369
+ const destination = join(root, safe);
370
+ mkdirSync(dirname(destination), { recursive: true });
371
+ cpSync(source, destination, { recursive: true, errorOnExist: true });
372
+ restored.push(safe);
373
+ }
374
+ return { restored, collisions: [], backupId };
375
+ }
376
+ export function cleanupUninstallRecovery(projectDir, recoveryIds) {
377
+ const root = resolve(projectDir);
378
+ const removed = [];
379
+ for (const id of recoveryIds.map(validateRecoveryId)) {
380
+ const target = join(root, ".rafi-uninstall", id);
381
+ if (!existsSync(target))
382
+ continue;
383
+ rmSync(target, { recursive: true, force: true });
384
+ removed.push(id);
385
+ }
386
+ return removed;
387
+ }
388
+ export function listUninstallRecoveries(projectDir) {
389
+ const directory = join(resolve(projectDir), ".rafi-uninstall");
390
+ if (!existsSync(directory))
391
+ return [];
392
+ return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^[A-Za-z0-9-]+$/.test(entry.name)).map((entry) => entry.name).sort();
393
+ }
394
+ function upgradeManifest(projectDir, manifest) {
395
+ if (manifest.version === 2)
396
+ return manifest;
397
+ const legacy = manifest;
398
+ const root = resolve(projectDir);
399
+ return {
400
+ version: 2, createdAt: legacy.createdAt, updatedAt: legacy.updatedAt,
401
+ repository: { rootIdentity: createHash("sha256").update(realpathSync(root)).digest("hex"), dirtyChoice: "legacy-unknown", baselineComplete: false },
402
+ files: legacy.files.map((entry) => ({ ...entry, category: categorizeOwnedPath(entry.path, entry.mode), installedSha256: entry.sha256 })),
403
+ dependencies: legacy.dependencies,
404
+ };
405
+ }
406
+ export function categorizeOwnedPath(path, mode) {
407
+ if (path.startsWith(".tickets/"))
408
+ return "tickets";
409
+ if (/rafi-plan|plans?\//i.test(path))
410
+ return "plans";
411
+ if (/\/(?:skills)\//.test(`/${path}`))
412
+ return "skills";
413
+ if (/\/(?:agents)\//.test(`/${path}`))
414
+ return "agents";
415
+ if (/rules/i.test(path) || ["AGENTS.md", "CLAUDE.md"].includes(path))
416
+ return "rules";
417
+ if (path === ".gitignore")
418
+ return "managed-gitignore";
419
+ if (path === "rafi-config.yaml" || path.endsWith("config.yaml"))
420
+ return "config";
421
+ if (path.startsWith(".rafi/") || path.startsWith(".foreman/"))
422
+ return "runtime-state";
423
+ if (/\.(?:md|mdx)$/i.test(path))
424
+ return mode === "modified" ? "documentation-modified" : "documentation-created";
425
+ return "generated-other";
426
+ }
427
+ function gitValue(cwd, args) {
428
+ try {
429
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
430
+ }
431
+ catch {
432
+ return undefined;
433
+ }
434
+ }
435
+ function validateRecoveryId(value) {
436
+ if (!/^[A-Za-z0-9-]+$/.test(value))
437
+ throw new Error(`invalid recovery ID: ${value}`);
438
+ return value;
439
+ }
440
+ function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
@@ -0,0 +1,12 @@
1
+ export interface LinuxProbeProcess {
2
+ pid: number;
3
+ group: number;
4
+ state: string;
5
+ start: string;
6
+ tagged: boolean;
7
+ }
8
+ /** procfs avoids ps dialects, PATH, output limits and environment truncation.
9
+ * Probe descendants inherit our uid; unreadable same-user environments fail closed.
10
+ * Privilege changes and deliberately scrubbed tags are outside tagged containment.
11
+ */
12
+ export declare function linuxProbeInventory(tag?: string, timeoutMs?: number, root?: string, uid?: number | undefined, self?: number): LinuxProbeProcess[] | undefined;
@@ -0,0 +1,48 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /** procfs avoids ps dialects, PATH, output limits and environment truncation.
4
+ * Probe descendants inherit our uid; unreadable same-user environments fail closed.
5
+ * Privilege changes and deliberately scrubbed tags are outside tagged containment.
6
+ */
7
+ export function linuxProbeInventory(tag, timeoutMs = 1000, root = "/proc", uid = process.getuid?.(), self = process.pid) {
8
+ const deadline = Date.now() + Math.max(1, timeoutMs);
9
+ try {
10
+ const rows = [];
11
+ for (const entry of readdirSync(root)) {
12
+ if (!/^\d+$/.test(entry))
13
+ continue;
14
+ if (Date.now() >= deadline)
15
+ return undefined;
16
+ const directory = join(root, entry);
17
+ try {
18
+ const stat = readFileSync(join(directory, "stat"), "utf8");
19
+ const end = stat.lastIndexOf(")");
20
+ const fields = stat.slice(end + 1).trim().split(/\s+/);
21
+ if (end < 0 || !/^\d+$/.test(fields[19] ?? "") || !/^\d+$/.test(fields[2] ?? ""))
22
+ return undefined;
23
+ let tagged = false;
24
+ if (tag && fields[0] !== "Z" && fields[0] !== "X" && statSync(directory).uid === uid) {
25
+ const environment = readFileSync(join(directory, "environ"));
26
+ tagged = environment.toString("utf8").split("\0").includes(`RAFI_PROBE_OWNER=${tag}`);
27
+ }
28
+ rows.push({ pid: Number(entry), group: Number(fields[2]), state: fields[0], start: fields[19], tagged });
29
+ }
30
+ catch (error) {
31
+ if (error.code === "ENOENT") {
32
+ try {
33
+ readFileSync(join(directory, "stat"));
34
+ }
35
+ catch (gone) {
36
+ if (gone.code === "ENOENT")
37
+ continue; // process exited during enumeration
38
+ }
39
+ }
40
+ return undefined;
41
+ }
42
+ }
43
+ return rows.some(row => row.pid === self) ? rows : undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
package/dist/log.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** A single structured log record. */
2
2
  export interface LogRecord {
3
3
  ts: string;
4
- event: "batch-start" | "batch-end" | "branch-plan" | "branch-start" | "branch-session" | "branch-resume" | "branch-issue" | "branch-complete" | "branch-push" | "github-readiness-failed" | "gitlab-readiness-failed" | "pr-created" | "pr-existing" | "pr-failed" | "pr-auto-merge-enabled" | "pr-auto-merge-failed" | "mr-created" | "mr-existing" | "mr-failed" | "mr-auto-merge-enabled" | "mr-auto-merge-failed" | "branch-auto-merge-wait" | "branch-direct-merge" | "step" | "question-round-trip" | "provider-lifecycle" | "runtime-readiness" | "permission" | "escalation" | "needs_input" | "blocked-recovery" | "preflight" | "rafi-plan" | "ticket-populate" | "uninstall-proposal" | "qa" | "qa-fix" | "qa-protected-files-changed" | "qa-evidence" | "agent-status" | "context-lifecycle" | "handoff-transfer" | "recovery-handoff-accepted" | "guided-recovery-required" | "safe-boundary-paused" | "session-unavailable" | "error";
4
+ event: "graph-maintenance" | "graph-evidence" | "batch-start" | "batch-end" | "branch-plan" | "branch-start" | "branch-session" | "branch-resume" | "branch-issue" | "branch-complete" | "branch-push" | "github-readiness-failed" | "gitlab-readiness-failed" | "pr-created" | "pr-existing" | "pr-failed" | "pr-auto-merge-enabled" | "pr-auto-merge-failed" | "mr-created" | "mr-existing" | "mr-failed" | "mr-auto-merge-enabled" | "mr-auto-merge-failed" | "branch-auto-merge-wait" | "branch-direct-merge" | "step" | "question-round-trip" | "provider-lifecycle" | "runtime-readiness" | "permission" | "escalation" | "needs_input" | "blocked-recovery" | "preflight" | "rafi-plan" | "ticket-populate" | "uninstall-proposal" | "qa" | "qa-fix" | "qa-protected-files-changed" | "qa-evidence" | "agent-status" | "context-lifecycle" | "handoff-transfer" | "recovery-handoff-accepted" | "guided-recovery-required" | "safe-boundary-paused" | "session-unavailable" | "error";
5
5
  [key: string]: unknown;
6
6
  }
7
7
  /** Append-only JSONL logger. One line per record, plus an echo to the console. */
package/dist/log.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { currentGraphDerivedAccess } from "./graph/derived.js";
2
+ import { bytesDigest } from "./graph/util.js";
1
3
  import { appendFileSync, mkdirSync } from "node:fs";
2
4
  import { dirname } from "node:path";
3
5
  /** Append-only JSONL logger. One line per record, plus an echo to the console. */
@@ -14,7 +16,11 @@ export class Log {
14
16
  const record = {
15
17
  ts: new Date().toISOString(),
16
18
  event,
17
- ...fields,
19
+ ...(currentGraphDerivedAccess() ? {
20
+ graphDerived: true,
21
+ fieldsDigest: bytesDigest(JSON.stringify(fields)),
22
+ limitation: "Graph-derived details are retained only in access-checked evidence storage",
23
+ } : fields),
18
24
  };
19
25
  appendFileSync(this.path, JSON.stringify(record) + "\n");
20
26
  }
@@ -0,0 +1,25 @@
1
+ import { type ManagerEvidencePageV2, type ManagerEvidenceRequestV2 } from "rafi-spec";
2
+ import { ManagerEvidenceArtifacts } from "./managerEvidenceArtifacts.js";
3
+ /** Per-host immutable snapshots and opaque continuations are independent of model lookup budgets. */
4
+ export declare class ManagerEvidenceService {
5
+ readonly projectDir: string;
6
+ private readonly now;
7
+ private readonly lifetimeMs;
8
+ readonly artifacts: ManagerEvidenceArtifacts;
9
+ private graphPermitted;
10
+ private readonly snapshots;
11
+ private readonly cursors;
12
+ private readonly hostMetadata;
13
+ constructor(projectDir: string, now?: () => number, lifetimeMs?: number);
14
+ close(): void;
15
+ execute(request: ManagerEvidenceRequestV2): ManagerEvidencePageV2;
16
+ more(cursor: string, expectedScope?: string): ManagerEvidencePageV2;
17
+ /** Full disclosure for host rendering; never substituted into a model packet. */
18
+ hostRedactions(snapshotId: string): ManagerEvidencePageV2["redactions"];
19
+ /** Preserve the already-sanitized structure; rendered report text need not be valid JSON. */
20
+ hostMetadataItem(handle: string): unknown;
21
+ private page;
22
+ private failure;
23
+ private populate;
24
+ }
25
+ export declare function parseManagerEvidenceRequestV2(text: string): ManagerEvidenceRequestV2 | undefined;