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,72 @@
1
+ import { z } from "zod";
2
+ import { CatherdError } from "./errors.ts";
3
+
4
+ /** One model at one effort, as the Slopalytics proxy indexes it: its intelligence and the facts that come with it. */
5
+ export interface SlopRecord {
6
+ family: string;
7
+ effort: string;
8
+ intelligence: number;
9
+ inputPrice: number | null;
10
+ outputPrice: number | null;
11
+ speed: number | null;
12
+ context: number | null;
13
+ }
14
+
15
+ const RecordSchema = z.object({
16
+ family: z.string(),
17
+ effort: z.string(),
18
+ intelligence: z.number(),
19
+ inputPrice: z.number().nullable(),
20
+ outputPrice: z.number().nullable(),
21
+ speed: z.number().nullable(),
22
+ context: z.number().nullable(),
23
+ });
24
+
25
+ /** `<data>/slopalytics.json`: the last good answer, when it was fetched and checked, and the last failure. */
26
+ export const IndexFileSchema = z.looseObject({
27
+ schema: z.literal(1),
28
+ checkedAt: z.iso.datetime(),
29
+ fetchedAt: z.string().nullable(),
30
+ error: z.object({ at: z.string(), message: z.string() }).nullable(),
31
+ models: z.array(RecordSchema),
32
+ });
33
+ export type IndexFile = z.infer<typeof IndexFileSchema>;
34
+
35
+ /** A finite number or a finite numeric string (the proxy sends some prices as strings), else null. */
36
+ const num = (v: unknown): number | null => {
37
+ const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
38
+ return typeof n === "number" && Number.isFinite(n) ? n : null;
39
+ };
40
+
41
+ /** The proxy's answer as the records catherd ranks on: only a record with a positive intelligence is a pair. */
42
+ export function parseProxy(body: unknown): { fetchedAt: string | null; models: SlopRecord[] } {
43
+ const b = body as { fetchedAt?: unknown; models?: unknown } | null;
44
+ if (typeof b !== "object" || b === null || !Array.isArray(b.models))
45
+ throw new CatherdError("E_INPUT_INVALID", "Slopalytics answered without a models list");
46
+ const models: SlopRecord[] = [];
47
+ for (const raw of b.models) {
48
+ const r = (raw ?? {}) as Record<string, unknown>;
49
+ const intelligence = num(r.intelligence);
50
+ if (typeof r.family !== "string" || !r.family) continue;
51
+ if (typeof r.effort !== "string" || !r.effort) continue;
52
+ if (intelligence === null || intelligence <= 0) continue;
53
+ const context = num(r.context);
54
+ models.push({
55
+ family: r.family,
56
+ effort: r.effort,
57
+ intelligence,
58
+ inputPrice: num(r.inputPrice),
59
+ outputPrice: num(r.outputPrice),
60
+ speed: num(r.speed),
61
+ context: context !== null && Number.isInteger(context) && context > 0 ? context : null,
62
+ });
63
+ }
64
+ return { fetchedAt: typeof b.fetchedAt === "string" ? b.fetchedAt : null, models };
65
+ }
66
+
67
+ /** A model's Slopalytics family key: its provider dropped, lower case, dots as dashes (`gpt-6.1-sol` → `gpt-6-1-sol`). */
68
+ export const slopKey = (model: string): string =>
69
+ model
70
+ .slice(model.lastIndexOf("/") + 1)
71
+ .toLowerCase()
72
+ .replaceAll(".", "-");
@@ -0,0 +1,39 @@
1
+ import { defineCommand } from "citty";
2
+ import { CatherdError } from "../domain/errors.ts";
3
+ import { answerUnit } from "../services/run-client.ts";
4
+ import { defaultDeps } from "./deps.ts";
5
+ import { JSON_ARG, printJson, refuseInRole } from "./cli-kit.ts";
6
+
7
+ /** What the phone message names: the run, the unit and the answer, as one command. */
8
+ export const answerCommand = defineCommand({
9
+ meta: {
10
+ name: "answer",
11
+ description: "Answer a parked unit of a run: it runs again with your answer",
12
+ },
13
+ args: {
14
+ run: { type: "positional", required: true, description: "the run id" },
15
+ unit: {
16
+ type: "positional",
17
+ required: true,
18
+ description: "the parked unit: the run, a milestone or a lane",
19
+ },
20
+ answer: {
21
+ type: "positional",
22
+ required: false,
23
+ description: "your answer (several words need no quotes)",
24
+ },
25
+ ...JSON_ARG,
26
+ },
27
+ async run({ args }) {
28
+ refuseInRole(process.env, "catherd answer");
29
+ // citty names the first words; the rest of the line is the answer's too
30
+ const answer = (args._ as string[]).slice(2).join(" ").trim();
31
+ if (!answer)
32
+ throw new CatherdError("E_INPUT_INVALID", "no answer given", {
33
+ fix: 'catherd answer <run> <unit> "<your answer>"',
34
+ });
35
+ const r = await answerUnit(defaultDeps(), { run: args.run, unit: args.unit, answer });
36
+ if (args.json) return printJson(r);
37
+ console.log(`answered ${r.unit}${r.parked.length ? `; still waiting: ${r.parked.join(", ")}` : ""}`);
38
+ },
39
+ });
@@ -1,4 +1,3 @@
1
- import { HOST_ARG, terminalHost } from "./host-arg.ts";
2
1
  import { defineCommand } from "citty";
3
2
  import { CatherdError, isCatherdError } from "../domain/errors.ts";
4
3
  import { ROLES, type Role } from "../domain/roles.ts";
@@ -6,68 +5,35 @@ import { gitToplevel } from "../infra/git.ts";
6
5
  import {
7
6
  type CatalogModel,
8
7
  catalogQuery,
8
+ type IndexStatus,
9
9
  type Refreshed,
10
10
  refreshDiscovery,
11
- saveTreatLike,
11
+ refreshIndex,
12
12
  } from "../services/catalog-service.ts";
13
- import { readDerived } from "../infra/sources/cache.ts";
14
- import { type SyncReport, syncSources } from "../services/source-sync.ts";
15
- import { clearTreatLike, type LeftOnStandIn, resetTreatLikes, suggestFor } from "../services/treat-likes.ts";
16
- import { exitCodeOf, JSON_ARG, mark, printError, refuseInRole } from "./cli-kit.ts";
13
+ import { indexCheck } from "../services/doctor-checks.ts";
14
+ import { exitCodeOf, JSON_ARG, mark, printError } from "./cli-kit.ts";
17
15
 
18
- /**
19
- * Spec 1.2 §9: a sync, one line per source (fetched, fresh, skipped, or failed with the answer it kept),
20
- * then the rungs newly scored, the user's stand-ins no longer needed and the cross-check warnings.
21
- */
22
- export function syncLines(r: SyncReport, plain = false): string[] {
23
- if (r.busy) return ["- sources: another sync is running; its results apply when it ends"];
24
- const out: string[] = [];
25
- for (const s of r.sources) {
26
- if (s.state === "fetched") out.push(`${mark("ok", plain)} ${s.source}: fetched`);
27
- else if (s.state === "fresh") out.push(`- ${s.source}: fresh (fetched ${s.fetchedAt})`);
28
- else if (s.state === "skipped") out.push(`- ${s.source}: ${s.detail}`);
29
- else
30
- out.push(
31
- `${mark("warn", plain)} ${s.source}: ${s.error}; ${s.fetchedAt ? `keeps the answer fetched ${s.fetchedAt}` : "no earlier answer"}`,
32
- );
33
- }
34
- if (r.newlyScored.length) out.push(`newly scored: ${r.newlyScored.join(", ")}`);
35
- for (const n of r.noLongerNeeded)
36
- out.push(`stand-in no longer needed: ${n.rung} has values of its own for what ${n.like} lent it`);
37
- for (const w of r.warnings) out.push(`${mark("warn", plain)} ${w}`);
38
- return out;
16
+ /** `sync`'s one line: the index's pair count and age, or why the last fetch failed and what was kept. */
17
+ export function syncLine(s: IndexStatus, plain = false, now = Date.now()): string {
18
+ const c = indexCheck(now, s);
19
+ return `${mark(c.state === "ok" ? "ok" : "warn", plain)} ${c.label}: ${c.detail}`;
39
20
  }
40
21
 
41
22
  const sync = defineCommand({
42
23
  meta: {
43
24
  name: "sync",
44
25
  description:
45
- "Fetch the public model facts and scores now (each source at most every 12 hours unless --force) and show what changed",
26
+ "Fetch the Slopalytics index now (it is refreshed at most hourly unless --force) and show how many pairs it holds",
46
27
  },
47
28
  args: {
48
- force: { type: "boolean", description: "fetch every source, however fresh" },
49
- unmatched: {
50
- type: "boolean",
51
- description: "also list each source's ids that match no model in the catalog",
52
- },
29
+ force: { type: "boolean", description: "fetch even when the index was checked within the hour" },
53
30
  ...JSON_ARG,
54
31
  },
55
32
  async run({ args }) {
56
- try {
57
- const r = await syncSources({ force: args.force === true });
58
- if (args.json) console.log(JSON.stringify(r, null, 2));
59
- else for (const l of syncLines(r)) console.log(l);
60
- if (args.unmatched && !args.json) {
61
- const unmatched = readDerived()?.unmatched ?? {};
62
- if (Object.keys(unmatched).length === 0) console.log("unmatched: none");
63
- for (const [source, ids] of Object.entries(unmatched))
64
- console.log(`unmatched in ${source}: ${ids.join(", ")}`);
65
- }
66
- const got = r.sources.some((s) => s.state === "fetched" || s.state === "fresh");
67
- process.exitCode = r.failed.length && !got ? 1 : 0;
68
- } catch (e) {
69
- fail(e);
70
- }
33
+ const s = await refreshIndex({ force: args.force === true });
34
+ if (args.json) console.log(JSON.stringify(s, null, 2));
35
+ else console.log(syncLine(s));
36
+ process.exitCode = s.pairs === 0 ? 1 : 0;
71
37
  },
72
38
  });
73
39
 
@@ -80,29 +46,19 @@ export function formatRefreshed(r: Refreshed, plain = false): string {
80
46
  ].join("\n");
81
47
  }
82
48
 
83
- /**
84
- * One model: its rungs scored and roles, then (spec 1.2 §4.1, §5.3, §8) its price and speed facts, and each
85
- * rung's values with their confidence and source, and the runs catherd has made on it.
86
- */
49
+ /** One model: its indexed pairs and roles, then each pair's intelligence, price, speed and the runs catherd has made on it. */
87
50
  export function formatModel(m: CatalogModel): string {
88
- const scored = m.rungs.filter((r) => r.enabled).length;
89
51
  const listed = m.listed === false ? " not offered by this account" : "";
90
52
  const lines = [
91
- `${m.backend}:${m.model} ${scored}/${m.rungs.length} rungs scored roles ${m.roles.join(",") || "none"}${listed}`,
92
- ];
93
- const facts = [
94
- ...(m.price ? [`$${m.price.input}/$${m.price.output} per M tokens in/out`] : []),
95
- ...Object.entries(m.speed).map(([k, v]) => `${k} ${Number(v.toPrecision(4))}`),
53
+ `${m.backend}:${m.model} ${m.rungs.length} ${m.rungs.length === 1 ? "pair" : "pairs"} roles ${m.roles.join(",") || "none"}${listed}`,
96
54
  ];
97
- if (facts.length) lines.push(` ${facts.join(" · ")}`);
98
55
  for (const r of m.rungs) {
99
- // spec 1.2 §5.3: each value with its confidence and source, as `route` reports it; a guess names its rung
100
- const values = Object.entries(r.scores).map(
101
- ([dim, v]) =>
102
- `${dim} ${Number(v.value.toPrecision(4))} (${v.from ? (v.lent === "treat-like" ? `like ${v.from}` : `inferred from ${v.from}`) : v.confidence}, ${v.source})`,
103
- );
104
- if (values.length === 0 && !r.evidence) continue;
105
- lines.push(` #${r.rung.slice(r.rung.lastIndexOf("#") + 1)} ${values.join(" · ") || "unscored"}`);
56
+ const parts = [
57
+ `intelligence ${r.intelligence.toFixed(1)}`,
58
+ ...(r.price ? [`$${r.price.input}/$${r.price.output} per M`] : []),
59
+ ...(r.speed !== null ? [`${Number(r.speed.toPrecision(3))} tok/s`] : []),
60
+ ];
61
+ lines.push(` #${r.rung.slice(r.rung.lastIndexOf("#") + 1)} ${parts.join(" · ")}`);
106
62
  if (r.evidence) lines.push(` runs: ${r.evidence}`);
107
63
  }
108
64
  return lines.join("\n");
@@ -129,12 +85,11 @@ const refresh = defineCommand({
129
85
  });
130
86
 
131
87
  const list = defineCommand({
132
- meta: { name: "list", description: "The models catherd can place, with their scored rungs" },
88
+ meta: { name: "list", description: "The models catherd can place, with their indexed pairs" },
133
89
  args: {
134
90
  backend: { type: "string", description: "only this backend or billing key" },
135
91
  role: { type: "string", description: `only models for this role (${ROLES.join(", ")})` },
136
92
  text: { type: "string", description: "only ids containing this" },
137
- scored: { type: "boolean", description: "only models with a scored rung" },
138
93
  ...JSON_ARG,
139
94
  },
140
95
  async run({ args }) {
@@ -149,7 +104,6 @@ const list = defineCommand({
149
104
  backend: args.backend,
150
105
  role: args.role as Role | undefined,
151
106
  text: args.text,
152
- scoredOnly: args.scored === true,
153
107
  limit: 10_000,
154
108
  repo: await hereRepo(),
155
109
  });
@@ -161,111 +115,8 @@ const list = defineCommand({
161
115
  },
162
116
  });
163
117
 
164
- /** Spec 1.2 §6.4: the profile rungs a removal leaves on an inferred stand-in or unscored, one line each. */
165
- export function leftLines(left: LeftOnStandIn[], plain = false): string[] {
166
- return left.map((l) => {
167
- const what = l.unscored
168
- ? ["unscored: routing skips it"]
169
- : [
170
- ...(l.dims.length ? [`on an inferred stand-in for ${l.dims.join(", ")}`] : []),
171
- ...(l.missing?.length
172
- ? [`with no ${l.missing.join(", ")} value: it clears no bar that needs one`]
173
- : []),
174
- ];
175
- return `${mark("warn", plain)} ${l.profile}: ${l.rung} is left ${what.join(", and ")}`;
176
- });
177
- }
178
-
179
- /** Spec 1.2 §6.4 `--suggest`: the three nearest stand-ins, each with its distance and the features it rests on. */
180
- export function suggestLines(r: ReturnType<typeof suggestFor>, rung: string): string[] {
181
- const head = r.lacking.length
182
- ? `${r.canonical} has no ${r.lacking.join(", ")} value of its own; the nearest stand-ins:`
183
- : `${r.canonical} has every value the bars use; the nearest scored rungs:`;
184
- if (r.suggestions.length === 0)
185
- return [
186
- `${r.canonical}: no scored rung shares 3 features with it; map it by hand: catherd catalog treat-like ${rung} <rung>`,
187
- ];
188
- return [
189
- head,
190
- ...r.suggestions.map(
191
- (s, i) =>
192
- ` ${i + 1}. ${s.like} distance ${s.distance.toFixed(2)} lends ${s.lends.join(", ")} on ${s.features.join(", ")}`,
193
- ),
194
- `map one: catherd catalog treat-like ${rung} <rung>`,
195
- ];
196
- }
197
-
198
- const treatLike = defineCommand({
199
- meta: {
200
- name: "treat-like",
201
- description:
202
- "Score an unscored rung as a scored one; --suggest ranks stand-ins, --clear and --reset remove your mappings",
203
- },
204
- args: {
205
- ...HOST_ARG,
206
- rung: {
207
- type: "positional",
208
- required: false,
209
- description: "the unscored rung, backend:model#effort or model#effort",
210
- },
211
- like: { type: "positional", required: false, description: "the scored rung whose scores it borrows" },
212
- suggest: { type: "string", description: "list the 3 nearest stand-ins for this rung" },
213
- clear: { type: "string", description: "remove your treat-like for this rung" },
214
- reset: { type: "boolean", description: "remove every treat-like of yours" },
215
- ...JSON_ARG,
216
- },
217
- async run({ args }) {
218
- try {
219
- const modes = [
220
- args.suggest !== undefined,
221
- args.clear !== undefined,
222
- args.reset === true,
223
- args.rung !== undefined,
224
- ];
225
- if (modes.filter(Boolean).length !== 1 || (args.rung !== undefined && args.like === undefined))
226
- throw new CatherdError(
227
- "E_INPUT_INVALID",
228
- "treat-like takes <rung> <like>, or one of --suggest, --clear, --reset",
229
- {
230
- fix: "catherd catalog treat-like <rung> <like> | --suggest <rung> | --clear <rung> | --reset",
231
- },
232
- );
233
- if (args.suggest !== undefined) {
234
- const r = suggestFor(args.suggest);
235
- if (args.json) console.log(JSON.stringify(r, null, 2));
236
- else for (const l of suggestLines(r, args.suggest)) console.log(l);
237
- return;
238
- }
239
- refuseInRole(process.env, "catherd catalog treat-like");
240
- if (args.clear !== undefined) {
241
- const r = await clearTreatLike(args.clear, terminalHost(args.host).host);
242
- if (args.json) return console.log(JSON.stringify(r, null, 2));
243
- for (const l of leftLines(r.left)) console.log(l);
244
- console.log(`${mark("ok")} ${r.rung} is no longer treated like ${r.like}`);
245
- return;
246
- }
247
- if (args.reset) {
248
- const r = await resetTreatLikes(terminalHost(args.host).host);
249
- if (args.json) return console.log(JSON.stringify(r, null, 2));
250
- for (const l of leftLines(r.left)) console.log(l);
251
- console.log(
252
- r.removed.length
253
- ? `${mark("ok")} removed ${r.removed.length} treat-like${r.removed.length === 1 ? "" : "s"}: ${r.removed.map(([a, b]) => `${a} → ${b}`).join(", ")}`
254
- : "no treat-like of yours to remove",
255
- );
256
- return;
257
- }
258
- const r = await saveTreatLike(args.rung as string, args.like as string);
259
- if (args.json) return console.log(JSON.stringify(r, null, 2));
260
- console.log(`${mark("ok")} ${r.rung} is treated like ${r.like}`);
261
- } catch (e) {
262
- fail(e);
263
- }
264
- },
265
- });
266
-
267
- /** Spec §8 and 1.2 §9: `catherd catalog refresh|list|sync|treat-like <rung> <like> | --suggest|--clear|--reset`. */
118
+ /** Spec §8: `catherd catalog refresh|list|sync`. */
268
119
  export const catalogCommand = defineCommand({
269
120
  meta: { name: "catalog", description: "The model catalog" },
270
- subCommands: { refresh, list, sync, "treat-like": treatLike },
121
+ subCommands: { refresh, list, sync },
271
122
  });
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bun
2
+ // The detached check worker's entry: it imports only what running one milestone check needs, never src/cli.ts.
3
+ import { errorMessage } from "../domain/errors.ts";
4
+ import { checkWorker } from "../services/milestone-check.ts";
5
+
6
+ const flight = process.argv[2];
7
+ if (!flight) {
8
+ console.error("usage: check-bin.ts <flight file>");
9
+ process.exit(2);
10
+ }
11
+ try {
12
+ await checkWorker(flight);
13
+ } catch (e) {
14
+ console.error(`catherd check: ${errorMessage(e)}`);
15
+ process.exit(1);
16
+ }
17
+ process.exit(0);
@@ -46,7 +46,7 @@ export const printJson = (v: unknown): void => console.log(JSON.stringify(v, nul
46
46
  export const JSON_ARG = { json: { type: "boolean", description: "print JSON" } } as const;
47
47
 
48
48
  /**
49
- * The glyph for a state, as in `✓ ready`: the dashboard's own (spec §9.3), ASCII with `--plain` only;
49
+ * The glyph for a state, as in `✓ ready`: catherd's own (spec §9.3), ASCII with `--plain` only;
50
50
  * NO_COLOR drops colour, never glyphs. The caller adds the word.
51
51
  */
52
52
  export const mark = (state: State, plain = false): string => glyph(state, plain);
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bun
2
+ // The detached run daemon's entry: like the supervisor's it imports only what driving a run needs, never
3
+ // src/cli.ts.
4
+ import { errorMessage } from "../domain/errors.ts";
5
+ import { runDaemon } from "../services/daemon.ts";
6
+ import { defaultDeps } from "./deps.ts";
7
+
8
+ const runId = process.argv[2];
9
+ if (!runId) {
10
+ console.error("usage: daemon-bin.ts <run id>");
11
+ process.exit(2);
12
+ }
13
+ try {
14
+ // nobody's role and no Claude Code session: the daemon answers to the run's events
15
+ const result = await runDaemon({ ...defaultDeps(), session: null, role: null }, runId);
16
+ if (result === "lost-lock") console.error(`catherd daemon: another daemon holds run ${runId}`);
17
+ } catch (e) {
18
+ console.error(`catherd daemon: ${errorMessage(e)}`);
19
+ process.exit(1);
20
+ }
21
+ // the lock is released and the run's events are written: a hook's call left hanging must not keep it alive
22
+ process.exit(0);
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Spec §9.3's glyphs, each with its ASCII twin for `--plain`: the dashboard and every CLI command draw
3
- * state with these. Free of OpenTUI, so `mcp`, `lock` and `_supervise` can load it (spec §3.1).
2
+ * Spec §9.3's glyphs, each with its ASCII twin for `--plain`: every CLI command draws state with these, so
3
+ * `mcp`, `lock` and `_supervise` can load them (spec §3.1).
4
4
  */
5
5
  const GLYPHS = {
6
6
  ok: ["✓", "+"],
@@ -32,3 +32,16 @@ export const glyph = (g: Glyph, plain: boolean): string => GLYPHS[g][plain ? 1 :
32
32
 
33
33
  /** The words state always travels with (spec §9.3: green/amber/red only for state, always with a word). */
34
34
  export type State = "ok" | "warn" | "fail" | "skip" | "info";
35
+
36
+ export type Mood = "good" | "working" | "waiting" | "failed";
37
+ export const FACES: Record<Mood, string> = {
38
+ good: "=^.^=",
39
+ working: "=o.o=",
40
+ waiting: "=-.-=",
41
+ failed: "=x.x=",
42
+ };
43
+
44
+ /** The conductor, for the init welcome only (spec §9.3). ASCII already. */
45
+ export function mascot(mood: Mood): [string, string, string] {
46
+ return [" /\\_/\\ .", `(${FACES[mood]})/`, ' (")(")'].map((l) => l.padEnd(9)) as [string, string, string];
47
+ }
@@ -6,20 +6,17 @@ import { errorMessage, isCatherdError } from "../domain/errors.ts";
6
6
  import { configDir } from "../infra/paths.ts";
7
7
  import { VERSION } from "../infra/version.ts";
8
8
  import { doctor } from "../services/doctor.ts";
9
- import { testAaKey } from "../infra/sources/artificial-analysis.ts";
10
- import { aaKey, saveAaKey } from "../services/credentials.ts";
9
+ import { type IndexStatus, refreshIndex } from "../services/catalog-service.ts";
11
10
  import { jevKey, saveJevKey, testJevKey } from "../services/jev-service.ts";
12
- import { type SyncReport, syncSources } from "../services/source-sync.ts";
13
11
  import { reinstallCommand } from "../services/doctor-checks.ts";
14
12
  import { ensureGlobal, type GlobalInstallDeps, realGlobalInstall } from "../services/global-install.ts";
15
13
  import { hasProfileFile, type InitResult, initSetup, moveLegacy } from "../services/setup.ts";
16
14
  import { activeName } from "../services/profile-store.ts";
17
- import { formatRefreshed, syncLines } from "./catalog-command.ts";
15
+ import { formatRefreshed } from "./catalog-command.ts";
18
16
  import { mark as markOf, refuseInRole } from "./cli-kit.ts";
19
17
  import { formatReport } from "./doctor-command.ts";
20
18
  import { mcpHandshake } from "./mcp/handshake.ts";
21
- import type { State } from "./glyphs.ts";
22
- import { mascot } from "./tui/theme.ts";
19
+ import { mascot, type State } from "./glyphs.ts";
23
20
  import { type Prompter, prompter } from "./prompt.ts";
24
21
 
25
22
  /** What `init` prints last (spec §9.1): the plugin install commands, to paste into a terminal. */
@@ -30,7 +27,7 @@ export const PLUGIN_STEPS = [
30
27
  "Then start a new Claude Code session, so it loads the plugin and the catherd agents.",
31
28
  ];
32
29
 
33
- /** Spec §9.3: the full mascot greets `init` (and the TUI's empty states), nowhere else. */
30
+ /** Spec §9.3: the full mascot greets `init` nowhere else. */
34
31
  export function welcomeLines(version: string): string[] {
35
32
  const [ears, face, paws] = mascot("good");
36
33
  return [ears, `${face} catherd ${version}`, `${paws} herds your coding agents`, ""];
@@ -72,68 +69,22 @@ export async function jevStep(
72
69
  }
73
70
 
74
71
  /**
75
- * Spec 1.2 §9: the Artificial Analysis key step, after Jev's. ARTIFICIAL_ANALYSIS_API_KEY wins, else the saved
76
- * key, else a prompt (Enter skips). A typed key is tested with one request: a 200 saves it, a 401 does not,
77
- * and a key that cannot be checked (no network) is not saved either. It never stops `init`.
78
- */
79
- export async function aaStep(
80
- ask: Prompter | null,
81
- d: {
82
- testAaKey?: (key: string) => Promise<{ result: "ok" | "refused" | "unchecked"; error?: string }>;
83
- saveAaKey?: (key: string) => void;
84
- } = {},
85
- plain = false,
86
- ): Promise<void> {
87
- const mark = (state: State) => markOf(state, plain);
88
- if (process.env.ARTIFICIAL_ANALYSIS_API_KEY?.trim()) {
89
- ask?.skip?.();
90
- return console.log(`${mark("ok")} Artificial Analysis: using ARTIFICIAL_ANALYSIS_API_KEY`);
91
- }
92
- if (aaKey()) {
93
- ask?.skip?.();
94
- return console.log(`${mark("ok")} Artificial Analysis: using the saved key`);
95
- }
96
- const key = ask ? await ask.secret("Artificial Analysis API key (optional; Enter skips): ") : "";
97
- if (!key)
98
- return console.log(
99
- "- Artificial Analysis: no key; scores come from the keyless sources (add one later with catherd init)",
100
- );
101
- const t = await (d.testAaKey ?? testAaKey)(key).catch((e: unknown) => ({
102
- result: "unchecked" as const,
103
- error: errorMessage(e),
104
- }));
105
- if (t.result === "refused")
106
- return console.log(`${mark("warn")} Artificial Analysis: the key was refused (401), so it was not saved`);
107
- if (t.result === "unchecked")
108
- return console.log(
109
- `${mark("warn")} Artificial Analysis: the key could not be checked (${t.error ?? "no answer"}), so it was not saved; run catherd init again to retry`,
110
- );
111
- try {
112
- (d.saveAaKey ?? saveAaKey)(key);
113
- console.log(`${mark("ok")} Artificial Analysis: the key answers; saved with mode 600`);
114
- } catch (e) {
115
- console.log(
116
- `${mark("warn")} Artificial Analysis: could not save the key: ${errorMessage(e).split("\n").join(" ")}`,
117
- );
118
- if (isCatherdError(e) && e.fix) console.log(` fix: ${e.fix}`);
119
- }
120
- }
121
-
122
- /**
123
- * Spec 1.2 §9: `init` syncs the public sources in the foreground, after the keys. It never stops `init`;
72
+ * `init` refreshes the Slopalytics index in the foreground, after the Jev key. It never stops `init`;
124
73
  * CATHERD_NO_SYNC=1 skips it.
125
74
  */
126
- export async function syncStep(o: { plain?: boolean; sync?: () => Promise<SyncReport> } = {}): Promise<void> {
75
+ export async function syncStep(
76
+ o: { plain?: boolean; sync?: () => Promise<IndexStatus> } = {},
77
+ ): Promise<void> {
127
78
  const plain = o.plain === true;
128
79
  if (process.env.CATHERD_NO_SYNC === "1")
129
- return console.log("- sources: not synced (CATHERD_NO_SYNC=1); catherd catalog sync fetches them");
130
- console.log("syncing the public model sources…");
131
- try {
132
- for (const l of syncLines(await (o.sync ?? (() => syncSources()))(), plain)) console.log(l);
133
- } catch (e) {
134
- console.log(`${markOf("warn", plain)} sources: ${errorMessage(e).split("\n").join(" ")}`);
135
- console.log(" fix: catherd catalog sync");
136
- }
80
+ return console.log("- catalog: not fetched (CATHERD_NO_SYNC=1); catherd catalog sync fetches it");
81
+ console.log("fetching the Slopalytics index…");
82
+ const s = await (o.sync ?? (() => refreshIndex({ auto: true })))().catch(
83
+ (e: unknown) => ({ error: errorMessage(e), pairs: 0 }) as Pick<IndexStatus, "error" | "pairs">,
84
+ );
85
+ if (s.error === null) return console.log(`${markOf("ok", plain)} Slopalytics: ${s.pairs} pairs indexed`);
86
+ console.log(`${markOf("warn", plain)} Slopalytics: ${s.error.split("\n").join(" ")}`);
87
+ console.log(" fix: catherd catalog sync");
137
88
  }
138
89
 
139
90
  /**
@@ -190,12 +141,12 @@ export function profileLines(r: InitResult, plain = false): string[] {
190
141
  return out;
191
142
  }
192
143
 
193
- /** Spec §8 `catherd init [--no-input]`: first-run setup without the TUI (plan 6 adds the TUI wizard). */
144
+ /** Spec §8 `catherd init [--no-input]`: first-run setup without the panel. */
194
145
  export const initCommand = defineCommand({
195
146
  meta: {
196
147
  name: "init",
197
148
  description:
198
- "First run: the global catherd command, the Jev key, the Artificial Analysis key, a sync of the public model sources, the default profile, its agents, and a readiness report. Piped, it reads the answers from stdin one per line, a line per question even when this machine skips it (the Jev key, the Artificial Analysis key, the profile, whether to replace it), and waits for stdin to close; --no-input asks nothing",
149
+ "First run: the global catherd command, the Jev key, a fetch of the Slopalytics model index, the default profile, its agents, and a readiness report. Piped, it reads the answers from stdin one per line, a line per question even when this machine skips it (the Jev key, the profile, whether to replace it), and waits for stdin to close; --no-input asks nothing",
199
150
  },
200
151
  args: {
201
152
  ...HOST_ARG,
@@ -224,7 +175,6 @@ export const initCommand = defineCommand({
224
175
  console.log(`catherd ${VERSION}: setting up in ${configDir()}`);
225
176
  await globalStep(VERSION, { skip: (args as { global?: boolean }).global === false, plain });
226
177
  await jevStep(ask, {}, plain);
227
- await aaStep(ask, {}, plain);
228
178
  await syncStep({ plain });
229
179
  const repo = await gitToplevel(process.cwd());
230
180
  // 0.x files go first, so a legacy profile about to be moved aside is never asked about