@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.
- package/LICENSE +100 -0
- package/README.md +733 -2
- package/assets/rudder.icns +0 -0
- package/assets/sounds/NOTICE.md +8 -0
- package/assets/sounds/ping.mp3 +0 -0
- package/dist/agent-attention.d.ts +5 -0
- package/dist/agent-attention.js +54 -0
- package/dist/agent-attention.js.map +1 -0
- package/dist/analytics.d.ts +36 -0
- package/dist/analytics.js +178 -0
- package/dist/analytics.js.map +1 -0
- package/dist/attach-session.d.ts +91 -0
- package/dist/attach-session.js +290 -0
- package/dist/attach-session.js.map +1 -0
- package/dist/auth.d.ts +36 -0
- package/dist/auth.js +392 -0
- package/dist/auth.js.map +1 -0
- package/dist/backends.d.ts +3 -0
- package/dist/backends.js +638 -0
- package/dist/backends.js.map +1 -0
- package/dist/board/board.css +1 -0
- package/dist/board/board.js +2 -0
- package/dist/board/daemon.d.ts +39 -0
- package/dist/board/daemon.js +1510 -0
- package/dist/board/daemon.js.map +1 -0
- package/dist/brain.d.ts +24 -0
- package/dist/brain.js +244 -0
- package/dist/brain.js.map +1 -0
- package/dist/bus.d.ts +9 -0
- package/dist/bus.js +23 -0
- package/dist/bus.js.map +1 -0
- package/dist/checkout-sync.d.ts +35 -0
- package/dist/checkout-sync.js +247 -0
- package/dist/checkout-sync.js.map +1 -0
- package/dist/cloud.d.ts +48 -0
- package/dist/cloud.js +4596 -0
- package/dist/cloud.js.map +1 -0
- package/dist/codex-binary.d.ts +12 -0
- package/dist/codex-binary.js +184 -0
- package/dist/codex-binary.js.map +1 -0
- package/dist/context-audit.d.ts +21 -0
- package/dist/context-audit.js +109 -0
- package/dist/context-audit.js.map +1 -0
- package/dist/daemon.d.ts +27 -0
- package/dist/daemon.js +203 -0
- package/dist/daemon.js.map +1 -0
- package/dist/effort.d.ts +9 -0
- package/dist/effort.js +72 -0
- package/dist/effort.js.map +1 -0
- package/dist/feedback.d.ts +46 -0
- package/dist/feedback.js +173 -0
- package/dist/feedback.js.map +1 -0
- package/dist/gc.d.ts +3 -0
- package/dist/gc.js +262 -0
- package/dist/gc.js.map +1 -0
- package/dist/git.d.ts +31 -0
- package/dist/git.js +455 -0
- package/dist/git.js.map +1 -0
- package/dist/goal.d.ts +33 -0
- package/dist/goal.js +95 -0
- package/dist/goal.js.map +1 -0
- package/dist/graph.d.ts +93 -0
- package/dist/graph.js +372 -0
- package/dist/graph.js.map +1 -0
- package/dist/handoff.d.ts +110 -0
- package/dist/handoff.js +565 -0
- package/dist/handoff.js.map +1 -0
- package/dist/improve/advisor.d.ts +10 -0
- package/dist/improve/advisor.js +115 -0
- package/dist/improve/advisor.js.map +1 -0
- package/dist/improve/collect.d.ts +49 -0
- package/dist/improve/collect.js +274 -0
- package/dist/improve/collect.js.map +1 -0
- package/dist/improve/gate.d.ts +17 -0
- package/dist/improve/gate.js +60 -0
- package/dist/improve/gate.js.map +1 -0
- package/dist/improve/index.d.ts +9 -0
- package/dist/improve/index.js +376 -0
- package/dist/improve/index.js.map +1 -0
- package/dist/improve/judge.d.ts +32 -0
- package/dist/improve/judge.js +114 -0
- package/dist/improve/judge.js.map +1 -0
- package/dist/improve/mine.d.ts +31 -0
- package/dist/improve/mine.js +192 -0
- package/dist/improve/mine.js.map +1 -0
- package/dist/improve/propose.d.ts +51 -0
- package/dist/improve/propose.js +150 -0
- package/dist/improve/propose.js.map +1 -0
- package/dist/improve/rank.d.ts +16 -0
- package/dist/improve/rank.js +49 -0
- package/dist/improve/rank.js.map +1 -0
- package/dist/improve/schedule.d.ts +1 -0
- package/dist/improve/schedule.js +95 -0
- package/dist/improve/schedule.js.map +1 -0
- package/dist/improve/ship.d.ts +25 -0
- package/dist/improve/ship.js +82 -0
- package/dist/improve/ship.js.map +1 -0
- package/dist/improve/state.d.ts +157 -0
- package/dist/improve/state.js +264 -0
- package/dist/improve/state.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/jj-binary.d.ts +36 -0
- package/dist/jj-binary.js +143 -0
- package/dist/jj-binary.js.map +1 -0
- package/dist/jj.d.ts +163 -0
- package/dist/jj.js +926 -0
- package/dist/jj.js.map +1 -0
- package/dist/logger.d.ts +22 -0
- package/dist/logger.js +110 -0
- package/dist/logger.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +1366 -0
- package/dist/main.js.map +1 -0
- package/dist/migration.d.ts +82 -0
- package/dist/migration.js +198 -0
- package/dist/migration.js.map +1 -0
- package/dist/models.d.ts +9 -0
- package/dist/models.js +623 -0
- package/dist/models.js.map +1 -0
- package/dist/move-home.d.ts +82 -0
- package/dist/move-home.js +472 -0
- package/dist/move-home.js.map +1 -0
- package/dist/native/darwin-arm64/rudder-native +0 -0
- package/dist/native/darwin-x64/rudder-native +0 -0
- package/dist/native/linux-x64/rudder-native +0 -0
- package/dist/native-agents.d.ts +8 -0
- package/dist/native-agents.js +178 -0
- package/dist/native-agents.js.map +1 -0
- package/dist/native-binary.d.ts +4 -0
- package/dist/native-binary.js +140 -0
- package/dist/native-binary.js.map +1 -0
- package/dist/paste-bridge.d.ts +36 -0
- package/dist/paste-bridge.js +279 -0
- package/dist/paste-bridge.js.map +1 -0
- package/dist/plan-mode.d.ts +2 -0
- package/dist/plan-mode.js +22 -0
- package/dist/plan-mode.js.map +1 -0
- package/dist/planner.d.ts +27 -0
- package/dist/planner.js +587 -0
- package/dist/planner.js.map +1 -0
- package/dist/predict.d.ts +67 -0
- package/dist/predict.js +369 -0
- package/dist/predict.js.map +1 -0
- package/dist/report.d.ts +74 -0
- package/dist/report.js +412 -0
- package/dist/report.js.map +1 -0
- package/dist/rudder-md.d.ts +33 -0
- package/dist/rudder-md.js +142 -0
- package/dist/rudder-md.js.map +1 -0
- package/dist/run-manager.d.ts +62 -0
- package/dist/run-manager.js +1400 -0
- package/dist/run-manager.js.map +1 -0
- package/dist/scheduler.d.ts +128 -0
- package/dist/scheduler.js +955 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/state.d.ts +127 -0
- package/dist/state.js +704 -0
- package/dist/state.js.map +1 -0
- package/dist/surfaces.d.ts +91 -0
- package/dist/surfaces.js +460 -0
- package/dist/surfaces.js.map +1 -0
- package/dist/task-summary.d.ts +30 -0
- package/dist/task-summary.js +472 -0
- package/dist/task-summary.js.map +1 -0
- package/dist/types.d.ts +492 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/usage.d.ts +198 -0
- package/dist/usage.js +830 -0
- package/dist/usage.js.map +1 -0
- package/dist/util.d.ts +73 -0
- package/dist/util.js +399 -0
- package/dist/util.js.map +1 -0
- package/dist/version-check.d.ts +16 -0
- package/dist/version-check.js +269 -0
- package/dist/version-check.js.map +1 -0
- package/package.json +68 -4
- package/scripts/copy-native.mjs +89 -0
- 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
|