catherd-cli 1.4.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 (320) hide show
  1. package/README.md +188 -169
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -239
  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 -356
  11. package/plugin/skills/catherd-setup/SKILL.md +21 -20
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +19 -4
  14. package/src/adapters/claude-code/index.ts +126 -14
  15. package/src/adapters/codex/events.ts +10 -0
  16. package/src/adapters/codex/index.ts +17 -5
  17. package/src/adapters/cursor/home.ts +1 -1
  18. package/src/adapters/cursor/index.ts +10 -4
  19. package/src/adapters/opencode/index.ts +38 -1
  20. package/src/cli.ts +27 -27
  21. package/src/domain/agents.ts +3 -1
  22. package/src/domain/bars.ts +33 -107
  23. package/src/domain/blocked.ts +72 -0
  24. package/src/domain/branch.ts +51 -0
  25. package/src/domain/brief.ts +106 -0
  26. package/src/domain/catalog.ts +69 -313
  27. package/src/domain/changes.ts +23 -1
  28. package/src/domain/cost.ts +19 -29
  29. package/src/domain/errors.ts +6 -2
  30. package/src/domain/fail-lines.ts +26 -0
  31. package/src/domain/failover.ts +37 -75
  32. package/src/domain/findings.ts +47 -0
  33. package/src/domain/gate-brief.ts +53 -0
  34. package/src/domain/hints.ts +13 -2
  35. package/src/domain/ids.ts +13 -2
  36. package/src/domain/jev.ts +47 -2
  37. package/src/domain/judge.ts +124 -0
  38. package/src/domain/lane.ts +118 -3
  39. package/src/domain/milestone-memory.ts +53 -0
  40. package/src/domain/notice.ts +32 -8
  41. package/src/domain/notify.ts +32 -0
  42. package/src/domain/plan-doc.ts +198 -0
  43. package/src/domain/plan-lint.ts +141 -0
  44. package/src/domain/profile-rules.ts +175 -138
  45. package/src/domain/profile.ts +140 -63
  46. package/src/domain/record.ts +68 -3
  47. package/src/domain/reply.ts +29 -0
  48. package/src/domain/role-prompts.ts +182 -56
  49. package/src/domain/role-scope.ts +76 -0
  50. package/src/domain/role-tools.ts +34 -0
  51. package/src/domain/roles.ts +2 -0
  52. package/src/domain/route.ts +90 -0
  53. package/src/domain/rules.ts +33 -0
  54. package/src/domain/run-events.ts +298 -0
  55. package/src/domain/run-machine.ts +668 -0
  56. package/src/domain/run-state.ts +1310 -0
  57. package/src/domain/scorecard.ts +197 -0
  58. package/src/domain/select.ts +229 -72
  59. package/src/domain/slopalytics.ts +72 -0
  60. package/src/domain/state.ts +5 -0
  61. package/src/domain/util.ts +22 -0
  62. package/src/domain/workspace.ts +130 -0
  63. package/src/entry/answer-command.ts +39 -0
  64. package/src/entry/catalog-command.ts +24 -165
  65. package/src/entry/check-bin.ts +17 -0
  66. package/src/entry/cli-kit.ts +18 -2
  67. package/src/entry/daemon-bin.ts +22 -0
  68. package/src/entry/deps.ts +2 -0
  69. package/src/entry/doctor-command.ts +20 -2
  70. package/src/entry/glyphs.ts +15 -2
  71. package/src/entry/host-arg.ts +28 -0
  72. package/src/entry/init-command.ts +20 -69
  73. package/src/entry/knowledge-command.ts +121 -5
  74. package/src/entry/lock-command.ts +103 -8
  75. package/src/entry/mcp/handshake.ts +6 -2
  76. package/src/entry/mcp/role-bin.ts +20 -0
  77. package/src/entry/mcp/role-server.ts +134 -0
  78. package/src/entry/mcp/server.ts +85 -21
  79. package/src/entry/mcp/session-tools.ts +149 -0
  80. package/src/entry/panel/openapi.ts +189 -0
  81. package/src/entry/panel/server.ts +410 -0
  82. package/src/entry/panel-command.ts +68 -0
  83. package/src/entry/pause-command.ts +67 -0
  84. package/src/entry/profile-command.ts +33 -18
  85. package/src/entry/role-cli-command.ts +159 -0
  86. package/src/entry/runs-command.ts +81 -27
  87. package/src/entry/scorecard-command.ts +187 -0
  88. package/src/entry/supervise-bin.ts +1 -1
  89. package/src/entry/supervise-command.ts +30 -3
  90. package/src/entry/workspace-command.ts +44 -0
  91. package/src/infra/boot-lock.ts +25 -0
  92. package/src/infra/delivery.ts +3 -0
  93. package/src/infra/dispatch-dir.ts +2 -0
  94. package/src/infra/env.ts +11 -3
  95. package/src/infra/git.ts +70 -0
  96. package/src/infra/heavy-lock.ts +133 -6
  97. package/src/infra/host-probe.ts +16 -0
  98. package/src/infra/launch.ts +63 -7
  99. package/src/infra/lock-activity.ts +88 -0
  100. package/src/infra/login-env.ts +48 -0
  101. package/src/infra/memory.ts +45 -0
  102. package/src/infra/ntfy.ts +40 -0
  103. package/src/infra/open-url.ts +42 -0
  104. package/src/infra/paths.ts +37 -2
  105. package/src/infra/role-mcp.ts +69 -0
  106. package/src/infra/slopalytics.ts +30 -0
  107. package/src/infra/store.ts +6 -1
  108. package/src/infra/supervisor.ts +107 -16
  109. package/src/services/admission.ts +133 -31
  110. package/src/services/backends.ts +4 -6
  111. package/src/services/budget.ts +26 -6
  112. package/src/services/capture.ts +1 -22
  113. package/src/services/catalog-service.ts +155 -254
  114. package/src/services/credentials.ts +15 -26
  115. package/src/services/daemon.ts +181 -0
  116. package/src/services/dispatch-service.ts +209 -17
  117. package/src/services/dispatches.ts +30 -2
  118. package/src/services/doctor-access.ts +8 -3
  119. package/src/services/doctor-backends.ts +1 -1
  120. package/src/services/doctor-checks.ts +47 -0
  121. package/src/services/doctor-docker.ts +221 -0
  122. package/src/services/doctor-role-mcp.ts +65 -0
  123. package/src/services/doctor.ts +72 -15
  124. package/src/services/end-push.ts +157 -0
  125. package/src/services/failover-rungs.ts +109 -0
  126. package/src/services/finalize.ts +24 -2
  127. package/src/services/gate-env.ts +217 -0
  128. package/src/services/gate-service.ts +231 -45
  129. package/src/services/global-install.ts +46 -13
  130. package/src/services/jev-service.ts +2 -4
  131. package/src/services/lane-edit.ts +154 -0
  132. package/src/services/lane-service.ts +291 -55
  133. package/src/services/milestone-check.ts +433 -0
  134. package/src/services/milestone-tree.ts +170 -0
  135. package/src/services/milestones.ts +167 -29
  136. package/src/services/notifier.ts +124 -4
  137. package/src/services/notify-service.ts +26 -0
  138. package/src/services/orchestrator-wait.ts +82 -0
  139. package/src/services/panel-setup.ts +116 -0
  140. package/src/services/pause.ts +161 -0
  141. package/src/services/peek.ts +79 -17
  142. package/src/services/plan-input.ts +151 -0
  143. package/src/services/ports.ts +60 -5
  144. package/src/services/preflight.ts +232 -24
  145. package/src/services/profile-service.ts +31 -3
  146. package/src/services/profile-store.ts +18 -6
  147. package/src/services/protocol.ts +123 -25
  148. package/src/services/provenance.ts +34 -89
  149. package/src/services/questions.ts +24 -0
  150. package/src/services/reconcile.ts +33 -1
  151. package/src/services/reentry.ts +7 -6
  152. package/src/services/role-access.ts +27 -0
  153. package/src/services/routing-service.ts +161 -42
  154. package/src/services/run-briefs.ts +477 -0
  155. package/src/services/run-client.ts +217 -0
  156. package/src/services/run-effects.ts +837 -0
  157. package/src/services/run-events-store.ts +174 -0
  158. package/src/services/run-evidence.ts +1 -1
  159. package/src/services/run-pin.ts +125 -0
  160. package/src/services/run-read.ts +221 -0
  161. package/src/services/run-report.ts +176 -0
  162. package/src/services/run-service.ts +204 -23
  163. package/src/services/run-store.ts +159 -18
  164. package/src/services/scorecard-metrics.ts +86 -0
  165. package/src/services/scorecard.ts +382 -0
  166. package/src/services/scratch.ts +60 -0
  167. package/src/services/sessions.ts +3 -1
  168. package/src/services/state.ts +21 -0
  169. package/src/services/summary.ts +76 -12
  170. package/src/services/tree-path.ts +27 -0
  171. package/src/services/verifier-brief.ts +23 -0
  172. package/src/services/verifier-step.ts +48 -0
  173. package/src/services/workspace-admission.ts +259 -0
  174. package/src/services/workspace-service.ts +259 -0
  175. package/src/services/workspace-store.ts +95 -0
  176. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  177. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  178. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  179. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  180. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  181. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  182. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  183. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  184. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  185. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  186. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  187. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  188. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  189. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  190. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  191. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  192. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  193. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  194. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  195. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  196. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  197. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  198. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  199. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  200. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  201. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  202. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  203. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  204. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  205. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  206. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  207. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  208. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  209. package/web/dist/assets/index-C5IbylvM.css +2 -0
  210. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  211. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  212. package/web/dist/assets/label-DFSYBc62.js +1 -0
  213. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  214. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  215. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  216. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  217. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  218. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  219. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  220. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  221. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  222. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  223. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  224. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  225. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  226. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  227. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  228. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  229. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  230. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  231. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  232. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  233. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  234. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  235. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  236. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  237. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  238. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  239. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  240. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  241. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  242. package/web/dist/assets/time--9kSQbbp.js +1 -0
  243. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  244. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  245. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  246. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  247. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  248. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  249. package/web/dist/index.html +15 -0
  250. package/catalog/ATTRIBUTION.md +0 -36
  251. package/catalog/scores.json +0 -3202
  252. package/catalog/sources.json +0 -97
  253. package/src/adapters/antigravity/events.ts +0 -131
  254. package/src/adapters/antigravity/home.ts +0 -46
  255. package/src/adapters/antigravity/index.ts +0 -320
  256. package/src/adapters/antigravity/models.ts +0 -37
  257. package/src/adapters/grok/events.ts +0 -112
  258. package/src/adapters/grok/home.ts +0 -132
  259. package/src/adapters/grok/index.ts +0 -339
  260. package/src/adapters/grok/models.ts +0 -48
  261. package/src/domain/calibration.ts +0 -125
  262. package/src/domain/sources.ts +0 -144
  263. package/src/entry/mcp/dispatch-tools.ts +0 -52
  264. package/src/entry/mcp/lane-tools.ts +0 -84
  265. package/src/entry/mcp/protocol-tools.ts +0 -57
  266. package/src/entry/mcp/run-tools.ts +0 -148
  267. package/src/entry/mcp/setup-tools.ts +0 -108
  268. package/src/entry/tui/commands.ts +0 -563
  269. package/src/entry/tui/effects.ts +0 -381
  270. package/src/entry/tui/fixtures.ts +0 -637
  271. package/src/entry/tui/profile-edits.ts +0 -146
  272. package/src/entry/tui/profile-tree.ts +0 -429
  273. package/src/entry/tui/providers/app.tsx +0 -162
  274. package/src/entry/tui/providers/boundary.tsx +0 -57
  275. package/src/entry/tui/providers/data.tsx +0 -160
  276. package/src/entry/tui/providers/keymap.tsx +0 -166
  277. package/src/entry/tui/providers/root.tsx +0 -45
  278. package/src/entry/tui/providers/theme.tsx +0 -17
  279. package/src/entry/tui/providers/toast.tsx +0 -77
  280. package/src/entry/tui/run.tsx +0 -114
  281. package/src/entry/tui/state.ts +0 -373
  282. package/src/entry/tui/stories.ts +0 -114
  283. package/src/entry/tui/text.ts +0 -130
  284. package/src/entry/tui/theme.ts +0 -140
  285. package/src/entry/tui/views/app.tsx +0 -208
  286. package/src/entry/tui/views/dialogs.tsx +0 -41
  287. package/src/entry/tui/views/profile-actions.ts +0 -331
  288. package/src/entry/tui/views/profiles.tsx +0 -520
  289. package/src/entry/tui/views/runs.tsx +0 -573
  290. package/src/entry/tui/views/save-dialog.tsx +0 -340
  291. package/src/entry/tui/views/status.tsx +0 -209
  292. package/src/entry/tui/widgets/chrome.tsx +0 -114
  293. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  294. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  295. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  296. package/src/entry/tui/widgets/dialog.tsx +0 -68
  297. package/src/entry/tui/widgets/line.tsx +0 -92
  298. package/src/entry/tui/widgets/list.tsx +0 -197
  299. package/src/entry/tui/widgets/spinner.tsx +0 -49
  300. package/src/entry/tui/widgets/toast.tsx +0 -62
  301. package/src/infra/sources/arena.ts +0 -87
  302. package/src/infra/sources/artificial-analysis.ts +0 -112
  303. package/src/infra/sources/cache.ts +0 -124
  304. package/src/infra/sources/csv.ts +0 -38
  305. package/src/infra/sources/epoch.ts +0 -82
  306. package/src/infra/sources/http.ts +0 -75
  307. package/src/infra/sources/litellm.ts +0 -58
  308. package/src/infra/sources/models-dev.ts +0 -59
  309. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  310. package/src/infra/sources/openrouter-models.ts +0 -60
  311. package/src/infra/sources/rows.ts +0 -38
  312. package/src/infra/sources/vectara.ts +0 -69
  313. package/src/infra/sources/zip.ts +0 -48
  314. package/src/services/catalog-refresh.ts +0 -201
  315. package/src/services/doctor-sources.ts +0 -76
  316. package/src/services/runs-page.ts +0 -255
  317. package/src/services/source-derive.ts +0 -374
  318. package/src/services/source-sync.ts +0 -305
  319. package/src/services/standins.ts +0 -204
  320. package/src/services/treat-likes.ts +0 -97
@@ -1,19 +1,29 @@
1
- import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync } from "node:fs";
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync } from "node:fs";
2
2
  import { dirname, join, relative, resolve, sep } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { CatherdError } from "../domain/errors.ts";
5
+ import { runSlug } from "../domain/branch.ts";
5
6
  import { assertId } from "../domain/ids.ts";
6
7
  import { type RunRecord, RunRecordSchema } from "../domain/record.ts";
7
- import type { OutcomeRow, RouteRow } from "../domain/route.ts";
8
- import { cell, slug } from "../domain/util.ts";
8
+ import type {
9
+ FailoverRouteRow,
10
+ OutcomeRouteRow,
11
+ OutcomeRow,
12
+ RoleRouteRow,
13
+ RouteRow,
14
+ } from "../domain/route.ts";
15
+ import { cell } from "../domain/util.ts";
9
16
  import { withFileLock } from "../infra/filelock.ts";
10
- import { dataDir, repoDir, runsDir } from "../infra/paths.ts";
17
+ import { gitOrigin } from "../infra/git.ts";
18
+ import { dataDir, originDir, repoDir, runsDir } from "../infra/paths.ts";
11
19
  import {
12
20
  appendJsonl,
13
21
  ensureJsonlHeader,
14
22
  ensurePrivateDir,
15
23
  PRIVATE_DIR,
24
+ PRIVATE_FILE,
16
25
  appendPrivate,
26
+ nonBlankLines,
17
27
  readJsonl,
18
28
  readVersioned,
19
29
  writeJsonAtomic,
@@ -38,6 +48,11 @@ const RunMetaSchema = z.looseObject({
38
48
  createdAt: z.string(),
39
49
  catherdVersion: z.string(),
40
50
  startedBy: StartedBySchema.nullable().optional(),
51
+ workspace: z.strictObject({ id: z.string(), step: z.string() }).optional(),
52
+ /** the repo's HEAD when run_start ran: where the first milestone's commit range starts (absent before 1.5) */
53
+ startHead: z.string().optional(),
54
+ /** plan 35: the branch on origin each milestone's own branch starts from; absent for a run in the repo itself */
55
+ base: z.string().optional(),
41
56
  });
42
57
  type RunMeta = z.infer<typeof RunMetaSchema>;
43
58
 
@@ -66,6 +81,8 @@ export function runPaths(dir: string) {
66
81
  sessions: join(dir, "sessions.jsonl"),
67
82
  roles: join(dir, "roles"),
68
83
  shots: join(dir, "shots"),
84
+ /** per dispatch name, the role's TMPDIR (spec 1.5 plan 21): `scratch/<name>/` */
85
+ scratch: join(dir, "scratch"),
69
86
  /** the admission lock's target: `admission.lock` guards dispatch admission */
70
87
  admission: join(dir, "admission"),
71
88
  };
@@ -73,7 +90,6 @@ export function runPaths(dir: string) {
73
90
 
74
91
  export const LEDGER_HEADER = "milestone | what | commit | minutes | evidence";
75
92
 
76
- const runSlug = (title: string) => slug(title).slice(0, 40) || "run";
77
93
  const stamp = (d: Date) => d.toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
78
94
 
79
95
  /** Creates a run folder under the repo's runs dir. `repo` is a git toplevel; meta.json is written last. */
@@ -83,6 +99,11 @@ export function createRun(o: {
83
99
  aLines: string[];
84
100
  version: string;
85
101
  now?: Date;
102
+ workspace?: { id: string; step: string };
103
+ /** the repo's full HEAD at the start, when it has a commit */
104
+ startHead?: string | null;
105
+ /** plan 35: the branch on origin each milestone's branch starts from */
106
+ base?: string;
86
107
  startedBy?: {
87
108
  host?: StartedBy["host"];
88
109
  sessionId: string;
@@ -90,6 +111,9 @@ export function createRun(o: {
90
111
  name: string | null;
91
112
  } | null;
92
113
  }): Run {
114
+ const workspace = o.workspace
115
+ ? { id: assertId("workspace", o.workspace.id), step: assertId("workspace step", o.workspace.step) }
116
+ : undefined;
93
117
  const now = o.now ?? new Date();
94
118
  const root = runsDir(o.repo);
95
119
  ensurePrivateDir(root);
@@ -130,6 +154,9 @@ export function createRun(o: {
130
154
  createdAt: now.toISOString(),
131
155
  catherdVersion: o.version,
132
156
  ...(o.startedBy ? { startedBy: StartedBySchema.parse(o.startedBy) } : {}),
157
+ ...(workspace ? { workspace } : {}),
158
+ ...(o.startHead ? { startHead: o.startHead } : {}),
159
+ ...(o.base ? { base: o.base } : {}),
133
160
  };
134
161
  writeJsonAtomic(p.meta, meta);
135
162
  return { id, dir, meta };
@@ -141,14 +168,18 @@ export interface RunListing {
141
168
  corrupt: { id: string; dir: string; reason: string }[];
142
169
  }
143
170
 
144
- /** Every run of every repo, newest first. */
145
- export function listRuns(): RunListing {
171
+ /** Every run, newest first; an explicit repo scope avoids reading unrelated histories. */
172
+ export function listRuns(repos?: readonly string[]): RunListing {
146
173
  const root = join(dataDir(), "repos");
147
174
  const out: RunListing = { runs: [], corrupt: [] };
148
175
  if (!existsSync(root)) return out;
149
- for (const repo of readdirSync(root, { withFileTypes: true })) {
150
- const runs = join(root, repo.name, "runs");
151
- if (!repo.isDirectory() || !existsSync(runs)) continue;
176
+ const directories = repos
177
+ ? [...new Set(repos.map(runsDir))]
178
+ : readdirSync(root, { withFileTypes: true })
179
+ .filter((repo) => repo.isDirectory())
180
+ .map((repo) => join(root, repo.name, "runs"));
181
+ for (const runs of directories) {
182
+ if (!existsSync(runs)) continue;
152
183
  for (const e of readdirSync(runs, { withFileTypes: true })) {
153
184
  if (!e.isDirectory()) continue;
154
185
  const dir = join(runs, e.name);
@@ -220,13 +251,39 @@ export function appendRecord(run: Run, r: RunRecord): Promise<RunRecord> {
220
251
  });
221
252
  }
222
253
 
254
+ /** The run's lane rows (routes and climbs); a role's rows outside a lane are left out (`readRoleRoutes`). */
223
255
  export function readRoutes(run: Run): RouteRow[] {
224
256
  return readJsonl<RouteRow>(runPaths(run.dir).routes).rows.filter(
225
- (r) => typeof r?.lane === "string" && Array.isArray(r.ladder) && typeof r.rung === "string",
257
+ (r) =>
258
+ typeof r?.lane === "string" &&
259
+ (r.source === "route" || r.source === "climb") &&
260
+ Array.isArray(r.ladder) &&
261
+ typeof r.rung === "string",
226
262
  );
227
263
  }
228
264
 
229
- export function appendRoute(run: Run, row: RouteRow): void {
265
+ /** Spec 1.5 plan 24: the lanes' outcome rows of routes.jsonl, oldest first (the last per lane wins). */
266
+ export function readOutcomeRoutes(run: Run): OutcomeRouteRow[] {
267
+ return readJsonl<OutcomeRouteRow>(runPaths(run.dir).routes).rows.filter(
268
+ (r) => typeof r?.lane === "string" && r.source === "outcome",
269
+ );
270
+ }
271
+
272
+ /** Spec 1.5 plan 24: the run's role decisions outside a lane, oldest first. */
273
+ export function readRoleRoutes(run: Run): RoleRouteRow[] {
274
+ return readJsonl<RoleRouteRow>(runPaths(run.dir).routes).rows.filter(
275
+ (r) =>
276
+ r?.lane === null &&
277
+ (r.source === "route" || r.source === "dispatch") &&
278
+ typeof r.role === "string" &&
279
+ typeof r.rung === "string",
280
+ );
281
+ }
282
+
283
+ export function appendRoute(
284
+ run: Run,
285
+ row: RouteRow | RoleRouteRow | OutcomeRouteRow | FailoverRouteRow,
286
+ ): void {
230
287
  const file = runPaths(run.dir).routes;
231
288
  ensureJsonlHeader(file, "routes");
232
289
  appendJsonl(file, row);
@@ -255,13 +312,22 @@ const AgentRunSchema = z.looseObject({
255
312
  status: z.enum(["ok", "failed", "cancelled"]),
256
313
  /** the lane the subagent worked, so catalog timings count it under that lane's kind; rows before it lack it */
257
314
  lane: z.string().nullable().optional(),
315
+ /** plan 23: a native verifier's verdict line (VERDICT: BLOCKED: environment — <probe>); rows before it lack it */
316
+ verdict: z.string().optional(),
317
+ /** plan 23: the STATUS word of the subagent's reply (a reviewer's partial is no review); rows before it lack it */
318
+ replyStatus: z.enum(["complete", "partial", "blocked", "refused", "flaky"]).optional(),
258
319
  });
259
320
  export type AgentRun = z.infer<typeof AgentRunSchema>;
260
321
 
261
322
  /** Native Claude subagent runs, as the orchestrator reported them through record_agent_run. */
262
- export function readAgentRuns(run: Run): AgentRun[] {
263
- return readJsonl<unknown>(runPaths(run.dir).agents).rows.flatMap((row) => {
323
+ export function readAgentRuns(run: Run, strict = false): AgentRun[] {
324
+ const { rows, corrupt } = readJsonl<unknown>(runPaths(run.dir).agents);
325
+ if (strict && corrupt)
326
+ throw new CatherdError("E_RUN_CORRUPT", `run ${run.id} has unreadable native cost records`);
327
+ return rows.flatMap((row) => {
264
328
  const r = AgentRunSchema.safeParse(row);
329
+ if (strict && !r.success)
330
+ throw new CatherdError("E_RUN_CORRUPT", `run ${run.id} has invalid native cost records`);
265
331
  return r.success ? [r.data] : [];
266
332
  });
267
333
  }
@@ -276,20 +342,95 @@ export function appendLedger(run: Run, row: string): void {
276
342
  appendPrivate(runPaths(run.dir).ledger, `${row}\n`);
277
343
  }
278
344
 
279
- /** Spec §4.7: what past runs of a repo learned, keyed by its git toplevel. */
345
+ /** Spec §4.7: what past runs of a repo learned, keyed by its git toplevel (before 1.5, and with no origin). */
280
346
  export const knowledgeFile = (toplevel: string): string => join(repoDir(toplevel), "knowledge.md");
281
347
 
348
+ /**
349
+ * Spec 1.5 "Knowledge keyed by git origin": the knowledge.md every worktree and clone of one repository
350
+ * shares, keyed by its `origin` remote, else by its toplevel. A toplevel-keyed file this worktree left before
351
+ * is migrated on read: its lines not there yet are appended, and it is renamed `knowledge.md.migrated`.
352
+ */
353
+ export async function knowledgeFor(toplevel: string): Promise<string> {
354
+ const origin = await gitOrigin(toplevel);
355
+ const legacy = knowledgeFile(toplevel);
356
+ if (!origin) return legacy;
357
+ const file = join(originDir(origin), "knowledge.md");
358
+ if (!existsSync(legacy)) return file;
359
+ ensurePrivateDir(dirname(file));
360
+ await withFileLock(file, () => {
361
+ if (!existsSync(legacy)) return;
362
+ const have = new Set(existsSync(file) ? nonBlankLines(file) : []);
363
+ const add = nonBlankLines(legacy).filter((l) => !have.has(l));
364
+ if (add.length) appendPrivate(file, `${add.join("\n")}\n`);
365
+ renameSync(legacy, `${legacy}.migrated`);
366
+ });
367
+ return file;
368
+ }
369
+
282
370
  /**
283
371
  * Appends one line to the repo's knowledge.md, `- <date> <source>: <text>` with `text` on one line, and returns
284
- * it. The single writer of knowledge.md: `land`'s `learned` and `catherd knowledge add` both come through here.
372
+ * it. `land`'s `learned` and `catherd knowledge add` both come through here; `removeKnowledgeLine` is the other writer.
285
373
  */
286
- export function appendKnowledge(toplevel: string, at: Date, source: string, text: string): string {
374
+ export async function appendKnowledge(
375
+ toplevel: string,
376
+ at: Date,
377
+ source: string,
378
+ text: string,
379
+ ): Promise<string> {
287
380
  const line = `- ${at.toISOString().slice(0, 10)} ${source}: ${cell(text)}`;
288
- appendPrivate(knowledgeFile(toplevel), `${line}\n`);
381
+ const file = await knowledgeFor(toplevel);
382
+ ensurePrivateDir(dirname(file));
383
+ await withFileLock(file, () => appendPrivate(file, `${line}\n`));
289
384
  return line;
290
385
  }
291
386
 
387
+ /** Removes the one line of the repo's knowledge.md that contains `match`, and returns it. */
388
+ export async function removeKnowledgeLine(toplevel: string, match: string): Promise<string> {
389
+ const file = await knowledgeFor(toplevel);
390
+ const needle = match.trim();
391
+ ensurePrivateDir(dirname(file));
392
+ return withFileLock(file, () => {
393
+ const lines = existsSync(file) ? nonBlankLines(file) : [];
394
+ const hits = needle ? lines.filter((l) => l.includes(needle)) : [];
395
+ if (hits.length === 0)
396
+ throw new CatherdError("E_INPUT_INVALID", `no line of knowledge.md contains "${match}"`, {
397
+ fix: "catherd knowledge show",
398
+ });
399
+ if (hits.length > 1)
400
+ throw new CatherdError(
401
+ "E_INPUT_INVALID",
402
+ `${hits.length} lines contain "${match}": quote more of the one to remove`,
403
+ {
404
+ fix: "catherd knowledge show",
405
+ },
406
+ );
407
+ const rest = lines.filter((l) => l !== hits[0]);
408
+ writeTextAtomic(file, rest.length ? `${rest.join("\n")}\n` : "", { mode: PRIVATE_FILE });
409
+ return hits[0] as string;
410
+ });
411
+ }
412
+
413
+ /** Spec 1.5 "runs supersede": the run that took over a closed one, and when. */
414
+ const SupersededSchema = z.looseObject({ schema: z.literal(1), by: z.string(), at: z.string() });
415
+ export type Superseded = z.infer<typeof SupersededSchema>;
416
+
417
+ export const supersededFile = (run: Run): string => join(run.dir, "superseded.json");
418
+
419
+ /** The pointer `runs supersede` left, or null: an open run, or a pointer that cannot be read. */
420
+ export function supersededBy(run: Run): Superseded | null {
421
+ const file = supersededFile(run);
422
+ if (!existsSync(file)) return null;
423
+ try {
424
+ return readVersioned(file, SupersededSchema, 1);
425
+ } catch {
426
+ return null;
427
+ }
428
+ }
429
+
292
430
  const SERVER_OWNED = new Set([
431
+ "superseded.json",
432
+ "pin.json",
433
+ "workspace-contract.md",
293
434
  "meta.json",
294
435
  "state.md",
295
436
  "state.json",
@@ -0,0 +1,86 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { sessionKey } from "../domain/host.ts";
4
+ import { type Interventions, interventionTotal } from "../domain/scorecard.ts";
5
+ import type { RunEvent } from "../domain/run-events.ts";
6
+ import { coveredMs } from "../domain/util.ts";
7
+ import { landedMilestones } from "./milestones.ts";
8
+ import { pauseSpans } from "./pause.ts";
9
+ import { runMilestones } from "./protocol.ts";
10
+ import { parkedSpans, questionCounts } from "./questions.ts";
11
+ import { replay } from "../domain/run-state.ts";
12
+ import { isDaemonRun, readEvents } from "./run-events-store.ts";
13
+ import { listRuns, readAgentRuns, readRecords, type Run, supersededBy } from "./run-store.ts";
14
+ import { readSessionRows } from "./sessions.ts";
15
+
16
+ // Plan 28: what a finished run's own files say about how much it needed a person. Read-only.
17
+
18
+ /**
19
+ * A daemon run's own events (plan 36, ruling 12): the owner's answers and the questions still open are parks (a
20
+ * judge's answer is not), every answer is shown, and an owner's cancel or plan approval each count.
21
+ */
22
+ function eventCounts(run: Run): { parks: number; answers: number; cancels: number; approvals: number } {
23
+ const events = readEvents(run);
24
+ const n = (f: (e: RunEvent) => boolean) => events.filter(f).length;
25
+ // one snapshot: an answer written between two reads would otherwise count nowhere
26
+ const open = Object.keys(replay(events, run.id).questions).length;
27
+ return {
28
+ parks: n((e) => e.type === "question.answered" && e.by === "owner") + open,
29
+ answers: n((e) => e.type === "question.answered"),
30
+ cancels: n((e) => e.type === "cancel.requested"),
31
+ approvals: n((e) => e.type === "plan.approved" && e.by === "owner"),
32
+ };
33
+ }
34
+
35
+ /** A 1.5 run keeps its questions in questions.jsonl and has no cancel or plan approval to count. */
36
+ const oldCounts = (run: Run) => {
37
+ const { questions, answers } = questionCounts(run);
38
+ return { parks: questions, answers, cancels: 0, approvals: 0 };
39
+ };
40
+
41
+ /** The interventions a run needed by `end` (epoch ms), counted from its records (plan 28, ruling 4). */
42
+ export function interventionsOf(run: Run, end: number, chat: number): Interventions {
43
+ const daemon = isDaemonRun(run);
44
+ const { parks, answers, cancels, approvals } = daemon ? eventCounts(run) : oldCounts(run);
45
+ const keys = [...(run.meta.startedBy ? [run.meta.startedBy] : []), ...readSessionRows(run)].map(sessionKey);
46
+ const takeovers = keys.filter((k, i) => i > 0 && k !== keys[i - 1]).length;
47
+ // a daemon run admits every dispatch without a session
48
+ const manualDispatches = daemon ? readRecords(run).records.filter((r) => r.sessionId).length : 0;
49
+ const from = Date.parse(run.meta.createdAt);
50
+ const pauses = pauseSpans(run).filter((s) => s.from >= from && s.from <= end).length;
51
+ const superseded = listRuns([run.meta.repo]).runs.filter((r) => supersededBy(r)?.by === run.id).length;
52
+ const i = { parks, answers, takeovers, manualDispatches, pauses, superseded, chat, cancels, approvals };
53
+ return { ...i, total: interventionTotal(i) };
54
+ }
55
+
56
+ /** When the run ended, in epoch ms: its last landing, else its last record or native agent run, else its start. */
57
+ export function runEnd(run: Run): number {
58
+ const last = landedMilestones(run).at(-1);
59
+ if (last) {
60
+ const file = join(run.dir, "digests", `${last}.md`);
61
+ const at = existsSync(file) ? / · landed (\S+)\s*$/m.exec(readFileSync(file, "utf8"))?.[1] : undefined;
62
+ const ms = at ? Date.parse(at) : NaN;
63
+ if (!Number.isNaN(ms)) return ms;
64
+ }
65
+ const times = [...readRecords(run).records.map((r) => r.endedAt), ...readAgentRuns(run).map((a) => a.at)]
66
+ .map((t) => Date.parse(t))
67
+ .filter((t) => !Number.isNaN(t));
68
+ return times.length ? Math.max(...times) : Date.parse(run.meta.createdAt);
69
+ }
70
+
71
+ /** The ms of the run, up to `end`, that it spent parked on the owner or paused; overlaps once. */
72
+ export const pausedMs = (run: Run, end: number): number =>
73
+ coveredMs(Date.parse(run.meta.createdAt), end, [
74
+ ...runMilestones(run).flatMap((m) => parkedSpans(run, m)),
75
+ ...pauseSpans(run),
76
+ ]);
77
+
78
+ /** A failover stand-in (failoverFrom set) ended ok and was not blocked or refused. */
79
+ export const failoverRecovered = (run: Run): boolean =>
80
+ readRecords(run).records.some(
81
+ (r) =>
82
+ r.failoverFrom !== null &&
83
+ r.status === "ok" &&
84
+ r.replyStatus !== "blocked" &&
85
+ r.replyStatus !== "refused",
86
+ );