catherd-cli 1.5.0 → 2.0.0-beta.20261022113309

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 (275) hide show
  1. package/README.md +164 -208
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -247
  5. package/package.json +6 -8
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/bin/catherd-mcp +1 -1
  9. package/plugin/commands/catherd.md +2 -2
  10. package/plugin/skills/catherd/SKILL.md +97 -421
  11. package/plugin/skills/catherd-setup/SKILL.md +20 -19
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +1 -2
  14. package/src/adapters/cursor/home.ts +1 -1
  15. package/src/adapters/cursor/index.ts +4 -4
  16. package/src/cli.ts +22 -27
  17. package/src/domain/bars.ts +33 -107
  18. package/src/domain/blocked.ts +72 -0
  19. package/src/domain/branch.ts +51 -0
  20. package/src/domain/brief.ts +21 -6
  21. package/src/domain/catalog.ts +69 -324
  22. package/src/domain/cost.ts +15 -30
  23. package/src/domain/errors.ts +2 -2
  24. package/src/domain/fail-lines.ts +26 -0
  25. package/src/domain/failover.ts +37 -87
  26. package/src/domain/findings.ts +47 -0
  27. package/src/domain/gate-brief.ts +1 -0
  28. package/src/domain/ids.ts +13 -2
  29. package/src/domain/jev.ts +47 -2
  30. package/src/domain/judge.ts +124 -0
  31. package/src/domain/lane.ts +22 -3
  32. package/src/domain/milestone-memory.ts +53 -0
  33. package/src/domain/notice.ts +25 -8
  34. package/src/domain/notify.ts +32 -0
  35. package/src/domain/plan-doc.ts +198 -0
  36. package/src/domain/plan-lint.ts +141 -0
  37. package/src/domain/profile-rules.ts +101 -160
  38. package/src/domain/profile.ts +116 -63
  39. package/src/domain/record.ts +53 -2
  40. package/src/domain/role-prompts.ts +131 -45
  41. package/src/domain/role-scope.ts +4 -34
  42. package/src/domain/role-tools.ts +2 -2
  43. package/src/domain/roles.ts +2 -0
  44. package/src/domain/route.ts +17 -0
  45. package/src/domain/rules.ts +33 -0
  46. package/src/domain/run-events.ts +298 -0
  47. package/src/domain/run-machine.ts +668 -0
  48. package/src/domain/run-state.ts +1310 -0
  49. package/src/domain/scorecard.ts +197 -0
  50. package/src/domain/select.ts +151 -146
  51. package/src/domain/slopalytics.ts +72 -0
  52. package/src/entry/answer-command.ts +39 -0
  53. package/src/entry/catalog-command.ts +25 -174
  54. package/src/entry/check-bin.ts +17 -0
  55. package/src/entry/cli-kit.ts +1 -1
  56. package/src/entry/daemon-bin.ts +22 -0
  57. package/src/entry/glyphs.ts +15 -2
  58. package/src/entry/init-command.ts +18 -68
  59. package/src/entry/knowledge-command.ts +45 -4
  60. package/src/entry/lock-command.ts +6 -7
  61. package/src/entry/mcp/role-server.ts +18 -0
  62. package/src/entry/mcp/server.ts +13 -40
  63. package/src/entry/mcp/session-tools.ts +149 -0
  64. package/src/entry/panel/openapi.ts +189 -0
  65. package/src/entry/panel/server.ts +410 -0
  66. package/src/entry/panel-command.ts +68 -0
  67. package/src/entry/profile-command.ts +10 -14
  68. package/src/entry/role-cli-command.ts +47 -3
  69. package/src/entry/runs-command.ts +3 -19
  70. package/src/entry/scorecard-command.ts +187 -0
  71. package/src/entry/supervise-bin.ts +1 -1
  72. package/src/infra/env.ts +0 -2
  73. package/src/infra/git.ts +64 -0
  74. package/src/infra/heavy-lock.ts +45 -7
  75. package/src/infra/launch.ts +63 -7
  76. package/src/infra/memory.ts +45 -0
  77. package/src/infra/ntfy.ts +40 -0
  78. package/src/infra/open-url.ts +42 -0
  79. package/src/infra/paths.ts +2 -2
  80. package/src/infra/slopalytics.ts +30 -0
  81. package/src/infra/store.ts +6 -1
  82. package/src/services/admission.ts +24 -9
  83. package/src/services/backends.ts +4 -6
  84. package/src/services/capture.ts +1 -22
  85. package/src/services/catalog-service.ts +155 -262
  86. package/src/services/credentials.ts +4 -20
  87. package/src/services/daemon.ts +181 -0
  88. package/src/services/dispatch-service.ts +5 -1
  89. package/src/services/doctor-backends.ts +1 -1
  90. package/src/services/doctor-checks.ts +47 -0
  91. package/src/services/doctor.ts +4 -9
  92. package/src/services/failover-rungs.ts +109 -0
  93. package/src/services/finalize.ts +12 -0
  94. package/src/services/gate-env.ts +1 -2
  95. package/src/services/gate-service.ts +21 -6
  96. package/src/services/jev-service.ts +2 -4
  97. package/src/services/lane-service.ts +34 -3
  98. package/src/services/milestone-check.ts +433 -0
  99. package/src/services/milestone-tree.ts +170 -0
  100. package/src/services/milestones.ts +35 -8
  101. package/src/services/notifier.ts +69 -2
  102. package/src/services/notify-service.ts +26 -0
  103. package/src/services/panel-setup.ts +116 -0
  104. package/src/services/plan-input.ts +151 -0
  105. package/src/services/ports.ts +30 -4
  106. package/src/services/preflight.ts +82 -8
  107. package/src/services/profile-service.ts +30 -3
  108. package/src/services/profile-store.ts +8 -3
  109. package/src/services/protocol.ts +4 -0
  110. package/src/services/provenance.ts +31 -90
  111. package/src/services/questions.ts +9 -0
  112. package/src/services/reconcile.ts +11 -0
  113. package/src/services/routing-service.ts +26 -15
  114. package/src/services/run-briefs.ts +477 -0
  115. package/src/services/run-client.ts +217 -0
  116. package/src/services/run-effects.ts +837 -0
  117. package/src/services/run-events-store.ts +174 -0
  118. package/src/services/run-evidence.ts +1 -1
  119. package/src/services/run-read.ts +221 -0
  120. package/src/services/run-report.ts +176 -0
  121. package/src/services/run-service.ts +55 -6
  122. package/src/services/run-store.ts +54 -7
  123. package/src/services/scorecard-metrics.ts +86 -0
  124. package/src/services/scorecard.ts +382 -0
  125. package/src/services/summary.ts +27 -3
  126. package/src/services/tree-path.ts +27 -0
  127. package/src/services/workspace-admission.ts +5 -13
  128. package/src/services/workspace-service.ts +5 -0
  129. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  130. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  131. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  132. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  133. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  134. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  135. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  136. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  137. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  138. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  139. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  140. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  141. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  142. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  143. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  144. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  145. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  146. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  147. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  148. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  149. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  150. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  151. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  152. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  153. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  154. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  155. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  156. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  157. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  158. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  159. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  160. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  161. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  162. package/web/dist/assets/index-C5IbylvM.css +2 -0
  163. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  164. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  165. package/web/dist/assets/label-DFSYBc62.js +1 -0
  166. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  167. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  168. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  169. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  170. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  171. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  172. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  173. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  174. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  175. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  176. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  177. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  178. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  179. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  180. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  181. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  182. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  183. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  184. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  185. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  186. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  187. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  188. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  189. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  190. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  191. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  192. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  193. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  194. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  195. package/web/dist/assets/time--9kSQbbp.js +1 -0
  196. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  197. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  198. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  199. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  200. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  201. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  202. package/web/dist/index.html +15 -0
  203. package/catalog/ATTRIBUTION.md +0 -44
  204. package/catalog/scores.json +0 -3202
  205. package/catalog/sources.json +0 -97
  206. package/src/adapters/antigravity/events.ts +0 -131
  207. package/src/adapters/antigravity/home.ts +0 -46
  208. package/src/adapters/antigravity/index.ts +0 -335
  209. package/src/adapters/antigravity/models.ts +0 -37
  210. package/src/adapters/grok/events.ts +0 -112
  211. package/src/adapters/grok/home.ts +0 -187
  212. package/src/adapters/grok/index.ts +0 -358
  213. package/src/adapters/grok/models.ts +0 -48
  214. package/src/domain/calibration.ts +0 -132
  215. package/src/domain/sources.ts +0 -144
  216. package/src/entry/mcp/dispatch-tools.ts +0 -52
  217. package/src/entry/mcp/lane-tools.ts +0 -131
  218. package/src/entry/mcp/protocol-tools.ts +0 -63
  219. package/src/entry/mcp/push-tools.ts +0 -16
  220. package/src/entry/mcp/run-tools.ts +0 -164
  221. package/src/entry/mcp/setup-tools.ts +0 -109
  222. package/src/entry/mcp/workspace-tools.ts +0 -142
  223. package/src/entry/tui/commands.ts +0 -563
  224. package/src/entry/tui/effects.ts +0 -385
  225. package/src/entry/tui/fixtures.ts +0 -637
  226. package/src/entry/tui/profile-edits.ts +0 -146
  227. package/src/entry/tui/profile-tree.ts +0 -429
  228. package/src/entry/tui/providers/app.tsx +0 -162
  229. package/src/entry/tui/providers/boundary.tsx +0 -57
  230. package/src/entry/tui/providers/data.tsx +0 -160
  231. package/src/entry/tui/providers/keymap.tsx +0 -166
  232. package/src/entry/tui/providers/root.tsx +0 -45
  233. package/src/entry/tui/providers/theme.tsx +0 -17
  234. package/src/entry/tui/providers/toast.tsx +0 -77
  235. package/src/entry/tui/run.tsx +0 -114
  236. package/src/entry/tui/state.ts +0 -400
  237. package/src/entry/tui/stories.ts +0 -114
  238. package/src/entry/tui/text.ts +0 -130
  239. package/src/entry/tui/theme.ts +0 -140
  240. package/src/entry/tui/views/app.tsx +0 -208
  241. package/src/entry/tui/views/dialogs.tsx +0 -41
  242. package/src/entry/tui/views/profile-actions.ts +0 -331
  243. package/src/entry/tui/views/profiles.tsx +0 -520
  244. package/src/entry/tui/views/runs.tsx +0 -619
  245. package/src/entry/tui/views/save-dialog.tsx +0 -344
  246. package/src/entry/tui/views/status.tsx +0 -213
  247. package/src/entry/tui/widgets/chrome.tsx +0 -114
  248. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  249. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  250. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  251. package/src/entry/tui/widgets/dialog.tsx +0 -68
  252. package/src/entry/tui/widgets/line.tsx +0 -92
  253. package/src/entry/tui/widgets/list.tsx +0 -197
  254. package/src/entry/tui/widgets/spinner.tsx +0 -49
  255. package/src/entry/tui/widgets/toast.tsx +0 -62
  256. package/src/infra/sources/arena.ts +0 -95
  257. package/src/infra/sources/artificial-analysis.ts +0 -119
  258. package/src/infra/sources/cache.ts +0 -126
  259. package/src/infra/sources/csv.ts +0 -38
  260. package/src/infra/sources/epoch.ts +0 -82
  261. package/src/infra/sources/http.ts +0 -75
  262. package/src/infra/sources/litellm.ts +0 -58
  263. package/src/infra/sources/models-dev.ts +0 -59
  264. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  265. package/src/infra/sources/openrouter-models.ts +0 -60
  266. package/src/infra/sources/rows.ts +0 -38
  267. package/src/infra/sources/vectara.ts +0 -69
  268. package/src/infra/sources/zip.ts +0 -48
  269. package/src/services/catalog-refresh.ts +0 -201
  270. package/src/services/doctor-sources.ts +0 -76
  271. package/src/services/runs-page.ts +0 -290
  272. package/src/services/source-derive.ts +0 -397
  273. package/src/services/source-sync.ts +0 -312
  274. package/src/services/standins.ts +0 -238
  275. package/src/services/treat-likes.ts +0 -113
@@ -0,0 +1,217 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CatherdError } from "../domain/errors.ts";
4
+ import type { ProfilePatch } from "../domain/profile.ts";
5
+ import { isApproval } from "../domain/plan-doc.ts";
6
+ import { ROLES } from "../domain/roles.ts";
7
+ import { RUN_UNIT } from "../domain/run-events.ts";
8
+ import { liveDispatches } from "../domain/run-state.ts";
9
+ import { fetchBase, git, gitToplevel } from "../infra/git.ts";
10
+ import { writeTextAtomic } from "../infra/store.ts";
11
+ import { ensureDaemon } from "./daemon.ts";
12
+ import type { Deps } from "./ports.ts";
13
+ import { copyPlanInputs, planInputPaths, resolvePlanInputs } from "./plan-input.ts";
14
+ import { answer, openQuestions } from "./questions.ts";
15
+ import { appendEvent, isDaemonRun, readEvents, replayRun, withEventsLock } from "./run-events-store.ts";
16
+ import { runProfile } from "./run-pin.ts";
17
+ import { findRun, type Run } from "./run-store.ts";
18
+ import { result, startRun } from "./run-service.ts";
19
+
20
+ /** What the session's tools, the panel and the CLI share: they only append events and read the run's state. */
21
+
22
+ const readText = (file: string): string => {
23
+ try {
24
+ return readFileSync(file, "utf8");
25
+ } catch {
26
+ return "";
27
+ }
28
+ };
29
+
30
+ /** A 1.5 run has no daemon: this client neither answers nor cancels it. */
31
+ function daemonRun(id: string): Run {
32
+ const run = findRun(id);
33
+ if (!isDaemonRun(run))
34
+ throw new CatherdError("E_RUN_NOT_LIVE", `run ${id} is a 1.5 run: catherd 2.0 does not drive it`, {
35
+ fix: "finish it with catherd 1.x",
36
+ });
37
+ return run;
38
+ }
39
+
40
+ /** Fetches `origin/<base>` and refuses a base origin does not have (E_INPUT_INVALID); works from the last fetch offline. */
41
+ async function assertBase(repo: string, base: string): Promise<void> {
42
+ await fetchBase(repo, base);
43
+ const r = await git(repo, ["rev-parse", "--verify", "--quiet", `origin/${base}^{commit}`]);
44
+ if (r.kind !== "ok")
45
+ throw new CatherdError("E_INPUT_INVALID", `origin/${base} does not exist in ${repo}`, {
46
+ fix: "pass a branch origin has, or omit base",
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Starts a 2.0 run: its folder, its pin, `run.started`, and its daemon. The owner's plan files (`plan`, and any
52
+ * `plan:` A-line) are checked before the run exists, copied into it, and approved as input.
53
+ */
54
+ export async function runStart(
55
+ deps: Deps,
56
+ i: { repo: string; title: string; aLines: string[]; plan?: string[]; base?: string },
57
+ ): Promise<{
58
+ run: string;
59
+ dir: string;
60
+ daemon: "revived" | "alive";
61
+ notifyUrl: string;
62
+ plan: string[];
63
+ next: string;
64
+ }> {
65
+ const top = await gitToplevel(i.repo);
66
+ // a repo outside git is startRun's refusal
67
+ const plan = top ? resolvePlanInputs(top, [...(i.plan ?? []), ...planInputPaths(i.aLines)]) : [];
68
+ // a base run's branch must exist on origin before any run folder does
69
+ if (i.base && top) await assertBase(top, i.base);
70
+ const started = await startRun(deps, { repo: i.repo, title: i.title, aLines: i.aLines, base: i.base });
71
+ const run = findRun(started.run);
72
+ copyPlanInputs(run, plan);
73
+ await appendEvent(
74
+ run,
75
+ {
76
+ type: "run.started",
77
+ repo: run.meta.repo,
78
+ title: i.title,
79
+ aLines: i.aLines,
80
+ plan,
81
+ base: i.base ?? null,
82
+ engine: "2.0",
83
+ rolesOff: ROLES.filter((r) => !runProfile(deps, run).roles[r]?.enabled),
84
+ },
85
+ new Date(deps.now()),
86
+ );
87
+ if (plan.length) await appendEvent(run, { type: "plan.approved", by: "input" }, new Date(deps.now()));
88
+ const notifyUrl = deps.profiles.notifyUrl();
89
+ const daemon = ensureDaemon(run) === "alive" ? "alive" : "revived";
90
+ return {
91
+ run: run.id,
92
+ dir: run.dir,
93
+ daemon,
94
+ notifyUrl,
95
+ plan,
96
+ next: plan.length
97
+ ? `the run implements ${plan.length} plan file${plan.length === 1 ? "" : "s"} as written: status(run) shows it; parked questions reach ${notifyUrl} and this session`
98
+ : `the architect writes a plan first, and the run waits for your approval (answer approve): status(run) shows it; parked questions reach ${notifyUrl} and this session`,
99
+ };
100
+ }
101
+
102
+ /** Answers a parked unit: the owner's words reach the daemon as `question.answered`. */
103
+ export async function answerUnit(
104
+ deps: Deps,
105
+ i: { run: string; unit: string; answer: string },
106
+ ): Promise<{ unit: string; parked: string[] }> {
107
+ const run = daemonRun(i.run);
108
+ // the check and the append are one step: two answers to one question never both get through
109
+ await withEventsLock(run, async (add) => {
110
+ const state = replayRun(run);
111
+ const open = Object.keys(state.questions);
112
+ if (!open.includes(i.unit))
113
+ throw new CatherdError("E_INPUT_INVALID", `${i.unit} has no open question`, {
114
+ fix: open.length
115
+ ? `the open ones: ${open.join(", ")}`
116
+ : "status(run) shows the run's state: nothing waits for an answer now",
117
+ });
118
+ // the owner answers when they have fixed the plan: the roles read the copies, so copy again
119
+ if (state.plan.length) copyPlanInputs(run, state.plan);
120
+ // a bare approving word approves the plan (the first approval or a re-approval): reduce closes the question
121
+ if (i.unit === RUN_UNIT && state.questions[RUN_UNIT]?.reason === "approval" && isApproval(i.answer)) {
122
+ writeTextAtomic(join(run.dir, "plan-approved.md"), readText(join(run.dir, "plan.md")));
123
+ add({ type: "plan.approved", by: "owner" }, new Date(deps.now()));
124
+ return;
125
+ }
126
+ // a milestone's park is in questions.jsonl too, which land's gate reads
127
+ if (openQuestions(run).some((q) => q.milestone === i.unit))
128
+ await answer(deps, { run: run.id, milestone: i.unit, answer: i.answer });
129
+ add({ type: "question.answered", unit: i.unit, answer: i.answer, by: "owner" }, new Date(deps.now()));
130
+ });
131
+ ensureDaemon(run);
132
+ return { unit: i.unit, parked: Object.keys(replayRun(run).questions) };
133
+ }
134
+
135
+ /** Cancels the run; asking twice appends nothing. The daemon stops every live role, then ends the run. */
136
+ export async function cancelRun(
137
+ deps: Deps,
138
+ i: { run: string },
139
+ ): Promise<{ cancelling: true; live: string[] }> {
140
+ const run = daemonRun(i.run);
141
+ const s = replayRun(run);
142
+ const live = liveDispatches(s).map((d) => d.name);
143
+ if (!s.cancelling && s.phase !== "ended") {
144
+ await appendEvent(run, { type: "cancel.requested", by: "owner" }, new Date(deps.now()));
145
+ ensureDaemon(run);
146
+ }
147
+ return { cancelling: true, live };
148
+ }
149
+
150
+ export interface RunResult {
151
+ phase: ReturnType<typeof replayRun>["phase"];
152
+ outcome: ReturnType<typeof replayRun>["outcome"];
153
+ reportPath: string | null;
154
+ report: string | null;
155
+ milestones: {
156
+ id: string;
157
+ status: string;
158
+ commit: string | null;
159
+ digestPath: string | null;
160
+ knownIssues: string[];
161
+ }[];
162
+ /** with a name: the role's record, as 1.5's `result(run, name)` gives it */
163
+ role?: unknown;
164
+ }
165
+
166
+ /** The run's outcome and report; with a role's name, that role's record too. */
167
+ export async function runResult(deps: Deps, i: { run: string; name?: string }): Promise<RunResult> {
168
+ const run = daemonRun(i.run);
169
+ const s = replayRun(run);
170
+ const digests = new Map(
171
+ readEvents(run).flatMap((e) =>
172
+ e.type === "milestone.landed" ? [[e.milestone, e.digestPath] as const] : [],
173
+ ),
174
+ );
175
+ const file = join(run.dir, "report.md");
176
+ const has = existsSync(file);
177
+ return {
178
+ phase: s.phase,
179
+ outcome: s.outcome,
180
+ reportPath: has ? file : null,
181
+ report: has ? readFileSync(file, "utf8") : null,
182
+ milestones: s.milestones.map((m) => ({
183
+ id: m.id,
184
+ status: m.status,
185
+ commit: m.commit,
186
+ digestPath: digests.get(m.id) ?? null,
187
+ knownIssues: m.knownIssues,
188
+ })),
189
+ ...(i.name === undefined ? {} : { role: await result(deps, { run: run.id, name: i.name }) }),
190
+ };
191
+ }
192
+
193
+ /** The one `profile` tool: get, validate or set a profile, and the phone's topic url. */
194
+ export function profileTool(
195
+ deps: Deps,
196
+ i: {
197
+ action: "get" | "validate" | "set";
198
+ name?: string;
199
+ repo?: string;
200
+ patch?: ProfilePatch;
201
+ notifyUrl?: string;
202
+ },
203
+ ): unknown {
204
+ const repo = i.repo ?? null;
205
+ if (i.notifyUrl !== undefined) {
206
+ deps.profiles.setNotifyUrl(i.notifyUrl);
207
+ if (i.action === "set" && i.patch === undefined) return { notifyUrl: deps.profiles.notifyUrl() };
208
+ }
209
+ switch (i.action) {
210
+ case "get":
211
+ return deps.profiles.get(i.name, repo);
212
+ case "validate":
213
+ return deps.profiles.validate(i.name, repo);
214
+ case "set":
215
+ return deps.profiles.set(i.name, i.patch ?? {}, repo);
216
+ }
217
+ }