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,22 +1,33 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { relative, sep } from "node:path";
2
+ import { join, relative, sep } from "node:path";
3
3
  import { CatherdError } from "../domain/errors.ts";
4
4
  import { assertId, ID_PATTERN, parseRung } from "../domain/ids.ts";
5
- import { assertLaneHeader, type Difficulty, type Kind } from "../domain/lane.ts";
5
+ import { quotaUsage } from "../domain/select.ts";
6
+ import { assertLaneHeader } from "../domain/lane.ts";
6
7
  import type { Role } from "../domain/roles.ts";
7
- import { cell } from "../domain/util.ts";
8
+ import { cell, coveredMs } from "../domain/util.ts";
8
9
  import {
9
10
  type ClimbReason,
10
11
  currentRoute,
11
12
  laneOutcome,
12
13
  nextRung,
13
- type RouteJev,
14
- type RouteSource,
14
+ type OutcomeRow,
15
+ outcomeRouteRow,
16
+ type RouteRow,
15
17
  } from "../domain/route.ts";
16
18
  import { withFileLock } from "../infra/filelock.ts";
17
19
  import { commitExists } from "../infra/git.ts";
18
20
  import { laneFile } from "./admission.ts";
19
21
  import { budgetOf } from "./budget.ts";
22
+ import { pendingDispatches } from "./dispatches.ts";
23
+ import { workspaceBudget } from "./workspace-admission.ts";
24
+ import {
25
+ findWorkspace,
26
+ WORKSPACE_LOCK_WAIT_MS,
27
+ workspaceChildren,
28
+ workspaceDirectory,
29
+ workspacePaths,
30
+ } from "./workspace-store.ts";
20
31
  import {
21
32
  isDocPath,
22
33
  isSourcePath,
@@ -24,9 +35,12 @@ import {
24
35
  fullCommit,
25
36
  milestoneFiles,
26
37
  milestoneStart,
38
+ partialReviewer,
27
39
  reviewerPassed,
40
+ milestoneCommits,
28
41
  milestoneVerifier,
29
42
  } from "./milestones.ts";
43
+ import { machineGate } from "./milestone-check.ts";
30
44
  import type { Deps, Verdict } from "./ports.ts";
31
45
  import {
32
46
  appendKnowledge,
@@ -34,31 +48,35 @@ import {
34
48
  appendOutcome,
35
49
  appendRoute,
36
50
  findRun,
51
+ readAgentRuns,
52
+ readRecords,
37
53
  readRoutes,
38
54
  type Run,
39
55
  runFile,
40
56
  runPaths,
41
57
  } from "./run-store.ts";
42
58
  import { writeDigest } from "./protocol.ts";
43
- import { openQuestions } from "./questions.ts";
44
- import { type Notes, type NotesPatch, refreshState } from "./state.ts";
59
+ import { pauseSpans } from "./pause.ts";
60
+ import { openQuestions, parkedSpans } from "./questions.ts";
61
+ import { runProfile } from "./run-pin.ts";
62
+ import { type Notes, type NotesPatch, patchNotes, refreshState } from "./state.ts";
45
63
 
46
64
  const withHints = (hints: string[]) => (hints.length ? { hints } : {});
47
65
 
66
+ /**
67
+ * Spec 1.5 plan 24: what `route` returns: little, since every call lands in the coordinator's context. The
68
+ * decision's source, kind, difficulty, Jev's answer and the provenance go to routes.jsonl.
69
+ */
48
70
  export interface RouteResult {
49
71
  lane: string | null;
50
72
  role: Role;
51
73
  rung: string;
52
74
  ladder: string[];
53
- source: RouteSource;
54
- kind: Kind | null;
55
- difficulty: Difficulty | null;
56
75
  backend: string;
57
76
  /** the native agent to run a `claude:` rung as */
58
77
  agent: string | null;
59
- /** the Jev question set asked, and what it said, when Jev was asked */
60
- questionSet: string | null;
61
- jev: RouteJev | null;
78
+ /** the decision in one line */
79
+ why: string;
62
80
  }
63
81
 
64
82
  function readLaneFile(run: Run, path: string): { lane: string; text: string } {
@@ -79,15 +97,51 @@ function readLaneFile(run: Run, path: string): { lane: string; text: string } {
79
97
  return { lane, text };
80
98
  }
81
99
 
82
- /** Spec §5.4 through the routing port; a lane's route is recorded in routes.jsonl. */
100
+ /**
101
+ * Spec 1.5 plan 24: the run's dispatches per quota, plus each lane routed but not dispatched yet at its current
102
+ * rung, so a tie between quotas goes to the one the run will have used least.
103
+ */
104
+ function runUsage(run: Run): Record<string, number> {
105
+ const records = readRecords(run).records;
106
+ const dispatched = new Set(records.flatMap((r) => (r.lane ? [r.lane] : [])));
107
+ const routes = readRoutes(run);
108
+ const waiting = [...new Set(routes.map((r) => r.lane))]
109
+ .filter((l) => !dispatched.has(l))
110
+ .flatMap((l) => currentRoute(routes, l)?.rung ?? []);
111
+ return quotaUsage([...records.map((r) => r.rung), ...waiting]);
112
+ }
113
+
114
+ /** Spec §5.4 through the routing port; every decision is recorded in routes.jsonl. */
83
115
  export async function route(
84
116
  deps: Deps,
85
117
  i: { run: string; laneFile?: string; role: Role },
86
118
  ): Promise<RouteResult> {
119
+ const [one] = await routeLanes(deps, { run: i.run, laneFiles: [i.laneFile], role: i.role });
120
+ return one as RouteResult;
121
+ }
122
+
123
+ /**
124
+ * Spec 1.5 plan 24, batch `route`: several lanes of one role in one call (Jev asked about all at once), each
125
+ * recorded as `route` records one. `laneFiles` holds `undefined` for a role routed without a lane.
126
+ */
127
+ export async function routeLanes(
128
+ deps: Deps,
129
+ i: { run: string; laneFiles: (string | undefined)[]; role: Role },
130
+ ): Promise<RouteResult[]> {
87
131
  const run = findRun(i.run);
88
- const lane = i.laneFile === undefined ? null : readLaneFile(run, i.laneFile);
89
- const profile = deps.profiles.forRepo(run.meta.repo);
90
- const a = await deps.routing.route({
132
+ const lanes = i.laneFiles.map((f) => (f === undefined ? null : readLaneFile(run, f)));
133
+ const dup = lanes.find((l, n) => l && lanes.findIndex((x) => x?.lane === l.lane) !== n);
134
+ if (dup)
135
+ throw new CatherdError("E_INPUT_INVALID", `route: lane ${dup.lane} is named twice`, {
136
+ fix: "name each lane file once",
137
+ });
138
+ // spec 1.5: the run's pinned profile, whatever the repo runs on now
139
+ const profile = runProfile(deps, run);
140
+ const spentFraction = Math.max(
141
+ budgetOf(run, profile.budget, deps.now())?.fraction ?? 0,
142
+ (await workspaceBudget(run, deps.now()))?.fraction ?? 0,
143
+ );
144
+ const reqs = lanes.map((lane) => ({
91
145
  host: deps.host.host,
92
146
  runDir: run.dir,
93
147
  repo: run.meta.repo,
@@ -95,13 +149,37 @@ export async function route(
95
149
  role: i.role,
96
150
  lane: lane?.lane ?? null,
97
151
  laneText: lane?.text ?? null,
98
- spentFraction: budgetOf(run, profile.budget, deps.now())?.fraction ?? 0,
99
- });
152
+ usage: runUsage(run),
153
+ spentFraction,
154
+ }));
155
+ const answers =
156
+ reqs.length === 1
157
+ ? [await deps.routing.route(reqs[0] as (typeof reqs)[number])]
158
+ : await deps.routing.routeMany(reqs);
159
+ return answers.map((a, n) => record(deps, run, i.role, lanes[n] ?? null, a));
160
+ }
161
+
162
+ /** One decision into routes.jsonl (spec 1.5 plan 24: every role's, with its source, ladder and provenance). */
163
+ function record(
164
+ deps: Deps,
165
+ run: Run,
166
+ role: Role,
167
+ lane: { lane: string } | null,
168
+ a: Awaited<ReturnType<Deps["routing"]["route"]>>,
169
+ ): RouteResult {
170
+ const at = new Date(deps.now()).toISOString();
171
+ const detail = {
172
+ why: a.why,
173
+ ...(a.jevSaid ? { jevSaid: a.jevSaid } : {}),
174
+ ...(a.noClear ? { noClear: a.noClear } : {}),
175
+ ...(a.tie ? { tie: a.tie } : {}),
176
+ ...(a.provenance ? { provenance: a.provenance } : {}),
177
+ };
100
178
  if (lane)
101
179
  appendRoute(run, {
102
- at: new Date(deps.now()).toISOString(),
180
+ at,
103
181
  lane: lane.lane,
104
- role: i.role,
182
+ role,
105
183
  rung: a.rung,
106
184
  ladder: a.ladder,
107
185
  source: "route",
@@ -112,18 +190,38 @@ export async function route(
112
190
  difficulty: a.difficulty,
113
191
  questionSet: a.questionSet,
114
192
  jev: a.jev,
193
+ ...detail,
194
+ });
195
+ else
196
+ appendRoute(run, {
197
+ at,
198
+ lane: null,
199
+ role,
200
+ name: null,
201
+ rung: a.rung,
202
+ ladder: a.ladder,
203
+ source: "route",
204
+ decidedBy: a.source,
205
+ ...detail,
115
206
  });
116
207
  return {
117
208
  lane: lane?.lane ?? null,
118
- role: i.role,
119
- ...a,
209
+ role,
210
+ rung: a.rung,
211
+ ladder: a.ladder,
120
212
  backend: parseRung(a.rung).backend,
121
- agent: deps.profiles.agentFor(run.meta.repo, i.role, a.rung),
213
+ agent: deps.profiles.agentFor(run.meta.repo, role, a.rung),
214
+ why: a.why,
122
215
  };
123
216
  }
124
217
 
125
- /** Evidence that the lane's own ownership is the problem: a design question, never a capability one. */
126
- const OWNERSHIP = /outside (the )?lane('s)? ownership|owned by/i;
218
+ /**
219
+ * Evidence that the lane's own ownership is the problem: a design question, never a capability one. "owned by"
220
+ * counts only for a lane (another lane, lane X, Mx.Ly), never for a user or a process: a socket "owned by root"
221
+ * is the environment's.
222
+ */
223
+ const OWNERSHIP =
224
+ /outside (the )?lane('s)? ownership|owned by (?:another |a different |the other )?lane\b|owned by [A-Za-z0-9_-]+\.L\d+\b/i;
127
225
 
128
226
  const CLIMB_DESIGN_FIX = "send it to the architect (ask/architect delta), not up the ladder";
129
227
 
@@ -156,6 +254,15 @@ async function refuseDesign(
156
254
  });
157
255
  }
158
256
 
257
+ /**
258
+ * A lane's final outcome: its outcomes.jsonl row (spec §5.6) and, beside Jev's answer, its routes.jsonl row
259
+ * (spec 1.5 plan 24). Called under the routes lock.
260
+ */
261
+ function writeOutcome(run: Run, routes: RouteRow[], o: OutcomeRow): void {
262
+ appendOutcome(run, o);
263
+ appendRoute(run, outcomeRouteRow(routes, o));
264
+ }
265
+
159
266
  /** Spec §4.5: one rung up the lane's ladder, on a fresh thread; the reason goes to routes.jsonl. */
160
267
  export async function climb(
161
268
  deps: Deps,
@@ -176,6 +283,18 @@ export async function climb(
176
283
  });
177
284
  // an unrouted lane is refused before Jev is asked about its evidence (no call, no jev.jsonl row)
178
285
  if (!currentRoute(readRoutes(run), i.lane)) throw unrouted();
286
+ // plan 23: a reply that named the environment (ENV: …) would stop a higher rung the same way
287
+ const last = readRecords(run)
288
+ .records.filter((r) => r.lane === i.lane)
289
+ .at(-1);
290
+ if (last?.environment)
291
+ throw new CatherdError(
292
+ "E_CLIMB_ENV",
293
+ `climb ${i.lane}: ${last.name} was stopped by the environment (${last.environment}), which a higher rung cannot fix`,
294
+ {
295
+ fix: `fix the environment (doctor; catherd knowledge env set for the gate environment) and dispatch ${i.lane} again at the same rung, or park the milestone when only the owner can fix it`,
296
+ },
297
+ );
179
298
  await refuseDesign(deps, run, i);
180
299
  const { cur, next } = await withFileLock(runPaths(run.dir).routes, () => {
181
300
  const cur = currentRoute(readRoutes(run), i.lane);
@@ -192,8 +311,9 @@ export async function climb(
192
311
  });
193
312
  // spec §5.6: a climb past the top rung ends the lane open
194
313
  if (!next) {
195
- const o = laneOutcome(readRoutes(run), i.lane, false, new Date(deps.now()).toISOString());
196
- if (o) appendOutcome(run, o);
314
+ const routes = readRoutes(run);
315
+ const o = laneOutcome(routes, i.lane, false, new Date(deps.now()).toISOString());
316
+ if (o) writeOutcome(run, routes, o);
197
317
  }
198
318
  return { cur, next };
199
319
  });
@@ -220,7 +340,7 @@ export type LandSkip = (typeof LAND_SKIPS)[number];
220
340
  * Spec 1.1 §6: a milestone lands only with a reviewer record and a verifier verdict since its lanes
221
341
  * started, or with a `skip` the commit range bears out. Throws E_LAND_GATE naming what is missing.
222
342
  */
223
- async function gate(run: Run, m: string, commit: string, skip: LandSkip | undefined): Promise<void> {
343
+ async function gate(run: Run, m: string, commit: string, skip: LandSkip | undefined): Promise<string[]> {
224
344
  // spec 1.1 §8: a parked milestone waits on the owner, whatever else it has
225
345
  const question = openQuestions(run).find((q) => q.milestone === m);
226
346
  if (question)
@@ -255,7 +375,7 @@ async function gate(run: Run, m: string, commit: string, skip: LandSkip | undefi
255
375
  }
256
376
  const against =
257
377
  skip === "docs-only" ? files.filter((f) => !isDocPath(f)) : files.filter((f) => isSourcePath(f));
258
- if (against.length === 0) return;
378
+ if (against.length === 0) return [];
259
379
  const shown = `${against.slice(0, 5).join(", ")}${against.length > 5 ? `, and ${against.length - 5} more` : ""}`;
260
380
  throw new CatherdError(
261
381
  "E_LAND_GATE",
@@ -264,8 +384,27 @@ async function gate(run: Run, m: string, commit: string, skip: LandSkip | undefi
264
384
  );
265
385
  }
266
386
  const start = milestoneStart(run, m);
387
+ // plan 23: a reviewer that stopped partial read part of the diff; that is not the milestone's review
388
+ const partial = partialReviewer(run, m, start);
389
+ if (partial)
390
+ throw new CatherdError(
391
+ "E_LAND_GATE",
392
+ `land ${m}: ${partial.name} replied STATUS: partial, which is not the milestone's review`,
393
+ {
394
+ fix: `dispatch a scoped second pass, reviewer-${m}-2, over the files ${partial.name} did not read (its reply names them), then land again`,
395
+ },
396
+ );
267
397
  // the latest verifier attempt, passed or not: a FAIL after a PASS undoes it
268
398
  const verdict = milestoneVerifier(run, m, start);
399
+ // plan 23: a verdict on the machine is a blocker for the owner, never a fix round
400
+ if (verdict?.blocked != null)
401
+ throw new CatherdError(
402
+ "E_LAND_GATE",
403
+ `land ${m}: ${verdict.name} is blocked by the environment${verdict.blocked ? `: ${verdict.blocked}` : ""}`,
404
+ {
405
+ fix: `surface it to the owner: park(run, "${m}", <the blocker and its probe>); once the environment is fixed, run a fresh verifier on ${m}. A fix round or a climb cannot help`,
406
+ },
407
+ );
269
408
  const missing = [
270
409
  ...(reviewerPassed(run, m, start)
271
410
  ? []
@@ -275,37 +414,123 @@ async function gate(run: Run, m: string, commit: string, skip: LandSkip | undefi
275
414
  ...(verdict?.passed
276
415
  ? []
277
416
  : [
278
- `a verifier verdict (record_agent_run with role verifier and a name holding ${m}, status ok; a headless verifier's reply opening VERDICT: PASS)${verdict ? `: the latest, ${verdict.name}${verdict.headless ? " (headless)" : ""}, is ${verdict.verdict}` : ""}`,
417
+ `a verifier verdict (record_agent_run with role verifier named exactly verifier-${m}, status ok; a headless verifier-${m}'s reply opening VERDICT: PASS)${verdict ? `: the latest, ${verdict.name}${verdict.headless ? " (headless)" : ""}, is ${verdict.verdict}` : ""}`,
279
418
  ]),
280
419
  ];
420
+ // a headless verifier that wrapped its verdict in markdown (**VERDICT: PASS**) gave no verdict: fail safe, and say why
421
+ const marked =
422
+ verdict?.headless === true && !verdict.passed && /VERDICT:\s*PASS\b/.test(verdict.verdict)
423
+ ? `the reply's first line must be the bare VERDICT: PASS, and ${verdict.name}'s is "${verdict.verdict}", which counts as no verdict (markdown around it, or text before it): dispatch the verifier again; otherwise `
424
+ : "";
281
425
  if (missing.length)
282
426
  throw new CatherdError(
283
427
  "E_LAND_GATE",
284
428
  `land ${m}: missing ${missing.join(" and ")}, since its lanes started`,
285
429
  {
286
- fix: `run reviewer-${m} (a dispatch, or a Claude subagent recorded with record_agent_run(name: "reviewer-${m}")) and the verifier on ${m}, recording it with record_agent_run(name: "verifier-${m}") (a FAIL with status "failed"), then land again; a docs-only milestone passes skip: "docs-only"`,
430
+ fix: `${marked}run reviewer-${m} (a dispatch, or a Claude subagent recorded with record_agent_run(name: "reviewer-${m}")) and the verifier on ${m}, recording it with record_agent_run(name: "verifier-${m}") (a FAIL with status "failed"), then land again; a docs-only milestone passes skip: "docs-only"`,
287
431
  },
288
432
  );
433
+ // plan 30b: the verdict must be about the commit being landed, and catherd's own check must have run on it
434
+ const landed = await fullCommit(run, commit);
435
+ const checked = verdict?.head ?? null;
436
+ if (verdict && checked === null)
437
+ throw new CatherdError(
438
+ "E_LAND_GATE",
439
+ `land ${m}: ${verdict.name}'s PASS names no HEAD, so it cannot be told to cover ${landed.slice(0, 12)}`,
440
+ {
441
+ fix: `a headless verifier's reply ends with HEAD: <sha>; run a fresh verifier-${m} on the committed milestone (${landed.slice(0, 7)})`,
442
+ },
443
+ );
444
+ if (verdict && checked !== null && !landed.startsWith(checked))
445
+ throw new CatherdError(
446
+ "E_LAND_GATE",
447
+ `land ${m}: ${verdict.name} checked ${checked.slice(0, 12)}, and the commit being landed is ${landed.slice(0, 12)}: a PASS from before a later commit does not count`,
448
+ {
449
+ fix: `run a fresh ${verdict.name} on ${landed.slice(0, 7)}: what passed is carried over by gate_check`,
450
+ },
451
+ );
452
+ const machine = await machineGate(run, m, landed);
453
+ if (machine.missing.length)
454
+ throw new CatherdError(
455
+ "E_LAND_GATE",
456
+ `land ${m}: the machine gate on ${landed.slice(0, 12)}: ${machine.missing.join("; ")}`,
457
+ {
458
+ fix: `run the full check with gate_run(run, "${m}", <command>) on ${landed.slice(0, 7)}; fix what fails, commit, and verify again`,
459
+ },
460
+ );
461
+ return machine.audit;
289
462
  }
290
463
 
291
464
  /**
292
465
  * Spec §4.7: the full five-column ledger row, with the minutes since the previous landing (or the
293
466
  * run's start), and `learned` appended to the repo's knowledge.md.
294
467
  */
295
- export async function land(
296
- deps: Deps,
297
- i: {
298
- run: string;
299
- milestone: string;
300
- what: string;
301
- commit: string;
302
- evidence: string;
303
- next: string;
304
- learned?: string;
305
- skip?: LandSkip;
306
- },
307
- ): Promise<{ ledger: string; minutes: number; digest: string; hints?: string[] }> {
468
+ type LandInput = {
469
+ run: string;
470
+ milestone: string;
471
+ what: string;
472
+ commit: string;
473
+ evidence: string;
474
+ next: string;
475
+ learned?: string;
476
+ skip?: LandSkip;
477
+ };
478
+
479
+ type LandResult = { ledger: string; minutes: number; digest: string; digestPath: string; hints?: string[] };
480
+
481
+ export async function land(deps: Deps, i: LandInput): Promise<LandResult> {
308
482
  const run = findRun(i.run);
483
+ const link = run.meta.workspace;
484
+ if (!link) return landRun(deps, i, run);
485
+ const workspace = findWorkspace(link.id);
486
+ const step = workspace.steps.find((s) => s.id === link.step);
487
+ if (!step || !workspaceChildren(workspace).some((child) => child.dir === run.dir))
488
+ throw new CatherdError("E_RUN_CORRUPT", "the run is not a member of its workspace execution");
489
+ // #43 finding 3: only the step's completion milestone waits for the child's dispatches
490
+ if (i.milestone !== step.milestone) {
491
+ const landed = await landRun(deps, i, run);
492
+ if (i.milestone.toLowerCase() !== step.milestone.toLowerCase()) return landed;
493
+ // #43 finding 7: m1 is not M1; the dependents wait on the step's own milestone
494
+ const hint = `${i.milestone} is not ${step.milestone}, the milestone workspace step ${step.id} completes on: its dependents still wait`;
495
+ return { ...landed, hints: [...(landed.hints ?? []), hint] };
496
+ }
497
+ // An unreadable newer native verdict must not leave an older PASS standing.
498
+ readAgentRuns(run, true);
499
+ // Completion and dispatch admission share a boundary, held only for the check and the ledger row: the git
500
+ // work runs outside it (#43 finding 5). Landing remains available at a spent budget.
501
+ return landRun(deps, i, run, (write) =>
502
+ withFileLock(
503
+ workspacePaths(workspaceDirectory(workspace.id)).admission,
504
+ async () => {
505
+ const pending = await withFileLock(runPaths(run.dir).runs, () => {
506
+ const { records, corrupt } = readRecords(run);
507
+ if (corrupt)
508
+ throw new CatherdError("E_RUN_CORRUPT", "workspace completion has unreadable dispatch records", {
509
+ fix: "repair the child's dispatch records before landing its milestone",
510
+ });
511
+ return pendingDispatches(run, deps.now(), records, true);
512
+ });
513
+ if (pending.length)
514
+ throw new CatherdError("E_LAND_GATE", "workspace completion waits for all admitted dispatches", {
515
+ fix: "finish and collect the child's dispatches before landing its milestone",
516
+ });
517
+ return write();
518
+ },
519
+ { timeoutMs: WORKSPACE_LOCK_WAIT_MS },
520
+ ),
521
+ );
522
+ }
523
+
524
+ /**
525
+ * The checks (commit, gate) first, then the ledger row and the notes, inside `critical` (a workspace
526
+ * completion's lock), with no git; then state.md, the lane outcomes, knowledge and the digest.
527
+ */
528
+ async function landRun(
529
+ deps: Deps,
530
+ i: LandInput,
531
+ run: Run,
532
+ critical: (write: () => Promise<Notes>) => Promise<Notes> = (write) => write(),
533
+ ): Promise<LandResult> {
309
534
  // the digest is named after the milestone: an id, checked before anything is written
310
535
  assertId("milestone", i.milestone);
311
536
  // commitExists throws E_IO_UNEXPECTED on a timeout, which reaches the caller as is
@@ -313,21 +538,29 @@ export async function land(
313
538
  throw new CatherdError("E_RUN_COMMIT", `no commit ${i.commit} in ${run.meta.repo}`, {
314
539
  fix: "commit the milestone first, then pass its hash",
315
540
  });
316
- await gate(run, i.milestone, i.commit, i.skip);
541
+ const audit = await gate(run, i.milestone, i.commit, i.skip);
542
+ // read before the ledger row: the range runs from the previous landing to this commit
543
+ const commits = await milestoneCommits(run, i.commit);
317
544
  const now = new Date(deps.now());
318
545
  let row = "";
319
546
  let minutes = 0;
547
+ let pausedMinutes = 0;
548
+ // spec 1.5 "The ledger": the minutes leave out the time the milestone was parked and any machine or
549
+ // workspace pause, so a night parked on a question does not count
550
+ const paused = [...parkedSpans(run, i.milestone), ...pauseSpans(run)];
320
551
  const landRow = (notes: Notes): NotesPatch => {
321
- minutes = Math.max(
322
- 0,
323
- Math.round((now.getTime() - Date.parse(notes.lastLandedAt ?? run.meta.createdAt)) / 60_000),
324
- );
552
+ const from = Date.parse(notes.lastLandedAt ?? run.meta.createdAt);
553
+ const left = coveredMs(from, now.getTime(), paused);
554
+ pausedMinutes = Math.round(left / 60_000);
555
+ minutes = Math.max(0, Math.round((now.getTime() - from - left) / 60_000));
325
556
  row = [i.milestone, i.what, i.commit, String(minutes), i.evidence].map(cell).join(" | ");
326
557
  appendLedger(run, row);
327
558
  return { lastCheck: cell(i.evidence), next: i.next, lastLandedAt: now.toISOString() };
328
559
  };
329
- // on a failed refresh the notes still reach state.json, so the next landing counts its minutes from this one
330
- const { hints } = await refreshState(run, landRow);
560
+ // the notes reach state.json first, with no git: the next landing counts its minutes from this one
561
+ await critical(() => patchNotes(run, landRow));
562
+ const { hints } = await refreshState(run);
563
+ hints.push(...audit);
331
564
  // spec §5.6: every routed lane of the milestone lands with it
332
565
  // under the routes lock, so a racing climb cannot slip between the read and the rows
333
566
  let routed: string[] = [];
@@ -339,7 +572,7 @@ export async function land(
339
572
  if (lane.startsWith(`${i.milestone}.`)) {
340
573
  landed++;
341
574
  const o = laneOutcome(routes, lane, true, now.toISOString());
342
- if (o) appendOutcome(run, o);
575
+ if (o) writeOutcome(run, routes, o);
343
576
  }
344
577
  });
345
578
  // a milestone name no routed lane starts with is most likely a typo: say so rather than record nothing
@@ -347,7 +580,7 @@ export async function land(
347
580
  hints.push(
348
581
  `land: no routed lane is in milestone "${i.milestone}" (routed: ${routed.slice(0, 5).join(", ")}${routed.length > 5 ? ", …" : ""}); check its name: no lane outcome was recorded`,
349
582
  );
350
- if (i.learned) appendKnowledge(run.meta.repo, now, `${run.meta.title} ${i.milestone}`, i.learned);
583
+ if (i.learned) await appendKnowledge(run.meta.repo, now, `${run.meta.title} ${i.milestone}`, i.learned);
351
584
  // spec 1.1 §10: the milestone's digest, which the milestone push links
352
585
  const digest = writeDigest(run, {
353
586
  milestone: i.milestone,
@@ -356,8 +589,11 @@ export async function land(
356
589
  evidence: i.evidence,
357
590
  minutes,
358
591
  at: now.toISOString(),
592
+ commits,
593
+ pausedMinutes,
359
594
  });
360
- return { ledger: row, minutes, digest, ...withHints(hints) };
595
+ // spec 1.5: the digest's full path, for the milestone push to link
596
+ return { ledger: row, minutes, digest, digestPath: join(run.dir, digest), ...withHints(hints) };
361
597
  }
362
598
 
363
599
  /** Jev's `finding` or `same-defect` answer, through the routing port. */