catherd-cli 1.0.0 → 1.1.1

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 (83) hide show
  1. package/README.md +66 -42
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/.mcp.json +2 -2
  5. package/plugin/bin/catherd-mcp +30 -0
  6. package/plugin/commands/catherd.md +1 -1
  7. package/plugin/skills/catherd/SKILL.md +112 -95
  8. package/plugin/skills/catherd-setup/SKILL.md +3 -3
  9. package/src/adapters/access.ts +118 -0
  10. package/src/adapters/backend.ts +19 -4
  11. package/src/adapters/claude-code/index.ts +95 -1
  12. package/src/adapters/codex/index.ts +78 -23
  13. package/src/adapters/opencode/index.ts +17 -0
  14. package/src/domain/errors.ts +2 -0
  15. package/src/domain/failover.ts +119 -0
  16. package/src/domain/lane.ts +23 -0
  17. package/src/domain/notice.ts +95 -0
  18. package/src/domain/profile-rules.ts +43 -9
  19. package/src/domain/profile.ts +12 -6
  20. package/src/domain/record.ts +2 -0
  21. package/src/domain/role-prompts.ts +33 -1
  22. package/src/domain/state.ts +18 -2
  23. package/src/entry/deps.ts +2 -1
  24. package/src/entry/doctor-command.ts +7 -1
  25. package/src/entry/glyphs.ts +2 -1
  26. package/src/entry/init-command.ts +41 -1
  27. package/src/entry/mcp/command.ts +5 -1
  28. package/src/entry/mcp/dispatch-tools.ts +10 -40
  29. package/src/entry/mcp/handshake.ts +19 -9
  30. package/src/entry/mcp/lane-tools.ts +7 -6
  31. package/src/entry/mcp/protocol-tools.ts +57 -0
  32. package/src/entry/mcp/run-tools.ts +4 -3
  33. package/src/entry/mcp/server.ts +17 -1
  34. package/src/entry/mcp/setup-tools.ts +1 -1
  35. package/src/entry/profile-command.ts +3 -5
  36. package/src/entry/runs-command.ts +64 -21
  37. package/src/entry/tui/commands.ts +1 -1
  38. package/src/entry/tui/effects.ts +98 -56
  39. package/src/entry/tui/fixtures.ts +352 -85
  40. package/src/entry/tui/profile-edits.ts +2 -2
  41. package/src/entry/tui/profile-tree.ts +2 -1
  42. package/src/entry/tui/providers/data.tsx +35 -8
  43. package/src/entry/tui/run.tsx +11 -1
  44. package/src/entry/tui/state.ts +26 -6
  45. package/src/entry/tui/stories.ts +36 -4
  46. package/src/entry/tui/text.ts +12 -0
  47. package/src/entry/tui/theme.ts +1 -0
  48. package/src/entry/tui/views/runs.tsx +409 -117
  49. package/src/entry/tui/views/status.tsx +5 -2
  50. package/src/entry/tui/widgets/list.tsx +4 -3
  51. package/src/infra/claude-session.ts +98 -0
  52. package/src/infra/dispatch-dir.ts +14 -6
  53. package/src/infra/env.ts +26 -2
  54. package/src/infra/peer-inbox.ts +92 -0
  55. package/src/infra/supervisor.ts +33 -3
  56. package/src/services/admission.ts +38 -12
  57. package/src/services/dispatch-service.ts +338 -197
  58. package/src/services/dispatches.ts +30 -1
  59. package/src/services/doctor-access.ts +231 -0
  60. package/src/services/doctor-backends.ts +3 -18
  61. package/src/services/doctor-checks.ts +46 -1
  62. package/src/services/doctor-push.ts +109 -0
  63. package/src/services/doctor.ts +59 -79
  64. package/src/services/finalize.ts +43 -25
  65. package/src/services/gate-service.ts +288 -0
  66. package/src/services/global-install.ts +99 -0
  67. package/src/services/lane-service.ts +150 -8
  68. package/src/services/milestones.ts +208 -0
  69. package/src/services/notifier.ts +296 -0
  70. package/src/services/peek.ts +103 -0
  71. package/src/services/ports.ts +7 -4
  72. package/src/services/preflight.ts +17 -3
  73. package/src/services/protocol.ts +212 -0
  74. package/src/services/questions.ts +102 -0
  75. package/src/services/reconcile.ts +40 -11
  76. package/src/services/reentry.ts +22 -0
  77. package/src/services/run-service.ts +42 -9
  78. package/src/services/run-store.ts +18 -0
  79. package/src/services/runs-page.ts +254 -0
  80. package/src/services/session-view.ts +152 -0
  81. package/src/services/sessions.ts +92 -0
  82. package/src/services/state.ts +10 -3
  83. package/src/services/summary.ts +14 -0
@@ -120,8 +120,12 @@ export interface AppState {
120
120
  drafts: Record<string, Draft>;
121
121
  /** the open dialogs; only the top one is drawn and takes keys */
122
122
  dialogs: Dialog[];
123
- /** the run the Runs tab has open; null shows the list */
124
- run: string | null;
123
+ /** spec §4: the session the Runs tab has open (`key` null: "earlier runs"); null shows the sessions */
124
+ session: { key: string | null } | null;
125
+ /** the role the open session shows, by its dispatch */
126
+ role: { run: string; dispatchId: string } | null;
127
+ /** spec 1.1 §10: the milestone the open session shows, its digest */
128
+ milestone: { run: string; name: string } | null;
125
129
  paused: boolean;
126
130
  armed: Armed | null;
127
131
  }
@@ -145,7 +149,14 @@ export type Action =
145
149
  | { type: "saving"; name: string; on: boolean }
146
150
  | { type: "input"; value: string }
147
151
  | { type: "invalid"; error: string | null }
148
- | { type: "run"; id: string | null }
152
+ /** opens a session of the Runs tab (`key` null: "earlier runs") */
153
+ | { type: "session"; key: string | null }
154
+ /** opens one role of the open session */
155
+ | { type: "role"; run: string; dispatchId: string }
156
+ /** opens one milestone of the open session, on its digest */
157
+ | { type: "milestone"; run: string; name: string }
158
+ /** the Runs tab goes back one level: a milestone or a role to its session, a session to the list */
159
+ | { type: "up" }
149
160
  | { type: "pause" }
150
161
  | { type: "arm"; what: Armed["what"]; target: string; at: number }
151
162
  | { type: "disarm" };
@@ -155,7 +166,9 @@ export const initialState = (tab: Tab = "status"): AppState => ({
155
166
  profile: null,
156
167
  drafts: {},
157
168
  dialogs: [],
158
- run: null,
169
+ session: null,
170
+ role: null,
171
+ milestone: null,
159
172
  paused: false,
160
173
  armed: null,
161
174
  });
@@ -324,8 +337,15 @@ export function reduce(s: AppState, a: Action): AppState {
324
337
  else if (top.kind === "save" && a.type === "invalid") next = { ...top, error: a.error };
325
338
  return next === top ? s : { ...s, dialogs: [...s.dialogs.slice(0, -1), next] };
326
339
  }
327
- case "run":
328
- return { ...s, run: a.id, armed: null };
340
+ case "session":
341
+ return { ...s, session: { key: a.key }, role: null, milestone: null, armed: null };
342
+ case "role":
343
+ return { ...s, role: { run: a.run, dispatchId: a.dispatchId }, milestone: null, armed: null };
344
+ case "milestone":
345
+ return { ...s, milestone: { run: a.run, name: a.name }, role: null, armed: null };
346
+ case "up":
347
+ if (s.milestone) return { ...s, milestone: null, armed: null };
348
+ return s.role ? { ...s, role: null, armed: null } : { ...s, session: null, armed: null };
329
349
  case "pause":
330
350
  return { ...s, paused: !s.paused };
331
351
  case "arm":
@@ -1,5 +1,6 @@
1
1
  import type { ProfilePatch } from "../../domain/profile.ts";
2
2
  import type { CommandId } from "./commands.ts";
3
+ import type { FixtureOptions } from "./fixtures.ts";
3
4
  import type { AppApi } from "./providers/app.tsx";
4
5
  import type { AppKeymap } from "./providers/keymap.tsx";
5
6
  import type { Action } from "./state.ts";
@@ -11,6 +12,8 @@ export interface Story {
11
12
  name: string;
12
13
  title: string;
13
14
  steps: Step[];
15
+ /** the fixtures the frame snapshots render it on (the storybook shows it on its one set) */
16
+ fixtures?: FixtureOptions;
14
17
  }
15
18
 
16
19
  const SHOWN: Step[] = [{ show: "default" }];
@@ -45,11 +48,40 @@ export const STORIES: Story[] = [
45
48
  steps: [{ command: "tab.status" }, { command: "app.palette" }],
46
49
  },
47
50
  { name: "help", title: "Keyboard shortcuts", steps: [{ command: "tab.status" }, { command: "app.help" }] },
48
- { name: "runs", title: "Runs: the list", steps: [{ command: "tab.runs" }] },
49
51
  {
50
- name: "run",
51
- title: "Runs: one live run",
52
- steps: [{ command: "tab.runs" }, { action: { type: "run", id: "20260926-114800-jobs-screen" } }],
52
+ name: "runs-empty",
53
+ title: "Runs: no runs yet",
54
+ steps: [{ command: "tab.runs" }],
55
+ fixtures: { runs: [], sessions: [] },
56
+ },
57
+ { name: "runs", title: "Runs: the sessions", steps: [{ command: "tab.runs" }] },
58
+ {
59
+ name: "session",
60
+ title: "Runs: one session with two runs, one continued elsewhere",
61
+ steps: [{ command: "tab.runs" }, { action: { type: "session", key: "s-auth" } }],
62
+ },
63
+ {
64
+ name: "session-live",
65
+ title: "Runs: a live session with three live roles",
66
+ steps: [{ command: "tab.runs" }, { action: { type: "session", key: "s-jobs" } }],
67
+ },
68
+ {
69
+ name: "role",
70
+ title: "Runs: a role opened",
71
+ steps: [
72
+ { command: "tab.runs" },
73
+ { action: { type: "session", key: "s-jobs" } },
74
+ { action: { type: "role", run: "20260926-114800-jobs-screen", dispatchId: "d1" } },
75
+ ],
76
+ },
77
+ {
78
+ name: "milestone",
79
+ title: "Runs: a milestone opened on its digest",
80
+ steps: [
81
+ { command: "tab.runs" },
82
+ { action: { type: "session", key: "s-jobs" } },
83
+ { action: { type: "milestone", run: "20260926-114800-jobs-screen", name: "M0" } },
84
+ ],
53
85
  },
54
86
  ];
55
87
 
@@ -57,6 +57,18 @@ export function wrap(s: string, max: number): string[] {
57
57
  return lines;
58
58
  }
59
59
 
60
+ /**
61
+ * `wrap` with a hanging indent: a line's leading spaces and list marker (`- `, `* `, `1. `) are kept on its first
62
+ * line, and its continuation lines are indented to match, so a wrapped bullet does not read as a new line.
63
+ */
64
+ export function wrapHanging(s: string, max: number): string[] {
65
+ const lead = /^\s*(?:[-*+] |\d+[.)] )?/.exec(s)?.[0] ?? "";
66
+ const hang = width(lead);
67
+ if (!lead || max - hang < 1) return wrap(s, max);
68
+ const pad = " ".repeat(hang);
69
+ return wrap(s.slice(lead.length), max - hang).map((l, i) => (i === 0 ? lead : pad) + l);
70
+ }
71
+
60
72
  /** `04:12`, or `1:02:03` past an hour. */
61
73
  export function clock(secs: number): string {
62
74
  const s = Math.max(0, Math.floor(secs));
@@ -109,6 +109,7 @@ export const STATE_TOKEN: Record<State, Token> = {
109
109
  warn: "warning",
110
110
  fail: "error",
111
111
  skip: "muted",
112
+ info: "info",
112
113
  };
113
114
 
114
115
  export type Mood = "good" | "working" | "waiting" | "failed";