@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,117 @@
1
+ const idPattern = /^[a-z][a-z0-9-]{0,39}$/;
2
+ const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3
+ const fail = (message) => {
4
+ throw new Error(message);
5
+ };
6
+ const text = (v, max = 4e3) => typeof v === "string" && v.length <= max && !v.includes("\0") ? v : fail("Template text is missing or too long");
7
+ const ident = (v) => typeof v === "string" && idPattern.test(v) ? v : fail("Use lowercase template identifiers (up to 40 characters)");
8
+ const list = (v, max = 24) => Array.isArray(v) && v.length <= max ? v.map((x) => text(x)) : fail("Invalid template list");
9
+ function validateTeamTemplate(raw) {
10
+ if (!object(raw) || raw.schemaVersion !== 1) return fail("Expected a version 1 team template");
11
+ const keys = ["schemaVersion", "id", "version", "name", "description", "scope", "project", "autoEnabled", "parameters", "roles", "tasks", "contracts", "workflow", "task", "criteria", "preferences"];
12
+ if (Object.keys(raw).some((k) => !keys.includes(k))) return fail("Templates cannot contain permissions, credentials, sessions, or unsupported fields");
13
+ if (!Number.isSafeInteger(raw.version) || raw.version < 1 || !["builtin", "personal", "project"].includes(raw.scope) || typeof raw.autoEnabled !== "boolean") return fail("Invalid template version or scope");
14
+ if (raw.scope === "project" !== (typeof raw.project === "string" && !!raw.project)) return fail("Project templates need a project; personal templates cannot bind one");
15
+ const parameters = list(raw.parameters).map(ident), roles = list(raw.roles, 6).map(ident);
16
+ if (new Set(parameters).size !== parameters.length || new Set(roles).size !== roles.length) return fail("Duplicate template parameters or roles");
17
+ if (!Array.isArray(raw.tasks) || raw.tasks.length > 24) return fail("A template can contain at most 24 tasks");
18
+ const tasks = raw.tasks.map((t) => {
19
+ if (!object(t) || Object.keys(t).some((k) => !["id", "title", "role", "kind", "instructions", "dependsOn", "dependencyTypes", "paths", "acceptance"].includes(k)) || !["implementation", "investigation"].includes(t.kind)) return fail("Invalid template task");
20
+ const task = { id: ident(t.id), title: text(t.title, 200), role: ident(t.role), kind: t.kind, instructions: text(t.instructions, 8e3), dependsOn: list(t.dependsOn).map(ident), paths: list(t.paths), acceptance: list(t.acceptance) };
21
+ if (t.dependencyTypes !== void 0) {
22
+ if (!object(t.dependencyTypes) || Object.entries(t.dependencyTypes).some(([id, kind]) => !task.dependsOn.includes(id) || !["completion", "artifact", "knowledge"].includes(kind))) return fail("Invalid template dependency types");
23
+ task.dependencyTypes = { ...t.dependencyTypes };
24
+ }
25
+ if (!roles.includes(task.role) || !task.title.trim() || !task.instructions.trim() || !task.acceptance.length || task.acceptance.some((c) => !c.trim()) || !task.paths.length) return fail("Every task needs a role, instructions, owned paths, and acceptance criteria");
26
+ for (const path of task.paths) if (!validTemplatePath(path)) return fail("Template paths must be repository relative");
27
+ return task;
28
+ });
29
+ const ids = new Set(tasks.map((t) => t.id));
30
+ if (ids.size !== tasks.length) return fail("Duplicate template task IDs");
31
+ const seen = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set();
32
+ const visit = (id) => {
33
+ if (visiting.has(id)) return fail("Template dependencies contain a cycle");
34
+ if (seen.has(id)) return;
35
+ const task = tasks.find((t) => t.id === id);
36
+ if (!task) return fail(`Unknown template dependency ${id}`);
37
+ visiting.add(id);
38
+ task.dependsOn.forEach(visit);
39
+ visiting.delete(id);
40
+ seen.add(id);
41
+ };
42
+ tasks.forEach((t) => visit(t.id));
43
+ const workflow = raw.workflow === void 0 ? void 0 : ident(raw.workflow);
44
+ if (workflow && tasks.length || !workflow && !tasks.length) return fail("Choose a fixed workflow or a task graph");
45
+ if (workflow && raw.autoEnabled) return fail("Fixed workflows are manual templates; Auto composes validated task graphs");
46
+ const result = { schemaVersion: 1, id: ident(raw.id), version: raw.version, name: text(raw.name, 100), description: text(raw.description), scope: raw.scope, ...raw.project ? { project: text(raw.project, 200) } : {}, autoEnabled: raw.autoEnabled, parameters, roles, tasks, contracts: list(raw.contracts), ...workflow ? { workflow } : {}, task: text(raw.task, 1e5), criteria: list(raw.criteria, 100) };
47
+ if (!result.name.trim() || !result.task.trim()) return fail("Give the template a name and a task prompt");
48
+ if (raw.preferences !== void 0) result.preferences = templatePreferences(raw.preferences);
49
+ const body = JSON.stringify([tasks, result.contracts, result.task, result.criteria]);
50
+ for (const match of body.matchAll(/\{\{([a-z][a-z0-9-]*)\}\}/g)) if (!parameters.includes(match[1])) return fail(`Undeclared parameter ${match[1]}`);
51
+ if (JSON.stringify(result).length > 15e4) return fail("Template is too large");
52
+ return result;
53
+ }
54
+ function templatePreferences(raw) {
55
+ if (!object(raw) || Object.keys(raw).some((k) => !["roles", "options", "agentSettings", "roleSettings"].includes(k))) return fail("Template preferences may contain only roles, options and model settings");
56
+ const record = (v, parse) => {
57
+ if (!object(v) || Object.keys(v).length > 64 || Object.keys(v).some((k) => !/^[a-zA-Z][a-zA-Z0-9_:/.-]{0,150}$/.test(k) || ["constructor", "prototype", "__proto__"].includes(k))) return fail("Invalid template preference record");
58
+ return Object.fromEntries(Object.entries(v).map(([k, value]) => [k, parse(value)]));
59
+ };
60
+ const settings = (v) => record(v, (value) => {
61
+ if (!object(value) || Object.keys(value).some((k) => !["model", "effort", "variants"].includes(k))) return fail("Only model and effort preferences are supported");
62
+ if (value.variants !== void 0 && (!Array.isArray(value.variants) || value.variants.length > 128)) return fail("Invalid model variants");
63
+ const variants = value.variants?.map((v2) => {
64
+ if (!object(v2) || Object.keys(v2).some((k) => !["model", "effort"].includes(k))) return fail("Invalid model variant");
65
+ return { model: text(v2.model, 256), ...v2.effort !== void 0 ? { effort: text(v2.effort, 64) } : {} };
66
+ });
67
+ return { ...value.model !== void 0 ? { model: text(value.model, 256) } : {}, ...value.effort !== void 0 ? { effort: text(value.effort, 64) } : {}, ...variants ? { variants } : {} };
68
+ });
69
+ return { roles: record(raw.roles ?? {}, (value) => list(value, 6)), options: record(raw.options ?? {}, (value) => typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) ? value : fail("Invalid workflow option")), agentSettings: settings(raw.agentSettings ?? {}), roleSettings: record(raw.roleSettings ?? {}, settings) };
70
+ }
71
+ function validTemplatePath(path) {
72
+ return path === "*" || !!path && !/^[/-]|^[A-Za-z]:|[\\\x00-\x1f]/.test(path) && !path.split("/").some((p) => !p || p === "." || p === ".." || p === ".git") && !path.includes("*");
73
+ }
74
+ function fillTeamTemplate(template, inputs) {
75
+ if (Object.keys(inputs).some((k) => !template.parameters.includes(k))) return fail("Unknown template input");
76
+ for (const key of template.parameters) if (typeof inputs[key] !== "string" || !inputs[key].trim() || inputs[key].length > 1e4) return fail(`Enter a value for ${key}`);
77
+ const fill = (s) => s.replace(/\{\{([a-z][a-z0-9-]*)\}\}/g, (_, key) => inputs[key]);
78
+ return validateTeamTemplate({ ...template, task: fill(template.task), criteria: template.criteria.map(fill), contracts: template.contracts.map(fill), tasks: template.tasks.map((t) => ({ ...t, title: fill(t.title), instructions: fill(t.instructions), acceptance: t.acceptance.map(fill), paths: t.paths.map(fill) })) });
79
+ }
80
+ function expandTemplateUses(plan, catalog) {
81
+ const tasks = [...plan.tasks], contracts = [...plan.contracts];
82
+ if ((plan.templateUses?.length ?? 0) > 6) return fail("Too many template uses");
83
+ const uses = /* @__PURE__ */ new Set();
84
+ for (const use of plan.templateUses ?? []) {
85
+ if (!/^[a-z][a-z0-9-]{0,11}$/.test(use.id) || uses.has(use.id)) return fail("Template instance IDs must be unique and at most 12 characters");
86
+ uses.add(use.id);
87
+ const source = catalog.find((t) => t.id === use.templateId && t.version === use.version && !t.workflow);
88
+ if (!source) return fail("Template version is not in this run's pinned catalog");
89
+ const inputs = Object.fromEntries(use.inputs.map((i) => [i.name, i.value]));
90
+ const assignments = Object.fromEntries(use.assignments.map((a) => [a.role, a.memberId]));
91
+ if (Object.keys(inputs).length !== use.inputs.length || Object.keys(assignments).length !== use.assignments.length || Object.keys(assignments).some((r) => !source.roles.includes(r))) return fail("Duplicate or unknown template bindings");
92
+ const template = fillTeamTemplate(source, inputs);
93
+ for (const role of template.roles) if (!assignments[role]) return fail(`Assign the ${role} template role`);
94
+ for (const t of template.tasks) {
95
+ const id = `${use.id}-${t.id}`;
96
+ if (id.length > 40) return fail("Expanded task ID is too long");
97
+ tasks.push({ id, title: t.title, kind: t.kind, owner: assignments[t.role], instructions: `${t.instructions}${use.instructions ? `
98
+ Task-specific additions:
99
+ ${text(use.instructions)}` : ""}`, dependsOn: [...use.dependsOn, ...t.dependsOn.map((d) => `${use.id}-${d}`)], ...t.dependencyTypes ? { dependencyTypes: Object.fromEntries(Object.entries(t.dependencyTypes).map(([id2, kind]) => [`${use.id}-${id2}`, kind])) } : {}, requirements: [...use.requirements], paths: [...t.paths], acceptance: [...t.acceptance] });
100
+ }
101
+ contracts.push(...template.contracts, ...template.criteria);
102
+ }
103
+ if (tasks.length > 24 || contracts.length > 24) return fail("Expanded templates exceed the run's task or contract limits");
104
+ return { ...plan, tasks, contracts };
105
+ }
106
+ function templateFromPlan(plan, input) {
107
+ const owners = [...new Set(plan.tasks.map((t) => t.owner))];
108
+ const role = (owner) => `worker-${owners.indexOf(owner) + 1}`;
109
+ return validateTeamTemplate({ schemaVersion: 1, ...input, version: 1, description: "Strategy saved from a run. Review task-specific instructions and replace them with parameters before reuse.", autoEnabled: false, parameters: ["task"], roles: owners.map(role), task: "{{task}}", criteria: [], contracts: [...plan.contracts], tasks: plan.tasks.map((t) => ({ id: t.id, role: role(t.owner), title: t.title, kind: t.kind, instructions: t.instructions, dependsOn: [...t.dependsOn], ...t.dependencyTypes ? { dependencyTypes: { ...t.dependencyTypes } } : {}, paths: [...t.paths], acceptance: [...t.acceptance] })) });
110
+ }
111
+ export {
112
+ expandTemplateUses,
113
+ fillTeamTemplate,
114
+ templateFromPlan,
115
+ templatePreferences,
116
+ validateTeamTemplate
117
+ };
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // Dependency-free stdio MCP transport. Only the parent-created member capability is used;
3
+ // this process has no file, shell, permission-approval, or identity-selection operations.
4
+ import { createInterface } from "node:readline";
5
+ import { readFileSync, unlinkSync } from "node:fs";
6
+
7
+ let connection;
8
+ try {
9
+ if (process.argv[2] === "--connection") {
10
+ connection = JSON.parse(readFileSync(process.argv[3], "utf8"));
11
+ // Read once, then gone: every CLI on this machine runs as the same user, so a credential
12
+ // left on disk could be read by another member's shell for as long as the turn lasts.
13
+ try { unlinkSync(process.argv[3]); } catch { /* already removed */ }
14
+ }
15
+ } catch { process.exit(1); }
16
+ const endpoint = connection?.endpoint ?? process.env.CODEFLOW_TEAM_ENDPOINT;
17
+ const token = connection?.token ?? process.env.CODEFLOW_TEAM_CAPABILITY;
18
+ delete process.env.CODEFLOW_TEAM_CAPABILITY;
19
+ delete process.env.CODEFLOW_TOKEN;
20
+ if (!endpoint || !/^http:\/\/127\.0\.0\.1:\d+\/v1\/runs\//.test(endpoint) || !token) process.exit(1);
21
+
22
+ let initialized = false;
23
+ const send = (value) => process.stdout.write(`${JSON.stringify(value)}\n`);
24
+ async function bridge(body) {
25
+ const response = await fetch(endpoint, {
26
+ method: "POST", redirect: "error", signal: AbortSignal.timeout(10_000),
27
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
28
+ body: JSON.stringify(body),
29
+ });
30
+ const result = await response.json();
31
+ if (!response.ok || result.error) throw new Error(result.error ?? "Team bridge request failed");
32
+ return result;
33
+ }
34
+ async function request(message) {
35
+ // JSON can parse successfully without yielding a JSON-RPC object (for example null).
36
+ // Reject the envelope before dereferencing it so malformed input cannot terminate MCP.
37
+ const validId = (id) => typeof id === "string" || typeof id === "number" && Number.isFinite(id);
38
+ if (!message || typeof message !== "object" || Array.isArray(message) || message.jsonrpc !== "2.0" ||
39
+ typeof message.method !== "string" || (message.id !== undefined && !validId(message.id))) {
40
+ send({ jsonrpc: "2.0", id: validId(message?.id) ? message.id : null, error: { code: -32600, message: "Invalid request" } }); return;
41
+ }
42
+ const id = message.id;
43
+ if (id === undefined) return;
44
+ try {
45
+ let result;
46
+ if (message.method === "initialize") {
47
+ initialized = true;
48
+ const supported = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
49
+ result = { protocolVersion: supported.includes(message.params?.protocolVersion) ? message.params.protocolVersion : "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "code-flow-team", version: "1.0.0" } };
50
+ } else if (message.method === "ping") result = {};
51
+ else if (!initialized) throw new Error("Initialize the team bridge first");
52
+ else if (message.method === "tools/list") result = await bridge({ method: "tools/list" });
53
+ else if (message.method === "tools/call") {
54
+ try {
55
+ const value = await bridge({ name: message.params?.name, arguments: message.params?.arguments ?? {} });
56
+ result = { content: [{ type: "text", text: JSON.stringify(value.result) }], isError: false };
57
+ } catch (error) { result = { content: [{ type: "text", text: error instanceof Error ? error.message : "Team command failed" }], isError: true }; }
58
+ } else {
59
+ send({ jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" } }); return;
60
+ }
61
+ send({ jsonrpc: "2.0", id, result });
62
+ } catch (error) { send({ jsonrpc: "2.0", id, error: { code: -32603, message: error instanceof Error ? error.message : "Team bridge failed" } }); }
63
+ }
64
+
65
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
66
+ let inflight = 0;
67
+ const limit = (id) => send({ jsonrpc: "2.0", id, error: { code: -32600, message: "Request exceeds bridge limits" } });
68
+ /** The id of an envelope too large to parse, so the caller's request still gets its answer instead of hanging. */
69
+ const idOf = (line) => { const match = /"id"\s*:\s*("(?:[^"\\]|\\.)*"|-?\d+)/.exec(line.slice(0, 4_096)); try { return match ? JSON.parse(match[1]) : null; } catch { return null; } };
70
+ for await (const line of input) {
71
+ if (line.length > 64 * 1024) { limit(idOf(line)); continue; }
72
+ try {
73
+ const message = JSON.parse(line);
74
+ // Answer with the request's own id: an `id: null` error leaves that request waiting until the CLI's tool timeout.
75
+ if (inflight >= 4) { limit(message && typeof message === "object" && (typeof message.id === "string" || typeof message.id === "number") ? message.id : null); continue; }
76
+ inflight++;
77
+ void request(message).finally(() => { inflight--; });
78
+ } catch { send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }); }
79
+ }
@@ -0,0 +1,176 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ const text = (maxLength = 6e3) => ({ type: "string", minLength: 1, maxLength });
4
+ const strings = (maxItems = 20) => ({ type: "array", maxItems, items: text(1e3) });
5
+ const operationId = text(160);
6
+ function tool(name, description, properties, required = []) {
7
+ return { name, description, inputSchema: { type: "object", properties, required, additionalProperties: false }, annotations: { readOnlyHint: name === "read_inbox" || name === "read_memory", destructiveHint: false, idempotentHint: true, openWorldHint: false } };
8
+ }
9
+ const TEAM_TOOLS = [
10
+ tool("send_message", "Send a fact to a teammate. Returns after persistence; delivery may be live or at their next turn. Keep operationId stable on retries.", { operationId, recipient: text(40), body: text(), evidence: strings(16) }, ["operationId", "recipient", "body"]),
11
+ tool("ask_member", "Ask a teammate or user. Returns a request ID immediately, never waits for an answer. If blocked, checkpoint and yield through your structured outcome.", { operationId, recipient: text(40), body: text(), blocking: { type: "boolean" }, deadlineMs: { type: "integer", minimum: 1e3, maximum: 36e5 }, evidence: strings(16) }, ["operationId", "recipient", "body"]),
12
+ tool("reply_request", "Answer an addressed request with evidence. This is agent data and cannot authorize user-only actions.", { operationId, requestId: text(200), body: text(), evidence: strings(16) }, ["operationId", "requestId", "body", "evidence"]),
13
+ tool("yield_task", "Persist a checkpoint and mark this task waiting. Then finish the current turn with a structured needs_info outcome so its process slot is released.", { operationId, checkpoint: text(12e3) }, ["operationId", "checkpoint"]),
14
+ tool("publish_finding", "Publish a team finding with source evidence; this does not promote project memory.", { operationId, title: text(200), body: text(), evidence: strings(16), paths: strings() }, ["operationId", "title", "body", "evidence"]),
15
+ tool("propose_contract", "Propose a team contract for coordinator validation. A proposal does not change user requirements or grant permissions.", { operationId, title: text(200), body: text(), evidence: strings(16) }, ["operationId", "body", "evidence"]),
16
+ tool("submit_task", "Record a submission checkpoint and artifacts. Finish with your structured outcome; only the engine's checks and independent review can complete the task.", { operationId, checkpoint: text(12e3), artifacts: strings() }, ["operationId", "checkpoint"]),
17
+ tool("request_help", "Ask the coordinator to resolve a blocker. Returns immediately; checkpoint and yield when no safe independent work remains.", { operationId, recipient: text(40), body: text(), evidence: strings(16) }, ["operationId", "body"]),
18
+ tool("read_inbox", "Read only messages addressed to this member or the team. An inbox read is not an answer or a claim of comprehension.", { afterId: text(200), limit: { type: "integer", minimum: 1, maximum: 50 } }),
19
+ tool("read_memory", "Read bounded memory selected for this member's current run and task. Private memory is restricted to this member; project entries retain provenance and applicability.", { scope: { enum: ["private", "team", "project"] }, query: text(1e3), limit: { type: "integer", minimum: 1, maximum: 20 } }, ["scope"]),
20
+ tool("write_memory", "Save private notes or propose team notes with run-derived provenance. Team proposals stay unaccepted; publish_finding shares immediate findings. This cannot create accepted project conventions or export private notes.", { operationId, scope: { enum: ["private", "team"] }, title: text(200), body: text(12e3), tags: strings(), paths: strings() }, ["operationId", "scope", "title", "body"]),
21
+ tool("propose_project_memory", "Propose a reusable project lesson for later review. Acceptance, trust, and repository applicability remain controlled by the server and user.", { operationId, title: text(200), body: text(12e3), tags: strings(), paths: strings() }, ["operationId", "title", "body"])
22
+ ];
23
+ const definitions = new Map(TEAM_TOOLS.map((value) => [value.name, value]));
24
+ const MAX_BODY = 64 * 1024;
25
+ const MAX_RESPONSE = 128 * 1024;
26
+ const MAX_TTL = 25 * 60 * 6e4;
27
+ function validateTeamToolRequest(value) {
28
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid tool request");
29
+ const input = value;
30
+ if (Object.keys(input).some((key) => key !== "name" && key !== "arguments")) throw new Error("Unexpected tool request field");
31
+ const definition = definitions.get(input.name);
32
+ if (!definition) throw new Error("Unknown team tool");
33
+ const args = input.arguments ?? {};
34
+ if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error("Tool arguments must be an object");
35
+ const schema = definition.inputSchema;
36
+ const fields = schema.properties;
37
+ for (const key of Object.keys(args)) if (!Object.hasOwn(fields, key)) throw new Error(`Unexpected argument: ${key}`);
38
+ for (const key of schema.required) if (!Object.hasOwn(args, key)) throw new Error(`Missing argument: ${key}`);
39
+ for (const [key, value2] of Object.entries(args)) {
40
+ const field = fields[key];
41
+ if (field.enum && !field.enum.includes(value2)) throw new Error(`Invalid argument: ${key}`);
42
+ if (field.type === "string" && (typeof value2 !== "string" || !value2.trim() || value2.length > field.maxLength)) throw new Error(`Invalid argument: ${key}`);
43
+ if (field.type === "boolean" && typeof value2 !== "boolean") throw new Error(`Invalid argument: ${key}`);
44
+ if (field.type === "integer" && (typeof value2 !== "number" || !Number.isSafeInteger(value2) || value2 < field.minimum || value2 > field.maximum)) throw new Error(`Invalid argument: ${key}`);
45
+ if (field.type === "array" && (!Array.isArray(value2) || value2.length > field.maxItems || value2.some((item) => typeof item !== "string" || !item.trim() || item.length > field.items.maxLength))) throw new Error(`Invalid argument: ${key}`);
46
+ }
47
+ return { name: definition.name, arguments: args };
48
+ }
49
+ class TeamToolBridge {
50
+ server;
51
+ listening;
52
+ grants = /* @__PURE__ */ new Map();
53
+ async issue(options) {
54
+ const { identity } = options;
55
+ if (!identity.runId || !identity.memberId || !identity.attemptId) throw new Error("A team capability requires a run, member and attempt");
56
+ if (options.signal?.aborted) throw new Error("Cannot issue a team capability for a stopped turn");
57
+ const base = await this.listen();
58
+ if (options.signal?.aborted) throw new Error("Cannot issue a team capability for a stopped turn");
59
+ const token = randomBytes(32).toString("base64url");
60
+ const key = digest(token);
61
+ const path = `/v1/runs/${encodeURIComponent(identity.runId)}/members/${encodeURIComponent(identity.memberId)}/attempts/${encodeURIComponent(identity.attemptId)}`;
62
+ const ttl = typeof options.ttlMs === "number" && Number.isFinite(options.ttlMs) ? Math.max(1, Math.min(MAX_TTL, options.ttlMs)) : 30 * 6e4;
63
+ const expiresAt = Date.now() + ttl;
64
+ this.grants.set(key, { identity: Object.freeze({ ...identity }), path, expiresAt, handler: options.handler, isActive: options.isActive, pending: 0 });
65
+ let timer;
66
+ const revoke = () => {
67
+ this.grants.delete(key);
68
+ clearTimeout(timer);
69
+ options.signal?.removeEventListener("abort", revoke);
70
+ };
71
+ timer = setTimeout(revoke, ttl);
72
+ timer.unref();
73
+ options.signal?.addEventListener("abort", revoke, { once: true });
74
+ return { connection: { endpoint: base + path, token, expiresAt }, revoke };
75
+ }
76
+ async close() {
77
+ this.grants.clear();
78
+ const server = this.server;
79
+ this.server = void 0;
80
+ this.listening = void 0;
81
+ if (server) await new Promise((resolve) => {
82
+ server.close(() => resolve());
83
+ server.closeAllConnections();
84
+ });
85
+ }
86
+ listen() {
87
+ if (this.listening) return this.listening;
88
+ const server = createServer((req, res) => {
89
+ void this.handle(req, res);
90
+ });
91
+ this.server = server;
92
+ server.requestTimeout = 1e4;
93
+ server.headersTimeout = 1e4;
94
+ server.maxHeadersCount = 30;
95
+ this.listening = new Promise((resolve, reject) => {
96
+ server.once("error", (error) => {
97
+ this.listening = void 0;
98
+ this.server = void 0;
99
+ reject(error);
100
+ });
101
+ server.listen(0, "127.0.0.1", () => {
102
+ server.unref();
103
+ const address = server.address();
104
+ if (!address || typeof address === "string") {
105
+ reject(new Error("Could not start team bridge"));
106
+ return;
107
+ }
108
+ resolve(`http://127.0.0.1:${address.port}`);
109
+ });
110
+ });
111
+ return this.listening;
112
+ }
113
+ async handle(req, res) {
114
+ const reply = (status, body) => {
115
+ if (res.destroyed || res.writableEnded) return;
116
+ const json = JSON.stringify(body ?? null);
117
+ res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store", "x-content-type-options": "nosniff" });
118
+ res.end(json.length <= MAX_RESPONSE ? json : JSON.stringify({ error: "Tool response exceeded its context limit" }));
119
+ };
120
+ if (req.method !== "POST" || req.headers.origin || req.headers["content-type"] !== "application/json") {
121
+ reply(403, { error: "Forbidden team bridge request" });
122
+ return;
123
+ }
124
+ const auth = req.headers.authorization;
125
+ const token = typeof auth === "string" && /^Bearer [A-Za-z0-9_-]{43}$/.test(auth) ? auth.slice(7) : "";
126
+ const grant = token ? this.grants.get(digest(token)) : void 0;
127
+ if (!grant || req.url !== grant.path || grant.expiresAt <= Date.now() || grant.isActive?.() === false) {
128
+ reply(403, { error: "Invalid or expired member capability" });
129
+ return;
130
+ }
131
+ if (grant.pending >= 4) {
132
+ reply(429, { error: "Too many active member commands" });
133
+ return;
134
+ }
135
+ grant.pending++;
136
+ try {
137
+ const chunks = [];
138
+ let size = 0;
139
+ for await (const chunk of req) {
140
+ size += chunk.length;
141
+ if (size > MAX_BODY) {
142
+ reply(413, { error: "Team command is too large" });
143
+ return;
144
+ }
145
+ chunks.push(chunk);
146
+ }
147
+ if (this.grants.get(digest(token)) !== grant || grant.expiresAt <= Date.now() || grant.isActive?.() === false) {
148
+ reply(403, { error: "Invalid or expired member capability" });
149
+ return;
150
+ }
151
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
152
+ if (body?.method === "tools/list" && Object.keys(body).length === 1) {
153
+ reply(200, { tools: TEAM_TOOLS });
154
+ return;
155
+ }
156
+ const request = validateTeamToolRequest(body);
157
+ const value = await grant.handler(request, grant.identity);
158
+ reply(200, { result: value ?? null });
159
+ } catch (error) {
160
+ const message = error instanceof Error ? error.message : "Team command failed";
161
+ reply(message.includes("no longer active") ? 403 : 400, { error: message.replaceAll(token, "[redacted]").slice(0, 1e3) });
162
+ } finally {
163
+ grant.pending--;
164
+ }
165
+ }
166
+ }
167
+ function digest(token) {
168
+ return createHash("sha256").update(token).digest("hex");
169
+ }
170
+ const teamToolBridge = new TeamToolBridge();
171
+ export {
172
+ TEAM_TOOLS,
173
+ TeamToolBridge,
174
+ teamToolBridge,
175
+ validateTeamToolRequest
176
+ };
package/src/teams.js ADDED
File without changes
@@ -0,0 +1,62 @@
1
+ const templates = [
2
+ {
3
+ id: "fix-issue",
4
+ name: "Fix an issue",
5
+ description: "Plan, build and review a fix for a GitHub issue; the PR closes it.",
6
+ pattern: "pipeline",
7
+ needs: "issue",
8
+ task: "Fix issue #{number}. Add a test that fails without the fix."
9
+ },
10
+ {
11
+ id: "fix-ci",
12
+ name: "Fix failing CI",
13
+ description: "Hand a PR's failing checks and their logs to an agent; push the fix to the PR.",
14
+ pattern: "pipeline",
15
+ needs: "pr",
16
+ ciLogs: true,
17
+ task: "Make CI pass on pull request #{number}. The failing checks and their logs are below. Fix the cause; don't disable, skip or weaken tests."
18
+ },
19
+ {
20
+ id: "review-pr",
21
+ name: "Review a PR as a council",
22
+ description: "Several agents review the PR independently; a chair merges their findings. You choose whether to post it.",
23
+ pattern: "council",
24
+ needs: "pr",
25
+ task: "Review pull request #{number}. Look for bugs, security problems, missing tests, and anything that doesn't match its description."
26
+ },
27
+ {
28
+ id: "upgrade-dependency",
29
+ name: "Upgrade a dependency",
30
+ description: "Bump a package, read its changelog for breaking changes, and keep the tests green.",
31
+ pattern: "pipeline",
32
+ task: "Upgrade PACKAGE to its latest version. Read its changelog for breaking changes, update every call site, and keep the tests passing."
33
+ },
34
+ {
35
+ id: "sweep-issues",
36
+ name: "Fix open issues",
37
+ description: "List the open issues, pick the ones to fix, and start one Plan \u2192 Build \u2192 Review run each. Results wait in your inbox.",
38
+ pattern: "pipeline",
39
+ sweep: "issues",
40
+ task: "Fix the GitHub issue below. Add a test that fails without the fix."
41
+ },
42
+ {
43
+ id: "sweep-dependabot",
44
+ name: "Fix Dependabot PRs",
45
+ description: "Every Dependabot PR whose CI fails gets a run that fixes it; approving pushes the fix to the PR. Green ones are left for you to merge.",
46
+ pattern: "pipeline",
47
+ sweep: "dependabot",
48
+ ciLogs: true,
49
+ task: "Make CI pass on the Dependabot pull request below by adapting the code to the new version. Don't pin it back, and don't disable, skip or weaken tests."
50
+ },
51
+ {
52
+ id: "sweep-alerts",
53
+ name: "Fix security alerts",
54
+ description: "Dependabot alerts (one run per package) and code scanning alerts (one per alert). Leaked secrets are listed for you to rotate; agents never get them.",
55
+ pattern: "pipeline",
56
+ sweep: "alerts",
57
+ task: "Fix the security alert below. Fix the cause rather than suppressing the alert, and keep the tests passing."
58
+ }
59
+ ];
60
+ export {
61
+ templates
62
+ };
@@ -0,0 +1,134 @@
1
+ import { constants } from "node:fs";
2
+ import { open } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { RUNS_DIR, waitForRunWrites } from "./store.js";
5
+ const TRANSCRIPT_LIMITS = { events: 500, bytes: 1024 * 1024, recordBytes: 64 * 1024 * 1024, scanBytes: 16 * 1024 * 1024, fieldChars: 32 * 1024 };
6
+ class TranscriptCursorError extends Error {
7
+ }
8
+ function transcriptPreview(event) {
9
+ const shorten = (value) => value.length > TRANSCRIPT_LIMITS.fieldChars ? value.slice(0, TRANSCRIPT_LIMITS.fieldChars) + "\n[Preview shortened; the complete event is in the run export.]" : value;
10
+ if (event.type === "log") return { ...event, text: shorten(event.text) };
11
+ if (event.type !== "agent") return event;
12
+ return { ...event, event: Object.fromEntries(Object.entries(event.event).map(([key, value]) => [key, typeof value === "string" ? shorten(value) : value])) };
13
+ }
14
+ async function* reverseLines(file, end) {
15
+ let position = end, pending = [], pendingBytes = 0;
16
+ while (position > 0) {
17
+ const size = Math.min(position, 64 * 1024);
18
+ position -= size;
19
+ const chunk = Buffer.allocUnsafe(size);
20
+ const { bytesRead } = await file.read(chunk, 0, size, position);
21
+ if (bytesRead !== size) throw new TranscriptCursorError("Transcript changed; reload its latest page");
22
+ let right = size;
23
+ for (let index = size - 1; index >= 0; index--) {
24
+ if (chunk[index] !== 10) continue;
25
+ const part = chunk.subarray(index + 1, right);
26
+ if (part.length + pendingBytes > TRANSCRIPT_LIMITS.recordBytes) throw new Error("Transcript record exceeds the 64 MiB bound");
27
+ if (part.length || pendingBytes) {
28
+ const text = pending.length ? Buffer.concat([part, ...pending.reverse()], part.length + pendingBytes).toString("utf8") : part.toString("utf8");
29
+ yield { text, offset: position + index + 1 };
30
+ } else {
31
+ while (index > 0 && chunk[index - 1] === 10) index--;
32
+ yield { text: "", offset: position === 0 && index === 0 ? 0 : position + index + 1 };
33
+ }
34
+ pending = [];
35
+ pendingBytes = 0;
36
+ right = index;
37
+ }
38
+ if (right) {
39
+ const part = chunk.subarray(0, right);
40
+ pending.push(part);
41
+ pendingBytes += part.length;
42
+ }
43
+ if (pendingBytes > TRANSCRIPT_LIMITS.recordBytes) throw new Error("Transcript record exceeds the 64 MiB bound");
44
+ }
45
+ if (pendingBytes) yield { text: Buffer.concat(pending.reverse(), pendingBytes).toString("utf8"), offset: 0 };
46
+ }
47
+ async function transcriptPage(id, input = {}, reader = reverseLines) {
48
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,199}$/.test(id)) throw new TranscriptCursorError("Invalid run ID");
49
+ const limit = input.limit ?? TRANSCRIPT_LIMITS.events;
50
+ if (!Number.isInteger(limit) || limit < 1 || limit > TRANSCRIPT_LIMITS.events) throw new TranscriptCursorError("Invalid transcript page size");
51
+ await waitForRunWrites(id);
52
+ input.signal?.throwIfAborted();
53
+ const file = await open(join(RUNS_DIR, `${id}.jsonl`), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
54
+ try {
55
+ const info = await file.stat({ bigint: true });
56
+ if (!info.isFile()) throw new Error("Transcript must be a regular file");
57
+ let end = Number(info.size);
58
+ if (input.before) {
59
+ let cursor;
60
+ try {
61
+ if (input.before.length > 1024) throw new Error();
62
+ cursor = JSON.parse(Buffer.from(input.before, "base64url").toString());
63
+ } catch {
64
+ throw new TranscriptCursorError("Invalid transcript cursor");
65
+ }
66
+ if (!cursor || cursor.run !== id || cursor.dev !== String(info.dev) || cursor.ino !== String(info.ino) || !Number.isSafeInteger(cursor.offset) || cursor.offset < 0 || cursor.offset > end) throw new TranscriptCursorError("Transcript changed; reload its latest page");
67
+ end = cursor.offset;
68
+ if (end) {
69
+ const byte = Buffer.alloc(1);
70
+ await file.read(byte, 0, 1, end - 1);
71
+ if (byte[0] !== 10) throw new TranscriptCursorError("Invalid transcript boundary");
72
+ }
73
+ }
74
+ const events = [];
75
+ let bytes = 0, offset = end, latestSeq = 0, resumed = input.after === 0;
76
+ for await (const line of reader(file, end)) {
77
+ input.signal?.throwIfAborted();
78
+ const skip = () => {
79
+ offset = line.offset;
80
+ return end - offset >= TRANSCRIPT_LIMITS.scanBytes;
81
+ };
82
+ if (!line.text.trim()) {
83
+ if (skip()) break;
84
+ continue;
85
+ }
86
+ let event;
87
+ try {
88
+ event = JSON.parse(line.text);
89
+ } catch {
90
+ if (skip()) break;
91
+ continue;
92
+ }
93
+ if (!event || event.runId !== id || !Number.isSafeInteger(event.seq)) {
94
+ if (skip()) break;
95
+ continue;
96
+ }
97
+ latestSeq = Math.max(latestSeq, event.seq);
98
+ if (input.after !== void 0 && event.seq <= input.after) {
99
+ resumed = true;
100
+ break;
101
+ }
102
+ if ((event.type === "agent" || event.type === "log") && (!input.step || event.type === "agent" && event.stepId === input.step)) {
103
+ if (event.type === "agent" && (!event.event || typeof event.event !== "object") || event.type === "log" && typeof event.text !== "string") {
104
+ if (skip()) break;
105
+ continue;
106
+ }
107
+ let preview = transcriptPreview(event), size = Buffer.byteLength(JSON.stringify(preview));
108
+ if (size > TRANSCRIPT_LIMITS.bytes) {
109
+ const text = "[Preview omitted because this event exceeds the display limit; the complete event is in the run export.]";
110
+ const ts = Number.isFinite(event.ts) ? event.ts : 0;
111
+ preview = event.type === "agent" ? { type: "agent", runId: id, stepId: typeof event.stepId === "string" ? event.stepId.slice(0, 200) : "", seq: event.seq, ts, event: { kind: "status", text } } : { type: "log", runId: id, seq: event.seq, ts, level: "info", text };
112
+ size = Buffer.byteLength(JSON.stringify(preview));
113
+ }
114
+ if (events.length >= limit || bytes + size > TRANSCRIPT_LIMITS.bytes) break;
115
+ events.push(preview);
116
+ bytes += size;
117
+ }
118
+ offset = line.offset;
119
+ if (end - offset >= TRANSCRIPT_LIMITS.scanBytes) break;
120
+ }
121
+ const after = await file.stat({ bigint: true });
122
+ if (after.size < info.size) throw new TranscriptCursorError("Transcript changed; reload its latest page");
123
+ const before = offset > 0 ? Buffer.from(JSON.stringify({ run: id, dev: String(info.dev), ino: String(info.ino), offset })).toString("base64url") : void 0;
124
+ return { events: events.reverse(), before, latestSeq, bytes, scannedBytes: end - offset, resumed: input.after !== void 0 && input.after <= latestSeq && (resumed || !before) };
125
+ } finally {
126
+ await file.close();
127
+ }
128
+ }
129
+ export {
130
+ TRANSCRIPT_LIMITS,
131
+ TranscriptCursorError,
132
+ transcriptPage,
133
+ transcriptPreview
134
+ };
@@ -0,0 +1,27 @@
1
+ import { git } from "./git.js";
2
+ class VerificationIntegrityError extends Error {
3
+ }
4
+ async function verificationIntegrity(repo, signal) {
5
+ const options = { signal, timeoutMs: 1e4 };
6
+ const initial = await git(["rev-parse", "--verify", "HEAD^{commit}"], repo, options);
7
+ const tree = await git(["rev-parse", "--verify", "HEAD^{tree}"], repo, options);
8
+ if (initial.code || tree.code) throw new VerificationIntegrityError("Cannot pin the verification checkout commit");
9
+ const assert = async (stage) => {
10
+ if (signal?.aborted) throw new VerificationIntegrityError("Verification was cancelled");
11
+ const [head, index, unstaged, flags] = await Promise.all([
12
+ git(["rev-parse", "--verify", "HEAD^{commit}"], repo, options),
13
+ git(["write-tree"], repo, options),
14
+ git(["diff", "--no-ext-diff", "--no-textconv", "--ignore-submodules=none", "--quiet", "--exit-code", "--"], repo, options),
15
+ git(["ls-files", "-v", "-z"], repo, options)
16
+ ]);
17
+ if (signal?.aborted) throw new VerificationIntegrityError("Verification was cancelled");
18
+ const hiddenFiles = flags.stdout.split("\0").some((file) => /^[a-zS] /.test(file));
19
+ if (head.code || index.code || unstaged.code || flags.code || hiddenFiles || head.stdout.trim() !== initial.stdout.trim() || index.stdout.trim() !== tree.stdout.trim()) throw new VerificationIntegrityError(`Verification ${stage} changed tracked files, the staged tree or the checkout commit, or hid tracked files from comparison. The original commit was not verified.`);
20
+ };
21
+ await assert("starting checkout");
22
+ return { head: initial.stdout.trim(), assert };
23
+ }
24
+ export {
25
+ VerificationIntegrityError,
26
+ verificationIntegrity
27
+ };