@exla/rudder 0.0.0-stage → 3.4.0

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 (181) hide show
  1. package/LICENSE +100 -0
  2. package/README.md +733 -2
  3. package/assets/rudder.icns +0 -0
  4. package/assets/sounds/NOTICE.md +8 -0
  5. package/assets/sounds/ping.mp3 +0 -0
  6. package/dist/agent-attention.d.ts +5 -0
  7. package/dist/agent-attention.js +54 -0
  8. package/dist/agent-attention.js.map +1 -0
  9. package/dist/analytics.d.ts +36 -0
  10. package/dist/analytics.js +178 -0
  11. package/dist/analytics.js.map +1 -0
  12. package/dist/attach-session.d.ts +91 -0
  13. package/dist/attach-session.js +290 -0
  14. package/dist/attach-session.js.map +1 -0
  15. package/dist/auth.d.ts +36 -0
  16. package/dist/auth.js +392 -0
  17. package/dist/auth.js.map +1 -0
  18. package/dist/backends.d.ts +3 -0
  19. package/dist/backends.js +638 -0
  20. package/dist/backends.js.map +1 -0
  21. package/dist/board/board.css +1 -0
  22. package/dist/board/board.js +2 -0
  23. package/dist/board/daemon.d.ts +39 -0
  24. package/dist/board/daemon.js +1510 -0
  25. package/dist/board/daemon.js.map +1 -0
  26. package/dist/brain.d.ts +24 -0
  27. package/dist/brain.js +244 -0
  28. package/dist/brain.js.map +1 -0
  29. package/dist/bus.d.ts +9 -0
  30. package/dist/bus.js +23 -0
  31. package/dist/bus.js.map +1 -0
  32. package/dist/checkout-sync.d.ts +35 -0
  33. package/dist/checkout-sync.js +247 -0
  34. package/dist/checkout-sync.js.map +1 -0
  35. package/dist/cloud.d.ts +48 -0
  36. package/dist/cloud.js +4596 -0
  37. package/dist/cloud.js.map +1 -0
  38. package/dist/codex-binary.d.ts +12 -0
  39. package/dist/codex-binary.js +184 -0
  40. package/dist/codex-binary.js.map +1 -0
  41. package/dist/context-audit.d.ts +21 -0
  42. package/dist/context-audit.js +109 -0
  43. package/dist/context-audit.js.map +1 -0
  44. package/dist/daemon.d.ts +27 -0
  45. package/dist/daemon.js +203 -0
  46. package/dist/daemon.js.map +1 -0
  47. package/dist/effort.d.ts +9 -0
  48. package/dist/effort.js +72 -0
  49. package/dist/effort.js.map +1 -0
  50. package/dist/feedback.d.ts +46 -0
  51. package/dist/feedback.js +173 -0
  52. package/dist/feedback.js.map +1 -0
  53. package/dist/gc.d.ts +3 -0
  54. package/dist/gc.js +262 -0
  55. package/dist/gc.js.map +1 -0
  56. package/dist/git.d.ts +31 -0
  57. package/dist/git.js +455 -0
  58. package/dist/git.js.map +1 -0
  59. package/dist/goal.d.ts +33 -0
  60. package/dist/goal.js +95 -0
  61. package/dist/goal.js.map +1 -0
  62. package/dist/graph.d.ts +93 -0
  63. package/dist/graph.js +372 -0
  64. package/dist/graph.js.map +1 -0
  65. package/dist/handoff.d.ts +110 -0
  66. package/dist/handoff.js +565 -0
  67. package/dist/handoff.js.map +1 -0
  68. package/dist/improve/advisor.d.ts +10 -0
  69. package/dist/improve/advisor.js +115 -0
  70. package/dist/improve/advisor.js.map +1 -0
  71. package/dist/improve/collect.d.ts +49 -0
  72. package/dist/improve/collect.js +274 -0
  73. package/dist/improve/collect.js.map +1 -0
  74. package/dist/improve/gate.d.ts +17 -0
  75. package/dist/improve/gate.js +60 -0
  76. package/dist/improve/gate.js.map +1 -0
  77. package/dist/improve/index.d.ts +9 -0
  78. package/dist/improve/index.js +376 -0
  79. package/dist/improve/index.js.map +1 -0
  80. package/dist/improve/judge.d.ts +32 -0
  81. package/dist/improve/judge.js +114 -0
  82. package/dist/improve/judge.js.map +1 -0
  83. package/dist/improve/mine.d.ts +31 -0
  84. package/dist/improve/mine.js +192 -0
  85. package/dist/improve/mine.js.map +1 -0
  86. package/dist/improve/propose.d.ts +51 -0
  87. package/dist/improve/propose.js +150 -0
  88. package/dist/improve/propose.js.map +1 -0
  89. package/dist/improve/rank.d.ts +16 -0
  90. package/dist/improve/rank.js +49 -0
  91. package/dist/improve/rank.js.map +1 -0
  92. package/dist/improve/schedule.d.ts +1 -0
  93. package/dist/improve/schedule.js +95 -0
  94. package/dist/improve/schedule.js.map +1 -0
  95. package/dist/improve/ship.d.ts +25 -0
  96. package/dist/improve/ship.js +82 -0
  97. package/dist/improve/ship.js.map +1 -0
  98. package/dist/improve/state.d.ts +157 -0
  99. package/dist/improve/state.js +264 -0
  100. package/dist/improve/state.js.map +1 -0
  101. package/dist/index.d.ts +2 -0
  102. package/dist/index.js +88 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/jj-binary.d.ts +36 -0
  105. package/dist/jj-binary.js +143 -0
  106. package/dist/jj-binary.js.map +1 -0
  107. package/dist/jj.d.ts +163 -0
  108. package/dist/jj.js +926 -0
  109. package/dist/jj.js.map +1 -0
  110. package/dist/logger.d.ts +22 -0
  111. package/dist/logger.js +110 -0
  112. package/dist/logger.js.map +1 -0
  113. package/dist/main.d.ts +3 -0
  114. package/dist/main.js +1366 -0
  115. package/dist/main.js.map +1 -0
  116. package/dist/migration.d.ts +82 -0
  117. package/dist/migration.js +198 -0
  118. package/dist/migration.js.map +1 -0
  119. package/dist/models.d.ts +9 -0
  120. package/dist/models.js +623 -0
  121. package/dist/models.js.map +1 -0
  122. package/dist/move-home.d.ts +82 -0
  123. package/dist/move-home.js +472 -0
  124. package/dist/move-home.js.map +1 -0
  125. package/dist/native/darwin-arm64/rudder-native +0 -0
  126. package/dist/native/darwin-x64/rudder-native +0 -0
  127. package/dist/native/linux-x64/rudder-native +0 -0
  128. package/dist/native-agents.d.ts +8 -0
  129. package/dist/native-agents.js +178 -0
  130. package/dist/native-agents.js.map +1 -0
  131. package/dist/native-binary.d.ts +4 -0
  132. package/dist/native-binary.js +140 -0
  133. package/dist/native-binary.js.map +1 -0
  134. package/dist/paste-bridge.d.ts +36 -0
  135. package/dist/paste-bridge.js +279 -0
  136. package/dist/paste-bridge.js.map +1 -0
  137. package/dist/plan-mode.d.ts +2 -0
  138. package/dist/plan-mode.js +22 -0
  139. package/dist/plan-mode.js.map +1 -0
  140. package/dist/planner.d.ts +27 -0
  141. package/dist/planner.js +587 -0
  142. package/dist/planner.js.map +1 -0
  143. package/dist/predict.d.ts +67 -0
  144. package/dist/predict.js +369 -0
  145. package/dist/predict.js.map +1 -0
  146. package/dist/report.d.ts +74 -0
  147. package/dist/report.js +412 -0
  148. package/dist/report.js.map +1 -0
  149. package/dist/rudder-md.d.ts +33 -0
  150. package/dist/rudder-md.js +142 -0
  151. package/dist/rudder-md.js.map +1 -0
  152. package/dist/run-manager.d.ts +62 -0
  153. package/dist/run-manager.js +1400 -0
  154. package/dist/run-manager.js.map +1 -0
  155. package/dist/scheduler.d.ts +128 -0
  156. package/dist/scheduler.js +955 -0
  157. package/dist/scheduler.js.map +1 -0
  158. package/dist/state.d.ts +127 -0
  159. package/dist/state.js +704 -0
  160. package/dist/state.js.map +1 -0
  161. package/dist/surfaces.d.ts +91 -0
  162. package/dist/surfaces.js +460 -0
  163. package/dist/surfaces.js.map +1 -0
  164. package/dist/task-summary.d.ts +30 -0
  165. package/dist/task-summary.js +472 -0
  166. package/dist/task-summary.js.map +1 -0
  167. package/dist/types.d.ts +492 -0
  168. package/dist/types.js +2 -0
  169. package/dist/types.js.map +1 -0
  170. package/dist/usage.d.ts +198 -0
  171. package/dist/usage.js +830 -0
  172. package/dist/usage.js.map +1 -0
  173. package/dist/util.d.ts +73 -0
  174. package/dist/util.js +399 -0
  175. package/dist/util.js.map +1 -0
  176. package/dist/version-check.d.ts +16 -0
  177. package/dist/version-check.js +269 -0
  178. package/dist/version-check.js.map +1 -0
  179. package/package.json +68 -4
  180. package/scripts/copy-native.mjs +89 -0
  181. package/scripts/postinstall.mjs +34 -0
package/dist/main.js ADDED
@@ -0,0 +1,1366 @@
1
+ import { spawn } from "node:child_process";
2
+ import fsp from "node:fs/promises";
3
+ import net from "node:net";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { authStoreExists, runDoctor, runOnboard } from "./auth.js";
8
+ import { codexLaunchEnv } from "./codex-binary.js";
9
+ import { runCloudCommand } from "./cloud.js";
10
+ import { printContextAudit } from "./context-audit.js";
11
+ import { currentBranch, findRepoRoot } from "./git.js";
12
+ import { runGc } from "./gc.js";
13
+ import { capture, pendingTelemetryNotice, projectHash, setTelemetryEnabled, telemetryEnabled, TELEMETRY_NOTICE } from "./analytics.js";
14
+ import { submitFeedback } from "./feedback.js";
15
+ import { dashboardRoot, runHandoff } from "./handoff.js";
16
+ import { ensureBoardRunning } from "./daemon.js";
17
+ import { ensureJjAvailable } from "./jj-binary.js";
18
+ import { createNodeWorkspace, currentJjChangeId, currentOpId, ensureColocated, ensureJj, undoLast, undoToOp, } from "./jj.js";
19
+ import { graphPath, mirrorPlanIntoGraph, updateGraph } from "./graph.js";
20
+ import { adoptMovedAgents } from "./move-home.js";
21
+ import { buildFanoutDag, gateDecision, planTask, scaffoldPlan } from "./planner.js";
22
+ import { backfillLlmTaskSummaries, listRuns, loadConfig, popUndoEntry, projectStateDir, registerProject, runsDir } from "./state.js";
23
+ import { discoverModelOptions } from "./models.js";
24
+ import { resolveNativeBinaryPath } from "./native-binary.js";
25
+ import { cleanupRuns, deleteRun, listProjectRuns, mergeRun, printLogs, startRun, statusRuns, stopRun, syncRun, watchRun, workerRun, } from "./run-manager.js";
26
+ import { appendCompletionNote, appendDecision, appendSharedContext, parseCompletionNoteArg, SHARED_CONTEXT_FILE, syncSharedContextToWorkspaces, writeCompletionNoteFile } from "./surfaces.js";
27
+ import { commandExists, isTty, MissingToolError, newRunId, runCommand } from "./util.js";
28
+ import { autoUpdateAndRerunIfNeeded, getUpdateAvailable } from "./version-check.js";
29
+ import { printReport } from "./report.js";
30
+ export async function main() {
31
+ const parsed = parseArgs(process.argv.slice(2));
32
+ if (shouldAutoUpdateForCommand(parsed.command) && await autoUpdateAndRerunIfNeeded(process.argv.slice(2))) {
33
+ return;
34
+ }
35
+ // One-time disclosure, before the dashboard takes over the screen. Printing it
36
+ // here (rather than burying it in docs) is the deal: telemetry is on, and the
37
+ // user is told what it is and how to stop it the first time they run anything.
38
+ if (!parsed.command?.startsWith("__")) {
39
+ const notice = await pendingTelemetryNotice();
40
+ if (notice) {
41
+ console.log(notice);
42
+ }
43
+ }
44
+ if (parsed.flags.version || parsed.command === "version") {
45
+ const current = await packageVersion();
46
+ console.log(current);
47
+ const update = await getUpdateAvailable().catch(() => null);
48
+ if (update && update.latest !== current) {
49
+ console.log(`update available: ${update.latest} (current ${current})`);
50
+ console.log(" npm i -g @exla/rudder");
51
+ }
52
+ return;
53
+ }
54
+ if (parsed.flags.cwd) {
55
+ process.chdir(parsed.flags.cwd);
56
+ }
57
+ // jj comes with Rudder: a machine without it gets jj's own release fetched
58
+ // into ~/.rudder/bin once (the npm postinstall usually did that already), and
59
+ // everything started from here finds it on PATH.
60
+ if (!parsed.flags.help && !JJ_FREE_COMMANDS.has(parsed.command ?? "") && (parsed.command || parsed.args.length > 0 || isTty())) {
61
+ await ensureJjAvailable({ onInstall: (message) => process.stderr.write(`${message}\n`) });
62
+ }
63
+ if (!parsed.command || parsed.flags.help) {
64
+ if (!parsed.command && parsed.args.length > 0) {
65
+ await maybeOnboard();
66
+ await startRun({
67
+ task: parsed.args.join(" "),
68
+ backend: parsed.flags.backend,
69
+ model: parsed.flags.model,
70
+ detach: parsed.flags.detach,
71
+ workspace: parsed.flags.workspace,
72
+ queue: parsed.flags.queue,
73
+ json: parsed.flags.json,
74
+ view: "shell",
75
+ });
76
+ return;
77
+ }
78
+ if (!parsed.command && isTty() && !parsed.flags.help) {
79
+ // A dashboard already running for this checkout owns its agents; a
80
+ // second one cannot see them. Bare `rudder` in another tab opens a full
81
+ // second view of the live one instead (same agents, its own selection
82
+ // and task bar). `rudder attach <agent>` mirrors a single pane.
83
+ if (await attachSocketLive()) {
84
+ await runNativeCommand(["attach", "--dashboard"]);
85
+ return;
86
+ }
87
+ await maybeOnboard();
88
+ await openDashboard(parsed);
89
+ return;
90
+ }
91
+ printHelp();
92
+ return;
93
+ }
94
+ switch (parsed.command) {
95
+ case "tmux":
96
+ throw new Error("The tmux dashboard has been removed. Run `rudder` or `rudder dashboard` to open the native dashboard.");
97
+ case "attach": {
98
+ await runNativeCommand(["attach", ...parsed.args]);
99
+ return;
100
+ }
101
+ case "dashboard":
102
+ await maybeOnboard();
103
+ await openDashboard(parsed);
104
+ return;
105
+ case "restart":
106
+ await maybeOnboard();
107
+ await resetLocalRudderSession(parsed);
108
+ await openDashboard(parsed);
109
+ return;
110
+ case "mouse-test":
111
+ await runNativeCommand(["mouse-test", ...parsed.args]);
112
+ return;
113
+ case "tui":
114
+ case "shell":
115
+ case "interactive":
116
+ case "legacy-shell":
117
+ throw new Error("Legacy interactive shells have been removed. Run `rudder` or `rudder dashboard` to open the native dashboard.");
118
+ case "__worker": {
119
+ const repo = parsed.flags.repo;
120
+ const run = parsed.flags.run;
121
+ if (!repo || !run) {
122
+ throw new Error("__worker requires --repo and --run");
123
+ }
124
+ await workerRun(repo, run, parsed.flags.attempt);
125
+ return;
126
+ }
127
+ case "__launch-node": {
128
+ await runLaunchNode(parsed);
129
+ return;
130
+ }
131
+ case "__graph-mirror": {
132
+ await runGraphMirror(parsed);
133
+ return;
134
+ }
135
+ case "__sync-checkout": {
136
+ // The home machine's half of `rudder cloud sync`, run in its clone.
137
+ // RUDDER_SYNC_IN (+ _HEAD, _FROM): the other side's work to take first;
138
+ // RUDDER_SYNC_OUT: where to bundle this side's work for the way back.
139
+ const { syncCheckout, importWork, exportWork } = await import("./checkout-sync.js");
140
+ const root = findRepoRoot();
141
+ const report = await syncCheckout(root);
142
+ const work = {};
143
+ if (process.env.RUDDER_SYNC_IN) {
144
+ work.received = await importWork(root, process.env.RUDDER_SYNC_IN, process.env.RUDDER_SYNC_IN_HEAD ?? "", process.env.RUDDER_SYNC_FROM ?? "laptop");
145
+ }
146
+ if (process.env.RUDDER_SYNC_OUT) {
147
+ work.sent = (await exportWork(root, process.env.RUDDER_SYNC_OUT)) ?? undefined;
148
+ }
149
+ if (work.received || work.sent) {
150
+ report.work = work;
151
+ }
152
+ console.log(JSON.stringify(report));
153
+ return;
154
+ }
155
+ case "__adopt-moved": {
156
+ await runAdoptMoved(parsed);
157
+ return;
158
+ }
159
+ case "__models": {
160
+ // The phone's /model picker, through the home supervisor: the same
161
+ // options as the dashboard's, and the backend + model new agents use.
162
+ const [claude, codex] = await Promise.all([
163
+ discoverModelOptions("claude").catch(() => []),
164
+ discoverModelOptions("codex").catch(() => []),
165
+ ]);
166
+ const config = await loadConfig().catch(() => null);
167
+ const backend = config?.lastUsedBackend ?? config?.defaultBackend ?? "claude";
168
+ const current = { backend, model: config?.backends?.[backend]?.model ?? null };
169
+ process.stdout.write(`${JSON.stringify({ current, claude, codex })}\n`);
170
+ return;
171
+ }
172
+ case "__refresh-models": {
173
+ // Spawned detached by the native TUI when the /model picker opens on a
174
+ // stale models.dev cache, so a long-lived dashboard session picks up
175
+ // newly released models without a restart. The picker re-reads the cache
176
+ // file on every render, so results appear as soon as this lands.
177
+ await refreshModelCache();
178
+ return;
179
+ }
180
+ case "onboard":
181
+ await runOnboard({
182
+ nonInteractive: parsed.flags.nonInteractive,
183
+ json: parsed.flags.json,
184
+ });
185
+ return;
186
+ case "doctor":
187
+ await runDoctor({ json: parsed.flags.json });
188
+ return;
189
+ case "login":
190
+ case "auth":
191
+ await runCloudCommand("cloud", ["login", ...parsed.args], {
192
+ json: parsed.flags.json,
193
+ homePaths: parsed.flags.homePaths,
194
+ sshHost: parsed.flags.sshHost,
195
+ });
196
+ return;
197
+ case "cloud":
198
+ case "sail":
199
+ await runCloudCommand(parsed.command, parsed.args, {
200
+ json: parsed.flags.json,
201
+ homePaths: parsed.flags.homePaths,
202
+ sshHost: parsed.flags.sshHost,
203
+ yes: Boolean(parsed.flags.force),
204
+ });
205
+ return;
206
+ case "run": {
207
+ await maybeOnboard();
208
+ const task = parsed.args.join(" ").trim();
209
+ if (!task) {
210
+ throw new Error("Missing task. Usage: rudder run \"fix the tests\"");
211
+ }
212
+ await startRun({
213
+ task,
214
+ backend: parsed.flags.backend,
215
+ model: parsed.flags.model,
216
+ detach: parsed.flags.detach,
217
+ workspace: parsed.flags.workspace,
218
+ queue: parsed.flags.queue,
219
+ json: parsed.flags.json,
220
+ view: "shell",
221
+ });
222
+ return;
223
+ }
224
+ case "claude":
225
+ case "codex": {
226
+ if (!commandExists(parsed.command)) {
227
+ throw new MissingToolError(parsed.command);
228
+ }
229
+ await maybeOnboard();
230
+ const task = parsed.args.join(" ").trim();
231
+ if (!task) {
232
+ throw new Error(`Missing task. Usage: rudder ${parsed.command} "fix the tests"`);
233
+ }
234
+ await startRun({
235
+ task,
236
+ backend: parsed.command,
237
+ model: parsed.flags.model,
238
+ detach: parsed.flags.detach,
239
+ workspace: parsed.flags.workspace,
240
+ queue: parsed.flags.queue,
241
+ json: parsed.flags.json,
242
+ view: "shell",
243
+ });
244
+ return;
245
+ }
246
+ case "acpx": {
247
+ if (!commandExists("acpx")) {
248
+ throw new MissingToolError("acpx");
249
+ }
250
+ await maybeOnboard();
251
+ const args = parsed.args[0] === "codex" ? parsed.args.slice(1) : parsed.args;
252
+ const task = args.join(" ").trim();
253
+ if (!task) {
254
+ throw new Error('Missing task. Usage: rudder acpx codex "fix the tests"');
255
+ }
256
+ await startRun({
257
+ task,
258
+ backend: "acpx",
259
+ model: parsed.flags.model,
260
+ detach: parsed.flags.detach,
261
+ workspace: parsed.flags.workspace,
262
+ queue: parsed.flags.queue,
263
+ json: parsed.flags.json,
264
+ view: "shell",
265
+ });
266
+ return;
267
+ }
268
+ case "watch":
269
+ await watchRun({ runId: parsed.args[0], follow: true });
270
+ return;
271
+ case "logs":
272
+ await printLogs(parsed.args[0], Boolean(parsed.flags.follow));
273
+ return;
274
+ case "status":
275
+ await statusRuns({ json: parsed.flags.json });
276
+ return;
277
+ case "runs":
278
+ await listProjectRuns({ json: parsed.flags.json });
279
+ return;
280
+ case "report": {
281
+ const repoRoot = findRepoRoot();
282
+ await printReport(repoRoot, {
283
+ format: parsed.flags.format,
284
+ status: parsed.flags.status ? parsed.flags.status.split(",") : undefined,
285
+ backend: parsed.flags.backend ? (Array.isArray(parsed.flags.backend) ? parsed.flags.backend : [parsed.flags.backend]) : undefined,
286
+ model: parsed.args[0],
287
+ after: parsed.flags.after,
288
+ before: parsed.flags.before,
289
+ summary: parsed.flags.summary !== false,
290
+ limit: parsed.flags.limit,
291
+ sortBy: parsed.flags.sortBy,
292
+ reverse: parsed.flags.reverse,
293
+ detailed: parsed.flags.detailed,
294
+ });
295
+ return;
296
+ }
297
+ case "stop": {
298
+ const run = parsed.args[0];
299
+ if (!run) {
300
+ throw new Error("Missing run id.");
301
+ }
302
+ await stopRun(run);
303
+ return;
304
+ }
305
+ case "delete": {
306
+ const run = parsed.args[0];
307
+ if (!run) {
308
+ throw new Error("Missing run id.");
309
+ }
310
+ await deleteRun(run, { force: Boolean(parsed.flags.force) });
311
+ return;
312
+ }
313
+ case "merge": {
314
+ const run = parsed.args[0];
315
+ if (!run) {
316
+ throw new Error("Missing run id.");
317
+ }
318
+ await mergeRun(run, Boolean(parsed.flags.allowDirty));
319
+ return;
320
+ }
321
+ case "sync":
322
+ await syncRun(parsed.args[0]);
323
+ return;
324
+ case "cleanup":
325
+ await cleanupRuns(Boolean(parsed.flags.force));
326
+ return;
327
+ case "gc":
328
+ await runGc({ dryRun: Boolean(parsed.flags.dryRun) });
329
+ return;
330
+ case "improve": {
331
+ const { runImprove } = await import("./improve/index.js");
332
+ await runImprove(parsed);
333
+ return;
334
+ }
335
+ case "board":
336
+ case "serve": {
337
+ await runBoard(parsed);
338
+ return;
339
+ }
340
+ case "undo": {
341
+ await runUndo(parsed.args[0]);
342
+ return;
343
+ }
344
+ case "remember": {
345
+ const insight = parsed.args.join(" ").trim();
346
+ if (!insight) {
347
+ throw new Error('Missing insight. Usage: rudder remember "the parser owns token budget"');
348
+ }
349
+ await runRemember(insight);
350
+ return;
351
+ }
352
+ case "share": {
353
+ const text = parsed.args.join(" ").trim();
354
+ if (!text) {
355
+ throw new Error('Missing shared context. Usage: rudder share "APIFY_TOKEN=..."');
356
+ }
357
+ await runShare(text);
358
+ return;
359
+ }
360
+ case "done": {
361
+ // Worker end-of-task report (summary + interfaces + recommended follow-ups).
362
+ await runDone(parsed);
363
+ return;
364
+ }
365
+ case "plan": {
366
+ await maybeOnboard();
367
+ const task = parsed.args.join(" ").trim();
368
+ if (!task) {
369
+ throw new Error('Missing task. Usage: rudder plan "split the parser work"');
370
+ }
371
+ await runPlan(task);
372
+ return;
373
+ }
374
+ case "fanout": {
375
+ await maybeOnboard();
376
+ const task = parsed.args.join(" ").trim();
377
+ if (!task) {
378
+ throw new Error('Missing task. Usage: rudder fanout "implement the cache" [--n 3]');
379
+ }
380
+ await runFanout(task, parsed.flags.n, parsed.flags.backend);
381
+ return;
382
+ }
383
+ case "context": {
384
+ await runContextCommand(parsed);
385
+ return;
386
+ }
387
+ case "telemetry": {
388
+ await runTelemetryCommand(parsed.args[0]);
389
+ return;
390
+ }
391
+ case "feedback": {
392
+ await runFeedbackCommand(parsed);
393
+ return;
394
+ }
395
+ case "__event": {
396
+ // Hidden: the native dashboard shells out to this so one telemetry
397
+ // implementation serves both halves of Rudder. Fire-and-forget by design —
398
+ // it must never fail loudly or delay the caller.
399
+ await runEventCommand(parsed);
400
+ return;
401
+ }
402
+ case "handoff": {
403
+ // Run from inside a live claude/codex chat: queues THIS conversation for
404
+ // the dashboard, which forks it into an agent pane with all of its context.
405
+ await runHandoff(parsed.args, parsed.flags.backend);
406
+ return;
407
+ }
408
+ default: {
409
+ // INTERNAL calls (`__event`, `__worker`, …) come from the native dashboard,
410
+ // which finds `rudder` on PATH — and that can be a DIFFERENT version than
411
+ // the binary calling it. Falling through here would treat the unknown
412
+ // internal command as a task and spawn a real agent named
413
+ // "__event dashboard_opened …". Version skew must be inert, not expensive.
414
+ if (isInternalCommand(parsed.command)) {
415
+ return;
416
+ }
417
+ await maybeOnboard();
418
+ await startRun({
419
+ task: [parsed.command, ...parsed.args].join(" "),
420
+ backend: parsed.flags.backend,
421
+ model: parsed.flags.model,
422
+ detach: parsed.flags.detach,
423
+ workspace: parsed.flags.workspace,
424
+ queue: parsed.flags.queue,
425
+ json: parsed.flags.json,
426
+ view: "shell",
427
+ });
428
+ }
429
+ }
430
+ }
431
+ /** Hidden cross-process entry points; never user tasks. */
432
+ export function isInternalCommand(command) {
433
+ return Boolean(command?.startsWith("__"));
434
+ }
435
+ // Commands that never touch a repository, so they never wait on a jj download.
436
+ const JJ_FREE_COMMANDS = new Set(["__event", "telemetry", "feedback", "login", "logout", "help"]);
437
+ function shouldAutoUpdateForCommand(command) {
438
+ if (command?.startsWith("__")) {
439
+ return false;
440
+ }
441
+ // `rudder done` is invoked by worker agents inside a live Rudder session; updating
442
+ // the global package in that callback can desync the running dashboard from its
443
+ // worker surface.
444
+ return command !== "done";
445
+ }
446
+ function parseArgs(argv) {
447
+ const parsed = { args: [], flags: {} };
448
+ for (let i = 0; i < argv.length; i += 1) {
449
+ const arg = argv[i] ?? "";
450
+ if (arg === "--") {
451
+ parsed.args.push(...argv.slice(i + 1));
452
+ break;
453
+ }
454
+ if (arg === "--help" || arg === "-h") {
455
+ parsed.flags.help = true;
456
+ continue;
457
+ }
458
+ if (arg === "--version" || arg === "-v") {
459
+ parsed.flags.version = true;
460
+ continue;
461
+ }
462
+ if (arg === "--json") {
463
+ parsed.flags.json = true;
464
+ continue;
465
+ }
466
+ if (arg === "--quiet" || arg === "-q") {
467
+ parsed.flags.quiet = true;
468
+ continue;
469
+ }
470
+ if (arg === "--detach" || arg === "-d") {
471
+ parsed.flags.detach = true;
472
+ continue;
473
+ }
474
+ if (arg === "--watch") {
475
+ parsed.flags.watch = true;
476
+ continue;
477
+ }
478
+ if (arg === "--follow" || arg === "-f") {
479
+ parsed.flags.follow = true;
480
+ continue;
481
+ }
482
+ // `--worktree` is the pre-rename spelling. Still accepted so muscle memory
483
+ // and existing scripts keep working; help only documents `--workspace`.
484
+ if (arg === "--workspace" || arg === "--worktree") {
485
+ parsed.flags.workspace = true;
486
+ continue;
487
+ }
488
+ if (arg === "--queue") {
489
+ parsed.flags.queue = true;
490
+ continue;
491
+ }
492
+ if (arg === "--allow-dirty") {
493
+ parsed.flags.allowDirty = true;
494
+ continue;
495
+ }
496
+ if (arg === "--force") {
497
+ parsed.flags.force = true;
498
+ continue;
499
+ }
500
+ if (arg === "--dry-run") {
501
+ parsed.flags.dryRun = true;
502
+ continue;
503
+ }
504
+ if (arg === "--non-interactive") {
505
+ parsed.flags.nonInteractive = true;
506
+ continue;
507
+ }
508
+ if (arg === "--no-tmux" || arg === "--no-native" || arg === "--headless") {
509
+ throw new Error(`${arg} was removed with the legacy dashboards. Run \`rudder\` for the native dashboard.`);
510
+ }
511
+ if (arg === "--open") {
512
+ parsed.flags.open = true;
513
+ continue;
514
+ }
515
+ if (arg === "--no-open") {
516
+ parsed.flags.open = false;
517
+ continue;
518
+ }
519
+ if (arg === "--no-issue") {
520
+ parsed.flags.issue = false;
521
+ continue;
522
+ }
523
+ if (takesValue(arg, "--context")) {
524
+ parsed.flags.contextFile = readValue(argv, ++i, arg);
525
+ continue;
526
+ }
527
+ if (arg.startsWith("--context=")) {
528
+ parsed.flags.contextFile = arg.slice("--context=".length);
529
+ continue;
530
+ }
531
+ if (takesValue(arg, "--props")) {
532
+ parsed.flags.props = readValue(argv, ++i, arg);
533
+ continue;
534
+ }
535
+ if (arg.startsWith("--props=")) {
536
+ parsed.flags.props = arg.slice("--props=".length);
537
+ continue;
538
+ }
539
+ if (takesValue(arg, "--port", "-p")) {
540
+ parsed.flags.port = Number.parseInt(readValue(argv, ++i, arg), 10);
541
+ continue;
542
+ }
543
+ if (arg.startsWith("--port=")) {
544
+ parsed.flags.port = Number.parseInt(arg.slice("--port=".length), 10);
545
+ continue;
546
+ }
547
+ if (takesValue(arg, "--n")) {
548
+ parsed.flags.n = Number.parseInt(readValue(argv, ++i, arg), 10);
549
+ continue;
550
+ }
551
+ if (arg.startsWith("--n=")) {
552
+ parsed.flags.n = Number.parseInt(arg.slice("--n=".length), 10);
553
+ continue;
554
+ }
555
+ if (takesValue(arg, "--model", "-m")) {
556
+ parsed.flags.model = readValue(argv, ++i, arg);
557
+ continue;
558
+ }
559
+ if (takesValue(arg, "--backend", "-b")) {
560
+ parsed.flags.backend = normalizeBackend(readValue(argv, ++i, arg));
561
+ continue;
562
+ }
563
+ if (takesValue(arg, "--cwd", "-C")) {
564
+ parsed.flags.cwd = readValue(argv, ++i, arg);
565
+ continue;
566
+ }
567
+ if (takesValue(arg, "--repo")) {
568
+ parsed.flags.repo = readValue(argv, ++i, arg);
569
+ continue;
570
+ }
571
+ if (takesValue(arg, "--run")) {
572
+ parsed.flags.run = readValue(argv, ++i, arg);
573
+ continue;
574
+ }
575
+ if (takesValue(arg, "--attempt")) {
576
+ parsed.flags.attempt = readValue(argv, ++i, arg);
577
+ continue;
578
+ }
579
+ if (takesValue(arg, "--task")) {
580
+ parsed.flags.task = readValue(argv, ++i, arg);
581
+ continue;
582
+ }
583
+ if (takesValue(arg, "--node")) {
584
+ parsed.flags.node = readValue(argv, ++i, arg);
585
+ continue;
586
+ }
587
+ if (takesValue(arg, "--base")) {
588
+ parsed.flags.base = readValue(argv, ++i, arg);
589
+ continue;
590
+ }
591
+ if (takesValue(arg, "--home-path")) {
592
+ parsed.flags.homePaths = [...(parsed.flags.homePaths ?? []), readValue(argv, ++i, arg)];
593
+ continue;
594
+ }
595
+ if (takesValue(arg, "--ssh", "--ssh-host")) {
596
+ parsed.flags.sshHost = readValue(argv, ++i, arg);
597
+ continue;
598
+ }
599
+ if (arg.startsWith("--model=")) {
600
+ parsed.flags.model = arg.slice("--model=".length);
601
+ continue;
602
+ }
603
+ if (arg.startsWith("--backend=")) {
604
+ parsed.flags.backend = normalizeBackend(arg.slice("--backend=".length));
605
+ continue;
606
+ }
607
+ if (arg.startsWith("--cwd=")) {
608
+ parsed.flags.cwd = arg.slice("--cwd=".length);
609
+ continue;
610
+ }
611
+ if (arg.startsWith("--repo=")) {
612
+ parsed.flags.repo = arg.slice("--repo=".length);
613
+ continue;
614
+ }
615
+ if (arg.startsWith("--run=")) {
616
+ parsed.flags.run = arg.slice("--run=".length);
617
+ continue;
618
+ }
619
+ if (arg.startsWith("--attempt=")) {
620
+ parsed.flags.attempt = arg.slice("--attempt=".length);
621
+ continue;
622
+ }
623
+ if (arg.startsWith("--task=")) {
624
+ parsed.flags.task = arg.slice("--task=".length);
625
+ continue;
626
+ }
627
+ if (arg.startsWith("--node=")) {
628
+ parsed.flags.node = arg.slice("--node=".length);
629
+ continue;
630
+ }
631
+ if (arg.startsWith("--base=")) {
632
+ parsed.flags.base = arg.slice("--base=".length);
633
+ continue;
634
+ }
635
+ if (arg.startsWith("--home-path=")) {
636
+ parsed.flags.homePaths = [...(parsed.flags.homePaths ?? []), arg.slice("--home-path=".length)];
637
+ continue;
638
+ }
639
+ if (arg.startsWith("--ssh=")) {
640
+ parsed.flags.sshHost = arg.slice("--ssh=".length);
641
+ continue;
642
+ }
643
+ if (arg.startsWith("--ssh-host=")) {
644
+ parsed.flags.sshHost = arg.slice("--ssh-host=".length);
645
+ continue;
646
+ }
647
+ if (takesValue(arg, "--format")) {
648
+ parsed.flags.format = readValue(argv, ++i, arg);
649
+ continue;
650
+ }
651
+ if (arg.startsWith("--format=")) {
652
+ parsed.flags.format = arg.slice("--format=".length);
653
+ continue;
654
+ }
655
+ if (takesValue(arg, "--status")) {
656
+ parsed.flags.status = readValue(argv, ++i, arg);
657
+ continue;
658
+ }
659
+ if (arg.startsWith("--status=")) {
660
+ parsed.flags.status = arg.slice("--status=".length);
661
+ continue;
662
+ }
663
+ if (arg === "--summary" || arg === "--no-summary") {
664
+ parsed.flags.summary = arg === "--summary";
665
+ continue;
666
+ }
667
+ if (takesValue(arg, "--limit")) {
668
+ parsed.flags.limit = Number.parseInt(readValue(argv, ++i, arg), 10);
669
+ continue;
670
+ }
671
+ if (arg.startsWith("--limit=")) {
672
+ parsed.flags.limit = Number.parseInt(arg.slice("--limit=".length), 10);
673
+ continue;
674
+ }
675
+ if (takesValue(arg, "--sort-by")) {
676
+ parsed.flags.sortBy = readValue(argv, ++i, arg);
677
+ continue;
678
+ }
679
+ if (arg.startsWith("--sort-by=")) {
680
+ parsed.flags.sortBy = arg.slice("--sort-by=".length);
681
+ continue;
682
+ }
683
+ if (arg === "--reverse") {
684
+ parsed.flags.reverse = true;
685
+ continue;
686
+ }
687
+ if (arg === "--detailed") {
688
+ parsed.flags.detailed = true;
689
+ continue;
690
+ }
691
+ if (takesValue(arg, "--after")) {
692
+ parsed.flags.after = readValue(argv, ++i, arg);
693
+ continue;
694
+ }
695
+ if (arg.startsWith("--after=")) {
696
+ parsed.flags.after = arg.slice("--after=".length);
697
+ continue;
698
+ }
699
+ if (takesValue(arg, "--before")) {
700
+ parsed.flags.before = readValue(argv, ++i, arg);
701
+ continue;
702
+ }
703
+ if (arg.startsWith("--before=")) {
704
+ parsed.flags.before = arg.slice("--before=".length);
705
+ continue;
706
+ }
707
+ if (!parsed.command && !arg.startsWith("-")) {
708
+ parsed.command = arg;
709
+ continue;
710
+ }
711
+ parsed.args.push(arg);
712
+ }
713
+ return parsed;
714
+ }
715
+ function takesValue(arg, ...names) {
716
+ return names.includes(arg);
717
+ }
718
+ function readValue(argv, index, flag) {
719
+ const value = argv[index];
720
+ if (!value) {
721
+ throw new Error(`${flag} requires a value`);
722
+ }
723
+ return value;
724
+ }
725
+ function normalizeBackend(value) {
726
+ if (value === "claude" || value === "codex" || value === "acpx" || value === "opencode") {
727
+ return value;
728
+ }
729
+ throw new Error(`Unknown backend: ${value}`);
730
+ }
731
+ async function maybeOnboard() {
732
+ if (await authStoreExists()) {
733
+ return;
734
+ }
735
+ if (!isTty()) {
736
+ await runOnboard({ nonInteractive: true });
737
+ return;
738
+ }
739
+ await runOnboard();
740
+ }
741
+ async function openDashboard(parsed) {
742
+ await refreshModelCache();
743
+ if (await runNativeDashboard()) {
744
+ return;
745
+ }
746
+ throw new Error("rudder-native binary is not available. Reinstall or rebuild Rudder, then run `rudder` again.");
747
+ }
748
+ async function refreshModelCache() {
749
+ await Promise.all([
750
+ discoverModelOptions("claude").catch(() => []),
751
+ discoverModelOptions("codex").catch(() => []),
752
+ ]);
753
+ }
754
+ async function runNativeDashboard() {
755
+ const nativeBinary = resolveNativeBinaryPath();
756
+ if (!nativeBinary) {
757
+ return false;
758
+ }
759
+ const update = await getUpdateAvailable().catch(() => null);
760
+ const env = await codexLaunchEnv(process.env);
761
+ const previousNativeTui = process.env.RUDDER_NATIVE_TUI;
762
+ process.env.RUDDER_NATIVE_TUI = "1";
763
+ env.RUDDER_NATIVE_TUI = "1";
764
+ if (update) {
765
+ env.RUDDER_UPDATE_AVAILABLE = update.latest;
766
+ env.RUDDER_UPDATE_CURRENT = update.current;
767
+ }
768
+ // Kick off background LLM task-summary backfill for run records in this repo.
769
+ // Native dashboard reads run.json directly so this is how we get nicer titles
770
+ // for new agents - the upgrades show up on the next launch. Fire-and-forget;
771
+ // never blocks startup.
772
+ try {
773
+ const repoRoot = findRepoRoot();
774
+ if (repoRoot) {
775
+ void backfillLlmTaskSummaries(repoRoot);
776
+ }
777
+ }
778
+ catch {
779
+ // ignore
780
+ }
781
+ // Start the board daemon in-process so the web board is live and reflects this
782
+ // TUI session (this also registers the project so /api/projects is populated).
783
+ // PROJECTOR-ONLY: the TUI is the sole scheduler. It schedules from its own
784
+ // in-memory planned_nodes and MIRRORS its plan + node statuses into graph.json.
785
+ // The daemon here must NOT run the launching scheduler, or both would launch
786
+ // the same graph.json node (double-launch). So we pass scheduler:false: the
787
+ // board still serves HTTP + SSE + fs.watch and reflects the mirrored graph,
788
+ // but never launches. Browser task/steer requests use the native control inbox
789
+ // in this mode, so the TUI handles them through the same path as task-pane input
790
+ // instead of writing graph state it cannot read back.
791
+ // ensureBoardRunning returns promptly; the listeners keep running on the Node
792
+ // event loop while the native TUI is in the foreground. Strictly non-fatal: if
793
+ // it throws, log a notice and keep launching the TUI - the daemon must never
794
+ // block or break the session.
795
+ let board;
796
+ try {
797
+ const repoRoot = findRepoRoot();
798
+ if (repoRoot) {
799
+ board = await ensureBoardRunning(repoRoot, { open: false, scheduler: false });
800
+ // Hand the native child a deep link to THIS project's board so the `o` key /
801
+ // `/web` opens straight into the current project (not the all-projects index).
802
+ // The URL itself stays hidden in the TUI; the hotkey is the way in, and the
803
+ // board's "all projects" link is the way back out to other projects.
804
+ env.RUDDER_BOARD_URL = board.projectUrl;
805
+ env.RUDDER_BOARD_PORT = String(board.port);
806
+ }
807
+ }
808
+ catch (err) {
809
+ if (isTty()) {
810
+ console.warn(`rudder board daemon did not start: ${err?.message ?? err}`);
811
+ }
812
+ }
813
+ try {
814
+ const code = await new Promise((resolve, reject) => {
815
+ const child = spawn(nativeBinary, process.argv.slice(2), {
816
+ stdio: "inherit",
817
+ env,
818
+ });
819
+ child.on("error", reject);
820
+ child.on("exit", (exitCode) => resolve(exitCode));
821
+ });
822
+ process.exitCode = code ?? 1;
823
+ // The in-process board daemon keeps the event loop alive, so without closing it
824
+ // the CLI would hang after the foreground TUI exits. Close it before returning.
825
+ await board?.handle?.close().catch(() => undefined);
826
+ restoreNativeTuiEnv(previousNativeTui);
827
+ return true;
828
+ }
829
+ catch {
830
+ await board?.handle?.close().catch(() => undefined);
831
+ restoreNativeTuiEnv(previousNativeTui);
832
+ return false;
833
+ }
834
+ }
835
+ function restoreNativeTuiEnv(previous) {
836
+ if (previous === undefined) {
837
+ delete process.env.RUDDER_NATIVE_TUI;
838
+ }
839
+ else {
840
+ process.env.RUDDER_NATIVE_TUI = previous;
841
+ }
842
+ }
843
+ /** Is a dashboard serving `rudder attach` for this checkout right now? A
844
+ * socket file alone proves nothing (a crashed dashboard leaves one behind);
845
+ * only a connection that is accepted does. */
846
+ async function attachSocketLive() {
847
+ let root;
848
+ try {
849
+ root = findRepoRoot();
850
+ }
851
+ catch {
852
+ return false;
853
+ }
854
+ // `.rudder/attach.sock` is a symlink to the real socket under ~/.rudder
855
+ // (unix socket paths are length-limited; checkout paths are not). realpath
856
+ // fails when the link dangles, which is the crashed-dashboard case.
857
+ let socketPath;
858
+ try {
859
+ socketPath = await fsp.realpath(path.join(root, ".rudder", "attach.sock"));
860
+ }
861
+ catch {
862
+ return false;
863
+ }
864
+ return await new Promise((resolve) => {
865
+ const socket = net.connect(socketPath);
866
+ const timer = setTimeout(() => {
867
+ socket.destroy();
868
+ resolve(false);
869
+ }, 500);
870
+ socket.once("connect", () => {
871
+ clearTimeout(timer);
872
+ socket.destroy();
873
+ resolve(true);
874
+ });
875
+ socket.once("error", () => {
876
+ clearTimeout(timer);
877
+ resolve(false);
878
+ });
879
+ });
880
+ }
881
+ async function runNativeCommand(args) {
882
+ const nativeBinary = resolveNativeBinaryPath();
883
+ if (!nativeBinary) {
884
+ throw new Error("rudder-native binary is not available in this package");
885
+ }
886
+ const code = await new Promise((resolve, reject) => {
887
+ const child = spawn(nativeBinary, args, {
888
+ stdio: "inherit",
889
+ env: process.env,
890
+ });
891
+ child.on("error", reject);
892
+ child.on("exit", (exitCode) => resolve(exitCode));
893
+ });
894
+ process.exitCode = code ?? 1;
895
+ }
896
+ async function resetLocalRudderSession(parsed) {
897
+ const repoRoot = findRepoRoot();
898
+ const stateDir = projectStateDir(repoRoot);
899
+ await Promise.all([
900
+ fsp.rm(path.join(stateDir, "session.json"), { force: true }),
901
+ fsp.rm(runsDir(repoRoot), { recursive: true, force: true }),
902
+ fsp.rm(path.join(stateDir, "tmux"), { recursive: true, force: true }),
903
+ ]);
904
+ await fsp.mkdir(runsDir(repoRoot), { recursive: true });
905
+ }
906
+ async function packageVersion() {
907
+ const packageFile = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
908
+ const raw = await fsp.readFile(packageFile, "utf8").catch(() => "");
909
+ if (!raw) {
910
+ return "unknown";
911
+ }
912
+ try {
913
+ const parsed = JSON.parse(raw);
914
+ return parsed.version ?? "unknown";
915
+ }
916
+ catch {
917
+ return "unknown";
918
+ }
919
+ }
920
+ async function runBoard(parsed) {
921
+ const repoRoot = findRepoRoot();
922
+ await registerProject(repoRoot).catch(() => undefined);
923
+ // `rudder board` defaults to opening the browser; `--no-open` suppresses it.
924
+ // `rudder serve` defaults to not opening unless `--open` is passed.
925
+ const open = parsed.flags.open ?? parsed.command === "board";
926
+ const result = await ensureBoardRunning(repoRoot, { port: parsed.flags.port, open });
927
+ if (!result.started) {
928
+ console.log(`rudder board already running on ${result.url}`);
929
+ return;
930
+ }
931
+ // Keep the process alive while the in-process daemon serves the board.
932
+ await new Promise(() => { });
933
+ }
934
+ /**
935
+ * `rudder __launch-node`: the native TUI calls this synchronously to isolate a
936
+ * worker in a jj workspace (replacing `git workspace add`). It colocates jj with
937
+ * the git repo, creates a per-node workspace, reads back its change id, and
938
+ * prints a single JSON line {path, workspaceName, jjChangeId} to stdout. Errors
939
+ * go to stderr and exit non-zero so the native side surfaces a clear failure.
940
+ */
941
+ async function runLaunchNode(parsed) {
942
+ const repoRoot = parsed.flags.repo;
943
+ const task = parsed.flags.task;
944
+ if (!repoRoot) {
945
+ throw new Error("__launch-node requires --repo");
946
+ }
947
+ if (!task) {
948
+ throw new Error("__launch-node requires --task");
949
+ }
950
+ const runId = parsed.flags.node ?? newRunId(task);
951
+ ensureJj();
952
+ await ensureColocated(repoRoot);
953
+ const workspace = await createNodeWorkspace({
954
+ repoRoot,
955
+ runId,
956
+ task,
957
+ ...(parsed.flags.base ? { atChangeId: parsed.flags.base } : {}),
958
+ });
959
+ const jjChangeId = await currentJjChangeId(workspace.path);
960
+ process.stdout.write(`${JSON.stringify({
961
+ path: workspace.path,
962
+ workspaceName: workspace.workspaceName,
963
+ jjChangeId,
964
+ })}\n`);
965
+ }
966
+ /**
967
+ * `rudder __adopt-moved --repo <clone> <bundle.tgz>`: the home machine's half of
968
+ * moving agents off a laptop (see src/move-home.ts). Run by the home supervisor;
969
+ * prints one JSON line with what was adopted and what failed.
970
+ */
971
+ async function runAdoptMoved(parsed) {
972
+ const repoRoot = parsed.flags.repo;
973
+ const archive = parsed.args[0];
974
+ if (!repoRoot || !archive) {
975
+ throw new Error("__adopt-moved requires --repo <clone> <bundle.tgz>");
976
+ }
977
+ const extractDir = await fsp.mkdtemp(path.join(os.tmpdir(), "rudder-move-"));
978
+ try {
979
+ await runCommand("tar", ["-xzf", archive, "-C", extractDir]);
980
+ const result = await adoptMovedAgents(path.resolve(repoRoot), extractDir);
981
+ process.stdout.write(`${JSON.stringify(result)}\n`);
982
+ }
983
+ finally {
984
+ await fsp.rm(extractDir, { recursive: true, force: true });
985
+ }
986
+ }
987
+ async function readStdin() {
988
+ const chunks = [];
989
+ for await (const chunk of process.stdin) {
990
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
991
+ }
992
+ return Buffer.concat(chunks).toString("utf8");
993
+ }
994
+ /**
995
+ * `rudder __graph-mirror --repo <root>`: read a JSON plan payload from stdin and
996
+ * MIRROR it into graph.json. The TUI is the sole scheduler; graph.json is a
997
+ * write-only projection the board reads (it is NEVER read back for scheduling).
998
+ * The schema lives in graph.ts: this shim just reads stdin and hands the payload
999
+ * to mirrorPlanIntoGraph under the graph lock. Prints `ok`/exit 0; the native
1000
+ * caller treats it as best-effort and never blocks on it.
1001
+ */
1002
+ async function runGraphMirror(parsed) {
1003
+ const repoRoot = parsed.flags.repo ?? findRepoRoot();
1004
+ if (!repoRoot) {
1005
+ throw new Error("__graph-mirror requires --repo");
1006
+ }
1007
+ const raw = await readStdin();
1008
+ let payload = {};
1009
+ if (raw.trim()) {
1010
+ payload = JSON.parse(raw);
1011
+ }
1012
+ await updateGraph(repoRoot, (graph) => mirrorPlanIntoGraph(graph, payload));
1013
+ process.stdout.write("ok\n");
1014
+ }
1015
+ async function runUndo(opId) {
1016
+ const repoRoot = findRepoRoot();
1017
+ ensureJj();
1018
+ await ensureColocated(repoRoot);
1019
+ if (opId) {
1020
+ await undoToOp(repoRoot, opId);
1021
+ console.log(`Restored jj to operation ${opId}.`);
1022
+ console.log("Note: jj op restore is global - it rewinds all workspaces and refs to that point.");
1023
+ return;
1024
+ }
1025
+ const entry = await popUndoEntry(repoRoot);
1026
+ if (entry) {
1027
+ if (entry.runIds.length) {
1028
+ console.log(`This undo reverts runs: ${entry.runIds.join(", ")}`);
1029
+ }
1030
+ await undoToOp(repoRoot, entry.opId);
1031
+ console.log(`Restored jj to operation ${entry.opId} (${entry.label}).`);
1032
+ console.log("Note: jj op restore is global - it rewinds all workspaces and refs to that point.");
1033
+ return;
1034
+ }
1035
+ const before = await currentOpId(repoRoot);
1036
+ await undoLast(repoRoot);
1037
+ console.log(`Undid the last jj operation${before ? ` (was at ${before})` : ""}.`);
1038
+ console.log("Note: jj undo is global - it rewinds all workspaces and refs.");
1039
+ }
1040
+ async function runRemember(insight) {
1041
+ // The bd-remember equivalent: append a durable insight to DECISIONS.md, the
1042
+ // tracked agent-authored knowledge surface the board's Memory view renders.
1043
+ const repoRoot = findRepoRoot();
1044
+ await appendDecision(repoRoot, insight, "cli");
1045
+ console.log("Remembered. Appended to DECISIONS.md (shared, jj-tracked).");
1046
+ }
1047
+ async function runTelemetryCommand(action) {
1048
+ const verb = (action ?? "status").trim().toLowerCase();
1049
+ if (verb === "on" || verb === "enable") {
1050
+ await setTelemetryEnabled(true);
1051
+ console.log(`Telemetry ON. ${TELEMETRY_NOTICE}`);
1052
+ return;
1053
+ }
1054
+ if (verb === "off" || verb === "disable") {
1055
+ await setTelemetryEnabled(false);
1056
+ console.log("Telemetry OFF. Nothing is sent; `rudder feedback` still works and stays local.");
1057
+ return;
1058
+ }
1059
+ if (verb === "status") {
1060
+ const enabled = await telemetryEnabled();
1061
+ console.log(enabled ? `Telemetry ON. ${TELEMETRY_NOTICE}` : "Telemetry OFF.");
1062
+ return;
1063
+ }
1064
+ throw new Error("Usage: rudder telemetry [status|on|off]");
1065
+ }
1066
+ async function runFeedbackCommand(parsed) {
1067
+ // `--context <file>` is how the dashboard hands over what was on screen; a
1068
+ // plain CLI report just carries the message.
1069
+ const contextFile = parsed.flags.contextFile;
1070
+ let context;
1071
+ if (contextFile) {
1072
+ const raw = await fsp.readFile(contextFile, "utf8").catch(() => "");
1073
+ try {
1074
+ context = JSON.parse(raw);
1075
+ }
1076
+ catch {
1077
+ context = undefined;
1078
+ }
1079
+ }
1080
+ const text = parsed.args.join(" ").trim();
1081
+ const report = await submitFeedback({
1082
+ text: text || context?.text || "",
1083
+ context,
1084
+ issue: parsed.flags.issue,
1085
+ });
1086
+ console.log(`Thanks. Saved locally to ${report.localPath.replace(os.homedir(), "~")}.`);
1087
+ if (report.issueUrl) {
1088
+ console.log(`Filed ${report.issueUrl}`);
1089
+ }
1090
+ for (const reason of report.skipped) {
1091
+ console.log(` note: ${reason}`);
1092
+ }
1093
+ }
1094
+ async function runEventCommand(parsed) {
1095
+ const event = parsed.args[0]?.trim();
1096
+ if (!event) {
1097
+ return;
1098
+ }
1099
+ let properties = {};
1100
+ const raw = parsed.flags.props;
1101
+ if (raw) {
1102
+ try {
1103
+ const parsedProps = JSON.parse(raw);
1104
+ for (const [key, value] of Object.entries(parsedProps)) {
1105
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1106
+ properties[key] = value;
1107
+ }
1108
+ }
1109
+ }
1110
+ catch {
1111
+ properties = {};
1112
+ }
1113
+ }
1114
+ // Stamp the project id here so ONE implementation owns it: the dashboard
1115
+ // shells out with its repo as cwd, and CLI invocations resolve the same root.
1116
+ properties.project ??= projectHash(dashboardRoot());
1117
+ await capture(event, properties);
1118
+ }
1119
+ async function runShare(text) {
1120
+ const repoRoot = findRepoRoot();
1121
+ await appendSharedContext(repoRoot, text, "cli share");
1122
+ const runs = await listRuns(repoRoot).catch(() => []);
1123
+ await syncSharedContextToWorkspaces(repoRoot, runs.map((run) => run.workspace.path)).catch(() => undefined);
1124
+ console.log(`Shared. Appended to ${SHARED_CONTEXT_FILE} (gitignored, mirrored to agents).`);
1125
+ }
1126
+ /** Read all of stdin when it is piped (never when interactive), with a short
1127
+ * timeout so a worker that calls `rudder done` without piping never hangs. */
1128
+ async function readPipedStdin(timeoutMs = 3000) {
1129
+ if (process.stdin.isTTY) {
1130
+ return "";
1131
+ }
1132
+ return await new Promise((resolve) => {
1133
+ const chunks = [];
1134
+ const done = () => {
1135
+ process.stdin.removeAllListeners("data");
1136
+ // Let go of stdin once we stop reading it. A worker's Bash tool hands the
1137
+ // child a socket that never closes, and a flowing, referenced stdin keeps
1138
+ // the event loop alive after `main` returns: `rudder done` wrote its note
1139
+ // and then sat for a day and a half in a Claude session's background
1140
+ // task list, still "running", with the agent spinning above it.
1141
+ process.stdin.pause();
1142
+ const stdin = process.stdin;
1143
+ if (typeof stdin.unref === "function") {
1144
+ stdin.unref();
1145
+ }
1146
+ resolve(Buffer.concat(chunks).toString("utf8"));
1147
+ };
1148
+ const timer = setTimeout(done, timeoutMs);
1149
+ process.stdin.on("data", (c) => chunks.push(Buffer.from(c)));
1150
+ process.stdin.on("end", () => {
1151
+ clearTimeout(timer);
1152
+ done();
1153
+ });
1154
+ process.stdin.on("error", () => {
1155
+ clearTimeout(timer);
1156
+ resolve("");
1157
+ });
1158
+ });
1159
+ }
1160
+ /** `rudder done [--node <id>] ['<json>']`: a worker's end-of-task report. Accepts
1161
+ * a CompletionNote JSON via stdin (piped) or as the joined args; a non-JSON arg
1162
+ * is treated as a freeform summary. The note travels back to the orchestrator over
1163
+ * two durable channels so a real Claude/Codex worker's report is
1164
+ * not lost to terminal rendering:
1165
+ * 1. RUDDER_DONE_FILE (set by the launcher to <workspace>/.rudder/done/<node>.json):
1166
+ * a machine-readable JSON drop the orchestrator reads straight off disk. This is
1167
+ * the authoritative channel - it never passes through the agent's TUI.
1168
+ * 2. DECISIONS.md: a human-legible bullet siblings re-read.
1169
+ * Never manages jj - just records, like `rudder remember`. */
1170
+ async function runDone(parsed) {
1171
+ const repoRoot = findRepoRoot();
1172
+ const node = parsed.flags.node;
1173
+ let raw = (await readPipedStdin()).trim();
1174
+ if (!raw && parsed.args.length) {
1175
+ raw = parsed.args.join(" ").trim();
1176
+ }
1177
+ let note = parseCompletionNoteArg(raw);
1178
+ if (node && !note.node) {
1179
+ note.node = node;
1180
+ }
1181
+ // (1) Authoritative file drop. Prefer the launcher-provided absolute path; otherwise
1182
+ // fall back to the conventional path under the cwd (.rudder/done/<node>.json) so a
1183
+ // hand-run `rudder done --node X` still reports.
1184
+ const doneFile = process.env.RUDDER_DONE_FILE ||
1185
+ (node ? path.join(process.cwd(), ".rudder", "done", `${node}.json`) : "");
1186
+ if (doneFile) {
1187
+ await writeCompletionNoteFile(doneFile, note);
1188
+ }
1189
+ // (2) Human-legible record for sibling agents and review history.
1190
+ await appendCompletionNote(repoRoot, note, node || "worker");
1191
+ }
1192
+ async function runPlan(task) {
1193
+ const repoRoot = findRepoRoot();
1194
+ ensureJj();
1195
+ await ensureColocated(repoRoot);
1196
+ const branch = await currentBranch(repoRoot).catch(() => "");
1197
+ const dag = await planTask(task, { root: repoRoot, branch });
1198
+ const gate = gateDecision(dag);
1199
+ if (gate.autoRun) {
1200
+ console.log(`auto: ${gate.reason}`);
1201
+ }
1202
+ else {
1203
+ console.log(`gate: ${gate.reason}`);
1204
+ console.log(`Plan: ${dag.nodes.length} node(s), ${dag.edges.length} edge(s)`);
1205
+ for (const node of dag.nodes) {
1206
+ const deps = node.deps.length
1207
+ ? ` deps=[${node.deps.map((dep) => `${dep.on}:${dep.type}`).join(", ")}]`
1208
+ : "";
1209
+ console.log(` - ${node.id} ${node.title}${deps}`);
1210
+ }
1211
+ }
1212
+ await scaffoldPlan(repoRoot, dag);
1213
+ console.log(`graph: ${graphPath(repoRoot)}`);
1214
+ }
1215
+ async function runFanout(task, n, backend) {
1216
+ const repoRoot = findRepoRoot();
1217
+ ensureJj();
1218
+ await ensureColocated(repoRoot);
1219
+ const requested = typeof n === "number" && Number.isFinite(n) ? n : 3;
1220
+ const dag = buildFanoutDag(task, requested, backend ? { backend } : {});
1221
+ const variants = dag.nodes.filter((node) => node.id !== "judge");
1222
+ console.log(`fanout: ${variants.length} variant(s) + 1 judge`);
1223
+ for (const node of dag.nodes) {
1224
+ const judgeOf = node.deps.filter((dep) => dep.type === "judge").map((dep) => dep.on);
1225
+ const suffix = judgeOf.length ? ` judges=[${judgeOf.join(", ")}]` : "";
1226
+ console.log(` - ${node.id} ${node.title}${suffix}`);
1227
+ }
1228
+ await scaffoldPlan(repoRoot, dag);
1229
+ console.log(`graph: ${graphPath(repoRoot)}`);
1230
+ console.log("The judge launches once every variant reaches review; variants are not merged.");
1231
+ }
1232
+ async function runContextCommand(parsed) {
1233
+ const sub = parsed.args[0] ?? "audit";
1234
+ const repoRoot = findRepoRoot();
1235
+ if (sub === "audit") {
1236
+ await printContextAudit({ json: parsed.flags.json, repoRoot });
1237
+ return;
1238
+ }
1239
+ throw new Error("Usage: rudder context [audit]");
1240
+ }
1241
+ function printHelp() {
1242
+ console.log(`rudder
1243
+
1244
+ Usage:
1245
+ rudder Open native dashboard with real agent panes
1246
+ rudder restart Clear local session and open dashboard
1247
+ rudder attach [agent] Mirror one live agent pane of the running dashboard in this terminal
1248
+ rudder "task"
1249
+ rudder run [options] "task"
1250
+ rudder claude [options] "task"
1251
+ rudder codex [options] "task"
1252
+ rudder login
1253
+ rudder cloud [name or task]
1254
+ rudder cloud help
1255
+ rudder cloud byoc <ssh-host>
1256
+ rudder cloud runtime [fly|byoc]
1257
+ rudder cloud vm <task>
1258
+ rudder cloud list
1259
+ rudder cloud attach <id>
1260
+ rudder cloud talk <id> "message"
1261
+ rudder cloud output <id>
1262
+ rudder cloud onload [runId]
1263
+ rudder cloud logs <id>
1264
+ rudder cloud bootstrap <id>
1265
+ rudder cloud quickstart
1266
+ rudder sail [name or task]
1267
+ Run management:
1268
+ rudder watch [run] Attach to live output
1269
+ rudder logs [run] [--follow] Print saved output
1270
+ rudder status [--json] Show active runs for this repo
1271
+ rudder runs [--json] List runs for this repo
1272
+ rudder report [model] [options] Collect and report on runs with filtering, aggregation, and multiple formats
1273
+ rudder stop <run> Cancel a run
1274
+ rudder delete <run> Delete a run and its workspace
1275
+ rudder merge <run> Merge a run into the current change
1276
+ rudder sync [run] Rebase a run's jj change onto its base without merging
1277
+ rudder cleanup [--force] Remove merged run workspaces
1278
+ rudder gc [--dry-run] Prune bounded diagnostics, old binaries, and stale run records
1279
+ rudder undo [opId] Rewind jj to an op id, or the last undo-stack entry (global)
1280
+
1281
+ Improvement loop:
1282
+ rudder improve run [--dry-run] [--budget-usd N] Mine telemetry, propose+judge fixes, ship a release
1283
+ rudder improve status Last cycle, shipped versions, ledger tail
1284
+ rudder improve report [date] Print a cycle report
1285
+ rudder improve schedule install|uninstall|status Nightly launchd schedule (macOS)
1286
+
1287
+ Planner:
1288
+ rudder plan "task" Decompose a task into a DAG, scaffold empty jj changes, write graph.json
1289
+ rudder fanout "task" [--n N] Fan out N variant agents on one task, then a judge picks/merges the best (default N=3)
1290
+ rudder context audit Audit agent context files for size, duplication, secrets, and injection text
1291
+
1292
+ Conversation handoff:
1293
+ rudder handoff "<next step>" Hand THIS chat to Rudder: it forks the conversation into a worker that already knows everything
1294
+ rudder handoff --here "..." Continue it in the main checkout instead of an isolated workspace
1295
+ rudder handoff --opencode "..." Hand off an opencode chat (default is the claude one; --codex for codex)
1296
+ rudder handoff --list List recent conversations in this repo with their session ids
1297
+
1298
+ Telemetry and feedback:
1299
+ rudder feedback "<what broke>" Send a report: message + version/model/recent notices (never prompts or code)
1300
+ rudder telemetry [status|on|off] Anonymous usage events; on by default, off in one command
1301
+
1302
+ Memory:
1303
+ rudder remember "<insight>" Append a durable cross-cutting decision to DECISIONS.md (shared, jj-tracked)
1304
+ rudder share "<token/context>" Append gitignored shared context to RUDDER_SHARED.md for all agents
1305
+
1306
+ Board:
1307
+ rudder board [--port N] [--open|--no-open] Serve the localhost board (opens browser by default)
1308
+ rudder serve [--port N] [--open] Ensure the board server is up
1309
+
1310
+ Setup:
1311
+ rudder onboard
1312
+ rudder doctor [--json]
1313
+ rudder mouse-test [raw|parsed] Show whether your terminal sends wheel events
1314
+
1315
+ Cloud:
1316
+ rudder login Open browser login and store cloud token
1317
+ rudder cloud [name or task] Start a cloud worker from this repo snapshot
1318
+ rudder cloud list List cloud workers/runs
1319
+ rudder cloud help Show cloud command help
1320
+ rudder cloud byoc <ssh> Use your own SSH host for BYOC cloud launches
1321
+ rudder cloud runtime [runtime] Show or set fly/byoc cloud runtime
1322
+ rudder cloud vm <task> Prepare a BYOC worker command for a task
1323
+ rudder cloud onload [runId] Move this Rudder workspace, or one run, to cloud
1324
+ rudder cloud logs <id> Show cloud worker status while log streaming is pending
1325
+ rudder cloud attach <id> Attach this terminal to a live cloud worker
1326
+ rudder cloud bootstrap <id> Regenerate a BYOC worker command
1327
+ rudder cloud pause <id> Pause an idle cloud worker
1328
+ rudder cloud resume <id> Resume a cloud worker
1329
+ rudder sail [name or task] Alias for starting a cloud worker
1330
+
1331
+ Report options:
1332
+ --format <format> Output format: table (default), json, markdown, csv
1333
+ --status <status> Filter by status (comma-separated: completed,merged,failed,etc)
1334
+ --backend <backend> Filter by backend: claude, codex, acpx, opencode
1335
+ --after <date> Filter by creation date (ISO 8601)
1336
+ --before <date> Filter by creation date (ISO 8601)
1337
+ --summary / --no-summary Show statistics (on by default)
1338
+ --limit <n> Limit results to N runs
1339
+ --sort-by <column> Sort by: created (default), updated, duration, tokens, status
1340
+ --reverse Reverse sort order
1341
+ --detailed Show full task text and extra details
1342
+
1343
+ General options:
1344
+ -d, --detach Start in background
1345
+ --workspace Always isolate the run in its own jj workspace
1346
+ --queue Queue mode (reserved)
1347
+ -m, --model <model> Backend model
1348
+ -b, --backend <backend> claude, codex, or opencode
1349
+ -C, --cwd <dir> Run from another directory
1350
+ -p, --port <port> Board server port (default 4774)
1351
+ --n <count> Number of fan-out variants for rudder fanout (default 3)
1352
+ --open / --no-open Open (or do not open) the browser for the board
1353
+ --home-path <path> Include extra HOME path in cloud snapshot
1354
+ --ssh <host> BYOC SSH host from ~/.ssh/config
1355
+ --json Machine-readable output
1356
+ -v, --version Print version
1357
+ --allow-dirty Allow merge into dirty target branch
1358
+ --dry-run Report gc cleanup without deleting
1359
+
1360
+ Rebase-first merge:
1361
+ Set {"mergeStrategy":"rebase"} in ~/.rudder/config.json to rebase before
1362
+ merging. If that rebase conflicts, Rudder leaves the workspace mid-rebase so
1363
+ you can resolve it, run git rebase --continue, and retry sync or merge.
1364
+ `);
1365
+ }
1366
+ //# sourceMappingURL=main.js.map