@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.1

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 (284) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -2
  3. package/bin/codeflow.mjs +8 -0
  4. package/bin/launcher.mjs +127 -0
  5. package/dist/remote-host.json +1 -0
  6. package/licenses/inter.txt +92 -0
  7. package/licenses/preact.txt +21 -0
  8. package/licenses/signals-core.txt +21 -0
  9. package/licenses/signals.txt +21 -0
  10. package/licenses/typescript-notices.txt +193 -0
  11. package/licenses/typescript.txt +55 -0
  12. package/licenses/uqr.txt +22 -0
  13. package/licenses/xterm-fit.txt +19 -0
  14. package/licenses/xterm.txt +21 -0
  15. package/package.json +34 -4
  16. package/public/assets/browser-7XI52A6V.js +8 -0
  17. package/public/assets/changes-EYFPASEI.js +1 -0
  18. package/public/assets/chunk-7H3ZLTKA.js +2 -0
  19. package/public/assets/chunk-L7TUCIYF.js +1 -0
  20. package/public/assets/chunk-LLMQG23J.js +163 -0
  21. package/public/assets/main-KIRM2OEK.js +1 -0
  22. package/public/assets/settings-QJZVW22Y.js +9 -0
  23. package/public/assets/styles-615EBB20.css +32 -0
  24. package/public/assets/terminal-A2FI2NAX.js +62 -0
  25. package/public/assets.json +1 -0
  26. package/public/bundle-meta.json +1 -0
  27. package/public/docs/.nojekyll +0 -0
  28. package/public/docs/accessibility.html +19 -0
  29. package/public/docs/acp-agents.html +117 -0
  30. package/public/docs/agent-containers.html +79 -0
  31. package/public/docs/api.html +1130 -0
  32. package/public/docs/app.js +342 -0
  33. package/public/docs/application-improvements.html +275 -0
  34. package/public/docs/architecture-explorer.html +19 -0
  35. package/public/docs/architecture.html +149 -0
  36. package/public/docs/auto-team.html +142 -0
  37. package/public/docs/automations.html +109 -0
  38. package/public/docs/checkpoint-review.html +37 -0
  39. package/public/docs/code-hosts.html +35 -0
  40. package/public/docs/code-intelligence.html +35 -0
  41. package/public/docs/configuration.html +346 -0
  42. package/public/docs/context-inspection.html +23 -0
  43. package/public/docs/coordinated-changes.html +28 -0
  44. package/public/docs/deferred-runs.html +25 -0
  45. package/public/docs/dependency-discovery.html +19 -0
  46. package/public/docs/documentation.html +84 -0
  47. package/public/docs/dynamic-teams-evaluation.html +174 -0
  48. package/public/docs/dynamic-teams-implementation-plan.html +882 -0
  49. package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
  50. package/public/docs/dynamic-teams-plan-review-0.html +204 -0
  51. package/public/docs/extensions.html +42 -0
  52. package/public/docs/external-sources.html +96 -0
  53. package/public/docs/filesystem-policy.html +26 -0
  54. package/public/docs/filtered-tasks.html +44 -0
  55. package/public/docs/font-license.txt +92 -0
  56. package/public/docs/getting-started.html +118 -0
  57. package/public/docs/github-review-threads.html +26 -0
  58. package/public/docs/github.html +157 -0
  59. package/public/docs/help.html +69 -0
  60. package/public/docs/index.html +77 -0
  61. package/public/docs/installation.html +74 -0
  62. package/public/docs/launch-profiles.html +48 -0
  63. package/public/docs/maintenance.html +60 -0
  64. package/public/docs/memory-claims.html +31 -0
  65. package/public/docs/memory-storage.html +58 -0
  66. package/public/docs/notifications.html +39 -0
  67. package/public/docs/patterns.html +195 -0
  68. package/public/docs/performance-results.html +210 -0
  69. package/public/docs/performance.html +166 -0
  70. package/public/docs/project-conventions.html +22 -0
  71. package/public/docs/project-first-navigation-plan.html +587 -0
  72. package/public/docs/project-teams-implementation-plan.html +772 -0
  73. package/public/docs/project-teams.html +99 -0
  74. package/public/docs/provider-compatibility.html +20 -0
  75. package/public/docs/releasing.html +61 -0
  76. package/public/docs/remote-workers.html +64 -0
  77. package/public/docs/review-inbox.html +26 -0
  78. package/public/docs/run-storage.html +23 -0
  79. package/public/docs/runs.html +141 -0
  80. package/public/docs/search.json +1 -0
  81. package/public/docs/security.html +133 -0
  82. package/public/docs/shared-projects.html +31 -0
  83. package/public/docs/ssh-projects.html +60 -0
  84. package/public/docs/style.css +768 -0
  85. package/public/docs/task-reruns.html +30 -0
  86. package/public/docs/team-collaboration.html +379 -0
  87. package/public/docs/verification.html +22 -0
  88. package/public/docs/vscode.html +53 -0
  89. package/public/docs/worker-permission-verification.html +122 -0
  90. package/public/index.html +17 -0
  91. package/scripts/check-node.mjs +7 -0
  92. package/scripts/codeflow.mjs +55 -0
  93. package/scripts/pty.py +273 -0
  94. package/src/acp-connections.js +110 -0
  95. package/src/adapters/acp-presets.js +37 -0
  96. package/src/adapters/acp-types.js +0 -0
  97. package/src/adapters/acp.js +328 -0
  98. package/src/adapters/agy-auth.js +67 -0
  99. package/src/adapters/agy-isolation.js +223 -0
  100. package/src/adapters/agy.js +297 -0
  101. package/src/adapters/claude.js +220 -0
  102. package/src/adapters/codex-app-server.js +518 -0
  103. package/src/adapters/codex.js +299 -0
  104. package/src/adapters/errors.js +57 -0
  105. package/src/adapters/index.js +91 -0
  106. package/src/adapters/managed-permissions.js +37 -0
  107. package/src/adapters/model-discovery.js +100 -0
  108. package/src/adapters/process.js +418 -0
  109. package/src/adapters/team-tools.js +102 -0
  110. package/src/adapters/types.js +0 -0
  111. package/src/agent-container.js +155 -0
  112. package/src/approvals.js +121 -0
  113. package/src/architecture.js +456 -0
  114. package/src/assets.js +44 -0
  115. package/src/automation.js +901 -0
  116. package/src/certificate-renewal.js +75 -0
  117. package/src/chat-handoff.js +55 -0
  118. package/src/checkpoint-review.js +72 -0
  119. package/src/checks.js +303 -0
  120. package/src/code-host-git.js +102 -0
  121. package/src/code-hosts-api.js +99 -0
  122. package/src/code-hosts.js +482 -0
  123. package/src/code-intelligence-worker.mjs +452 -0
  124. package/src/code-intelligence.js +141 -0
  125. package/src/collab/api.js +365 -0
  126. package/src/collab/checkpoints.js +198 -0
  127. package/src/collab/connections.js +139 -0
  128. package/src/collab/host-execution.js +347 -0
  129. package/src/collab/hub-client.js +210 -0
  130. package/src/collab/link-api.js +229 -0
  131. package/src/collab/link.js +595 -0
  132. package/src/collab/model.js +496 -0
  133. package/src/collab/runner.js +1014 -0
  134. package/src/collab/store.js +1452 -0
  135. package/src/collab/terminals.js +361 -0
  136. package/src/collaboration-reviews.js +116 -0
  137. package/src/config.js +48 -0
  138. package/src/context-budget.js +14 -0
  139. package/src/context-inspection.js +204 -0
  140. package/src/context.js +154 -0
  141. package/src/conventions.js +283 -0
  142. package/src/coordinated-api.js +247 -0
  143. package/src/deferred-launch.js +82 -0
  144. package/src/deferred-runs.js +515 -0
  145. package/src/dependency-health.js +35 -0
  146. package/src/diff.js +43 -0
  147. package/src/docs-help.js +316 -0
  148. package/src/doctor.js +232 -0
  149. package/src/engine.js +4366 -0
  150. package/src/env.js +44 -0
  151. package/src/experience.js +50 -0
  152. package/src/extensions.js +144 -0
  153. package/src/filesystem-access.js +107 -0
  154. package/src/filesystem-policy.js +238 -0
  155. package/src/filtered-task-limits.js +93 -0
  156. package/src/filtered-tasks-api.js +53 -0
  157. package/src/filtered-tasks.js +576 -0
  158. package/src/fs.js +57 -0
  159. package/src/git.js +626 -0
  160. package/src/github-review-threads.js +181 -0
  161. package/src/github.js +538 -0
  162. package/src/http.js +69 -0
  163. package/src/improve-settings.js +38 -0
  164. package/src/improve.js +187 -0
  165. package/src/integrations.js +373 -0
  166. package/src/integrity.js +259 -0
  167. package/src/journey-api.js +77 -0
  168. package/src/journey-replay.js +65 -0
  169. package/src/journey-store.js +97 -0
  170. package/src/journeys.js +60 -0
  171. package/src/json.js +81 -0
  172. package/src/launch-profiles.js +292 -0
  173. package/src/learnings.js +80 -0
  174. package/src/log.js +39 -0
  175. package/src/maintenance.js +335 -0
  176. package/src/memory-analysis.js +182 -0
  177. package/src/memory-claims.js +153 -0
  178. package/src/memory-sqlite.js +236 -0
  179. package/src/memory.js +552 -0
  180. package/src/merge-validation.js +58 -0
  181. package/src/model-catalog.js +238 -0
  182. package/src/model-research.js +91 -0
  183. package/src/notes.js +176 -0
  184. package/src/notifications.js +413 -0
  185. package/src/patterns/auto-team-model.js +212 -0
  186. package/src/patterns/auto-team.js +659 -0
  187. package/src/patterns/council.js +199 -0
  188. package/src/patterns/debate.js +188 -0
  189. package/src/patterns/findings.js +141 -0
  190. package/src/patterns/index.js +19 -0
  191. package/src/patterns/loop.js +158 -0
  192. package/src/patterns/pipeline.js +125 -0
  193. package/src/patterns/prompts.js +181 -0
  194. package/src/patterns/race.js +147 -0
  195. package/src/patterns/schemas.js +40 -0
  196. package/src/patterns/solo.js +49 -0
  197. package/src/patterns/supervisor.js +235 -0
  198. package/src/patterns/task-draft.js +29 -0
  199. package/src/patterns/tdd.js +231 -0
  200. package/src/patterns/team-strategies.js +67 -0
  201. package/src/performance.js +43 -0
  202. package/src/persistence-status.js +35 -0
  203. package/src/phone-sessions.js +96 -0
  204. package/src/preview-overlay.js +383 -0
  205. package/src/preview.js +219 -0
  206. package/src/privacy.js +23 -0
  207. package/src/project-report.js +22 -0
  208. package/src/project-team-model.js +178 -0
  209. package/src/project-team-policy.js +42 -0
  210. package/src/project-team-store.js +213 -0
  211. package/src/project-teams-api.js +36 -0
  212. package/src/project-teams.js +551 -0
  213. package/src/projects.js +286 -0
  214. package/src/provider-eligibility.js +9 -0
  215. package/src/readiness.js +98 -0
  216. package/src/remote-tls.js +66 -0
  217. package/src/remote-workers/bundles.js +198 -0
  218. package/src/remote-workers/client.js +332 -0
  219. package/src/remote-workers/durability.js +37 -0
  220. package/src/remote-workers/protocol.js +68 -0
  221. package/src/remote-workers/stdio.js +138 -0
  222. package/src/remote-workers/worker.js +304 -0
  223. package/src/remote.js +204 -0
  224. package/src/repo-config.js +201 -0
  225. package/src/repo-coordination.js +732 -0
  226. package/src/repo-tree.js +106 -0
  227. package/src/retention-policy.js +466 -0
  228. package/src/review-inbox.js +38 -0
  229. package/src/routing.js +49 -0
  230. package/src/run-analysis.js +111 -0
  231. package/src/run-cold-store.js +231 -0
  232. package/src/run-environment.js +15 -0
  233. package/src/run-history.js +91 -0
  234. package/src/run-log.js +160 -0
  235. package/src/run-permissions.js +110 -0
  236. package/src/run-projection.js +75 -0
  237. package/src/run-recovery.js +53 -0
  238. package/src/run-status.js +54 -0
  239. package/src/serve.js +138 -0
  240. package/src/server.js +2738 -0
  241. package/src/shared-access.js +103 -0
  242. package/src/shared-accounts.js +301 -0
  243. package/src/shared-api.js +239 -0
  244. package/src/shell.js +346 -0
  245. package/src/ship.js +344 -0
  246. package/src/slots.js +74 -0
  247. package/src/snapshot.js +388 -0
  248. package/src/source-context.js +17 -0
  249. package/src/sse-writer.js +70 -0
  250. package/src/ssh-projects/bootstrap.mjs +95 -0
  251. package/src/ssh-projects/config.js +45 -0
  252. package/src/ssh-projects/gateway.js +143 -0
  253. package/src/ssh-projects/manager.js +320 -0
  254. package/src/ssh-projects/team-autostart.mjs +93 -0
  255. package/src/ssh-projects/team-remove.mjs +69 -0
  256. package/src/ssh-projects/team-status.mjs +39 -0
  257. package/src/ssh-projects/transport.js +116 -0
  258. package/src/stats.js +139 -0
  259. package/src/store.js +544 -0
  260. package/src/stream.js +91 -0
  261. package/src/sweeps.js +241 -0
  262. package/src/tailscale.js +101 -0
  263. package/src/task-draft.js +23 -0
  264. package/src/team-evaluation.js +242 -0
  265. package/src/team-mailbox.js +160 -0
  266. package/src/team-memory.js +84 -0
  267. package/src/team-rerun-graph.js +15 -0
  268. package/src/team-reruns.js +130 -0
  269. package/src/team-scheduler.js +121 -0
  270. package/src/team-template-store.js +98 -0
  271. package/src/team-templates.js +117 -0
  272. package/src/team-tool-client.mjs +79 -0
  273. package/src/team-tools.js +176 -0
  274. package/src/teams.js +0 -0
  275. package/src/templates.js +62 -0
  276. package/src/transcript.js +134 -0
  277. package/src/verification-integrity.js +27 -0
  278. package/src/verification.js +183 -0
  279. package/src/watch.js +195 -0
  280. package/src/work-queue.js +47 -0
  281. package/src/workflow-control.js +83 -0
  282. package/src/workspace-api.js +207 -0
  283. package/src/workspace-changes.js +61 -0
  284. package/src/workspace.js +146 -0
@@ -0,0 +1,106 @@
1
+ import { open, readdir, realpath, stat } from "node:fs/promises";
2
+ import { basename, join, sep } from "node:path";
3
+ import { git } from "./git.js";
4
+ import { assertProjectDirectoryAccess, assertProjectFileAccess, filePolicyMetadataOnly, filePolicyRegistryActive, filterAllowedProjectEntries } from "./filesystem-policy.js";
5
+ const MAX_ENTRIES = 1e3;
6
+ const PANEL_TIMEOUT_MS = 3e4;
7
+ function normalizeRepoPath(raw) {
8
+ const parts = raw.replace(/\\/g, "/").split("/").filter((p) => p && p !== ".");
9
+ if (parts.some((p) => p === "..")) throw new Error(`Path "${raw}" must stay inside the repository`);
10
+ return parts.join("/");
11
+ }
12
+ async function resolveInsideRepo(repo, rel) {
13
+ const abs = rel ? join(repo, ...rel.split("/")) : repo;
14
+ const [real, realRepo] = await Promise.all([realpath(abs), realpath(repo)]);
15
+ if (real !== realRepo && !real.startsWith(realRepo + sep)) {
16
+ throw new Error(`Path "${rel}" points outside the repository`);
17
+ }
18
+ return abs;
19
+ }
20
+ const folderOf = (repo) => typeof repo === "string" ? repo : repo.path;
21
+ async function listTree(repo, relDir = "") {
22
+ const rel = normalizeRepoPath(relDir);
23
+ await assertProjectDirectoryAccess(folderOf(repo), rel || ".");
24
+ const abs = await resolveInsideRepo(folderOf(repo), rel);
25
+ const dirents = (await readdir(abs, { withFileTypes: true })).filter((d) => d.name !== ".git" && (!filePolicyRegistryActive() || !d.isSymbolicLink()));
26
+ const candidates = await Promise.all(
27
+ dirents.map(async (d) => {
28
+ const path = rel ? `${rel}/${d.name}` : d.name;
29
+ const isDir = d.isDirectory() || d.isSymbolicLink() && await stat(join(abs, d.name)).then((s) => s.isDirectory(), () => false);
30
+ return { name: d.name, path, type: isDir ? "dir" : "file" };
31
+ })
32
+ );
33
+ const ignored = await ignoredPaths(repo, candidates.map((c) => c.type === "dir" ? `${c.path}/` : c.path));
34
+ const statuses = filePolicyMetadataOnly() ? /* @__PURE__ */ new Map() : await childStatuses(repo, rel);
35
+ const entries = (await filterAllowedProjectEntries(folderOf(repo), candidates)).filter((c) => !ignored.has(c.type === "dir" ? `${c.path}/` : c.path)).map((c) => ({ ...c, status: statuses.get(c.name) })).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name, void 0, { sensitivity: "base" }) : a.type === "dir" ? -1 : 1);
36
+ return { path: rel, entries: entries.slice(0, MAX_ENTRIES), truncated: entries.length > MAX_ENTRIES };
37
+ }
38
+ async function searchFiles(repo, query, limit = 100) {
39
+ const q = query.trim().toLowerCase();
40
+ if (!q) return [];
41
+ const res = await git(["-c", "core.fsmonitor=false", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], repo, { timeoutMs: PANEL_TIMEOUT_MS });
42
+ if (res.code !== 0) throw new Error(res.stderr.trim() || "git ls-files failed");
43
+ const unique = [...new Set(res.stdout.split("\0").filter(Boolean))];
44
+ const nameHits = [];
45
+ const pathHits = [];
46
+ for (const path of unique) {
47
+ const lower = path.toLowerCase();
48
+ if (basename(lower).includes(q)) nameHits.push(path);
49
+ else if (lower.includes(q)) pathHits.push(path);
50
+ }
51
+ return (await filterAllowedProjectEntries(folderOf(repo), [...nameHits, ...pathHits].map((path) => ({ name: basename(path), path, type: "file" })))).slice(0, limit);
52
+ }
53
+ async function ignoredPaths(repo, paths) {
54
+ if (!paths.length) return /* @__PURE__ */ new Set();
55
+ const res = await git(["-c", "core.fsmonitor=false", "check-ignore", "-z", "--stdin"], repo, { input: paths.join("\0") + "\0", timeoutMs: PANEL_TIMEOUT_MS });
56
+ if (res.timedOut) throw new Error(res.stderr.split("\n")[0]);
57
+ if (res.code !== 0 && res.code !== 1) return /* @__PURE__ */ new Set();
58
+ return new Set(res.stdout.split("\0").filter(Boolean));
59
+ }
60
+ async function childStatuses(repo, rel) {
61
+ const res = await git(["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", rel || "."], repo, { timeoutMs: PANEL_TIMEOUT_MS });
62
+ const statuses = /* @__PURE__ */ new Map();
63
+ if (res.code !== 0) return statuses;
64
+ const records = res.stdout.split("\0");
65
+ for (let i = 0; i < records.length; i++) {
66
+ const record = records[i];
67
+ if (record.length < 4) continue;
68
+ const xy = record.slice(0, 2);
69
+ const path = record.slice(3);
70
+ if (xy[0] === "R" || xy[0] === "C") i++;
71
+ const inside = rel ? path.startsWith(`${rel}/`) ? path.slice(rel.length + 1) : null : path;
72
+ if (!inside) continue;
73
+ const child = inside.split("/")[0];
74
+ const status = xy === "??" ? "U" : xy === "A " ? "A" : "M";
75
+ const previous = statuses.get(child);
76
+ statuses.set(child, previous && previous !== status ? "M" : status);
77
+ }
78
+ return statuses;
79
+ }
80
+ const MAX_FILE_BYTES = 512 * 1024;
81
+ async function readRepoFile(repo, relPath) {
82
+ await assertProjectFileAccess(folderOf(repo), relPath, "read");
83
+ const rel = normalizeRepoPath(relPath);
84
+ if (!rel) throw new Error("Name a file");
85
+ const abs = await resolveInsideRepo(folderOf(repo), rel);
86
+ const info = await stat(abs);
87
+ if (info.isDirectory()) throw new Error(`"${rel}" is a folder`);
88
+ const handle = await open(abs, "r");
89
+ try {
90
+ const buf = Buffer.alloc(Math.min(info.size, MAX_FILE_BYTES));
91
+ const { bytesRead } = await handle.read(buf, 0, buf.length, 0);
92
+ await assertProjectFileAccess(folderOf(repo), relPath, "read");
93
+ const head = buf.subarray(0, Math.min(bytesRead, 8e3));
94
+ if (head.includes(0)) return { path: rel, size: info.size, binary: true, truncated: false };
95
+ return { path: rel, size: info.size, text: buf.subarray(0, bytesRead).toString("utf8"), binary: false, truncated: info.size > bytesRead };
96
+ } finally {
97
+ await handle.close();
98
+ }
99
+ }
100
+ export {
101
+ listTree,
102
+ normalizeRepoPath,
103
+ readRepoFile,
104
+ resolveInsideRepo,
105
+ searchFiles
106
+ };
@@ -0,0 +1,466 @@
1
+ import { constants } from "node:fs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { lstat, mkdir, open, readdir, readlink, realpath, rename, rm, unlink } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { CODEFLOW_HOME, WORKTREE_DIR } from "./config.js";
6
+ import { acquireLock } from "./store.js";
7
+ import { git } from "./git.js";
8
+ import { readOnlyGitMetadata } from "./agent-container.js";
9
+ import { MemoryStore, validateDocument } from "./memory.js";
10
+ import { validateDeferredDocument } from "./deferred-runs.js";
11
+ import { validateCodeHostDocument } from "./code-hosts.js";
12
+ import { validateCoordinationDocument } from "./repo-coordination.js";
13
+ import { StrictRunLogReader } from "./run-log.js";
14
+ class RetentionPolicyConflictError extends Error {
15
+ constructor(current) {
16
+ super("Retention settings changed. Reload before saving.");
17
+ this.current = current;
18
+ }
19
+ status = 409;
20
+ }
21
+ const RETENTION_LIMITS = { runs: 2e3, memory: 5e4, targets: 1e3, files: 1e5, readBytes: 256 * 1024 * 1024, logBytes: 64 * 1024 * 1024, planAgeMs: 24 * 60 * 6e4 };
22
+ const KINDS = ["archivedRuns", "attachments", "memory", "worktrees"];
23
+ const sha = (value) => createHash("sha256").update(value).digest("hex");
24
+ const canonical = (value) => JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
25
+ const validId = (value) => typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,199}$/.test(value);
26
+ const inside = (parent, path) => {
27
+ const rel = relative(parent, path);
28
+ return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
29
+ };
30
+ const policyQueues = /* @__PURE__ */ new Map();
31
+ const defaults = () => ({ version: 1, revision: 0, days: { archivedRuns: null, attachments: null, memory: null, worktrees: null }, keepRunIds: [], keepMemoryIds: [] });
32
+ function validatePolicy(value, revision) {
33
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Choose valid retention settings");
34
+ const raw = value;
35
+ if (Object.keys(raw).some((key) => !["version", "revision", "days", "keepRunIds", "keepMemoryIds"].includes(key)) || !raw.days || typeof raw.days !== "object" || Array.isArray(raw.days) || Object.keys(raw.days).some((key) => !KINDS.includes(key))) throw new Error("Unsupported retention policy fields");
36
+ const result = defaults();
37
+ result.revision = revision;
38
+ for (const kind of KINDS) {
39
+ const days = raw.days[kind];
40
+ if (days !== null && (!Number.isInteger(days) || days < 1 || days > 36500)) throw new Error("Retention days must be 1\u201336,500, or null to keep that data");
41
+ result.days[kind] = days;
42
+ }
43
+ for (const field of ["keepRunIds", "keepMemoryIds"]) {
44
+ if (!Array.isArray(raw[field]) || raw[field].length > 1e3 || raw[field].some((id) => !validId(id))) throw new Error(`Choose at most 1,000 valid ${field}`);
45
+ result[field] = [...new Set(raw[field])].sort();
46
+ }
47
+ return result;
48
+ }
49
+ async function atomicJson(file, value) {
50
+ await mkdir(dirname(file), { recursive: true });
51
+ await safeParents(file);
52
+ const temp = `${file}.${randomUUID()}.tmp`;
53
+ try {
54
+ const handle = await open(temp, "wx", 384);
55
+ try {
56
+ await handle.writeFile(JSON.stringify(value, null, 2));
57
+ await handle.sync();
58
+ } finally {
59
+ await handle.close();
60
+ }
61
+ await rename(temp, file);
62
+ if (process.platform !== "win32") {
63
+ const folder = await open(dirname(file), "r");
64
+ try {
65
+ await folder.sync();
66
+ } finally {
67
+ await folder.close();
68
+ }
69
+ }
70
+ } finally {
71
+ await rm(temp, { force: true });
72
+ }
73
+ }
74
+ class RetentionPolicyStore {
75
+ file;
76
+ constructor(file = join(CODEFLOW_HOME, "retention-policy.json")) {
77
+ this.file = resolve(file);
78
+ }
79
+ async get() {
80
+ await policyQueues.get(this.file);
81
+ return readPolicy(this.file);
82
+ }
83
+ set(value, expectedRevision) {
84
+ const work = (policyQueues.get(this.file) ?? Promise.resolve()).then(async () => {
85
+ const previous = await readPolicy(this.file);
86
+ if (!Number.isSafeInteger(expectedRevision) || expectedRevision !== previous.revision) throw new RetentionPolicyConflictError(previous);
87
+ const next = validatePolicy(value, previous.revision + 1);
88
+ await atomicJson(this.file, next);
89
+ return structuredClone(next);
90
+ });
91
+ const tail = work.then(() => void 0, () => void 0);
92
+ policyQueues.set(this.file, tail);
93
+ void tail.then(() => {
94
+ if (policyQueues.get(this.file) === tail) policyQueues.delete(this.file);
95
+ });
96
+ return work;
97
+ }
98
+ }
99
+ async function readPolicy(file) {
100
+ try {
101
+ await safeParents(file);
102
+ const info = await lstat(file, { bigint: true });
103
+ if (!info.isFile() || info.size > BigInt(512 * 1024)) throw new Error("Policy must be a regular file under 512 KiB");
104
+ const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
105
+ try {
106
+ const before = await handle.stat({ bigint: true });
107
+ if (identity(before) !== identity(info)) throw new Error("Policy changed during inspection");
108
+ const bytes = await handle.readFile();
109
+ if (identity(before) !== identity(await handle.stat({ bigint: true })) || bytes.length !== Number(before.size)) throw new Error("Policy changed during inspection");
110
+ const raw = JSON.parse(bytes.toString("utf8"));
111
+ if (raw.version !== 1 || !Number.isSafeInteger(raw.revision) || raw.revision < 1) throw new Error("Unsupported retention policy version");
112
+ return validatePolicy(raw, raw.revision);
113
+ } finally {
114
+ await handle.close();
115
+ }
116
+ } catch (error) {
117
+ if (error.code === "ENOENT") return defaults();
118
+ throw new Error(`Retention policy cannot be read: ${error.message}`);
119
+ }
120
+ }
121
+ const exists = async (path) => lstat(path).then((info) => info, (error) => error.code === "ENOENT" ? void 0 : Promise.reject(error));
122
+ async function safeParents(path) {
123
+ for (let folder = dirname(path); ; folder = dirname(folder)) {
124
+ const info = await lstat(folder);
125
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("A destination ancestor is a symbolic link or not a directory");
126
+ if (folder === dirname(folder)) break;
127
+ }
128
+ }
129
+ function identity(info) {
130
+ return [String(info.dev), String(info.ino), String(info.mode), String(info.size), String(info.mtimeNs), String(info.ctimeNs)].join(":");
131
+ }
132
+ async function readFingerprint(path, budget, maximum = RETENTION_LIMITS.logBytes) {
133
+ await safeParents(path);
134
+ const info = await lstat(path, { bigint: true });
135
+ if (!info.isFile() || info.isSymbolicLink() || info.size > BigInt(maximum)) throw new Error("Only bounded regular files without symbolic links can be retained");
136
+ if (++budget.files > RETENTION_LIMITS.files || budget.bytes + Number(info.size) > RETENTION_LIMITS.readBytes) throw new Error("Retention scan reached its file or byte limit");
137
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
138
+ try {
139
+ const before = await handle.stat({ bigint: true });
140
+ if (identity(before) !== identity(info)) throw new Error("File changed during retention inspection");
141
+ const bytes = await handle.readFile();
142
+ budget.bytes += bytes.length;
143
+ const after = await handle.stat({ bigint: true });
144
+ if (identity(before) !== identity(after) || bytes.length !== Number(before.size)) throw new Error("File changed during retention inspection");
145
+ return { file: { path, bytes: bytes.length, fingerprint: sha(identity(before) + ":" + sha(bytes)) }, text: bytes.toString("utf8"), mtime: Number(before.mtimeMs) };
146
+ } finally {
147
+ await handle.close();
148
+ }
149
+ }
150
+ async function inventory(path, budget) {
151
+ await safeParents(path);
152
+ const root = await lstat(path, { bigint: true });
153
+ if (!root.isDirectory() || root.isSymbolicLink()) throw new Error("Workspace must be a regular directory without symbolic links");
154
+ const items = [];
155
+ let bytes = 0, newest = 0;
156
+ const walk = async (folder) => {
157
+ const before = await lstat(folder, { bigint: true });
158
+ items.push(`${relative(path, folder)}:${identity(before)}`);
159
+ newest = Math.max(newest, Number(before.mtimeMs));
160
+ for (const name of (await readdir(folder)).sort()) {
161
+ const file = join(folder, name), info = await lstat(file, { bigint: true });
162
+ if (++budget.files > RETENTION_LIMITS.files) throw new Error("Retention scan reached its file limit");
163
+ if (name === ".git" && file !== join(path, ".git")) throw new Error("A nested Git repository or submodule protects this workspace");
164
+ if (info.isDirectory()) await walk(file);
165
+ else if (info.isFile()) {
166
+ items.push(`${relative(path, file)}:${identity(info)}`);
167
+ bytes += Number(info.size);
168
+ newest = Math.max(newest, Number(info.mtimeMs));
169
+ } else if (info.isSymbolicLink()) items.push(`${relative(path, file)}:${identity(info)}:link:${await readlink(file)}`);
170
+ else throw new Error("A special file protects this workspace");
171
+ }
172
+ if (identity(before) !== identity(await lstat(folder, { bigint: true }))) throw new Error("Workspace changed during retention inspection");
173
+ };
174
+ await walk(path);
175
+ return { fingerprint: sha(items.join("\n")), bytes, newest };
176
+ }
177
+ async function runRecord(home, id, budget) {
178
+ const log = await readFingerprint(join(home, "runs", `${id}.jsonl`), budget);
179
+ if (!log.text.endsWith("\n")) throw new Error("Incomplete run log; recovery may still need its sources");
180
+ let run;
181
+ const reader = new StrictRunLogReader(id);
182
+ for (let start = 0; start < log.text.length; ) {
183
+ const end = log.text.indexOf("\n", start), line = log.text.slice(start, end < 0 ? log.text.length : end);
184
+ start = end < 0 ? log.text.length : end + 1;
185
+ if (!line.trim()) continue;
186
+ const value = JSON.parse(line);
187
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid run event");
188
+ const event = reader.decode(value);
189
+ if (event?.type === "run") run = event.run;
190
+ }
191
+ if (!run) throw new Error("No authoritative run snapshot");
192
+ return { run, log: log.file, mtime: log.mtime };
193
+ }
194
+ async function managedRoot(run) {
195
+ if (!validId(run.id) || typeof run.repo !== "string" || !isAbsolute(run.repo) || typeof run.worktreeDir !== "string") throw new Error("Managed workspace ownership is unavailable");
196
+ const repo = await realpath(run.repo), expected = resolve(repo, WORKTREE_DIR, run.id);
197
+ if (resolve(run.worktreeDir) !== expected) throw new Error("Recorded workspace is outside its managed run directory");
198
+ await safeParents(join(expected, ".context"));
199
+ const info = await lstat(expected);
200
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Managed run directory is not a regular directory");
201
+ return expected;
202
+ }
203
+ function terminal(run) {
204
+ return ["done", "failed", "cancelled"].includes(run.status) && run.archived === true && !run.pinned && !run.retentionPinned && !run.retention?.pinned && !run.followUps?.some((follow) => ["pending", "running"].includes(follow.status)) && !Object.values(run.watches ?? {}).some((watch) => !["closed", "stopped"].includes(watch.state));
205
+ }
206
+ function memoryReferences(run) {
207
+ return [...(run.memoryManifest?.included ?? []).map((ref) => ref.id), ...(run.team?.contextManifests ?? []).flatMap((manifest) => (manifest.memoryIds ?? []).map((id) => String(id).split("@")[0]))];
208
+ }
209
+ async function previewRetention(input) {
210
+ const homeInfo = await lstat(input.home);
211
+ if (!homeInfo.isDirectory() || homeInfo.isSymbolicLink()) throw new Error("Choose a regular application data directory");
212
+ const home = await realpath(input.home), now = input.now ?? Date.now();
213
+ if (!Number.isSafeInteger(now) || now < 0) throw new Error("Invalid retention preview time");
214
+ const policy = await new RetentionPolicyStore(join(home, "retention-policy.json")).get();
215
+ const cutoffs = Object.fromEntries(KINDS.map((kind) => [kind, policy.days[kind] === null ? null : now - policy.days[kind] * 864e5]));
216
+ const plan = { version: 1, home, createdAt: now, expiresAt: now + RETENTION_LIMITS.planAgeMs, policyRevision: policy.revision, policy, cutoffs, targets: [], protected: [], sources: {}, truncated: false, fingerprint: "", summary: { archivedRuns: 0, attachments: 0, memory: 0, worktrees: 0, bytes: 0 } };
217
+ const budget = { files: 0, bytes: 0 }, records = /* @__PURE__ */ new Map(), heldRuns = /* @__PURE__ */ new Map(), heldMemory = /* @__PURE__ */ new Map();
218
+ const protect = (kind, id, reason) => {
219
+ if (plan.protected.length < 5e3) plan.protected.push({ kind, id, reason: reason.slice(0, 500) });
220
+ };
221
+ const incomplete = (kind, id, error) => {
222
+ plan.truncated = true;
223
+ protect(kind, id, error.message);
224
+ };
225
+ const target = (value) => {
226
+ if (plan.targets.length >= RETENTION_LIMITS.targets) {
227
+ plan.truncated = true;
228
+ protect(value.kind, value.id, "Retention target limit reached");
229
+ } else plan.targets.push(value);
230
+ };
231
+ const runDirectory = await exists(join(home, "runs"));
232
+ if (runDirectory) {
233
+ if (!runDirectory.isDirectory() || runDirectory.isSymbolicLink()) throw new Error("Run storage must be a regular directory without a symbolic link");
234
+ const files = (await readdir(join(home, "runs"))).filter((file) => file.endsWith(".jsonl") && !file.endsWith(".journal.jsonl")).sort();
235
+ if (files.length > RETENTION_LIMITS.runs) plan.truncated = true;
236
+ plan.sources["run-directory"] = sha(files.join("\n"));
237
+ for (const file of files.slice(0, RETENTION_LIMITS.runs)) {
238
+ const id = file.slice(0, -6);
239
+ if (!validId(id)) {
240
+ incomplete("archivedRuns", id, new Error("Invalid run log name"));
241
+ continue;
242
+ }
243
+ try {
244
+ const record = await runRecord(home, id, budget);
245
+ records.set(id, record);
246
+ plan.sources[record.log.path] = record.log.fingerprint;
247
+ } catch (error) {
248
+ incomplete("archivedRuns", id, error);
249
+ }
250
+ }
251
+ }
252
+ let memory = [];
253
+ try {
254
+ const file = join(home, "memory.sqlite"), legacy = join(home, "memory.json");
255
+ if (await exists(file)) {
256
+ const before = (await readFingerprint(file, budget)).file.fingerprint;
257
+ memory = await new MemoryStore(file).maintenanceEntries();
258
+ const after = (await readFingerprint(file, budget)).file.fingerprint;
259
+ if (before !== after) throw new Error("Memory changed during retention inspection");
260
+ plan.sources[file] = after;
261
+ } else if (await exists(legacy)) {
262
+ const data = await readFingerprint(legacy, budget, 32 * 1024 * 1024);
263
+ plan.sources[legacy] = data.file.fingerprint;
264
+ memory = validateDocument(data.text).entries;
265
+ }
266
+ memory = memory.filter((entry) => entry.status !== "deleted");
267
+ if (memory.length > RETENTION_LIMITS.memory) {
268
+ plan.truncated = true;
269
+ memory = memory.slice(0, RETENTION_LIMITS.memory);
270
+ }
271
+ } catch (error) {
272
+ incomplete("memory", "memory-store", error);
273
+ }
274
+ for (const id of policy.keepRunIds) heldRuns.set(id, "Kept by the saved retention policy");
275
+ for (const id of policy.keepMemoryIds) heldMemory.set(id, "Kept by the saved retention policy");
276
+ for (const { run } of records.values()) {
277
+ if (run.parentRunId) heldRuns.set(run.parentRunId, `Source of retained child run ${run.id}`);
278
+ if (run.teamRerun?.parentRunId) heldRuns.set(run.teamRerun.parentRunId, `Pinned baseline for retained rerun ${run.id}`);
279
+ if (!terminal(run) || policy.keepRunIds.includes(run.id)) for (const id of memoryReferences(run)) heldMemory.set(id, `Referenced by active, resumable or kept run ${run.id}`);
280
+ }
281
+ for (const entry of memory) for (const source of entry.derivedFrom) heldMemory.set(source.id, `Provenance source for retained memory ${entry.id}`);
282
+ try {
283
+ const file = join(home, "deferred-runs.json");
284
+ if (await exists(file)) {
285
+ const data = await readFingerprint(file, budget, 32 * 1024 * 1024);
286
+ plan.sources[file] = data.file.fingerprint;
287
+ for (const job of validateDeferredDocument(JSON.parse(data.text)).jobs) if (["queued", "waiting-time", "waiting-provider", "claimed", "unknown"].includes(job.status) && job.run.parentRunId) heldRuns.set(job.run.parentRunId, `Source of pending deferred job ${job.id}`);
288
+ }
289
+ const triggerFile = join(home, "triggers.json");
290
+ if (await exists(triggerFile)) {
291
+ const data = await readFingerprint(triggerFile, budget, 16 * 1024 * 1024);
292
+ plan.sources[triggerFile] = data.file.fingerprint;
293
+ const triggers = JSON.parse(data.text);
294
+ if (!Array.isArray(triggers)) throw new Error("Invalid automation settings");
295
+ for (const trigger of triggers) if (trigger?.enabled && typeof trigger.run?.parentRunId === "string") heldRuns.set(trigger.run.parentRunId, "Source of an enabled automation");
296
+ }
297
+ const coordinationFile = join(home, "coordinated-changes.json");
298
+ if (await exists(coordinationFile)) {
299
+ const data = await readFingerprint(coordinationFile, budget, 32 * 1024 * 1024);
300
+ plan.sources[coordinationFile] = data.file.fingerprint;
301
+ for (const job of validateCoordinationDocument(JSON.parse(data.text)).jobs) for (const node of job.nodes) for (const runId of [node.runId, node.result?.runId, ...(node.dependencies ?? []).map((receipt) => receipt.runId)].filter((id) => !!id)) heldRuns.set(runId, `Exact launch, dependency or shipping receipt for retained coordinated change ${job.id}`);
302
+ }
303
+ const codeHostFile = join(home, "code-host-operations.json");
304
+ if (await exists(codeHostFile)) {
305
+ const data = await readFingerprint(codeHostFile, budget, 512e3);
306
+ plan.sources[codeHostFile] = data.file.fingerprint;
307
+ for (const receipt of validateCodeHostDocument(JSON.parse(data.text)).operations) if (receipt.runId) heldRuns.set(receipt.runId, `Exact code host action receipt ${receipt.id}`);
308
+ }
309
+ } catch (error) {
310
+ incomplete("archivedRuns", "pending-launch-sources", error);
311
+ }
312
+ for (const entry of memory) {
313
+ const owner = entry.runId ? records.get(entry.runId)?.run : void 0;
314
+ let reason = entry.scope === "project" ? "Project knowledge requires individual review and is never bulk-deleted" : entry.tags.includes("pinned") ? "Pinned memory" : heldMemory.get(entry.id);
315
+ if (!reason && (!owner || !terminal(owner) || heldRuns.has(owner.id))) reason = "Its owner run is active, resumable, kept or unavailable";
316
+ if (!reason && (cutoffs.memory === null || entry.updatedAt >= cutoffs.memory)) reason = "Memory retention is disabled or this revision is recent";
317
+ if (reason) protect("memory", entry.id, reason);
318
+ else {
319
+ const ref = { id: entry.id, revision: entry.revision };
320
+ target({ id: `memory:${entry.id}`, kind: "memory", runId: entry.runId, memory: ref, fingerprint: sha(canonical(entry)), bytes: 0, detail: "Erase this old private/team body, retaining its provenance tombstone" });
321
+ }
322
+ }
323
+ const retiringMemory = new Set(plan.targets.filter((item) => item.kind === "memory").map((item) => item.memory.id));
324
+ for (const entry of memory.filter((entry2) => !retiringMemory.has(entry2.id))) for (const id of [entry.runId, ...entry.origins.map((origin) => origin.runId)].filter((id2) => !!id2)) heldRuns.set(id, `Provenance for retained ${entry.scope} memory ${entry.id}`);
325
+ for (const [id, { run, log, mtime }] of records) {
326
+ const reason = !terminal(run) ? "Run is active, resumable, unarchived, pinned or still watched" : heldRuns.get(id);
327
+ if (reason) {
328
+ for (const kind of ["archivedRuns", "attachments", "worktrees"]) protect(kind, id, reason);
329
+ continue;
330
+ }
331
+ if (cutoffs.archivedRuns !== null && mtime < cutoffs.archivedRuns) {
332
+ try {
333
+ const files = [log];
334
+ for (const suffix of [".journal.jsonl", ".checkpoint.json", ".cold-index.json", ".cold-detail.json"]) {
335
+ const path = join(home, "runs", id + suffix);
336
+ if (await exists(path)) {
337
+ const file = await readFingerprint(path, budget);
338
+ if (!suffix.startsWith(".cold-") && file.mtime >= cutoffs.archivedRuns) throw new Error("A recovery record was updated after the retention cutoff");
339
+ files.push(file.file);
340
+ }
341
+ }
342
+ target({ id: `archivedRuns:${id}`, kind: "archivedRuns", runId: id, files, fingerprint: sha(canonical(files)), bytes: files.reduce((sum, file) => sum + file.bytes, 0), detail: "Delete this archived terminal log, journal and optional checkpoint" });
343
+ } catch (error) {
344
+ incomplete("archivedRuns", id, error);
345
+ }
346
+ } else protect("archivedRuns", id, "Run-log retention is disabled or the run was recently updated");
347
+ if (cutoffs.attachments === null && cutoffs.worktrees === null) continue;
348
+ let managed;
349
+ try {
350
+ managed = await managedRoot(run);
351
+ } catch (error) {
352
+ protect("attachments", id, error.message);
353
+ protect("worktrees", id, error.message);
354
+ continue;
355
+ }
356
+ const trees = Array.isArray(run.worktrees) ? run.worktrees : [];
357
+ if (cutoffs.worktrees !== null) for (const tree of trees) {
358
+ try {
359
+ if (typeof tree.path !== "string" || dirname(resolve(tree.path)) !== managed || typeof tree.branch !== "string") throw new Error("Workspace is outside this run's managed directory");
360
+ if (!await exists(tree.path)) continue;
361
+ readOnlyGitMetadata(tree.path);
362
+ const [listing, status, parentTracked, head, flags] = await Promise.all([git(["worktree", "list", "--porcelain", "-z"], run.repo, { timeoutMs: 5e3 }), git(["status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], tree.path, { timeoutMs: 5e3 }), inside(run.repo, tree.path) ? git(["--literal-pathspecs", "ls-files", "-z", "--", relative(run.repo, tree.path)], run.repo, { timeoutMs: 5e3 }) : Promise.resolve({ code: 0, stdout: "" }), git(["rev-parse", "--verify", "HEAD"], tree.path, { timeoutMs: 5e3 }), git(["ls-files", "-v", "-z"], tree.path, { timeoutMs: 5e3 })]);
363
+ const record = listing.stdout.split("\0\0").find((record2) => record2.split("\0").includes(`worktree ${tree.path}`));
364
+ if (listing.code || !record || !record.split("\0").includes(`branch refs/heads/${tree.branch}`) || record.split("\0").some((field) => /^locked|^prunable/.test(field)) || status.code || status.stdout.trim() || parentTracked.code || parentTracked.stdout.trim() || head.code || flags.code || flags.stdout.split("\0").some((file) => /^[a-zS] /.test(file))) throw new Error("Workspace is dirty, untracked, locked, nested, hides tracked changes or no longer owns its branch");
365
+ const view = await inventory(tree.path, budget);
366
+ if (Math.max(mtime, view.newest) >= cutoffs.worktrees) {
367
+ protect("worktrees", tree.path, "Workspace files or run metadata were updated after the cutoff");
368
+ continue;
369
+ }
370
+ target({ id: `worktrees:${id}:${tree.name ?? tree.branch}`, kind: "worktrees", runId: id, path: tree.path, repo: run.repo, branch: tree.branch, fingerprint: sha(view.fingerprint + ":" + head.stdout.trim()), bytes: view.bytes, detail: "Remove this clean linked workspace without force; its Git branch is preserved" });
371
+ } catch (error) {
372
+ protect("worktrees", String(tree.path ?? id), error.message);
373
+ if (/limit|changed during/.test(error.message)) plan.truncated = true;
374
+ }
375
+ }
376
+ if (cutoffs.attachments !== null) {
377
+ const folders = [join(managed, ".context", "attachments"), join(managed, ".context", "messages"), ...trees.filter((tree) => typeof tree.path === "string" && dirname(resolve(tree.path)) === managed).flatMap((tree) => [join(tree.path, ".codeflow-context", "attachments"), join(tree.path, ".codeflow-context", "messages")])];
378
+ for (const folder of folders) {
379
+ try {
380
+ const info = await exists(folder);
381
+ if (!info) continue;
382
+ await safeParents(join(folder, "placeholder"));
383
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Attachment storage is not a regular directory");
384
+ for (const name of (await readdir(folder)).sort()) {
385
+ const path = join(folder, name);
386
+ if (plan.targets.some((item) => item.kind === "worktrees" && path.startsWith(item.path + sep))) continue;
387
+ const data = await readFingerprint(path, budget, 10 * 1024 * 1024);
388
+ if (Math.max(mtime, data.mtime) >= cutoffs.attachments) {
389
+ protect("attachments", path, "Attachment or run metadata is recent");
390
+ continue;
391
+ }
392
+ const owner = trees.find((tree) => typeof tree.path === "string" && inside(tree.path, path))?.path ?? run.repo;
393
+ const tracked = inside(owner, path) ? await git(["--literal-pathspecs", "ls-files", "--error-unmatch", "--", relative(owner, path)], owner, { timeoutMs: 5e3 }) : { code: 1 };
394
+ if (tracked.code !== 1) {
395
+ protect("attachments", path, "Tracked or unverified repository material is preserved");
396
+ continue;
397
+ }
398
+ target({ id: `attachments:${id}:${relative(managed, path)}`, kind: "attachments", runId: id, path, files: [data.file], fingerprint: data.file.fingerprint, bytes: data.file.bytes, detail: "Delete this staged upload/message attachment or generated workspace copy" });
399
+ }
400
+ } catch (error) {
401
+ protect("attachments", folder, error.message);
402
+ if (/limit|changed during/.test(error.message)) plan.truncated = true;
403
+ }
404
+ }
405
+ }
406
+ }
407
+ plan.targets.sort((a, b) => a.id.localeCompare(b.id));
408
+ plan.protected.sort((a, b) => `${a.kind}:${a.id}:${a.reason}`.localeCompare(`${b.kind}:${b.id}:${b.reason}`));
409
+ for (const item of plan.targets) {
410
+ plan.summary[item.kind]++;
411
+ plan.summary.bytes += item.bytes;
412
+ }
413
+ plan.fingerprint = sha(canonical({ ...plan, fingerprint: "" }));
414
+ return plan;
415
+ }
416
+ async function applyRetention(input) {
417
+ const now = input.now ?? Date.now(), supplied = input.plan;
418
+ if (!supplied || supplied.version !== 1 || !Number.isSafeInteger(supplied.createdAt) || supplied.createdAt > now + 5e3 || now - supplied.createdAt > RETENTION_LIMITS.planAgeMs || supplied.expiresAt !== supplied.createdAt + RETENTION_LIMITS.planAgeMs || !/^[a-f0-9]{64}$/.test(supplied.fingerprint)) throw new Error("Choose a valid retention preview from the last 24 hours");
419
+ const info = await lstat(input.home);
420
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Application data directory must be regular");
421
+ const home = await realpath(input.home);
422
+ if (home !== supplied.home) throw new Error("Retention preview belongs to a different data directory");
423
+ const release = await acquireLock(0, home);
424
+ try {
425
+ const plan = await previewRetention({ home, now: supplied.createdAt });
426
+ if (plan.truncated || supplied.truncated) throw new Error("Incomplete retention scans cannot be applied; resolve the protected scan limits or invalid data first");
427
+ if (plan.fingerprint !== supplied.fingerprint || canonical(plan) !== canonical(supplied)) throw new Error("Data, ownership or policy changed after preview. No changes applied; create and review another preview.");
428
+ const applied = [], failed = [];
429
+ const memory = plan.targets.filter((item) => item.kind === "memory");
430
+ if (memory.length) {
431
+ await new MemoryStore(join(home, "memory.sqlite")).retireMaintenance(memory.map((item) => item.memory));
432
+ applied.push(...memory.map((item) => item.id));
433
+ }
434
+ for (const item of [...plan.targets.filter((item2) => item2.kind === "worktrees"), ...plan.targets.filter((item2) => item2.kind === "attachments"), ...plan.targets.filter((item2) => item2.kind === "archivedRuns")]) {
435
+ try {
436
+ if (item.kind === "worktrees") {
437
+ await safeParents(item.path);
438
+ readOnlyGitMetadata(item.path);
439
+ const current = await inventory(item.path, { files: 0, bytes: 0 }), head = await git(["rev-parse", "--verify", "HEAD"], item.path, { timeoutMs: 5e3 });
440
+ if (head.code || sha(current.fingerprint + ":" + head.stdout.trim()) !== item.fingerprint) throw new Error("Workspace changed before removal; stopped applying this plan");
441
+ const removed = await git(["worktree", "remove", "--", item.path], item.repo, { timeoutMs: 3e4 });
442
+ if (removed.code) throw new Error(removed.stderr.trim() || "Git refused workspace removal");
443
+ } else for (const file of item.files ?? []) {
444
+ await safeParents(file.path);
445
+ const current = await readFingerprint(file.path, { files: 0, bytes: 0 });
446
+ if (current.file.fingerprint !== file.fingerprint) throw new Error("File changed before deletion; stopped applying this plan");
447
+ await unlink(file.path);
448
+ }
449
+ applied.push(item.id);
450
+ } catch (error) {
451
+ failed.push({ id: item.id, reason: error.message });
452
+ break;
453
+ }
454
+ }
455
+ return { fingerprint: plan.fingerprint, applied, failed, remaining: plan.targets.filter((item) => !applied.includes(item.id)).map((item) => item.id) };
456
+ } finally {
457
+ release();
458
+ }
459
+ }
460
+ export {
461
+ RETENTION_LIMITS,
462
+ RetentionPolicyConflictError,
463
+ RetentionPolicyStore,
464
+ applyRetention,
465
+ previewRetention
466
+ };
@@ -0,0 +1,38 @@
1
+ import { checksLabel } from "./run-status.js";
2
+ function reviewReason(run) {
3
+ const question = run.status === "running" && !!run.team?.awaitingRequestId;
4
+ if (run.archived || run.projectTeam && !run.awaiting && !question) return;
5
+ if (run.awaiting || question) return "answer";
6
+ if (run.team?.status === "blocked") return "blocked";
7
+ if (!run.seen && (run.status === "failed" || run.status === "interrupted")) return "failed";
8
+ if (!run.seen && run.status !== "running") {
9
+ if (checksLabel(run) === "Blocked" || run.status === "stopped") return "blocked";
10
+ if (checksLabel(run) === "Failed") return "failed";
11
+ }
12
+ if (run.triage?.state === "pending") return "review";
13
+ if (!run.seen && run.status === "done" && run.artifacts?.some((artifact) => artifact.stat?.trim())) return "review";
14
+ }
15
+ const REVIEW_REASON_LABELS = { answer: "Waiting for your answer", blocked: "Blocked work", failed: "Failed checks or execution", review: "Results to review" };
16
+ const reasonOrder = { answer: 0, blocked: 1, failed: 2, review: 3 };
17
+ function prioritizedReviewRuns(runs, input = {}) {
18
+ const words = (input.query ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
19
+ return runs.filter((run) => {
20
+ const reason = reviewReason(run);
21
+ return reason && (!input.reason || reason === input.reason) && words.every((word) => [run.task, run.repo, run.id, run.trigger?.name ?? "", run.source?.title ?? ""].join("\n").toLowerCase().includes(word));
22
+ }).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || reasonOrder[reviewReason(a)] - reasonOrder[reviewReason(b)] || a.createdAt - b.createdAt || a.id.localeCompare(b.id));
23
+ }
24
+ function groupReviewRuns(runs, key) {
25
+ const groups = /* @__PURE__ */ new Map();
26
+ for (const run of runs) {
27
+ const group = key(run);
28
+ if (!groups.has(group.id)) groups.set(group.id, { ...group, runs: [] });
29
+ groups.get(group.id).runs.push(run);
30
+ }
31
+ return [...groups.values()];
32
+ }
33
+ export {
34
+ REVIEW_REASON_LABELS,
35
+ groupReviewRuns,
36
+ prioritizedReviewRuns,
37
+ reviewReason
38
+ };