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

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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -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/changes-62ZIEQSN.js +1 -0
  17. package/public/assets/chunk-2PZVOKID.js +1 -0
  18. package/public/assets/chunk-4D5IENEB.js +1 -0
  19. package/public/assets/chunk-5SARP5MU.js +1 -0
  20. package/public/assets/chunk-63YLZ3PR.js +1 -0
  21. package/public/assets/chunk-7FUHKF2M.js +1 -0
  22. package/public/assets/chunk-APQEVJ2K.js +1 -0
  23. package/public/assets/chunk-CGOZQMEZ.js +2 -0
  24. package/public/assets/chunk-J4VYS3KV.js +2 -0
  25. package/public/assets/chunk-L7TUCIYF.js +1 -0
  26. package/public/assets/chunk-SIHDWM2Y.js +154 -0
  27. package/public/assets/chunk-TEJVYIKC.js +1 -0
  28. package/public/assets/code-browser-VB2HGZSG.js +4 -0
  29. package/public/assets/filtered-tasks-ES5CP4KV.js +1 -0
  30. package/public/assets/interactive-browser-7F5ZXRUV.js +7 -0
  31. package/public/assets/join-project-6RTFHNCA.js +1 -0
  32. package/public/assets/main-65EW73SB.js +1 -0
  33. package/public/assets/project-invitations-view-GMPWGWYM.js +1 -0
  34. package/public/assets/settings-UB5Q4AQM.js +9 -0
  35. package/public/assets/styles-18792BB4.css +32 -0
  36. package/public/assets/team-server-wizard-2GLDWS6D.js +8 -0
  37. package/public/assets/terminal-IWAIM4DE.js +62 -0
  38. package/public/assets.json +1 -0
  39. package/public/bundle-meta.json +1 -0
  40. package/public/docs/.nojekyll +0 -0
  41. package/public/docs/accessibility.html +19 -0
  42. package/public/docs/acp-agents.html +117 -0
  43. package/public/docs/agent-containers.html +79 -0
  44. package/public/docs/api.html +1130 -0
  45. package/public/docs/app.js +342 -0
  46. package/public/docs/application-improvements.html +275 -0
  47. package/public/docs/architecture-explorer.html +19 -0
  48. package/public/docs/architecture.html +149 -0
  49. package/public/docs/auto-team.html +148 -0
  50. package/public/docs/automations.html +109 -0
  51. package/public/docs/checkpoint-review.html +37 -0
  52. package/public/docs/code-hosts.html +35 -0
  53. package/public/docs/code-intelligence.html +35 -0
  54. package/public/docs/configuration.html +346 -0
  55. package/public/docs/context-inspection.html +23 -0
  56. package/public/docs/coordinated-changes.html +28 -0
  57. package/public/docs/deferred-runs.html +25 -0
  58. package/public/docs/dependency-discovery.html +19 -0
  59. package/public/docs/documentation.html +84 -0
  60. package/public/docs/dynamic-teams-evaluation.html +174 -0
  61. package/public/docs/dynamic-teams-implementation-plan.html +882 -0
  62. package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
  63. package/public/docs/dynamic-teams-plan-review-0.html +204 -0
  64. package/public/docs/extensions.html +42 -0
  65. package/public/docs/external-sources.html +96 -0
  66. package/public/docs/filesystem-policy.html +26 -0
  67. package/public/docs/filtered-tasks.html +44 -0
  68. package/public/docs/font-license.txt +92 -0
  69. package/public/docs/getting-started.html +143 -0
  70. package/public/docs/github-review-threads.html +26 -0
  71. package/public/docs/github.html +157 -0
  72. package/public/docs/help.html +69 -0
  73. package/public/docs/index.html +88 -0
  74. package/public/docs/installation.html +80 -0
  75. package/public/docs/launch-profiles.html +48 -0
  76. package/public/docs/maintenance.html +60 -0
  77. package/public/docs/memory-claims.html +31 -0
  78. package/public/docs/memory-storage.html +58 -0
  79. package/public/docs/notifications.html +39 -0
  80. package/public/docs/patterns.html +195 -0
  81. package/public/docs/performance-results.html +210 -0
  82. package/public/docs/performance.html +166 -0
  83. package/public/docs/project-conventions.html +22 -0
  84. package/public/docs/project-first-navigation-plan.html +587 -0
  85. package/public/docs/project-teams-implementation-plan.html +772 -0
  86. package/public/docs/project-teams.html +129 -0
  87. package/public/docs/provider-compatibility.html +20 -0
  88. package/public/docs/releasing.html +65 -0
  89. package/public/docs/remote-workers.html +64 -0
  90. package/public/docs/review-inbox.html +26 -0
  91. package/public/docs/run-storage.html +23 -0
  92. package/public/docs/runs.html +141 -0
  93. package/public/docs/search.json +1 -0
  94. package/public/docs/security.html +133 -0
  95. package/public/docs/shared-projects.html +31 -0
  96. package/public/docs/ssh-projects.html +60 -0
  97. package/public/docs/style.css +768 -0
  98. package/public/docs/task-reruns.html +30 -0
  99. package/public/docs/team-collaboration.html +412 -0
  100. package/public/docs/verification.html +22 -0
  101. package/public/docs/vscode.html +53 -0
  102. package/public/docs/worker-permission-verification.html +131 -0
  103. package/public/index.html +17 -0
  104. package/scripts/check-node.mjs +7 -0
  105. package/scripts/codeflow.mjs +55 -0
  106. package/scripts/computer-use.swift +64 -0
  107. package/scripts/pty.py +273 -0
  108. package/src/acp-connections.js +110 -0
  109. package/src/adapters/acp-presets.js +37 -0
  110. package/src/adapters/acp-types.js +0 -0
  111. package/src/adapters/acp.js +328 -0
  112. package/src/adapters/agent-tools.js +28 -0
  113. package/src/adapters/agy-auth.js +67 -0
  114. package/src/adapters/agy-isolation.js +242 -0
  115. package/src/adapters/agy-tools.js +45 -0
  116. package/src/adapters/agy.js +326 -0
  117. package/src/adapters/claude-stream.js +58 -0
  118. package/src/adapters/claude.js +282 -0
  119. package/src/adapters/codex-app-server.js +555 -0
  120. package/src/adapters/codex.js +311 -0
  121. package/src/adapters/errors.js +57 -0
  122. package/src/adapters/index.js +91 -0
  123. package/src/adapters/managed-permissions.js +37 -0
  124. package/src/adapters/model-discovery.js +100 -0
  125. package/src/adapters/process.js +433 -0
  126. package/src/adapters/team-tools.js +103 -0
  127. package/src/adapters/types.js +0 -0
  128. package/src/agent-browser.js +688 -0
  129. package/src/agent-computer.js +88 -0
  130. package/src/agent-container.js +155 -0
  131. package/src/agent-team-definitions.js +131 -0
  132. package/src/agent-tool-client.mjs +79 -0
  133. package/src/agent-tools.js +296 -0
  134. package/src/approvals.js +134 -0
  135. package/src/architecture.js +456 -0
  136. package/src/assets.js +44 -0
  137. package/src/automation.js +901 -0
  138. package/src/browser-files.js +293 -0
  139. package/src/browser-ownership.js +150 -0
  140. package/src/certificate-renewal.js +75 -0
  141. package/src/chat-family.js +25 -0
  142. package/src/chat-handoff.js +87 -0
  143. package/src/checkpoint-review.js +72 -0
  144. package/src/checks.js +303 -0
  145. package/src/code-host-git.js +102 -0
  146. package/src/code-hosts-api.js +99 -0
  147. package/src/code-hosts.js +482 -0
  148. package/src/code-intelligence-worker.mjs +452 -0
  149. package/src/code-intelligence.js +141 -0
  150. package/src/collab/api.js +365 -0
  151. package/src/collab/checkpoints.js +198 -0
  152. package/src/collab/connections.js +139 -0
  153. package/src/collab/host-execution.js +347 -0
  154. package/src/collab/hub-client.js +210 -0
  155. package/src/collab/link-api.js +229 -0
  156. package/src/collab/link.js +595 -0
  157. package/src/collab/model.js +496 -0
  158. package/src/collab/runner.js +1014 -0
  159. package/src/collab/store.js +1452 -0
  160. package/src/collab/terminals.js +361 -0
  161. package/src/collaboration-reviews.js +116 -0
  162. package/src/config.js +48 -0
  163. package/src/context-budget.js +14 -0
  164. package/src/context-inspection.js +204 -0
  165. package/src/context.js +154 -0
  166. package/src/conventions.js +283 -0
  167. package/src/coordinated-api.js +247 -0
  168. package/src/deferred-launch.js +82 -0
  169. package/src/deferred-runs.js +515 -0
  170. package/src/dependency-health.js +35 -0
  171. package/src/diff.js +43 -0
  172. package/src/docs-help.js +316 -0
  173. package/src/doctor.js +232 -0
  174. package/src/documentation-changes.js +20 -0
  175. package/src/engine.js +5258 -0
  176. package/src/env.js +44 -0
  177. package/src/execution-budget.js +8 -0
  178. package/src/experience.js +50 -0
  179. package/src/extensions.js +144 -0
  180. package/src/filesystem-access.js +107 -0
  181. package/src/filesystem-policy.js +238 -0
  182. package/src/filtered-task-limits.js +93 -0
  183. package/src/filtered-tasks-api.js +53 -0
  184. package/src/filtered-tasks.js +576 -0
  185. package/src/frame-policy.js +66 -0
  186. package/src/fs.js +57 -0
  187. package/src/git.js +626 -0
  188. package/src/github-review-threads.js +181 -0
  189. package/src/github.js +538 -0
  190. package/src/http.js +69 -0
  191. package/src/improve-settings.js +38 -0
  192. package/src/improve.js +187 -0
  193. package/src/integrations.js +373 -0
  194. package/src/integrity.js +259 -0
  195. package/src/journey-api.js +77 -0
  196. package/src/journey-replay.js +65 -0
  197. package/src/journey-store.js +97 -0
  198. package/src/journeys.js +60 -0
  199. package/src/json.js +81 -0
  200. package/src/launch-profiles.js +292 -0
  201. package/src/learnings.js +80 -0
  202. package/src/local-app-url.js +19 -0
  203. package/src/log.js +39 -0
  204. package/src/maintenance.js +335 -0
  205. package/src/memory-analysis.js +182 -0
  206. package/src/memory-claims.js +153 -0
  207. package/src/memory-sqlite.js +236 -0
  208. package/src/memory.js +552 -0
  209. package/src/merge-validation.js +58 -0
  210. package/src/model-catalog.js +239 -0
  211. package/src/model-research.js +91 -0
  212. package/src/notes.js +176 -0
  213. package/src/notifications.js +413 -0
  214. package/src/patterns/auto-team-model.js +230 -0
  215. package/src/patterns/auto-team.js +816 -0
  216. package/src/patterns/council.js +199 -0
  217. package/src/patterns/debate.js +188 -0
  218. package/src/patterns/findings.js +141 -0
  219. package/src/patterns/index.js +21 -0
  220. package/src/patterns/loop.js +158 -0
  221. package/src/patterns/pipeline.js +125 -0
  222. package/src/patterns/prompts.js +181 -0
  223. package/src/patterns/race.js +147 -0
  224. package/src/patterns/schemas.js +40 -0
  225. package/src/patterns/solo.js +46 -0
  226. package/src/patterns/supervisor.js +235 -0
  227. package/src/patterns/task-draft.js +29 -0
  228. package/src/patterns/task-review.js +45 -0
  229. package/src/patterns/tdd.js +231 -0
  230. package/src/patterns/team-strategies.js +67 -0
  231. package/src/performance.js +43 -0
  232. package/src/persistence-status.js +35 -0
  233. package/src/phone-sessions.js +96 -0
  234. package/src/preview-overlay.js +392 -0
  235. package/src/preview.js +230 -0
  236. package/src/privacy.js +23 -0
  237. package/src/project-report.js +22 -0
  238. package/src/project-team-budget.js +22 -0
  239. package/src/project-team-model.js +225 -0
  240. package/src/project-team-policy.js +42 -0
  241. package/src/project-team-store.js +234 -0
  242. package/src/project-team-usage.js +24 -0
  243. package/src/project-teams-api.js +43 -0
  244. package/src/project-teams.js +644 -0
  245. package/src/projects.js +290 -0
  246. package/src/provider-eligibility.js +9 -0
  247. package/src/readiness.js +98 -0
  248. package/src/remote-tls.js +66 -0
  249. package/src/remote-workers/bundles.js +198 -0
  250. package/src/remote-workers/client.js +332 -0
  251. package/src/remote-workers/durability.js +37 -0
  252. package/src/remote-workers/protocol.js +68 -0
  253. package/src/remote-workers/stdio.js +138 -0
  254. package/src/remote-workers/worker.js +304 -0
  255. package/src/remote.js +204 -0
  256. package/src/repo-config.js +201 -0
  257. package/src/repo-coordination.js +725 -0
  258. package/src/repo-tree.js +106 -0
  259. package/src/repository-identity.js +13 -0
  260. package/src/retention-policy.js +466 -0
  261. package/src/review-inbox.js +38 -0
  262. package/src/routing.js +49 -0
  263. package/src/run-analysis.js +111 -0
  264. package/src/run-cold-store.js +231 -0
  265. package/src/run-environment.js +15 -0
  266. package/src/run-history.js +91 -0
  267. package/src/run-log.js +160 -0
  268. package/src/run-permissions.js +111 -0
  269. package/src/run-projection.js +77 -0
  270. package/src/run-recovery.js +53 -0
  271. package/src/run-status.js +56 -0
  272. package/src/serve.js +138 -0
  273. package/src/server.js +2871 -0
  274. package/src/shared-access.js +103 -0
  275. package/src/shared-accounts.js +381 -0
  276. package/src/shared-api.js +239 -0
  277. package/src/shell.js +346 -0
  278. package/src/ship.js +344 -0
  279. package/src/slots.js +74 -0
  280. package/src/snapshot.js +388 -0
  281. package/src/source-context.js +17 -0
  282. package/src/sse-writer.js +70 -0
  283. package/src/ssh-projects/bootstrap.mjs +95 -0
  284. package/src/ssh-projects/config.js +45 -0
  285. package/src/ssh-projects/gateway.js +143 -0
  286. package/src/ssh-projects/manager.js +320 -0
  287. package/src/ssh-projects/team-autostart.mjs +93 -0
  288. package/src/ssh-projects/team-remove.mjs +69 -0
  289. package/src/ssh-projects/team-status.mjs +39 -0
  290. package/src/ssh-projects/transport.js +133 -0
  291. package/src/stats.js +139 -0
  292. package/src/store.js +544 -0
  293. package/src/stream.js +91 -0
  294. package/src/sweeps.js +241 -0
  295. package/src/tailscale.js +101 -0
  296. package/src/task-draft.js +23 -0
  297. package/src/task-team.js +164 -0
  298. package/src/team-evaluation.js +243 -0
  299. package/src/team-mailbox.js +160 -0
  300. package/src/team-memory.js +84 -0
  301. package/src/team-rerun-graph.js +15 -0
  302. package/src/team-reruns.js +130 -0
  303. package/src/team-scheduler.js +121 -0
  304. package/src/team-steering.js +21 -0
  305. package/src/team-template-store.js +98 -0
  306. package/src/team-templates.js +117 -0
  307. package/src/team-tool-client.mjs +79 -0
  308. package/src/team-tools.js +235 -0
  309. package/src/teams.js +0 -0
  310. package/src/templates.js +62 -0
  311. package/src/transcript.js +134 -0
  312. package/src/verification-integrity.js +27 -0
  313. package/src/verification.js +183 -0
  314. package/src/watch.js +195 -0
  315. package/src/work-queue.js +47 -0
  316. package/src/workflow-control.js +83 -0
  317. package/src/workspace-api.js +207 -0
  318. package/src/workspace-changes.js +61 -0
  319. package/src/workspace.js +146 -0
@@ -0,0 +1,413 @@
1
+ import { checksLabel } from "./run-status.js";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { CODEFLOW_HOME } from "./config.js";
6
+ import { registerPrivateEnvNames } from "./env.js";
7
+ import { logWarn } from "./log.js";
8
+ import { redactExport, redactText } from "./privacy.js";
9
+ const EVENTS = ["done", "failure", "needs-attention"];
10
+ const MAX_DELIVERIES = 200;
11
+ const MAX_SEEN = 2e3;
12
+ const MAX_ATTEMPTS = 3;
13
+ const MAX_PENDING_AGE = 24 * 60 * 6e4;
14
+ const EMPTY = () => ({ version: 1, settings: /* @__PURE__ */ Object.create(null), deliveries: [], seen: [] });
15
+ const defaults = () => ({ enabled: false, events: [], projects: [] });
16
+ const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
17
+ const text = (value, max) => typeof value === "string" ? value.slice(0, max) : "";
18
+ function notificationEvents(run) {
19
+ if (run.archived || run.projectTeam) return [];
20
+ const candidates = [];
21
+ if (run.endedAt && ["done", "failed", "stopped", "interrupted"].includes(run.status)) {
22
+ const failed = run.status !== "done" || ["Failed", "Blocked"].includes(checksLabel(run));
23
+ candidates.push({ event: failed ? "failure" : "done", key: `${run.id}:ended:${run.endedAt}` });
24
+ }
25
+ const attention = run.awaiting ? ["approval", run.awaiting.kind, run.awaiting.at] : run.team?.awaitingRequestId ? ["question", run.team.awaitingRequestId] : run.triage?.state === "pending" ? ["triage", run.triage.at] : run.team?.status === "blocked" ? ["blocked", run.team.gaps] : void 0;
26
+ if (attention) candidates.push({ event: "needs-attention", key: `${run.id}:attention:${digest(attention)}` });
27
+ for (const watch of Object.values(run.watches ?? {})) if (watch.state === "needs-you") candidates.push({ event: "needs-attention", key: `${run.id}:watch:${digest([watch.branch, watch.detail, watch.fixes])}` });
28
+ return candidates;
29
+ }
30
+ function validateNotificationSettings(value) {
31
+ if (!value || typeof value !== "object") throw new Error("Choose notification settings");
32
+ const raw = value;
33
+ if (typeof raw.enabled !== "boolean" || !Array.isArray(raw.events) || raw.events.length > 3 || raw.events.some((event) => !EVENTS.includes(event))) throw new Error("Choose valid notification events and enabled state");
34
+ const events = [...new Set(raw.events)];
35
+ if (raw.projects !== "all" && (!Array.isArray(raw.projects) || raw.projects.length > 100 || raw.projects.some((project) => typeof project !== "string" || !/^[\w-]{1,100}$/.test(project)))) throw new Error("Choose all projects or explicit project IDs");
36
+ const projects = raw.projects === "all" ? "all" : [...new Set(raw.projects)];
37
+ if (raw.enabled && (!events.length || projects !== "all" && !projects.length)) throw new Error("Enabling notifications requires an event and a project scope");
38
+ return { enabled: raw.enabled, events, projects };
39
+ }
40
+ class Notifications {
41
+ configs = [];
42
+ state = EMPTY();
43
+ loaded = false;
44
+ queue = Promise.resolve();
45
+ draining;
46
+ timer;
47
+ stopped = false;
48
+ file;
49
+ environment;
50
+ fetcher;
51
+ now;
52
+ constructor(options = {}) {
53
+ this.file = options.file ?? join(CODEFLOW_HOME, "notifications.json");
54
+ this.environment = options.environment ?? process.env;
55
+ this.fetcher = options.fetch ?? fetch;
56
+ this.now = options.now ?? Date.now;
57
+ }
58
+ async load(input) {
59
+ this.loaded = false;
60
+ this.configs = [];
61
+ this.state = EMPTY();
62
+ if (input === void 0) {
63
+ const content = this.environment.CODEFLOW_NOTIFICATIONS_FILE ? await readFile(this.environment.CODEFLOW_NOTIFICATIONS_FILE, "utf8") : this.environment.CODEFLOW_NOTIFICATIONS_JSON;
64
+ if (content && content.length > 128e3) throw new Error("Notification configuration exceeds 128 KB");
65
+ try {
66
+ input = content ? JSON.parse(content) : [];
67
+ } catch {
68
+ throw new Error("Notification configuration is not valid JSON");
69
+ }
70
+ }
71
+ if (!Array.isArray(input) || input.length > 20) throw new Error("Configure at most 20 notification destinations");
72
+ const ids = /* @__PURE__ */ new Set();
73
+ const configs = input.map((raw) => {
74
+ if (!raw || !/^[a-z][a-z0-9-]{0,63}$/.test(raw.id) || ids.has(raw.id) || !["webhook", "slack"].includes(raw.protocol)) throw new Error("Notification destinations need unique IDs and webhook or slack protocol");
75
+ ids.add(raw.id);
76
+ if (typeof raw.urlEnv !== "string" || !/^[A-Z_][A-Z0-9_]{0,99}$/.test(raw.urlEnv) || raw.secretEnv !== void 0 && (typeof raw.secretEnv !== "string" || !/^[A-Z_][A-Z0-9_]{0,99}$/.test(raw.secretEnv))) throw new Error("Notification credentials must name server environment variables");
77
+ return { id: raw.id, name: text(raw.name, 80) || raw.id, protocol: raw.protocol, urlEnv: raw.urlEnv, secretEnv: raw.secretEnv };
78
+ });
79
+ registerPrivateEnvNames(["CODEFLOW_NOTIFICATIONS_FILE", "CODEFLOW_NOTIFICATIONS_JSON", ...configs.flatMap((c) => [c.urlEnv, ...c.secretEnv ? [c.secretEnv] : []])]);
80
+ this.configs = configs;
81
+ const saved = await readFile(this.file, "utf8").catch((error) => {
82
+ if (error.code === "ENOENT") return void 0;
83
+ throw error;
84
+ });
85
+ let state = EMPTY();
86
+ if (saved !== void 0) {
87
+ if (saved.length > 2 * 1024 * 1024) throw new Error("Notification state exceeds its limit; notifications remain disabled");
88
+ try {
89
+ const parsed = JSON.parse(saved);
90
+ if (parsed.version !== 1 || !parsed.settings || typeof parsed.settings !== "object" || !Array.isArray(parsed.deliveries) || !Array.isArray(parsed.seen)) throw new Error("Invalid state");
91
+ state = { version: 1, settings: /* @__PURE__ */ Object.create(null), deliveries: [], seen: parsed.seen.filter((key) => typeof key === "string" && key.length <= 200).slice(-MAX_SEEN) };
92
+ for (const c of configs) {
93
+ const value = Object.hasOwn(parsed.settings, c.id) ? parsed.settings[c.id] : void 0;
94
+ if (value) state.settings[c.id] = { ...validateNotificationSettings(value), destinationHash: text(value.destinationHash, 64), dropped: Number.isSafeInteger(value.dropped) && value.dropped > 0 ? value.dropped : 0, lastError: text(value.lastError, 300) || void 0 };
95
+ }
96
+ for (const value of parsed.deliveries.slice(-MAX_DELIVERIES)) {
97
+ if (!value || !/^notification-[a-f0-9]{24}$/.test(value.id) || !configs.some((c) => c.id === value.target) || !EVENTS.includes(value.event) || !["pending", "sending", "delivered", "failed", "cancelled"].includes(value.state) || !Number.isFinite(value.createdAt) || !Number.isFinite(value.updatedAt) || !Number.isInteger(value.attempts) || value.attempts < 0 || value.attempts > MAX_ATTEMPTS || !value.payload || value.payload.version !== 1 || value.payload.id !== value.id || value.payload.runId !== value.runId || value.payload.event !== value.event || value.payload.project !== value.project) throw new Error("Invalid delivery");
98
+ const delivery = {
99
+ id: value.id,
100
+ target: value.target,
101
+ event: value.event,
102
+ runId: text(value.runId, 100),
103
+ teamId: text(value.payload.teamId, 100) || void 0,
104
+ cycleId: text(value.payload.cycleId, 100) || void 0,
105
+ project: text(value.project, 100) || void 0,
106
+ state: value.state,
107
+ attempts: value.attempts,
108
+ totalAttempts: Number.isSafeInteger(value.totalAttempts) && value.totalAttempts >= value.attempts ? value.totalAttempts : value.attempts,
109
+ retryCount: Number.isSafeInteger(value.retryCount) && value.retryCount > 0 ? value.retryCount : 0,
110
+ createdAt: value.createdAt,
111
+ updatedAt: value.updatedAt,
112
+ nextAttemptAt: Number.isFinite(value.nextAttemptAt) ? value.nextAttemptAt : void 0,
113
+ httpStatus: Number.isInteger(value.httpStatus) && value.httpStatus >= 100 && value.httpStatus <= 599 ? value.httpStatus : void 0,
114
+ reason: text(value.reason, 300) || void 0,
115
+ manual: value.manual === true || void 0,
116
+ payload: this.payload(value.payload),
117
+ destinationHash: text(value.destinationHash, 64)
118
+ };
119
+ if (delivery.state === "sending") {
120
+ delivery.state = "failed";
121
+ delivery.reason = "Delivery outcome is unknown after restart; retry manually if needed";
122
+ delivery.nextAttemptAt = void 0;
123
+ }
124
+ state.deliveries.push(delivery);
125
+ }
126
+ } catch {
127
+ throw new Error("Notification state is damaged; notifications remain disabled and the file was preserved");
128
+ }
129
+ }
130
+ this.configs = configs;
131
+ for (const c of configs) {
132
+ const preference = state.settings[c.id] ?? defaults();
133
+ const destination = this.destination(c);
134
+ if (preference.enabled && (!destination || preference.destinationHash !== destination.hash)) {
135
+ preference.enabled = false;
136
+ preference.lastError = "Destination changed or is unavailable; review settings and enable again";
137
+ }
138
+ state.settings[c.id] = preference;
139
+ }
140
+ this.state = state;
141
+ this.loaded = true;
142
+ }
143
+ list() {
144
+ return this.redact(this.configs.map((c) => {
145
+ const destination = this.destination(c), setting = this.state.settings[c.id] ?? defaults();
146
+ return { id: c.id, name: c.name, protocol: c.protocol, available: this.loaded, configured: !!destination, host: destination?.url.hostname, settings: { enabled: setting.enabled, events: setting.events, projects: setting.projects }, dropped: setting.dropped ?? 0, lastError: setting.lastError };
147
+ }));
148
+ }
149
+ deliveries() {
150
+ return this.redact([...this.state.deliveries].reverse().map(({ payload: _payload, destinationHash: _hash, ...delivery }) => delivery));
151
+ }
152
+ async update(id, value) {
153
+ const c = this.config(id), settings = validateNotificationSettings(value), destination = this.destination(c);
154
+ if (settings.enabled && !destination) throw new Error("Configure a valid HTTPS destination and credentials before enabling notifications");
155
+ await this.mutate(() => {
156
+ this.state.settings[id] = { ...this.state.settings[id], ...settings, destinationHash: destination?.hash, lastError: void 0 };
157
+ for (const job of this.state.deliveries) if (job.target === id && job.state === "pending" && !this.allowed(job, settings)) {
158
+ job.state = "cancelled";
159
+ job.updatedAt = this.now();
160
+ job.reason = "Notification settings no longer allow this delivery";
161
+ }
162
+ });
163
+ return this.list().find((target) => target.id === id);
164
+ }
165
+ async observe(run, extraAttentionKey) {
166
+ if (!this.loaded || this.stopped) return { queued: 0, dropped: 0 };
167
+ const candidates = notificationEvents(run);
168
+ if (extraAttentionKey && !run.archived && !run.projectTeam) candidates.push({ event: "needs-attention", key: `${run.id}:external-attention:${digest(extraAttentionKey.slice(0, 200))}` });
169
+ return this.enqueue(candidates, { runId: run.id, project: run.project, pattern: run.pattern, status: run.status, result: { Passed: "verified", Failed: "needs-changes", Blocked: "blocked" }[checksLabel(run)] ?? "unchecked", title: run.task?.split("\n")[0] ?? "Run update" });
170
+ }
171
+ async observeProjectTeam(team) {
172
+ for (const cycle of team.cycles) {
173
+ const opportunities = team.opportunities.filter((o) => o.cycleId === cycle.id || o.id === cycle.opportunityId);
174
+ const actionable = opportunities.some((o) => ["proposed", "ready"].includes(o.status));
175
+ const failed = ["blocked", "failed", "unknown"].includes(cycle.status);
176
+ if (!failed && !(cycle.status === "completed" && actionable)) continue;
177
+ const event = failed ? "failure" : "needs-attention";
178
+ await this.enqueue([{ event, key: `${team.id}:${cycle.id}:${cycle.status}` }], { runId: "", teamId: team.id, cycleId: cycle.id, project: team.projectId, pattern: "project-team", status: cycle.status, result: failed ? "blocked" : "ready-for-review", title: `${team.name}: ${cycle.reason ?? cycle.summary ?? "An improvement is ready for review"}` });
179
+ }
180
+ }
181
+ async enqueue(candidates, subject) {
182
+ if (!this.loaded || this.stopped) return { queued: 0, dropped: 0 };
183
+ if (!candidates.length || !this.configs.some((c) => {
184
+ const settings = this.state.settings[c.id] ?? defaults();
185
+ const destination = this.destination(c);
186
+ return destination && settings.destinationHash === destination.hash && settings.enabled && candidates.some((candidate) => this.allowed({ event: candidate.event, project: subject.project }, settings) && !this.state.seen.includes(digest([c.id, candidate.event, candidate.key])));
187
+ })) return { queued: 0, dropped: 0 };
188
+ let queued = 0, dropped = 0;
189
+ await this.mutate(() => {
190
+ for (const c of this.configs) {
191
+ const settings = this.state.settings[c.id] ?? defaults(), destination = this.destination(c);
192
+ if (!destination || !settings.enabled || settings.destinationHash !== destination.hash) continue;
193
+ for (const candidate of candidates) {
194
+ const identity = digest([c.id, candidate.event, candidate.key]);
195
+ if (!this.allowed({ event: candidate.event, project: subject.project }, settings) || this.state.seen.includes(identity)) continue;
196
+ this.state.seen = [...this.state.seen, identity].slice(-MAX_SEEN);
197
+ if (!this.room()) {
198
+ dropped++;
199
+ settings.dropped = (settings.dropped ?? 0) + 1;
200
+ settings.lastError = `Notification queue is full; event for run ${subject.runId} was dropped`;
201
+ continue;
202
+ }
203
+ const payload = this.payload({ ...subject, version: 1, id: `notification-${randomBytes(12).toString("hex")}`, event: candidate.event });
204
+ this.state.deliveries.push(this.job(c, destination.hash, payload));
205
+ queued++;
206
+ }
207
+ }
208
+ });
209
+ if (queued) void this.drain().catch((error) => logWarn("Notification delivery could not be persisted", { error: error.message }));
210
+ return { queued, dropped };
211
+ }
212
+ async test(id) {
213
+ const c = this.config(id), destination = this.destination(c);
214
+ if (!destination) throw new Error("Configure a valid HTTPS destination and credentials first");
215
+ let delivery;
216
+ await this.mutate(() => {
217
+ if (!this.room()) throw new Error("Notification queue is full");
218
+ delivery = this.job(c, destination.hash, this.payload({ version: 1, id: `notification-${randomBytes(12).toString("hex")}`, event: "done", runId: "test", pattern: "test", status: "test", result: "test", title: "code-flow notification test", test: true }));
219
+ delivery.manual = true;
220
+ this.state.deliveries.push(delivery);
221
+ });
222
+ void this.drain().catch((error) => logWarn("Test notification could not be persisted", { error: error.message }));
223
+ return this.deliveries().find((value) => value.id === delivery.id);
224
+ }
225
+ async retry(id) {
226
+ await this.mutate(() => {
227
+ const job = this.state.deliveries.find((value) => value.id === id);
228
+ if (!job || job.state !== "failed") throw new Error("Only a failed notification can be retried");
229
+ const c = this.config(job.target), destination = this.destination(c);
230
+ if (!destination || !job.manual && !this.allowed(job, this.state.settings[c.id] ?? defaults())) throw new Error("Enable this destination and its event/project scope before retrying");
231
+ job.destinationHash = destination.hash;
232
+ job.attempts = 0;
233
+ job.retryCount = (job.retryCount ?? 0) + 1;
234
+ job.state = "pending";
235
+ job.nextAttemptAt = this.now();
236
+ job.updatedAt = this.now();
237
+ job.reason = "Explicit retry requested";
238
+ });
239
+ void this.drain().catch((error) => logWarn("Notification retry could not be persisted", { error: error.message }));
240
+ return this.deliveries().find((value) => value.id === id);
241
+ }
242
+ start(intervalMs = 1e4) {
243
+ if (this.timer || !this.loaded) return;
244
+ this.stopped = false;
245
+ this.timer = setInterval(() => void this.drain().catch((error) => logWarn("Notification retry failed", { error: error.message })), Math.max(1e3, intervalMs));
246
+ this.timer.unref?.();
247
+ }
248
+ async stop() {
249
+ this.stopped = true;
250
+ if (this.timer) clearInterval(this.timer);
251
+ this.timer = void 0;
252
+ await this.draining;
253
+ await this.queue;
254
+ }
255
+ drain() {
256
+ this.draining ??= this.drainAll().finally(() => {
257
+ this.draining = void 0;
258
+ });
259
+ return this.draining;
260
+ }
261
+ async drainAll() {
262
+ if (!this.loaded || this.stopped) return;
263
+ for (let count = 0; count < 20; count++) {
264
+ if (this.stopped) return;
265
+ const candidate = this.state.deliveries.find((value) => value.state === "pending" && (value.nextAttemptAt ?? 0) <= this.now());
266
+ if (!candidate) return;
267
+ const claimed = await this.mutate(() => {
268
+ const job2 = this.state.deliveries.find((value) => value.id === candidate.id);
269
+ if (!job2 || job2.state !== "pending" || (job2.nextAttemptAt ?? 0) > this.now()) return void 0;
270
+ if (job2.attempts >= MAX_ATTEMPTS) {
271
+ job2.state = "failed";
272
+ job2.reason = "Automatic retry limit reached";
273
+ job2.updatedAt = this.now();
274
+ return void 0;
275
+ }
276
+ const c2 = this.configs.find((config) => config.id === job2.target), destination2 = c2 && this.destination(c2);
277
+ if (!c2 || !destination2 || destination2.hash !== job2.destinationHash || !job2.manual && !this.allowed(job2, this.state.settings[job2.target] ?? defaults()) || this.now() - (job2.nextAttemptAt ?? job2.createdAt) > MAX_PENDING_AGE) {
278
+ job2.state = "cancelled";
279
+ job2.reason = "Destination or scope changed, or this pending delivery expired";
280
+ job2.updatedAt = this.now();
281
+ return void 0;
282
+ }
283
+ job2.state = "sending";
284
+ job2.attempts++;
285
+ job2.totalAttempts = (job2.totalAttempts ?? 0) + 1;
286
+ job2.updatedAt = this.now();
287
+ job2.nextAttemptAt = void 0;
288
+ return { job: job2, c: c2, destination: destination2 };
289
+ });
290
+ if (!claimed) continue;
291
+ const { job, c, destination } = claimed;
292
+ let status, retryable = true, retryAfter = 3e4, delivered = false;
293
+ try {
294
+ const headers = { "content-type": "application/json", "idempotency-key": job.id };
295
+ if (c.secretEnv) headers.authorization = `Bearer ${this.environment[c.secretEnv]}`;
296
+ const message = `code-flow: ${job.event}
297
+ ${job.payload.title}
298
+ ${job.payload.teamId ? `Team ${job.payload.teamId}` : `Run ${job.runId}`} \xB7 ${job.payload.status} \xB7 ${job.payload.result}`;
299
+ const slackText = message.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
300
+ const body = c.protocol === "slack" ? { text: slackText, blocks: [{ type: "section", text: { type: "plain_text", text: message, emoji: false } }], unfurl_links: false, unfurl_media: false } : job.payload;
301
+ const response = await this.fetcher(destination.url, { method: "POST", headers, body: JSON.stringify(body), redirect: "error", signal: AbortSignal.timeout(1e4) });
302
+ status = response.status;
303
+ retryable = status === 429 || status >= 500;
304
+ const retry = Number(response.headers.get("retry-after"));
305
+ if (Number.isFinite(retry) && retry > 0) retryAfter = Math.min(3e5, retry * 1e3);
306
+ await response.body?.cancel();
307
+ delivered = response.ok;
308
+ } catch {
309
+ }
310
+ if (delivered) {
311
+ await this.mutate(() => {
312
+ job.state = "delivered";
313
+ job.httpStatus = status;
314
+ job.reason = void 0;
315
+ job.updatedAt = this.now();
316
+ });
317
+ continue;
318
+ }
319
+ await this.mutate(() => {
320
+ job.httpStatus = status;
321
+ job.updatedAt = this.now();
322
+ job.reason = status ? `Destination returned HTTP ${status}` : "Connection, TLS or timeout failure";
323
+ if (retryable && job.attempts < MAX_ATTEMPTS) {
324
+ job.state = "pending";
325
+ job.nextAttemptAt = this.now() + Math.min(3e5, Math.max(retryAfter, 3e4 * 2 ** (job.attempts - 1)));
326
+ } else job.state = "failed";
327
+ });
328
+ }
329
+ }
330
+ allowed(job, settings) {
331
+ return settings.enabled && settings.events.includes(job.event) && (settings.projects === "all" || settings.projects.includes(job.project ?? "unassigned"));
332
+ }
333
+ config(id) {
334
+ const c = this.configs.find((value) => value.id === id);
335
+ if (!c) throw new Error("Notification destination not found");
336
+ return c;
337
+ }
338
+ destination(c) {
339
+ const value = this.environment[c.urlEnv];
340
+ if (!value || value.length > 4096 || c.secretEnv && !this.environment[c.secretEnv]) return void 0;
341
+ try {
342
+ const url = new URL(value);
343
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) return void 0;
344
+ return { url, hash: digest([c.protocol, url.href]) };
345
+ } catch {
346
+ return void 0;
347
+ }
348
+ }
349
+ payload(value) {
350
+ return this.redact({ version: 1, id: text(value.id, 100), event: value.event, runId: text(value.runId, 100), teamId: value.teamId ? text(value.teamId, 100) : void 0, cycleId: value.cycleId ? text(value.cycleId, 100) : void 0, project: value.project ? text(value.project, 100) : void 0, pattern: text(value.pattern, 100), status: text(value.status, 100), result: text(value.result, 100), title: text(this.redact(value.title), 200), ...value.test ? { test: true } : {} });
351
+ }
352
+ redact(value) {
353
+ const secrets = this.configs.flatMap((c) => [this.environment[c.urlEnv], c.secretEnv ? this.environment[c.secretEnv] : void 0]).filter((secret) => !!secret && secret.length >= 8);
354
+ const visit = (item) => typeof item === "string" ? redactText(item, secrets) : Array.isArray(item) ? item.map(visit) : item && typeof item === "object" ? Object.fromEntries(Object.entries(item).map(([key, entry]) => [key, visit(entry)])) : item;
355
+ return visit(redactExport(value));
356
+ }
357
+ job(c, destinationHash, payload) {
358
+ const at = this.now();
359
+ return { id: payload.id, target: c.id, event: payload.event, runId: payload.runId, teamId: payload.teamId, cycleId: payload.cycleId, project: payload.project, state: "pending", attempts: 0, totalAttempts: 0, retryCount: 0, createdAt: at, updatedAt: at, nextAttemptAt: at, destinationHash, payload };
360
+ }
361
+ room() {
362
+ if (this.state.deliveries.length < MAX_DELIVERIES) return true;
363
+ const index = this.state.deliveries.findIndex((job) => !["pending", "sending"].includes(job.state));
364
+ if (index < 0) return false;
365
+ this.state.deliveries.splice(index, 1);
366
+ return true;
367
+ }
368
+ mutate(change) {
369
+ if (!this.loaded) return Promise.reject(new Error("Notifications are unavailable until their configuration and saved state load successfully"));
370
+ const operation = this.queue.then(async () => {
371
+ const previous = structuredClone(this.state);
372
+ try {
373
+ const result = change();
374
+ await this.save();
375
+ return result;
376
+ } catch (error) {
377
+ this.state = previous;
378
+ throw error;
379
+ }
380
+ });
381
+ this.queue = operation.catch(() => void 0);
382
+ return operation;
383
+ }
384
+ async save() {
385
+ await mkdir(dirname(this.file), { recursive: true });
386
+ const temporary = `${this.file}.${randomBytes(8).toString("hex")}.tmp`;
387
+ try {
388
+ const handle = await open(temporary, "wx", 384);
389
+ try {
390
+ await handle.writeFile(JSON.stringify(this.state) + "\n");
391
+ await handle.sync();
392
+ } finally {
393
+ await handle.close();
394
+ }
395
+ await rename(temporary, this.file);
396
+ if (process.platform !== "win32") {
397
+ const directory = await open(dirname(this.file), "r");
398
+ try {
399
+ await directory.sync();
400
+ } finally {
401
+ await directory.close();
402
+ }
403
+ }
404
+ } finally {
405
+ await rm(temporary, { force: true });
406
+ }
407
+ }
408
+ }
409
+ export {
410
+ Notifications,
411
+ notificationEvents,
412
+ validateNotificationSettings
413
+ };
@@ -0,0 +1,230 @@
1
+ function replacementSettings(provider, role) {
2
+ const configured = provider.settingsByRole?.[role];
3
+ const model = configured?.model || provider.model || provider.catalog?.defaultModel || provider.models?.[0]?.id || "";
4
+ const effort = configured?.effort ?? (model === provider.model ? provider.effort : void 0) ?? "";
5
+ return { model, effort };
6
+ }
7
+ const obj = (properties) => ({ type: "object", properties, required: Object.keys(properties), additionalProperties: false });
8
+ const str = { type: "string" };
9
+ const identifier = { type: "string", pattern: "^[a-z][a-z0-9-]{0,39}$", description: "Stable lowercase identifier, e.g. api, add-validation, goal; never T1 or R1" };
10
+ const strings = { type: "array", items: str };
11
+ const array = (items) => ({ type: "array", items });
12
+ const member = obj({ id: identifier, name: str, role: { type: "string", enum: ["coordinator", "worker", "reviewer"] }, provider: { type: "string", description: "Exact provider ID from the supplied eligible inventory and selected pool" }, model: str, effort: str, reason: str });
13
+ const legacyTask = obj({ id: identifier, title: str, instructions: str, owner: { type: "string", description: "An active worker member ID (or coordinator for investigation). Never the reviewer: code-flow schedules independent review automatically." }, kind: { type: "string", enum: ["implementation", "investigation"] }, dependsOn: strings, requirements: strings, acceptance: strings, paths: strings });
14
+ const task = obj({ ...legacyTask.properties, dependencyTypes: array(obj({ taskId: identifier, kind: { type: "string", enum: ["completion", "artifact", "knowledge"] } })) });
15
+ const subplans = array(obj({ id: { type: "string", pattern: "^[a-z][a-z0-9-]{0,27}$" }, strategy: { type: "string", enum: ["investigate-implement", "implement-verify", "bounded-repair"] }, owner: str, requirements: strings, acceptance: strings, paths: strings, dependsOn: strings, instructions: str }));
16
+ const LEGACY_TEAM_PLAN_SCHEMA = obj({ requirements: array(obj({ id: identifier, description: str })), members: array(member), tasks: array(legacyTask), contracts: strings, rationale: str });
17
+ const TEAM_PLAN_SCHEMA = obj({ ...LEGACY_TEAM_PLAN_SCHEMA.properties, tasks: array(task), subplans });
18
+ const TEMPLATE_TEAM_PLAN_SCHEMA = obj({ ...TEAM_PLAN_SCHEMA.properties, templateUses: array(obj({
19
+ id: { type: "string", pattern: "^[a-z][a-z0-9-]{0,11}$" },
20
+ templateId: identifier,
21
+ version: { type: "integer", minimum: 1 },
22
+ inputs: array(obj({ name: str, value: str })),
23
+ assignments: array(obj({ role: str, memberId: identifier })),
24
+ dependsOn: strings,
25
+ requirements: strings,
26
+ instructions: str
27
+ })) });
28
+ const reply = obj({ requestId: str, body: str, evidence: strings });
29
+ const TASK_OUTCOME_SCHEMA = obj({
30
+ taskId: str,
31
+ memberId: str,
32
+ status: { type: "string", enum: ["submitted", "needs_info", "failed"], description: "submitted means the assigned artifact is ready for engine integration and verification; it does not claim final checks or review passed" },
33
+ checkpoint: str,
34
+ handover: str,
35
+ artifacts: strings,
36
+ unresolvedRequirements: strings,
37
+ questions: array(obj({ id: identifier, recipient: str, body: str, blocking: { type: "boolean" } })),
38
+ replies: array(reply),
39
+ findings: array(obj({ title: str, body: str, evidence: strings, paths: strings }))
40
+ });
41
+ function normalizeTaskOutcomeEvidence(outcome) {
42
+ return { ...outcome, findings: outcome.findings.map((finding) => {
43
+ const urls = finding.paths.filter((path) => /^https?:\/\//i.test(path));
44
+ return urls.length ? { ...finding, paths: finding.paths.filter((path) => !urls.includes(path)), evidence: [.../* @__PURE__ */ new Set([...finding.evidence, ...urls])] } : finding;
45
+ }) };
46
+ }
47
+ function taskOutcomeSchema(requirementIds) {
48
+ return obj({ ...TASK_OUTCOME_SCHEMA.properties, unresolvedRequirements: { type: "array", items: { type: "string", enum: requirementIds }, description: "Only known IDs of unfinished implementation requirements. Use [] on submission. Pending engine commits, checks, proof and independent review belong in handover text, not here." } });
49
+ }
50
+ const TEAM_ANSWERS_SCHEMA = obj({ replies: array(reply), unresolved: strings });
51
+ const LEGACY_TEAM_REVISION_SCHEMA = obj({
52
+ reason: str,
53
+ addTasks: array(legacyTask),
54
+ reassignments: array(obj({ taskId: str, memberId: str })),
55
+ retryTasks: strings,
56
+ contracts: strings,
57
+ answers: array(reply),
58
+ addMembers: array(member),
59
+ retireMembers: strings
60
+ });
61
+ const TEAM_REVISION_SCHEMA = obj({ ...LEGACY_TEAM_REVISION_SCHEMA.properties, addTasks: array(task), contractRequirements: strings });
62
+ const ID = /^[a-z][a-z0-9-]{0,39}$/;
63
+ const unique = (items) => new Set(items).size === items.length;
64
+ const nonempty = (value) => !!value.trim();
65
+ function pathAllowed(path) {
66
+ return path === "*" || !!path && path.length <= 500 && !/[\u0000-\u001f\u007f]/.test(path) && !path.startsWith("/") && !path.startsWith("-") && !path.includes("\\") && !path.split("/").some((p) => p === "." || p === ".." || p === ".git" || p === "") && !path.includes("*") && !/^[A-Za-z]:/.test(path);
67
+ }
68
+ function ownsPath(paths, file) {
69
+ return paths.some((p) => p === "*" || file === p || file.startsWith(`${p}/`));
70
+ }
71
+ function overlaps(a, b) {
72
+ return a.kind === "implementation" && b.kind === "implementation" && a.paths.some((p) => b.paths.some((q) => p === "*" || q === "*" || p === q || p.startsWith(`${q}/`) || q.startsWith(`${p}/`)));
73
+ }
74
+ function planProblem(plan, policy, retainedMembers = /* @__PURE__ */ new Set()) {
75
+ if (!plan.requirements.length || !plan.tasks.length) return "include requirements and at least one task";
76
+ if (plan.requirements.length > 48 || plan.contracts.length > 24 || plan.rationale.length > 4e3) return "requirements, contracts, and rationale must fit the bounded plan";
77
+ const activeMembers = plan.members.filter((m) => m.status !== "retired");
78
+ if (activeMembers.length > policy.maxMembers || activeMembers.length < 2) return `team size must be between 2 and ${policy.maxMembers}`;
79
+ if (plan.tasks.length > policy.maxTasks) return `the plan exceeds ${policy.maxTasks} tasks`;
80
+ for (const [kind, values] of [["member", plan.members], ["task", plan.tasks], ["requirement", plan.requirements]]) {
81
+ if (!unique(values.map((v) => v.id)) || values.some((v) => !ID.test(v.id))) return `${kind} IDs must be unique short lowercase identifiers`;
82
+ }
83
+ if (plan.requirements.some((r) => !nonempty(r.description) || r.description.length > 4e3)) return "requirements need bounded descriptions";
84
+ if (plan.contracts.some((c) => !nonempty(c) || c.length > 8e3)) return "contracts cannot be empty or exceed 8000 characters";
85
+ const coordinator = activeMembers.filter((m) => m.role === "coordinator");
86
+ const reviewers = activeMembers.filter((m) => m.role === "reviewer");
87
+ if (coordinator.length !== 1 || reviewers.length !== 1) return "include exactly one coordinator and one independent reviewer";
88
+ if (coordinator[0].id !== "coordinator") return "the coordinator member ID must be coordinator (preserving its planning session identity)";
89
+ if (coordinator[0].provider !== policy.coordinator) return "the coordinator must preserve the user's selected provider";
90
+ const available = policy.inventory.providers.filter((p) => p.available && !p.blockedReason && (policy.pool.includes(p.id) || p.id === policy.coordinator));
91
+ for (const m of activeMembers) {
92
+ if (m.id === "user") return "user is reserved for authenticated user replies";
93
+ if (m.role !== "coordinator" && !policy.pool.includes(m.provider)) return `${m.id} chose a provider outside the user's worker/reviewer pool`;
94
+ const provider = available.find((p) => p.id === m.provider) ?? (retainedMembers.has(m.id) ? policy.inventory.providers.find((p) => p.id === m.provider) : void 0);
95
+ if (!provider) return `${m.id} chose unavailable, exhausted, or unauthorized provider ${m.provider}`;
96
+ if (!nonempty(m.name) || m.name.length > 120 || !nonempty(m.reason) || m.reason.length > 2e3) return "every member needs a bounded name and composition reason";
97
+ const settings = provider.settingsByRole?.[m.role] ?? provider;
98
+ const model = provider.models?.find((model2) => model2.id === (m.model || provider.catalog?.defaultModel) || model2.aliases?.includes(m.model));
99
+ if (provider.catalog && !model) return `${m.id} chose a model whose availability in this environment is unknown`;
100
+ if (provider.catalog && m.effort && !model?.efforts?.includes(m.effort)) return `${m.id} chose an unsupported effort for this exact model`;
101
+ if (m.role === "coordinator" && (m.model !== (settings.model ?? "") || m.effort !== (settings.effort ?? ""))) return "The coordinator model settings and effort must remain unchanged";
102
+ const variants = m.role !== "coordinator" && "variants" in settings ? settings.variants : void 0;
103
+ const choices = variants?.length ? [settings, ...variants] : [settings];
104
+ const matching = choices.filter((choice) => variants?.length ? m.model === (choice.model ?? "") : choice.model ? m.model === choice.model : !m.model || !!model);
105
+ if (!matching.length) return `${m.id} must preserve supplied model settings or choose a supplied supported model (empty means default)`;
106
+ const efforts = model?.efforts ?? provider.efforts ?? [];
107
+ if (!matching.some((choice) => choice.effort ? m.effort === choice.effort : !m.effort || efforts.includes(m.effort))) return `${m.id} must preserve supplied effort settings or choose a supplied supported effort (empty means default)`;
108
+ }
109
+ for (const t of plan.tasks) {
110
+ const owner = plan.members.find((m) => m.id === t.owner);
111
+ if (!owner || owner.role === "reviewer" || owner.status === "retired" && !["submitted", "completed"].includes(t.status ?? "")) return `${t.id} must be assigned to an active non-reviewer member. The runtime supplies final independent review and checks; remove redundant final-review tasks instead of reassigning them.`;
112
+ if (t.kind === "implementation" && owner.role !== "worker") return `${t.id}: implementation belongs to a worker, keeping coordinator and reviewer read-only`;
113
+ if (!nonempty(t.title) || t.title.length > 180 || !nonempty(t.instructions) || t.instructions.length > 16e3 || !t.acceptance.length || t.acceptance.length > 16 || t.acceptance.some((a) => !nonempty(a) || a.length > 2e3)) return `${t.id} needs bounded instructions and acceptance criteria`;
114
+ if (!t.requirements.length || t.requirements.some((id) => !plan.requirements.some((r) => r.id === id))) return `${t.id} must cover existing requirements`;
115
+ if (t.dependencyTypes && (Object.keys(t.dependencyTypes).some((id) => !t.dependsOn.includes(id)) || Object.values(t.dependencyTypes).some((kind) => !["completion", "artifact", "knowledge"].includes(kind)))) return `${t.id} has invalid typed dependencies`;
116
+ if (!unique(t.dependsOn) || t.dependsOn.some((id) => id === t.id || !plan.tasks.some((other) => other.id === id))) return `${t.id} has an invalid dependency`;
117
+ if (t.paths.some((p) => !pathAllowed(p)) || t.kind === "implementation" && !t.paths.length) return `${t.id} needs safe owned repository paths (directories, files, or *)`;
118
+ }
119
+ if (plan.requirements.some((r) => !plan.tasks.some((t) => t.requirements.includes(r.id)))) return "every requirement must have a task; requirements cannot silently disappear";
120
+ const visited = /* @__PURE__ */ new Set();
121
+ const visiting = /* @__PURE__ */ new Set();
122
+ const cycle = (id) => {
123
+ if (visiting.has(id)) return true;
124
+ if (visited.has(id)) return false;
125
+ visiting.add(id);
126
+ if (plan.tasks.find((t) => t.id === id).dependsOn.some(cycle)) return true;
127
+ visiting.delete(id);
128
+ visited.add(id);
129
+ return false;
130
+ };
131
+ if (plan.tasks.some((t) => cycle(t.id))) return "task dependencies must be acyclic";
132
+ const author = plan.members.find((m) => m.id === plan.tasks.find((t) => t.kind === "implementation")?.owner) ?? coordinator[0];
133
+ if (reviewers[0].provider === author.provider && (policy.differentProviderReview || available.some((p) => policy.pool.includes(p.id) && p.id !== author.provider))) return "use a reviewer from another eligible provider (or disable strict provider independence when only one provider is available)";
134
+ return void 0;
135
+ }
136
+ function readyTasks(tasks, messages = [], eligible = () => true, priorities = {}) {
137
+ const priority = (id) => Object.hasOwn(priorities, id) && Number.isFinite(priorities[id]) ? priorities[id] : 0;
138
+ const selected = [];
139
+ for (const t of [...tasks].sort((a, b) => priority(b.id) - priority(a.id) || a.id.localeCompare(b.id))) {
140
+ if (t.status !== "pending" || !eligible(t)) continue;
141
+ if (tasks.some((active) => active.status === "running" && (active.owner === t.owner || overlaps(t, active)))) continue;
142
+ if (!t.dependsOn.every((id) => {
143
+ const dependency = tasks.find((d) => d.id === id);
144
+ if (!dependency) return false;
145
+ const kind = t.dependencyTypes?.[id] ?? "completion";
146
+ const finished = ["submitted", "completed"].includes(dependency.status ?? "") && !dependency.evidenceStale;
147
+ if (kind === "artifact") return finished && !!dependency.head && !!dependency.artifacts?.length;
148
+ if (kind === "knowledge" && dependency.evidenceStale) return false;
149
+ if (kind === "knowledge") return finished && !!dependency.artifacts?.length || messages.some((m) => m.kind === "reply" && m.sender === dependency.owner && m.recipient === t.owner && m.taskId === t.id && m.evidence.length > 0 && m.status !== "open");
150
+ return finished;
151
+ })) continue;
152
+ if (selected.some((other) => other.owner === t.owner || overlaps(t, other))) continue;
153
+ selected.push(t);
154
+ }
155
+ return selected;
156
+ }
157
+ function outcomeProblem(outcome, task2, member2, state) {
158
+ if (outcome.taskId !== task2.id || outcome.memberId !== member2.id) return "taskId and memberId must match this assignment";
159
+ if (!nonempty(outcome.checkpoint) || outcome.checkpoint.length > 12e3) return "include an observable checkpoint of at most 12000 characters";
160
+ if (!nonempty(outcome.handover) || outcome.handover.length > 8e3) return "publish a shareable handover of at most 8000 characters, separate from your private checkpoint";
161
+ if (outcome.questions.length > 8 || outcome.replies.length > 16 || outcome.findings.length > 6 || outcome.artifacts.length > 24) return "outcome exceeds the bounded message, artifact, or finding limit";
162
+ if (outcome.findings.some((f) => !f.title.trim() || f.title.length > 200 || !f.body.trim() || f.body.length > 8e3 || !f.evidence.some(nonempty) || f.paths.some((p) => !pathAllowed(p) || p === "*"))) return "findings need bounded text, evidence, and safe repository paths";
163
+ if (outcome.unresolvedRequirements.some((id) => !state.requirements.some((r) => r.id === id))) return "unresolved requirements must use known IDs";
164
+ if (outcome.status === "submitted" && outcome.unresolvedRequirements.length) return "unresolved requirements cannot be submitted as completed work";
165
+ if (!unique(outcome.questions.map((q) => q.id)) || outcome.questions.some((q) => !ID.test(q.id) || !nonempty(q.body) || q.body.length > 8e3)) return "questions need unique local IDs and bounded nonempty bodies";
166
+ if (outcome.questions.some((q) => q.recipient !== "user" && (!state.members.some((m) => m.id === q.recipient) || q.recipient === member2.id))) return "questions must address another known member or user";
167
+ if (outcome.replies.some((r) => !validReply(r, member2.id, state))) return "replies must answer questions addressed to this member, with evidence";
168
+ if (outcome.status === "needs_info" && !outcome.questions.some((q) => q.blocking) && !state.messages.some((q) => q.kind === "question" && q.taskId === task2.id && q.blocking && (q.status === "open" || q.sourceAttempt === task2.attempts))) return "needs_info requires a blocking question";
169
+ if (outcome.status === "submitted" && (outcome.questions.some((q) => q.blocking) || state.messages.some((q) => q.kind === "question" && q.taskId === task2.id && q.blocking && q.status === "open"))) return "a blocking question cannot accompany submitted work";
170
+ if (outcome.status === "submitted" && task2.kind === "investigation" && !outcome.artifacts.some(nonempty)) return "investigations need evidence-bearing artifacts";
171
+ return void 0;
172
+ }
173
+ function validReply(reply2, sender, state, coordinator = false) {
174
+ const question = state.messages.find((m) => m.id === reply2.requestId && m.kind === "question" && (m.status === "open" || state.messages.some((r) => r.kind === "reply" && r.requestId === m.id && r.sender === sender && r.body === reply2.body)));
175
+ return !!question && question.recipient !== "user" && (coordinator || question.recipient === sender) && !!reply2.body.trim() && reply2.body.length <= 8e3 && reply2.evidence.length <= 16 && reply2.evidence.some(nonempty);
176
+ }
177
+ function revisionProblem(revision, state, policy) {
178
+ const existing = new Set(state.tasks.map((t) => t.id));
179
+ if (revision.addTasks.some((t) => existing.has(t.id))) return "new tasks must have new IDs";
180
+ if (!unique(revision.reassignments.map((r) => r.taskId)) || !unique(revision.retryTasks)) return "each task may be revised only once per patch";
181
+ if (!unique(revision.retireMembers)) return "retire each member only once";
182
+ if (revision.addMembers.some((m) => m.role !== "worker" || state.members.some((old) => old.id === m.id))) return "new members need fresh IDs and must be workers; coordinator and reviewer are pinned";
183
+ if (state.members.length + revision.addMembers.length > policy.maxTasks + 2) return "member history exceeds the bounded replacement limit";
184
+ for (const id of revision.retireMembers) {
185
+ const member2 = state.members.find((m) => m.id === id);
186
+ if (!member2 || member2.role !== "worker" || member2.status === "working" || member2.status === "retired") return "only idle worker members can be retired";
187
+ if (state.tasks.some((t) => t.owner === id && !["submitted", "completed"].includes(t.status ?? "") && !revision.reassignments.some((r) => r.taskId === t.id && r.memberId !== id))) return "retiring a member requires handing off its unfinished assignments";
188
+ if (state.scheduler !== "event" && state.messages.some((m) => m.kind === "question" && m.status === "open" && m.recipient === id)) return "answer outstanding requests before retiring their recipient";
189
+ }
190
+ for (const r of revision.reassignments) {
191
+ const target = state.tasks.find((t) => t.id === r.taskId);
192
+ if (!target || ["submitted", "completed", "running"].includes(target.status ?? "")) return "only unfinished idle tasks can be reassigned";
193
+ }
194
+ for (const id of revision.retryTasks) if (!state.tasks.some((t) => t.id === id && t.status === "failed" && (t.attempts ?? 0) - (t.allocationStartAttempt ?? 0) <= state.maxRepairs)) return "only failed tasks within the repair limit can be retried";
195
+ if (revision.answers.some((r) => !validReply(r, "", state, true))) return "coordinator answers need an open non-user request and evidence";
196
+ if (revision.contractRequirements?.some((id) => !state.requirements.some((r) => r.id === id))) return "contract changes may only invalidate known requirements";
197
+ if (revision.contracts.some((c) => !nonempty(c))) return "contracts cannot be empty";
198
+ const plan = { ...state, contracts: [...state.contracts, ...revision.contracts.filter((contract) => !state.contracts.includes(contract))], members: state.members.map((m) => revision.retireMembers.includes(m.id) ? { ...m, status: "retired" } : m).concat(revision.addMembers), tasks: state.tasks.map((t) => ({ ...t, owner: revision.reassignments.find((r) => r.taskId === t.id)?.memberId ?? t.owner })).concat(revision.addTasks) };
199
+ const admittedOwners = /* @__PURE__ */ new Set([
200
+ ...revision.addTasks.map((t) => t.owner),
201
+ ...revision.reassignments.map((r) => r.memberId),
202
+ ...revision.retryTasks.map((id) => plan.tasks.find((t) => t.id === id).owner)
203
+ ]);
204
+ for (const id of admittedOwners) {
205
+ const member2 = plan.members.find((m) => m.id === id);
206
+ const provider = member2 && policy.inventory.providers.find((p) => p.id === member2.provider);
207
+ if (!provider?.available || provider.blockedReason) return `${id} cannot receive new or retried work while its provider is unavailable`;
208
+ }
209
+ return planProblem(plan, policy, new Set(state.members.map((m) => m.id)));
210
+ }
211
+ export {
212
+ LEGACY_TEAM_PLAN_SCHEMA,
213
+ LEGACY_TEAM_REVISION_SCHEMA,
214
+ TASK_OUTCOME_SCHEMA,
215
+ TEAM_ANSWERS_SCHEMA,
216
+ TEAM_PLAN_SCHEMA,
217
+ TEAM_REVISION_SCHEMA,
218
+ TEMPLATE_TEAM_PLAN_SCHEMA,
219
+ normalizeTaskOutcomeEvidence,
220
+ outcomeProblem,
221
+ overlaps,
222
+ ownsPath,
223
+ pathAllowed,
224
+ planProblem,
225
+ readyTasks,
226
+ replacementSettings,
227
+ revisionProblem,
228
+ taskOutcomeSchema,
229
+ validReply
230
+ };