@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,496 @@
1
+ import { randomBytes } from "node:crypto";
2
+ const TASK_STATUSES = ["planned", "working", "blocked", "review", "done"];
3
+ const ATTEMPT_STATES = ["claimed", "running", "paused", "finished", "failed", "stopped", "unknown", "superseded"];
4
+ class CollabError extends Error {
5
+ constructor(message, status = 400) {
6
+ super(message);
7
+ this.status = status;
8
+ }
9
+ }
10
+ const MAX = {
11
+ tasks: 500,
12
+ attempts: 50,
13
+ checkpoints: 50,
14
+ decisions: 100,
15
+ discussion: 500,
16
+ instructions: 50,
17
+ reviews: 100,
18
+ handoffs: 50,
19
+ audit: 200,
20
+ collaborators: 20,
21
+ dependsOn: 20,
22
+ labels: 10,
23
+ repositories: 50,
24
+ title: 200,
25
+ goal: 16 * 1024,
26
+ acceptance: 8 * 1024,
27
+ text: 4e3,
28
+ note: 4e3,
29
+ decision: 2e3,
30
+ blocker: 1e3,
31
+ reason: 1e3,
32
+ summary: 4 * 1024,
33
+ requirements: 16 * 1024,
34
+ remaining: 8 * 1024,
35
+ changedFiles: 100,
36
+ path: 300,
37
+ blockers: 10,
38
+ blockerItem: 500,
39
+ stage: 200,
40
+ command: 200,
41
+ runStatus: 40,
42
+ steps: 1e5,
43
+ label: 40,
44
+ repoLabel: 80,
45
+ repoRemote: 300,
46
+ branch: 100,
47
+ baseRef: 100,
48
+ runnerName: 80,
49
+ displayName: 120,
50
+ inboxText: 500,
51
+ auditAction: 60,
52
+ auditDetail: 500,
53
+ bundle: 64 * 1024 * 1024,
54
+ runners: 64,
55
+ runnersPerUser: 8,
56
+ inboxPerUser: 500,
57
+ inboxTotal: 5e3,
58
+ bundlesOnDisk: 200,
59
+ heartbeatLiveMs: 9e4
60
+ };
61
+ const TASK_ID = /^t-[a-f0-9]{16}$/, ATTEMPT_ID = /^[a-f0-9]{32}$/, CHECKPOINT_ID = /^[a-f0-9]{32}$/, RUNNER_ID = /^r-[a-f0-9]{16}$/, PREFIXED_ID = /^[a-z]{1,4}-[a-f0-9]{16}$/, PATH_ID = /^[a-zA-Z0-9_-]{1,100}$/, REPO_KEY = /^[a-z][a-z0-9-]{0,63}$/, COMMIT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/, SHA256_HEX = /^[a-f0-9]{64}$/, RUNNER_TOKEN = /^tr-[A-Za-z0-9_-]{43}$/;
62
+ const ID_PREFIX = { comment: "c", decision: "d", instruction: "i", review: "rv", handoff: "h", inbox: "n" };
63
+ const newTaskId = (random = randomBytes) => `t-${random(8).toString("hex")}`;
64
+ const newAttemptId = (random = randomBytes) => random(16).toString("hex");
65
+ const newCheckpointId = (random = randomBytes) => random(16).toString("hex");
66
+ const newRunnerId = (random = randomBytes) => `r-${random(8).toString("hex")}`;
67
+ const newId = (kind, random = randomBytes) => `${ID_PREFIX[kind]}-${random(8).toString("hex")}`;
68
+ const newRunnerToken = (random = randomBytes) => `tr-${random(32).toString("base64url")}`;
69
+ const SHARED_ROLE_RANK = { viewer: 0, reviewer: 1, contributor: 2, maintainer: 3 };
70
+ function roleAtLeast(role, needed) {
71
+ return role !== void 0 && Object.hasOwn(SHARED_ROLE_RANK, role) && SHARED_ROLE_RANK[role] >= SHARED_ROLE_RANK[needed];
72
+ }
73
+ function actorRole(actor, projectId) {
74
+ if (actor.kind === "owner") return "maintainer";
75
+ return Object.hasOwn(actor.roles, projectId) ? actor.roles[projectId] : void 0;
76
+ }
77
+ function isTaskOwner(task, actor) {
78
+ return task.owner !== void 0 && task.owner.userId === actor.userId;
79
+ }
80
+ function whoOf(actor) {
81
+ return { userId: actor.userId, displayName: actor.displayName, via: actor.kind === "owner" ? "local" : actor.via };
82
+ }
83
+ function clone(value) {
84
+ return structuredClone(value);
85
+ }
86
+ const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
87
+ const integer = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
88
+ const absent = (v) => v === void 0 || v === null;
89
+ const UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
90
+ const CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/, LINE_CONTROL = /[\x00-\x1f\x7f]/, REF_FORBIDDEN = /[\s~^:?*[\\\x00-\x1f\x7f]/;
91
+ const bytes = (max) => max >= 1024 && max % 1024 === 0;
92
+ const limitLabel = (max) => bytes(max) ? `${max / 1024} KiB` : `${max} characters`;
93
+ function assertKeys(value, allowed, what) {
94
+ const extra = Object.keys(value).find((key) => !allowed.includes(key));
95
+ if (extra !== void 0) throw new CollabError(`Unknown ${what} field ${JSON.stringify(extra.slice(0, 40))}; the allowed fields are ${allowed.join(", ")}`);
96
+ }
97
+ function validateText(value, max, label, options = {}) {
98
+ if (absent(value)) {
99
+ if (options.required) throw new CollabError(`Enter a ${label} of 1\u2013${limitLabel(max)}`);
100
+ return "";
101
+ }
102
+ if (typeof value !== "string") throw new CollabError(`Enter the ${label} as text`);
103
+ const text = options.singleLine ? value.trim() : value;
104
+ if ((options.singleLine ? LINE_CONTROL : CONTROL).test(text)) throw new CollabError(`Remove control characters from the ${label}`);
105
+ if (options.required && !text.trim()) throw new CollabError(`Enter a ${label} of 1\u2013${limitLabel(max)}`);
106
+ if ((bytes(max) ? Buffer.byteLength(text) : [...text].length) > max) throw new CollabError(`Keep the ${label} within ${limitLabel(max)}`);
107
+ return text;
108
+ }
109
+ function validateUserRef(value, label = "teammate") {
110
+ if (typeof value !== "string" || !PATH_ID.test(value) || UNSAFE_KEYS.includes(value)) throw new CollabError(`Choose the ${label} from the project's members`);
111
+ return value;
112
+ }
113
+ function validatePathId(value, label) {
114
+ if (typeof value !== "string" || !PATH_ID.test(value) || UNSAFE_KEYS.includes(value)) throw new CollabError(`Use a ${label} of 1\u2013100 letters, digits, underscores or dashes`);
115
+ return value;
116
+ }
117
+ function validateRepoKey(value) {
118
+ if (typeof value !== "string" || !REPO_KEY.test(value)) throw new CollabError("Use a repository key of up to 64 lowercase letters, digits or dashes that starts with a letter");
119
+ return value;
120
+ }
121
+ function validateCommit(value, label = "commit") {
122
+ if (typeof value !== "string" || !COMMIT_ID.test(value)) throw new CollabError(`Send the ${label} as a full lowercase commit hash`);
123
+ return value;
124
+ }
125
+ function validateRef(value, max, label) {
126
+ if (typeof value !== "string" || !value || value.length > max || REF_FORBIDDEN.test(value) || value.includes("..") || value.includes("@{") || value.includes("//") || value.startsWith("-") || value.startsWith("/") || value.endsWith("/") || value.endsWith(".") || value.endsWith(".lock") || value.split("/").some((part) => part.startsWith("."))) throw new CollabError(`Use a Git branch, tag or commit of up to ${max} characters for the ${label}`);
127
+ return value;
128
+ }
129
+ function validateStatus(value) {
130
+ if (typeof value !== "string" || !TASK_STATUSES.includes(value)) throw new CollabError("Choose a status: planned, working, blocked, review or done");
131
+ return value;
132
+ }
133
+ function validateTime(value, label) {
134
+ if (!integer(value)) throw new CollabError(`Send ${label} as a Unix time in milliseconds`);
135
+ return value;
136
+ }
137
+ function validateTests(value) {
138
+ if (!object(value)) throw new CollabError("Report tests as { ok, command, at }");
139
+ assertKeys(value, ["ok", "command", "at"], "test result");
140
+ if (typeof value.ok !== "boolean") throw new CollabError("Report the test result ok flag as true or false");
141
+ return { ok: value.ok, command: validateText(value.command, MAX.command, "test command", { required: true, singleLine: true }), at: validateTime(value.at, "the test time") };
142
+ }
143
+ function stringList(value, maxItems, maxLength, label, item, singleLine = false) {
144
+ if (absent(value)) return [];
145
+ if (!Array.isArray(value) || value.length > maxItems) throw new CollabError(`List at most ${maxItems} ${label}`);
146
+ const items = [];
147
+ for (const entry of value) {
148
+ const text = validateText(entry, maxLength, item, { required: true, singleLine });
149
+ if (!items.includes(text)) items.push(text);
150
+ }
151
+ return items;
152
+ }
153
+ function userList(value, max, label, item) {
154
+ if (absent(value)) return [];
155
+ if (!Array.isArray(value) || value.length > max) throw new CollabError(`Add at most ${max} ${label}`);
156
+ const ids = [];
157
+ for (const entry of value) {
158
+ const id = validateUserRef(entry, item);
159
+ if (!ids.includes(id)) ids.push(id);
160
+ }
161
+ return ids;
162
+ }
163
+ function taskIdList(value) {
164
+ if (absent(value)) return [];
165
+ if (!Array.isArray(value) || value.length > MAX.dependsOn) throw new CollabError(`A task can depend on at most ${MAX.dependsOn} other tasks`);
166
+ const ids = [];
167
+ for (const entry of value) {
168
+ if (typeof entry !== "string" || !TASK_ID.test(entry)) throw new CollabError("Choose the tasks this one depends on from the board");
169
+ if (!ids.includes(entry)) ids.push(entry);
170
+ }
171
+ return ids;
172
+ }
173
+ function validatePaths(value, label = "changed files") {
174
+ if (absent(value)) return [];
175
+ if (!Array.isArray(value) || value.length > MAX.changedFiles) throw new CollabError(`List at most ${MAX.changedFiles} ${label}`);
176
+ const paths = [];
177
+ for (const entry of value) {
178
+ if (typeof entry !== "string" || !entry || entry.length > MAX.path || LINE_CONTROL.test(entry)) throw new CollabError(`Each of the ${label} must be a path of 1\u2013${MAX.path} characters without control characters`);
179
+ if (!paths.includes(entry)) paths.push(entry);
180
+ }
181
+ return paths;
182
+ }
183
+ function validateRepoRefs(value) {
184
+ if (!Array.isArray(value) || value.length > MAX.repositories) throw new CollabError(`Define at most ${MAX.repositories} repositories per project`);
185
+ const refs = [], seen = /* @__PURE__ */ new Set();
186
+ for (const entry of value) {
187
+ if (!object(entry)) throw new CollabError("Each repository needs a key and a label, and may add a remote and a default branch");
188
+ assertKeys(entry, ["key", "label", "remote", "defaultBranch"], "repository");
189
+ const key = validateRepoKey(entry.key);
190
+ if (seen.has(key)) throw new CollabError(`Repository key ${key} is used twice; keys must be unique within a project`);
191
+ seen.add(key);
192
+ const ref = { key, label: validateText(entry.label ?? key, MAX.repoLabel, "repository label", { required: true, singleLine: true }) };
193
+ const remote = validateText(entry.remote, MAX.repoRemote, "repository remote", { singleLine: true });
194
+ if (remote) ref.remote = remote;
195
+ if (!absent(entry.defaultBranch) && entry.defaultBranch !== "") ref.defaultBranch = validateRef(entry.defaultBranch, MAX.branch, "default branch");
196
+ refs.push(ref);
197
+ }
198
+ return refs;
199
+ }
200
+ const TASK_FIELDS = ["projectId", "repository", "title", "goal", "acceptance", "status", "blocker", "baseRef", "owner", "reviewer", "collaborators", "dependsOn", "labels"];
201
+ const BLOCKER_REQUIRED = "Describe what blocks the task before marking it blocked";
202
+ function validateTaskInput(value) {
203
+ if (!object(value)) throw new CollabError("Send the task as a JSON object");
204
+ assertKeys(value, TASK_FIELDS, "task");
205
+ const projectId = validatePathId(value.projectId, "project id"), repository = validateRepoKey(value.repository);
206
+ const title = validateText(value.title, MAX.title, "title", { required: true, singleLine: true }), goal = validateText(value.goal, MAX.goal, "goal"), acceptance = validateText(value.acceptance, MAX.acceptance, "acceptance criteria");
207
+ const status = value.status === void 0 ? "planned" : validateStatus(value.status), blocker = validateText(value.blocker, MAX.blocker, "blocker");
208
+ if (status === "blocked" && !blocker.trim()) throw new CollabError(BLOCKER_REQUIRED);
209
+ const input = { projectId, repository, title, goal, acceptance, status, collaborators: userList(value.collaborators, MAX.collaborators, "collaborators", "collaborator"), dependsOn: taskIdList(value.dependsOn), labels: stringList(value.labels, MAX.labels, MAX.label, "labels", "label", true) };
210
+ if (status === "blocked") input.blocker = blocker.trim();
211
+ if (!absent(value.baseRef) && value.baseRef !== "") input.baseRef = validateRef(value.baseRef, MAX.baseRef, "base branch");
212
+ if (!absent(value.owner) && value.owner !== "") input.owner = validateUserRef(value.owner, "owner");
213
+ if (!absent(value.reviewer) && value.reviewer !== "") input.reviewer = validateUserRef(value.reviewer, "reviewer");
214
+ return input;
215
+ }
216
+ function validateTaskPatch(value) {
217
+ if (!object(value)) throw new CollabError("Send the task changes as a JSON object");
218
+ if (value.projectId !== void 0) throw new CollabError("Tasks cannot move between projects; create a new task in the other project instead");
219
+ assertKeys(value, [...TASK_FIELDS, "revision", "id"], "task");
220
+ const patch = {}, cleared = (v) => v === null || v === "";
221
+ if (value.repository !== void 0) patch.repository = validateRepoKey(value.repository);
222
+ if (value.title !== void 0) patch.title = validateText(value.title, MAX.title, "title", { required: true, singleLine: true });
223
+ if (value.goal !== void 0) patch.goal = validateText(value.goal, MAX.goal, "goal");
224
+ if (value.acceptance !== void 0) patch.acceptance = validateText(value.acceptance, MAX.acceptance, "acceptance criteria");
225
+ if (value.status !== void 0) patch.status = validateStatus(value.status);
226
+ if (value.blocker !== void 0) patch.blocker = cleared(value.blocker) ? null : validateText(value.blocker, MAX.blocker, "blocker").trim() || null;
227
+ if (value.baseRef !== void 0) patch.baseRef = cleared(value.baseRef) ? null : validateRef(value.baseRef, MAX.baseRef, "base branch");
228
+ if (value.owner !== void 0) patch.owner = cleared(value.owner) ? null : validateUserRef(value.owner, "owner");
229
+ if (value.reviewer !== void 0) patch.reviewer = cleared(value.reviewer) ? null : validateUserRef(value.reviewer, "reviewer");
230
+ if (value.collaborators !== void 0) patch.collaborators = userList(value.collaborators, MAX.collaborators, "collaborators", "collaborator");
231
+ if (value.dependsOn !== void 0) patch.dependsOn = taskIdList(value.dependsOn);
232
+ if (value.labels !== void 0) patch.labels = stringList(value.labels, MAX.labels, MAX.label, "labels", "label", true);
233
+ if (patch.status === "blocked" && patch.blocker === null) throw new CollabError(BLOCKER_REQUIRED);
234
+ return patch;
235
+ }
236
+ const PROGRESS_FIELDS = ["stage", "summary", "runStatus", "changedFiles", "tests", "verdict", "blockers", "head", "branch", "steps", "updatedAt", "seq", "runId"];
237
+ function validateSeq(value) {
238
+ if (!integer(value) || value < 1) throw new CollabError("Send seq as a positive whole number that increases with every report");
239
+ return value;
240
+ }
241
+ function validateRunId(value) {
242
+ return validatePathId(value, "run id");
243
+ }
244
+ function validateProgress(value, now = Date.now()) {
245
+ if (!object(value)) throw new CollabError("Send the progress report as a JSON object");
246
+ assertKeys(value, PROGRESS_FIELDS, "progress");
247
+ if (!absent(value.seq)) validateSeq(value.seq);
248
+ if (!absent(value.runId)) validateRunId(value.runId);
249
+ const progress = { updatedAt: absent(value.updatedAt) ? now : validateTime(value.updatedAt, "updatedAt") };
250
+ const stage = validateText(value.stage, MAX.stage, "stage", { singleLine: true });
251
+ if (stage) progress.stage = stage;
252
+ const summary = validateText(value.summary, MAX.summary, "summary");
253
+ if (summary) progress.summary = summary;
254
+ const runStatus = validateText(value.runStatus, MAX.runStatus, "run status", { singleLine: true });
255
+ if (runStatus) progress.runStatus = runStatus;
256
+ if (!absent(value.changedFiles)) progress.changedFiles = validatePaths(value.changedFiles);
257
+ if (!absent(value.tests)) progress.tests = validateTests(value.tests);
258
+ if (!absent(value.verdict)) {
259
+ if (value.verdict !== "passed" && value.verdict !== "failing") throw new CollabError('Report the verdict as "passed" or "failing"');
260
+ progress.verdict = value.verdict;
261
+ }
262
+ if (!absent(value.blockers)) progress.blockers = stringList(value.blockers, MAX.blockers, MAX.blockerItem, "blockers", "blocker");
263
+ if (!absent(value.head)) progress.head = validateCommit(value.head, "head");
264
+ if (!absent(value.branch)) progress.branch = validateRef(value.branch, MAX.branch, "branch");
265
+ if (!absent(value.steps)) {
266
+ if (!object(value.steps)) throw new CollabError("Report steps as { done, total? } counts");
267
+ assertKeys(value.steps, ["done", "total"], "steps");
268
+ if (!integer(value.steps.done) || value.steps.done > MAX.steps || !absent(value.steps.total) && (!integer(value.steps.total) || value.steps.total > MAX.steps || value.steps.total < value.steps.done)) throw new CollabError("Report steps as { done, total? } counts with done no larger than total");
269
+ progress.steps = absent(value.steps.total) ? { done: value.steps.done } : { done: value.steps.done, total: value.steps.total };
270
+ }
271
+ return progress;
272
+ }
273
+ function validateProgressReport(value, now = Date.now()) {
274
+ const progress = validateProgress(value, now), body = value, report = { progress, seq: validateSeq(body.seq) };
275
+ if (!absent(body.runId)) report.runId = validateRunId(body.runId);
276
+ return report;
277
+ }
278
+ const CHECKPOINT_FIELDS = ["id", "commit", "baseCommit", "branch", "size", "sha256", "note", "requirements", "decisions", "testResults", "remaining", "changedFiles"];
279
+ function validateCheckpointInput(value) {
280
+ if (!object(value)) throw new CollabError("Send the checkpoint manifest as a JSON object");
281
+ assertKeys(value, CHECKPOINT_FIELDS, "checkpoint");
282
+ const commit = validateCommit(value.commit, "commit"), baseCommit = validateCommit(value.baseCommit, "base commit"), branch = validateRef(value.branch, MAX.branch, "branch");
283
+ if (!integer(value.size) || value.size < 1 || value.size > MAX.bundle) throw new CollabError(`Checkpoint bundles must be between 1 byte and ${MAX.bundle / 1024 / 1024} MiB`);
284
+ if (typeof value.sha256 !== "string" || !SHA256_HEX.test(value.sha256)) throw new CollabError("Send the bundle digest as 64 lowercase hex characters of SHA-256");
285
+ const input = { commit, baseCommit, branch, size: value.size, sha256: value.sha256, note: validateText(value.note, MAX.note, "note"), requirements: validateText(value.requirements, MAX.requirements, "requirements"), decisions: stringList(value.decisions, MAX.decisions, MAX.decision, "decisions", "decision"), remaining: validateText(value.remaining, MAX.remaining, "remaining work"), changedFiles: validatePaths(value.changedFiles) };
286
+ if (!absent(value.id)) {
287
+ if (typeof value.id !== "string" || !CHECKPOINT_ID.test(value.id)) throw new CollabError("Send the checkpoint id as 32 lowercase hexadecimal characters");
288
+ input.id = value.id;
289
+ }
290
+ if (!absent(value.testResults)) input.testResults = validateTests(value.testResults);
291
+ return input;
292
+ }
293
+ function mentionsIn(text, members) {
294
+ const byName = /* @__PURE__ */ new Map();
295
+ for (const member of members) {
296
+ const name = member.username.toLowerCase();
297
+ if (!byName.has(name)) byName.set(name, member.userId);
298
+ }
299
+ const ids = [];
300
+ for (const match of text.slice(0, 64 * 1024).matchAll(/(?<![A-Za-z0-9_.@-])@([A-Za-z0-9][A-Za-z0-9._-]{0,63})/g)) {
301
+ let name = match[1].toLowerCase();
302
+ while (name && !byName.has(name) && /[._-]$/.test(name)) name = name.slice(0, -1);
303
+ const id = byName.get(name);
304
+ if (id !== void 0 && !ids.includes(id)) ids.push(id);
305
+ }
306
+ return ids;
307
+ }
308
+ const CURRENT_STATES = ["claimed", "running", "paused", "unknown"], BUSY_STATES = ["claimed", "running", "unknown"];
309
+ const latestAttempt = (task, states) => {
310
+ for (let index = task.attempts.length - 1; index >= 0; index--) {
311
+ const attempt = task.attempts[index];
312
+ if (states.includes(attempt.state)) return attempt;
313
+ }
314
+ return void 0;
315
+ };
316
+ function busyAttempt(task) {
317
+ return latestAttempt(task, ["claimed", "running"]) ?? latestAttempt(task, BUSY_STATES);
318
+ }
319
+ function currentAttempt(task) {
320
+ return busyAttempt(task) ?? latestAttempt(task, CURRENT_STATES);
321
+ }
322
+ function attemptIsLive(attempt, now = Date.now()) {
323
+ return (attempt.state === "claimed" || attempt.state === "running") && now - attempt.lastHeartbeatAt < MAX.heartbeatLiveMs;
324
+ }
325
+ function liveAttempt(task, now = Date.now()) {
326
+ const busy = busyAttempt(task);
327
+ return busy && attemptIsLive(busy, now) ? busy : void 0;
328
+ }
329
+ const assertNotBusy = (task, before) => {
330
+ const busy = busyAttempt(task);
331
+ if (busy) throw new CollabError(`Stop the attempt running on ${busy.runnerName} before ${before}`, 409);
332
+ };
333
+ function latestCheckpoint(task) {
334
+ let latest;
335
+ for (const checkpoint of task.checkpoints) if (!latest || checkpoint.at >= latest.at) latest = checkpoint;
336
+ return latest;
337
+ }
338
+ function reviewState(task) {
339
+ const latest = latestCheckpoint(task);
340
+ if (!latest) return "none";
341
+ const reviews = task.reviews.filter((review) => review.checkpointId === latest.id && !review.stale).sort((a, b) => a.at - b.at), last = reviews[reviews.length - 1];
342
+ if (last) return last.verdict === "approve" ? "approved" : "changes-requested";
343
+ return task.reviewer ? "pending" : "none";
344
+ }
345
+ const nextBlocker = (task, blocker) => blocker === void 0 ? task.blocker : blocker === null ? void 0 : blocker;
346
+ function transitionProblem(task, to, actor, blocker) {
347
+ if (!TASK_STATUSES.includes(to)) return { message: "Choose a status: planned, working, blocked, review or done", status: 400 };
348
+ if (!roleAtLeast(actorRole(actor, task.projectId), "contributor")) return { message: "A project contributor role is required to change the status", status: 403 };
349
+ if (to === "blocked" && !nextBlocker(task, blocker)?.trim()) return { message: BLOCKER_REQUIRED, status: 400 };
350
+ if (to === task.status) return void 0;
351
+ if (to === "done") {
352
+ const busy = busyAttempt(task);
353
+ if (busy) return { message: `Stop the attempt running on ${busy.runnerName} before marking the task done`, status: 409 };
354
+ if (task.handoff?.state === "offered") return { message: "Accept, decline or cancel the offered hand-over before marking the task done", status: 409 };
355
+ }
356
+ return void 0;
357
+ }
358
+ function canTransition(task, to, actor, blocker) {
359
+ return transitionProblem(task, to, actor, blocker)?.message;
360
+ }
361
+ function transition(task, to, actor, blocker, now = Date.now()) {
362
+ const problem = transitionProblem(task, to, actor, blocker);
363
+ if (problem) throw new CollabError(problem.message, problem.status);
364
+ task.status = to;
365
+ if (to === "blocked") task.blocker = nextBlocker(task, blocker).trim();
366
+ else delete task.blocker;
367
+ task.updatedAt = now;
368
+ return task;
369
+ }
370
+ function assertCanChangeOwner(task, actor, nextOwner) {
371
+ if (nextOwner?.userId === task.owner?.userId) return;
372
+ const role = actorRole(actor, task.projectId);
373
+ if (!roleAtLeast(role, "contributor")) throw new CollabError("A project contributor role is required to take a task", 403);
374
+ assertNotBusy(task, "changing the owner");
375
+ if (roleAtLeast(role, "maintainer")) return;
376
+ if (nextOwner === void 0) {
377
+ if (isTaskOwner(task, actor)) return;
378
+ throw new CollabError("Only the task owner or a project maintainer may unassign this task", 403);
379
+ }
380
+ if (nextOwner.userId === actor.userId && !task.owner) return;
381
+ throw new CollabError(task.owner ? "Only a project maintainer may reassign a task that already has an owner" : "Only a project maintainer may assign a task to someone else", 403);
382
+ }
383
+ function assignOwner(task, nextOwner, now = Date.now()) {
384
+ if (nextOwner?.userId !== task.owner?.userId) {
385
+ assertNotBusy(task, "changing the owner");
386
+ for (const attempt of task.attempts) if (attempt.state === "paused" || attempt.state === "stopped" || attempt.state === "unknown") {
387
+ attempt.state = "superseded";
388
+ attempt.endedAt = now;
389
+ }
390
+ }
391
+ if (nextOwner) task.owner = { ...nextOwner };
392
+ else delete task.owner;
393
+ task.updatedAt = now;
394
+ return task;
395
+ }
396
+ function ownerChangedMessage(task) {
397
+ return task.owner ? `The task now belongs to ${task.owner.displayName}; they continue it from its latest checkpoint, or a project maintainer reassigns it` : "The task has no owner now; take it to start a new attempt from its latest checkpoint";
398
+ }
399
+ const publicWho = (who) => ({ ...who });
400
+ function summarize(task, forUserId, now = Date.now(), unreadMentions) {
401
+ const attempt = currentAttempt(task), checkpoint = latestCheckpoint(task);
402
+ const summary = { id: task.id, projectId: task.projectId, repository: task.repository, title: task.title, status: task.status, collaborators: task.collaborators.map(publicWho), dependsOn: [...task.dependsOn], labels: [...task.labels], revision: task.revision, createdAt: task.createdAt, updatedAt: task.updatedAt, reviewState: reviewState(task) };
403
+ if (task.blocker) summary.blocker = task.blocker;
404
+ if (task.owner) summary.owner = publicWho(task.owner);
405
+ if (task.reviewer) summary.reviewer = publicWho(task.reviewer);
406
+ if (attempt) summary.attempt = { id: attempt.id, state: attempt.state, runnerName: attempt.runnerName, user: publicWho(attempt.user), lastHeartbeatAt: attempt.lastHeartbeatAt, progress: clone(attempt.progress) };
407
+ if (checkpoint) summary.checkpoint = { id: checkpoint.id, commit: checkpoint.commit, at: checkpoint.at };
408
+ if (task.handoff) summary.handoff = { id: task.handoff.id, to: publicWho(task.handoff.to), from: publicWho(task.handoff.from), state: task.handoff.state };
409
+ if (forUserId !== void 0 && unreadMentions !== void 0) summary.unreadMentions = unreadMentions;
410
+ return summary;
411
+ }
412
+ function pushBounded(list, item, max, label, overflow = "refuse") {
413
+ if (list.length >= max) {
414
+ if (overflow === "refuse") throw new CollabError(`At most ${max} ${label} can be kept here; finish or remove some before adding more`);
415
+ list.splice(0, list.length - max + 1);
416
+ }
417
+ list.push(item);
418
+ return list;
419
+ }
420
+ function trimAudit(task) {
421
+ if (task.audit.length > MAX.audit) task.audit.splice(0, task.audit.length - MAX.audit);
422
+ return task;
423
+ }
424
+ function truncateText(value, max) {
425
+ const chars = [...value];
426
+ return chars.length <= max ? value : `${chars.slice(0, Math.max(0, max - 1)).join("")}\u2026`;
427
+ }
428
+ function recordAudit(task, by, action, detail, now = Date.now()) {
429
+ const entry = { at: now, by: { userId: by.userId, displayName: by.displayName }, action: truncateText(action, MAX.auditAction) };
430
+ if (detail) entry.detail = truncateText(detail, MAX.auditDetail);
431
+ pushBounded(task.audit, entry, MAX.audit, "audit entries", "drop-oldest");
432
+ return entry;
433
+ }
434
+ export {
435
+ ATTEMPT_ID,
436
+ ATTEMPT_STATES,
437
+ BLOCKER_REQUIRED,
438
+ CHECKPOINT_ID,
439
+ COMMIT_ID,
440
+ CollabError,
441
+ ID_PREFIX,
442
+ MAX,
443
+ PATH_ID,
444
+ PREFIXED_ID,
445
+ REPO_KEY,
446
+ RUNNER_ID,
447
+ RUNNER_TOKEN,
448
+ SHA256_HEX,
449
+ SHARED_ROLE_RANK,
450
+ TASK_ID,
451
+ TASK_STATUSES,
452
+ actorRole,
453
+ assertCanChangeOwner,
454
+ assignOwner,
455
+ attemptIsLive,
456
+ busyAttempt,
457
+ canTransition,
458
+ clone,
459
+ currentAttempt,
460
+ isTaskOwner,
461
+ latestCheckpoint,
462
+ liveAttempt,
463
+ mentionsIn,
464
+ newAttemptId,
465
+ newCheckpointId,
466
+ newId,
467
+ newRunnerId,
468
+ newRunnerToken,
469
+ newTaskId,
470
+ ownerChangedMessage,
471
+ pushBounded,
472
+ recordAudit,
473
+ reviewState,
474
+ roleAtLeast,
475
+ summarize,
476
+ transition,
477
+ trimAudit,
478
+ truncateText,
479
+ validateCheckpointInput,
480
+ validateCommit,
481
+ validatePathId,
482
+ validatePaths,
483
+ validateProgress,
484
+ validateProgressReport,
485
+ validateRef,
486
+ validateRepoKey,
487
+ validateRepoRefs,
488
+ validateRunId,
489
+ validateSeq,
490
+ validateStatus,
491
+ validateTaskInput,
492
+ validateTaskPatch,
493
+ validateText,
494
+ validateUserRef,
495
+ whoOf
496
+ };