tryharness 0.7.22 → 0.7.25

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 (64) hide show
  1. package/README.md +48 -8
  2. package/assets/opencode-plugin.js +48 -10
  3. package/assets/pi-extension.ts +106 -26
  4. package/cli.mjs +1281 -289
  5. package/lib/activity.mjs +197 -40
  6. package/lib/adapters.mjs +337 -54
  7. package/lib/agentnode.mjs +175 -35
  8. package/lib/atlas.mjs +604 -0
  9. package/lib/bip39-english.txt +2048 -0
  10. package/lib/cards.mjs +177 -39
  11. package/lib/claudemem.mjs +74 -18
  12. package/lib/commands.mjs +13 -5
  13. package/lib/context.mjs +628 -0
  14. package/lib/control-client.mjs +5 -2
  15. package/lib/coordinator.mjs +281 -56
  16. package/lib/daemon.mjs +5590 -838
  17. package/lib/desktop.mjs +149 -35
  18. package/lib/digest.mjs +81 -0
  19. package/lib/environment.mjs +180 -0
  20. package/lib/files.mjs +246 -13
  21. package/lib/firewall.mjs +288 -0
  22. package/lib/folders.mjs +223 -57
  23. package/lib/frames.mjs +333 -0
  24. package/lib/inference.mjs +663 -0
  25. package/lib/learn.mjs +182 -0
  26. package/lib/live.mjs +286 -61
  27. package/lib/localmodels.mjs +571 -0
  28. package/lib/mcp-stdio.mjs +912 -103
  29. package/lib/media.mjs +228 -0
  30. package/lib/members.json +1 -1
  31. package/lib/members.mjs +35 -9
  32. package/lib/models.mjs +342 -80
  33. package/lib/network.mjs +201 -42
  34. package/lib/notify.mjs +49 -12
  35. package/lib/nudge.mjs +190 -32
  36. package/lib/ocr.mjs +380 -0
  37. package/lib/onboard.mjs +321 -70
  38. package/lib/parsers.mjs +132 -41
  39. package/lib/paths.mjs +33 -10
  40. package/lib/personal.mjs +77 -0
  41. package/lib/phones.mjs +398 -51
  42. package/lib/places.mjs +566 -102
  43. package/lib/provenance.mjs +19 -4
  44. package/lib/receipts.mjs +82 -0
  45. package/lib/redact.mjs +730 -0
  46. package/lib/runner.mjs +347 -55
  47. package/lib/runtime-install.mjs +59 -17
  48. package/lib/screens.mjs +212 -0
  49. package/lib/sealed.mjs +160 -28
  50. package/lib/secrets.mjs +18 -5
  51. package/lib/service.mjs +173 -35
  52. package/lib/snippets.mjs +140 -0
  53. package/lib/speech.mjs +993 -0
  54. package/lib/store.mjs +1678 -301
  55. package/lib/swarm.mjs +598 -117
  56. package/lib/t3.mjs +400 -79
  57. package/lib/trails.mjs +401 -0
  58. package/lib/ui.mjs +95 -23
  59. package/lib/vectors.mjs +5 -2
  60. package/lib/wakes.mjs +284 -68
  61. package/lib/watches.mjs +362 -0
  62. package/lib/worktrees.mjs +186 -38
  63. package/package.json +6 -4
  64. package/lib/embedder.mjs +0 -156
package/cli.mjs CHANGED
@@ -4,7 +4,14 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { configureAgents, AGENTS, MCP_CLIENTS, detectMcpClients, mcpClientSnippet, removeAgentConfigurations } from "./lib/adapters.mjs";
7
+ import {
8
+ configureAgents,
9
+ AGENTS,
10
+ MCP_CLIENTS,
11
+ detectMcpClients,
12
+ mcpClientSnippet,
13
+ removeAgentConfigurations,
14
+ } from "./lib/adapters.mjs";
8
15
  import { banner, setupSwarm, testAgent } from "./lib/onboard.mjs";
9
16
  import { createUi } from "./lib/ui.mjs";
10
17
  import pc from "picocolors";
@@ -14,16 +21,34 @@ import { daemonRequest } from "./lib/control-client.mjs";
14
21
  import { ensureDir, readJson, writeJsonAtomic } from "./lib/files.mjs";
15
22
  import { paths } from "./lib/paths.mjs";
16
23
  import { currentRuntime, installRuntime } from "./lib/runtime-install.mjs";
17
- import { ensureModel, modelPresent } from "./lib/embedder.mjs";
18
- import { installService, serviceInfo, uninstallService } from "./lib/service.mjs";
19
- import { canShowDesktop, openDesktop, retireDesktop, setupDesktop } from "./lib/desktop.mjs";
24
+ import {
25
+ MODELS,
26
+ ensureLlama,
27
+ ensureModels,
28
+ modelsPresent,
29
+ } from "./lib/localmodels.mjs";
30
+ import {
31
+ installService,
32
+ serviceInfo,
33
+ uninstallService,
34
+ } from "./lib/service.mjs";
35
+ import {
36
+ canShowDesktop,
37
+ openDesktop,
38
+ retireDesktop,
39
+ setupDesktop,
40
+ } from "./lib/desktop.mjs";
20
41
 
21
42
  const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
22
- const PACKAGE = readJson(path.join(SOURCE_ROOT, "package.json"), { version: "0.4.3" });
43
+ const PACKAGE = readJson(path.join(SOURCE_ROOT, "package.json"), {
44
+ version: "0.4.3",
45
+ });
23
46
  const VERSION = PACKAGE.version;
24
47
  const NODE_MAJOR = Number(process.versions.node.split(".")[0]);
25
48
  if (!Number.isFinite(NODE_MAJOR) || NODE_MAJOR < 18) {
26
- console.error(`tryharness requires Node.js 18 or newer (current: ${process.version}). Upgrade Node, then rerun: npx tryharness`);
49
+ console.error(
50
+ `tryharness requires Node.js 18 or newer (current: ${process.version}). Upgrade Node, then rerun: npx tryharness`
51
+ );
27
52
  process.exit(1);
28
53
  }
29
54
  const argv = process.argv.slice(2);
@@ -31,41 +56,69 @@ const command = ["--help", "-h"].includes(argv[0])
31
56
  ? (argv.shift(), "help")
32
57
  : argv[0] === "--version"
33
58
  ? (argv.shift(), "version")
34
- : argv[0] && !argv[0].startsWith("-") ? argv.shift() : "connect";
59
+ : argv[0] && !argv[0].startsWith("-")
60
+ ? argv.shift()
61
+ : "connect";
35
62
 
36
63
  function arg(flag, fallback = null) {
37
64
  const index = argv.indexOf(flag);
38
65
  return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
39
66
  }
40
- function has(flag) { return argv.includes(flag); }
41
- function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
42
- function shellQuote(value) { return `'${String(value).replace(/'/g, `'\\''`)}'`; }
67
+ function has(flag) {
68
+ return argv.includes(flag);
69
+ }
70
+ function sleep(ms) {
71
+ return new Promise((resolve) => setTimeout(resolve, ms));
72
+ }
73
+ function shellQuote(value) {
74
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
75
+ }
43
76
  function resumeCommand(agent, sessionId, cwd) {
44
- const command = agent === "claude-code"
45
- ? `claude --dangerously-skip-permissions --resume ${shellQuote(sessionId)}`
46
- : agent === "codex"
47
- ? `codex resume --dangerously-bypass-approvals-and-sandbox ${shellQuote(sessionId)}`
48
- : agent === "pi"
49
- ? `pi --approve --session ${shellQuote(sessionId)}`
50
- : `opencode --auto --session ${shellQuote(sessionId)}`;
77
+ const command =
78
+ agent === "claude-code"
79
+ ? `claude --dangerously-skip-permissions --resume ${shellQuote(sessionId)}`
80
+ : agent === "codex"
81
+ ? `codex resume --dangerously-bypass-approvals-and-sandbox ${shellQuote(sessionId)}`
82
+ : agent === "pi"
83
+ ? `pi --approve --session ${shellQuote(sessionId)}`
84
+ : `opencode --auto --session ${shellQuote(sessionId)}`;
51
85
  return cwd ? `cd ${shellQuote(cwd)} && ${command}` : command;
52
86
  }
53
87
  function origin() {
54
88
  const saved = readJson(paths.config, {});
55
- return String(arg("--origin", saved.origin || "https://tryharness.ai")).replace(/\/$/, "");
89
+ return String(
90
+ arg("--origin", saved.origin || "https://tryharness.ai")
91
+ ).replace(/\/$/, "");
56
92
  }
57
93
 
58
94
  async function validateConnection(config) {
59
95
  if (!config.origin || !config.token) return false;
60
96
  try {
61
- const response = await fetch(String(config.origin).replace(/\/$/, "") + "/api/mcp", {
62
- method: "POST",
63
- headers: { "content-type": "application/json", authorization: "Bearer " + config.token },
64
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "tryharness", version: VERSION } } }),
65
- signal: AbortSignal.timeout(10_000),
66
- });
67
- return response.ok && !((await response.json().catch(() => null))?.error);
68
- } catch { return false; }
97
+ const response = await fetch(
98
+ String(config.origin).replace(/\/$/, "") + "/api/mcp",
99
+ {
100
+ method: "POST",
101
+ headers: {
102
+ "content-type": "application/json",
103
+ authorization: "Bearer " + config.token,
104
+ },
105
+ body: JSON.stringify({
106
+ jsonrpc: "2.0",
107
+ id: 1,
108
+ method: "initialize",
109
+ params: {
110
+ protocolVersion: "2025-06-18",
111
+ capabilities: {},
112
+ clientInfo: { name: "tryharness", version: VERSION },
113
+ },
114
+ }),
115
+ signal: AbortSignal.timeout(10_000),
116
+ }
117
+ );
118
+ return response.ok && !(await response.json().catch(() => null))?.error;
119
+ } catch {
120
+ return false;
121
+ }
69
122
  }
70
123
 
71
124
  async function pair(pairOrigin, agents, ui) {
@@ -75,34 +128,60 @@ async function pair(pairOrigin, agents, ui) {
75
128
  body: JSON.stringify({ hostname: os.hostname().slice(0, 64), agents }),
76
129
  signal: AbortSignal.timeout(15_000),
77
130
  });
78
- if (!response.ok) throw new Error(`pairing unavailable (HTTP ${response.status})`);
131
+ if (!response.ok)
132
+ throw new Error(`pairing unavailable (HTTP ${response.status})`);
79
133
  const { pairId, code } = await response.json();
80
- ui.note(`${pc.bold(code)}\n\n${pc.dim("In Harness: Settings → Connections → Connect a machine, then enter this code.")}`, "Pair this computer");
134
+ ui.note(
135
+ `${pc.bold(code)}\n\n${pc.dim("In Harness: Settings → Connections → Connect a machine, then enter this code.")}`,
136
+ "Pair this computer"
137
+ );
81
138
  const spin = ui.spinner();
82
139
  spin.start("Waiting for your approval (the code works for 10 minutes)");
83
140
  return new Promise((resolve, reject) => {
84
- fetch(pairOrigin + "/api/pair/wait/" + pairId, { headers: { accept: "text/event-stream" } }).then(async (waitResponse) => {
85
- if (!waitResponse.ok || !waitResponse.body) throw new Error("pairing stream failed");
86
- const reader = waitResponse.body.getReader();
87
- const decoder = new TextDecoder();
88
- let buffer = "";
89
- for (;;) {
90
- const { done, value } = await reader.read();
91
- if (done) break;
92
- buffer += decoder.decode(value, { stream: true });
93
- const frames = buffer.split("\n\n");
94
- buffer = frames.pop() || "";
95
- for (const frame of frames) {
96
- const line = frame.split("\n").find((entry) => entry.startsWith("data: "));
97
- if (!line) continue;
98
- const payload = JSON.parse(line.slice(6));
99
- if (payload.token) { resolve(payload.token); return; }
100
- if (payload.error) { reject(new Error(payload.error)); return; }
141
+ fetch(pairOrigin + "/api/pair/wait/" + pairId, {
142
+ headers: { accept: "text/event-stream" },
143
+ })
144
+ .then(async (waitResponse) => {
145
+ if (!waitResponse.ok || !waitResponse.body)
146
+ throw new Error("pairing stream failed");
147
+ const reader = waitResponse.body.getReader();
148
+ const decoder = new TextDecoder();
149
+ let buffer = "";
150
+ for (;;) {
151
+ const { done, value } = await reader.read();
152
+ if (done) break;
153
+ buffer += decoder.decode(value, { stream: true });
154
+ const frames = buffer.split("\n\n");
155
+ buffer = frames.pop() || "";
156
+ for (const frame of frames) {
157
+ const line = frame
158
+ .split("\n")
159
+ .find((entry) => entry.startsWith("data: "));
160
+ if (!line) continue;
161
+ const payload = JSON.parse(line.slice(6));
162
+ if (payload.token) {
163
+ resolve(payload.token);
164
+ return;
165
+ }
166
+ if (payload.error) {
167
+ reject(new Error(payload.error));
168
+ return;
169
+ }
170
+ }
101
171
  }
102
- }
103
- reject(new Error("pairing stream ended without approval"));
104
- }).catch(reject);
105
- }).then((token) => { spin.stop("Paired with Harness"); return token; }, (error) => { spin.stop("Pairing did not finish", 2); throw error; });
172
+ reject(new Error("pairing stream ended without approval"));
173
+ })
174
+ .catch(reject);
175
+ }).then(
176
+ (token) => {
177
+ spin.stop("Paired with Harness");
178
+ return token;
179
+ },
180
+ (error) => {
181
+ spin.stop("Pairing did not finish", 2);
182
+ throw error;
183
+ }
184
+ );
106
185
  }
107
186
 
108
187
  async function waitForDaemon(timeoutMs = 12_000) {
@@ -114,29 +193,51 @@ async function waitForDaemon(timeoutMs = 12_000) {
114
193
  lastStatus = await daemonRequest({ command: "status" }, 1_000);
115
194
  if (lastStatus.connected) return lastStatus;
116
195
  if (lastStatus.lastError) lastError = new Error(lastStatus.lastError);
196
+ } catch (error) {
197
+ lastError = error;
117
198
  }
118
- catch (error) { lastError = error; }
119
199
  await new Promise((resolve) => setTimeout(resolve, 250));
120
200
  }
121
- throw new Error(`daemon started but its authenticated Harness bridge did not become ready: ${lastError?.message || lastStatus?.lastError || "timeout"}. Run: npx tryharness doctor`);
201
+ throw new Error(
202
+ `daemon started but its authenticated Harness bridge did not become ready: ${lastError?.message || lastStatus?.lastError || "timeout"}. Run: npx tryharness doctor`
203
+ );
122
204
  }
123
205
 
124
206
  async function connect() {
125
207
  ensureDir(paths.home);
126
208
  const detected = detectAgents();
127
- const installed = detected.filter((entry) => entry.path).map((entry) => entry.agent);
128
- const requested = argv.flatMap((value, index) => value === "--agent" && argv[index + 1] ? [argv[index + 1]] : []);
129
- for (const agent of requested) if (!AGENTS.includes(agent)) throw new Error(`unknown agent '${agent}'`);
130
- const selected = requested.length ? installed.filter((agent) => requested.includes(agent)) : installed;
209
+ const installed = detected
210
+ .filter((entry) => entry.path)
211
+ .map((entry) => entry.agent);
212
+ const requested = argv.flatMap((value, index) =>
213
+ value === "--agent" && argv[index + 1] ? [argv[index + 1]] : []
214
+ );
215
+ for (const agent of requested)
216
+ if (!AGENTS.includes(agent)) throw new Error(`unknown agent '${agent}'`);
217
+ const selected = requested.length
218
+ ? installed.filter((agent) => requested.includes(agent))
219
+ : installed;
131
220
  const ui = createUi();
132
221
  console.log(`\n${banner(VERSION)}\n`);
133
- ui.intro(`${pc.bgCyan(pc.black(" tryharness "))} Setting up Harness on this computer`);
134
- ui.step(installed.length ? `Found ${installed.join(", ")}` : "No supported coding agents found");
222
+ ui.intro(
223
+ `${pc.bgCyan(pc.black(" tryharness "))} Setting up Harness on this computer`
224
+ );
225
+ ui.step(
226
+ installed.length
227
+ ? `Found ${installed.join(", ")}`
228
+ : "No supported coding agents found"
229
+ );
135
230
 
136
231
  const pairOrigin = origin();
137
232
  let config = readJson(paths.config, {});
138
- const reusable = !has("--reconnect") && config.origin === pairOrigin && await validateConnection(config);
139
- if (reusable) ui.success(`Connected to Harness ${pc.dim(`· ${new URL(pairOrigin).host}`)}`);
233
+ const reusable =
234
+ !has("--reconnect") &&
235
+ config.origin === pairOrigin &&
236
+ (await validateConnection(config));
237
+ if (reusable)
238
+ ui.success(
239
+ `Connected to Harness ${pc.dim(`· ${new URL(pairOrigin).host}`)}`
240
+ );
140
241
  else {
141
242
  const token = await pair(pairOrigin, selected, ui);
142
243
  config = { ...config, origin: pairOrigin, token };
@@ -147,25 +248,52 @@ async function connect() {
147
248
  // point into npm's temporary npx cache.
148
249
  const spin = ui.spinner();
149
250
  spin.start("Installing the Harness runtime and connecting your agents");
150
- const runtimeRoot = installRuntime({ sourceRoot: SOURCE_ROOT, version: VERSION });
251
+ const runtimeRoot = installRuntime({
252
+ sourceRoot: SOURCE_ROOT,
253
+ version: VERSION,
254
+ });
151
255
  const states = configureAgents({ runtimeRoot, selected });
152
- const stateMap = Object.fromEntries(states.map((state) => [state.agent, state]));
153
- config = { ...readJson(paths.config, config), version: VERSION, runtimeRoot, agentStates: stateMap };
256
+ const stateMap = Object.fromEntries(
257
+ states.map((state) => [state.agent, state])
258
+ );
259
+ config = {
260
+ ...readJson(paths.config, config),
261
+ version: VERSION,
262
+ runtimeRoot,
263
+ agentStates: stateMap,
264
+ };
154
265
  writeJsonAtomic(paths.config, config);
155
266
  const service = installService({ runtimeRoot });
156
267
  spin.message("Starting the daemon");
157
- try { await waitForDaemon(); } catch (error) { spin.stop("The daemon did not connect to Harness", 2); throw error; }
158
- spin.stop(`Daemon running ${pc.dim(`· ${service.manager}, connected to Harness`)}`);
268
+ try {
269
+ await waitForDaemon();
270
+ } catch (error) {
271
+ spin.stop("The daemon did not connect to Harness", 2);
272
+ throw error;
273
+ }
274
+ spin.stop(
275
+ `Daemon running ${pc.dim(`· ${service.manager}, connected to Harness`)}`
276
+ );
159
277
  await setupSwarm({ runtimeRoot, states, daemonRequest, ui });
160
278
  try {
161
279
  const devices = await daemonRequest({ command: "phone.list" });
162
- if (devices.ok && !devices.phones.some((device) => device.kind === "browser" && !device.revoked_at)) {
280
+ if (
281
+ devices.ok &&
282
+ !devices.phones.some(
283
+ (device) => device.kind === "browser" && !device.revoked_at
284
+ )
285
+ ) {
163
286
  const offer = await browserLink();
164
- ui.info(offer.opened ? "Opened Harness in your browser to connect it to this computer end to end. The link works once, for 10 minutes."
165
- : `Open this in the browser you use Harness in. It works once, for 10 minutes, and is the pairing secret:\n${offer.url}`);
287
+ ui.info(
288
+ offer.opened
289
+ ? "Opened Harness in your browser to connect it to this computer end to end. The link works once, for 10 minutes."
290
+ : `Open this in the browser you use Harness in. It works once, for 10 minutes, and is the pairing secret:\n${offer.url}`
291
+ );
166
292
  } else if (devices.ok) ui.success("Browser connected end to end");
167
293
  } catch (error) {
168
- ui.warn(`Browser not connected (${error.message}). Run: npx tryharness link`);
294
+ ui.warn(
295
+ `Browser not connected (${error.message}). Run: npx tryharness link`
296
+ );
169
297
  }
170
298
  if (!has("--no-desktop")) {
171
299
  const spin = ui.spinner();
@@ -173,34 +301,90 @@ async function connect() {
173
301
  const shown = await desktop();
174
302
  spin.stop(shown.text, shown.ok ? 0 : 2);
175
303
  }
176
- // The context space's embedder lives with the daemon and comes down with setup, so the machine
177
- // can search a task by meaning before the first task exists. About 110 MB, once.
178
- if (modelPresent(paths.models)) ui.success("Embedder ready");
304
+ // The relay's core local models (localmodels.mjs) and the server that runs them come down with setup, so agents get
305
+ // context packs, code search, the handoff firewall and images checked before they leave from the first task. Each file
306
+ // is checked against its pinned hash.
307
+ const core = MODELS.filter((entry) => entry.tier === "core").map(
308
+ (entry) => entry.id
309
+ );
310
+ const localRoot = path.join(paths.home, "local");
311
+ if (modelsPresent(localRoot, core)) ui.success("Local models ready");
179
312
  else {
180
313
  const download = ui.spinner();
181
- download.start("Downloading the embedder (about 110 MB, once)");
182
- try { await ensureModel({ modelsDir: paths.models, log: (line) => download.message(`Downloading the embedder · ${line.slice(0, 30)}`) }); download.stop("Embedder ready"); }
183
- catch (error) { download.stop(`Embedder not downloaded (${error.message}). Task search uses words until the next setup run`, 2); }
314
+ const size = Math.round(
315
+ MODELS.filter((entry) => core.includes(entry.id))
316
+ .flatMap((entry) => entry.files)
317
+ .reduce((sum, file) => sum + file.size, 0) / 1e6
318
+ );
319
+ download.start(`Downloading the local models (about ${size} MB, once)`);
320
+ // The classifiers run without the model server, so one that cannot start here does not hold them back.
321
+ const failures = [];
322
+ for (const step of [
323
+ () =>
324
+ ensureLlama({
325
+ root: localRoot,
326
+ log: (line) =>
327
+ download.message(
328
+ `Downloading the local models · ${line.slice(0, 40)}`
329
+ ),
330
+ }),
331
+ () =>
332
+ ensureModels({
333
+ root: localRoot,
334
+ ids: core,
335
+ log: (line) =>
336
+ download.message(
337
+ `Downloading the local models · ${line.slice(0, 40)}`
338
+ ),
339
+ }),
340
+ ]) {
341
+ try {
342
+ await step();
343
+ } catch (error) {
344
+ failures.push(error.message);
345
+ }
346
+ }
347
+ if (failures.length)
348
+ download.stop(
349
+ `Local models not ready (${failures.join("; ")}). Agents work as before until the next setup run`,
350
+ 2
351
+ );
352
+ else download.stop("Local models ready");
184
353
  }
185
- const failed = states.filter((state) => selected.includes(state.agent) && state.installed && !state.configured);
354
+ const failed = states.filter(
355
+ (state) =>
356
+ selected.includes(state.agent) && state.installed && !state.configured
357
+ );
186
358
  if (failed.length) {
187
- throw new Error(`setup is incomplete for ${failed.map((state) => state.agent).join(", ")}. Nothing was hidden; run npx tryharness doctor after fixing the reported CLI error, then rerun setup.`);
359
+ throw new Error(
360
+ `setup is incomplete for ${failed.map((state) => state.agent).join(", ")}. Nothing was hidden; run npx tryharness doctor after fixing the reported CLI error, then rerun setup.`
361
+ );
188
362
  }
189
363
  ui.info("History indexes locally in the background");
190
- ui.info(`Your coding agents start the tasks you ask for in folders they have worked in, up to 10 a day each, without a Start. Tasks another agent wrote wait for you.\n${pc.dim("Change it: npx tryharness board standing")}`);
364
+ ui.info(
365
+ `Your coding agents start the tasks you ask for in folders they have worked in, up to 10 a day each, without a Start. Tasks another agent wrote wait for you.\n${pc.dim("Change it: npx tryharness board standing")}`
366
+ );
191
367
  ui.outro("Done. You can close this terminal.");
192
368
  }
193
369
 
194
370
  // The desktop app: the newest build for this machine, installed and opened. Never fatal; the
195
371
  // daemon works without it.
196
372
  async function desktop() {
197
- if (!canShowDesktop({})) return { ok: false, text: "Desktop app skipped (no screen on this login)" };
373
+ if (!canShowDesktop({}))
374
+ return { ok: false, text: "Desktop app skipped (no screen on this login)" };
198
375
  try {
199
376
  const installed = await setupDesktop({ sourceRoot: SOURCE_ROOT });
200
- if (!installed) return { ok: false, text: `No desktop app for ${process.platform} ${process.arch} yet` };
377
+ if (!installed)
378
+ return {
379
+ ok: false,
380
+ text: `No desktop app for ${process.platform} ${process.arch} yet`,
381
+ };
201
382
  const { target, updated } = installed;
202
383
  openDesktop({ target });
203
- return { ok: true, text: `Desktop app ${updated ? "installed and opened" : "opened"}. The Harness mark is at the top right of your screen; it starts when you log in.` };
384
+ return {
385
+ ok: true,
386
+ text: `Desktop app ${updated ? "installed and opened" : "opened"}. The Harness mark is at the top right of your screen; it starts when you log in.`,
387
+ };
204
388
  } catch (error) {
205
389
  return { ok: false, text: `Desktop app not opened (${error.message})` };
206
390
  }
@@ -209,34 +393,180 @@ async function desktop() {
209
393
  async function status() {
210
394
  const fallback = readJson(paths.status, null);
211
395
  let current;
212
- try { current = await daemonRequest({ command: "status" }); }
213
- catch { current = fallback ? { ok: false, ...fallback } : null; }
396
+ try {
397
+ current = await daemonRequest({ command: "status" });
398
+ } catch {
399
+ current = fallback ? { ok: false, ...fallback } : null;
400
+ }
214
401
  const service = serviceInfo();
215
402
  if (!current) {
216
403
  console.log("Harness daemon: not running");
217
- console.log(`service: ${service.installed ? "installed" : "not installed"}`);
404
+ console.log(
405
+ `service: ${service.installed ? "installed" : "not installed"}`
406
+ );
218
407
  process.exitCode = 1;
219
408
  return;
220
409
  }
221
- console.log(`Harness daemon ${current.version || "unknown"}: ${current.ok === false ? "not responding" : "running"} (pid ${current.pid || "?"})`);
222
- console.log(`Harness bridge: ${current.connected ? "connected" : "disconnected"}`);
223
- console.log(`history: ${current.indexing ? "indexing" : "ready"}${current.lastIndexedAt ? ` · last scan ${current.lastIndexedAt}` : ""}`);
410
+ console.log(
411
+ `Harness daemon ${current.version || "unknown"}: ${current.ok === false ? "not responding" : "running"} (pid ${current.pid || "?"})`
412
+ );
413
+ console.log(
414
+ `Harness bridge: ${current.connected ? "connected" : "disconnected"}`
415
+ );
416
+ console.log(
417
+ `history: ${current.indexing ? "indexing" : "ready"}${current.lastIndexedAt ? ` · last scan ${current.lastIndexedAt}` : ""}`
418
+ );
224
419
  for (const state of current.agents || []) {
225
420
  const count = Number(current.counts?.[state.agent] || 0);
226
- console.log(` ${state.agent}: ${state.installed ? state.configured ? "ready" : "installed, configuration needs repair" : "not installed"} · ${count} sessions`);
421
+ console.log(
422
+ ` ${state.agent}: ${state.installed ? (state.configured ? "ready" : "installed, configuration needs repair") : "not installed"} · ${count} sessions`
423
+ );
424
+ }
425
+ }
426
+
427
+ // The relay's local models: each one's job, licence, size and whether it is here, where they run, what the context engine
428
+ // has indexed, and what the overnight learner last did. `tryharness local learn` starts the learner now; `local add
429
+ // <folder>` and `local remove <folder>` name folders of recordings and screenshots for search_media and briefs.
430
+ async function local() {
431
+ const root = path.join(paths.home, "local");
432
+ if (argv[0] === "add" || argv[0] === "remove") {
433
+ const response = await daemonRequest({
434
+ command: `local.${argv[0]}`,
435
+ folder: path.resolve(argv[1] || process.cwd()),
436
+ });
437
+ if (!response?.ok)
438
+ throw new Error(response?.error || "the daemon did not answer");
439
+ console.log(
440
+ argv[0] === "add"
441
+ ? `Added. The relay reads the recordings and screenshots there in the background${response.downloading ? ", once the image and audio part of the search model has downloaded (555 MB)" : ""}.`
442
+ : "Removed."
443
+ );
444
+ return;
445
+ }
446
+ for (const entry of MODELS) {
447
+ const here = modelsPresent(root, [entry.id]);
448
+ const mb = Math.round(
449
+ entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6
450
+ );
451
+ console.log(
452
+ `${here ? "ready " : "missing"} ${entry.name} (${entry.tier}, ${entry.licence}, ${mb} MB): ${entry.purpose}`
453
+ );
454
+ }
455
+ let current = null;
456
+ try {
457
+ current = await daemonRequest({
458
+ command: argv[0] === "learn" ? "local.learn" : "local.status",
459
+ });
460
+ } catch (error) {
461
+ console.log(`daemon: ${error.message}`);
462
+ process.exitCode = 1;
463
+ return;
464
+ }
465
+ if (!current?.ok) {
466
+ console.log(current?.error || "the daemon did not answer");
467
+ process.exitCode = 1;
468
+ return;
469
+ }
470
+ if (argv[0] === "learn") {
471
+ console.log(
472
+ "Learning from your agents' runs now; tryharness local shows the result when it is done."
473
+ );
474
+ return;
475
+ }
476
+ console.log(
477
+ `runs on: ${current.device ? (current.device.gpu ? "GPU" : "CPU") : "not checked yet"}`
478
+ );
479
+ for (const indexed of current.roots || [])
480
+ console.log(`indexed: ${indexed.root} (${indexed.files} files)`);
481
+ if (current.pieces?.waiting)
482
+ console.log(`waiting to be embedded: ${current.pieces.waiting} pieces`);
483
+ console.log(
484
+ current.model
485
+ ? `ranking: learned ${current.model.trainedAt}, held-out recall at 10 ${JSON.stringify(current.model.heldOut)}`
486
+ : "ranking: the default until the learner has enough runs"
487
+ );
488
+ for (const folder of current.media?.folders || [])
489
+ console.log(`recordings and screenshots: ${folder}`);
490
+ if (current.media?.folders?.length)
491
+ console.log(`media files read: ${current.media.files}`);
492
+ if (current.learning)
493
+ console.log(
494
+ `last learning: ${current.learning.at} · ${current.learning.kept ? "kept a new model" : `kept the old one${current.learning.why ? ` (${current.learning.why})` : ""}`}`
495
+ );
496
+ }
497
+
498
+ // Watches (watches.mjs): `tryharness watch` lists them and what each last read; `watch add` starts one; agents' suggestions
499
+ // wait for `watch start`.
500
+ async function watch() {
501
+ const sub = argv[0] || "list";
502
+ const minutes = (text) => {
503
+ const found = String(text || "").match(/^(\d+(?:\.\d+)?)\s*(s|m|h)?$/);
504
+ return found
505
+ ? Number(found[1]) * { s: 1, m: 60, h: 3600 }[found[2] || "m"]
506
+ : null;
507
+ };
508
+ const response =
509
+ sub === "add"
510
+ ? await daemonRequest({
511
+ command: "board.watch.add",
512
+ agent: "user",
513
+ name: argv[1],
514
+ watchCommand: arg("--run", ""),
515
+ folder: path.resolve(arg("--in", process.cwd())),
516
+ everySeconds: minutes(arg("--every", "5m")) ?? 300,
517
+ field: arg("--field", undefined),
518
+ age: arg("--age", undefined),
519
+ count: arg("--count", undefined),
520
+ above: arg("--above", undefined),
521
+ below: arg("--below", undefined),
522
+ })
523
+ : sub === "start" || sub === "remove"
524
+ ? await daemonRequest({
525
+ command: `board.watch.${sub}`,
526
+ agent: "user",
527
+ id: argv[1],
528
+ })
529
+ : await daemonRequest({ command: "board.watch.list", agent: "user" });
530
+ if (!response?.ok)
531
+ throw new Error(response?.error || "the daemon did not answer");
532
+ if (sub === "add") {
533
+ console.log(
534
+ `Watching ${response.watch.name} every ${response.watch.everySeconds} s (${response.watch.id}).`
535
+ );
536
+ return;
537
+ }
538
+ if (sub !== "list") {
539
+ console.log("Done.");
540
+ return;
541
+ }
542
+ if (!response.watches.length)
543
+ console.log(
544
+ 'No watches. Add one: tryharness watch add "keeper heartbeat" --run "<command>" --age completedAt --above 900'
545
+ );
546
+ for (const entry of response.watches) {
547
+ const last = entry.samples.at(-1);
548
+ console.log(
549
+ `${entry.state === "on" ? (entry.off ? "OFF " : "ok ") : "asks "} ${entry.name} · every ${entry.everySeconds} s · ${last ? (last.error ? `last check failed: ${last.error}` : `last read ${last.value} (${last.ms} ms)`) : "not checked yet"}${entry.state === "suggested" ? ` · suggested by ${entry.by}: tryharness watch start ${entry.id}` : ""}${entry.off ? ` · ${entry.off.text}` : ""}`
550
+ );
227
551
  }
228
552
  }
229
553
 
230
554
  async function doctor() {
231
555
  console.log(`tryharness ${VERSION}`);
232
556
  const config = readJson(paths.config, {});
233
- console.log(`pairing: ${await validateConnection(config) ? "valid" : "missing or invalid"}`);
557
+ console.log(
558
+ `pairing: ${(await validateConnection(config)) ? "valid" : "missing or invalid"}`
559
+ );
234
560
  const service = serviceInfo();
235
- console.log(`service: ${service.installed ? `installed (${service.manager})` : `not installed (${service.manager})`}`);
561
+ console.log(
562
+ `service: ${service.installed ? `installed (${service.manager})` : `not installed (${service.manager})`}`
563
+ );
236
564
  try {
237
565
  const report = await daemonRequest({ command: "doctor" });
238
566
  console.log(`daemon: running (pid ${report.status.pid})`);
239
- console.log(`bridge: ${report.status.connected ? "connected" : `disconnected${report.status.lastError ? ` — ${report.status.lastError}` : ""}`}`);
567
+ console.log(
568
+ `bridge: ${report.status.connected ? "connected" : `disconnected${report.status.lastError ? ` — ${report.status.lastError}` : ""}`}`
569
+ );
240
570
  console.log(`database: ${report.database}`);
241
571
  console.log(`control socket: ${report.socket}`);
242
572
  console.log(`log: ${report.log}`);
@@ -244,66 +574,120 @@ async function doctor() {
244
574
  console.log(`daemon: unavailable — ${error.message}`);
245
575
  process.exitCode = 1;
246
576
  }
247
- for (const found of detectAgents()) console.log(`${found.agent}: ${found.path || "not installed"}`);
577
+ for (const found of detectAgents())
578
+ console.log(`${found.agent}: ${found.path || "not installed"}`);
248
579
  }
249
580
 
250
581
  async function sync() {
251
582
  const agent = arg("--agent", "all");
252
- if (agent !== "all" && !AGENTS.includes(agent)) throw new Error(`unknown agent '${agent}'`);
583
+ if (agent !== "all" && !AGENTS.includes(agent))
584
+ throw new Error(`unknown agent '${agent}'`);
253
585
  const response = await daemonRequest({ command: "sync", agent });
254
586
  if (!response.ok) throw new Error(response.error || "sync rejected");
255
- console.log(`Local history scan started for ${agent}. It runs in the daemon; this terminal does not need to stay open.`);
587
+ console.log(
588
+ `Local history scan started for ${agent}. It runs in the daemon; this terminal does not need to stay open.`
589
+ );
256
590
  }
257
591
 
258
592
  async function notify() {
259
593
  const mode = argv[0];
260
- if (!["on", "off"].includes(mode)) { const config = readJson(paths.config, {}); console.log(`desktop notifications: ${config.notifications === false ? "off" : "on"} (tryharness notify on|off)`); return; }
594
+ if (!["on", "off"].includes(mode)) {
595
+ const config = readJson(paths.config, {});
596
+ console.log(
597
+ `desktop notifications: ${config.notifications === false ? "off" : "on"} (tryharness notify on|off)`
598
+ );
599
+ return;
600
+ }
261
601
  const config = readJson(paths.config, {});
262
602
  writeJsonAtomic(paths.config, { ...config, notifications: mode === "on" });
263
- console.log(`desktop notifications ${mode}. The daemon picks this up on its next change.`);
603
+ console.log(
604
+ `desktop notifications ${mode}. The daemon picks this up on its next change.`
605
+ );
264
606
  }
265
607
 
266
608
  async function connectAgent() {
267
609
  // Ask another person's listed agent to work an issue with one of ours: a request their Harness shows them.
268
610
  const to = String(argv[0] || "");
269
- if (!/^[a-z0-9][a-z0-9-]{1,19}\/[a-z0-9][a-z0-9._-]{0,63}$/.test(to)) throw new Error("usage: tryharness connect <handle/agent> --about <text> [--agent <your agent>] [--file <brief.md>]");
611
+ if (!/^[a-z0-9][a-z0-9-]{1,19}\/[a-z0-9][a-z0-9._-]{0,63}$/.test(to))
612
+ throw new Error(
613
+ "usage: tryharness connect <handle/agent> --about <text> [--agent <your agent>] [--file <brief.md>]"
614
+ );
270
615
  const fromAgent = arg("--agent", "claude-code");
271
- if (!AGENTS.includes(fromAgent)) throw new Error(`--agent must be one of ${AGENTS.join(", ")}`);
616
+ if (!AGENTS.includes(fromAgent))
617
+ throw new Error(`--agent must be one of ${AGENTS.join(", ")}`);
272
618
  const about = arg("--about", "");
273
619
  if (!about.trim()) throw new Error("connect needs --about");
274
620
  const file = arg("--file");
275
621
  let brief = "";
276
622
  if (file) brief = fs.readFileSync(path.resolve(file), "utf8");
277
- else if (!process.stdin.isTTY) for await (const chunk of process.stdin) brief += chunk;
278
- const response = await daemonRequest({ command: "relay.request", to, fromAgent, about, brief, trustNewKey: has("--trust-new-key") }, 20_000);
623
+ else if (!process.stdin.isTTY)
624
+ for await (const chunk of process.stdin) brief += chunk;
625
+ const response = await daemonRequest(
626
+ {
627
+ command: "relay.request",
628
+ to,
629
+ fromAgent,
630
+ about,
631
+ brief,
632
+ trustNewKey: has("--trust-new-key"),
633
+ },
634
+ 20_000
635
+ );
279
636
  if (!response.ok) throw new Error(response.error || "request failed");
280
- console.log(`Asked ${to} to work "${about}" with your ${fromAgent}. Thread ${String(response.threadId || "").slice(0, 8)} is ${response.status || "requested"}; once they accept, it appears on your board and your ${fromAgent} is told.`);
281
- console.log(response.sealed
282
- ? `Sealed end to end to ${to.split("/")[0]}'s computer (key ${response.fingerprint}); Harness carries it and cannot read it. Check the key with them once: npx tryharness peers`
283
- : `Not end to end: ${to.split("/")[0]}'s computer runs a Harness without sealed threads yet, so Harness can read this thread.`);
637
+ console.log(
638
+ `Asked ${to} to work "${about}" with your ${fromAgent}. Thread ${String(response.threadId || "").slice(0, 8)} is ${response.status || "requested"}; once they accept, it appears on your board and your ${fromAgent} is told.`
639
+ );
640
+ console.log(
641
+ response.sealed
642
+ ? `Sealed end to end to ${to.split("/")[0]}'s computer (key ${response.fingerprint}); Harness carries it and cannot read it. Check the key with them once: npx tryharness peers`
643
+ : `Not end to end: ${to.split("/")[0]}'s computer runs a Harness without sealed threads yet, so Harness can read this thread.`
644
+ );
284
645
  }
285
646
 
286
647
  // The keys of other people's computers this one has worked with, pinned the first time, and its own.
287
648
  async function peers() {
288
649
  const response = await daemonRequest({ command: "relay.peers" });
289
- if (!response.ok) throw new Error(response.error || "the daemon could not list keys");
650
+ if (!response.ok)
651
+ throw new Error(response.error || "the daemon could not list keys");
290
652
  console.log(`This computer: ${response.mine ?? "no key yet"}`);
291
- if (!response.peers.length) { console.log("No other computers yet. A key is pinned the first time you connect with someone."); return; }
292
- for (const peer of response.peers) console.log(`${peer.handle.padEnd(20)} ${peer.fingerprint} pinned ${peer.pinned_at.slice(0, 10)}`);
293
- console.log("Read a fingerprint to its owner over a channel you trust; if it matches what their npx tryharness peers shows, nobody stands between you.");
653
+ if (!response.peers.length) {
654
+ console.log(
655
+ "No other computers yet. A key is pinned the first time you connect with someone."
656
+ );
657
+ return;
658
+ }
659
+ for (const peer of response.peers)
660
+ console.log(
661
+ `${peer.handle.padEnd(20)} ${peer.fingerprint} pinned ${peer.pinned_at.slice(0, 10)}`
662
+ );
663
+ console.log(
664
+ "Read a fingerprint to its owner over a channel you trust; if it matches what their npx tryharness peers shows, nobody stands between you."
665
+ );
294
666
  }
295
667
 
296
668
  async function mcpSnippet() {
297
669
  const client = argv[0];
298
- if (!MCP_CLIENTS.includes(client)) throw new Error(`usage: tryharness mcp <${MCP_CLIENTS.join("|")}>`);
670
+ if (!MCP_CLIENTS.includes(client))
671
+ throw new Error(`usage: tryharness mcp <${MCP_CLIENTS.join("|")}>`);
299
672
  const config = readJson(paths.config, {});
300
673
  const runtimeRoot = config.runtimeRoot || currentRuntime();
301
- if (!runtimeRoot) throw new Error("no Harness runtime is installed yet; run npx tryharness first");
674
+ if (!runtimeRoot)
675
+ throw new Error(
676
+ "no Harness runtime is installed yet; run npx tryharness first"
677
+ );
302
678
  const found = detectMcpClients().find((entry) => entry.client === client);
303
- console.log(`${client}: ${found?.path ? `found at ${found.path}` : "not found on this machine; the snippet still works once it is installed"}`);
304
- console.log(`In ${client === "grok-bot" ? "Grok Bot, open Settings, MCP, add a server" : client} and paste this:\n`);
305
- console.log(JSON.stringify(mcpClientSnippet({ runtimeRoot, client }), null, 2));
306
- console.log(`\nIt then sees the board through list_tasks, get_task, post_task, claim_task and report_task as "${client}".`);
679
+ console.log(
680
+ `${client}: ${found?.path ? `found at ${found.path}` : "not found on this machine; the snippet still works once it is installed"}`
681
+ );
682
+ console.log(
683
+ `In ${client === "grok-bot" ? "Grok Bot, open Settings, MCP, add a server" : client} and paste this:\n`
684
+ );
685
+ console.log(
686
+ JSON.stringify(mcpClientSnippet({ runtimeRoot, client }), null, 2)
687
+ );
688
+ console.log(
689
+ `\nIt then sees the board through list_tasks, get_task, post_task, claim_task and report_task as "${client}".`
690
+ );
307
691
  }
308
692
 
309
693
  async function board() {
@@ -311,85 +695,256 @@ async function board() {
311
695
  const agent = arg("--as", "user");
312
696
  if (sub === "post") {
313
697
  // --for codex,claude-code posts one task for several agents at once.
314
- const targets = String(arg("--for", "")).split(",").map((value) => value.trim()).filter(Boolean);
698
+ const targets = String(arg("--for", ""))
699
+ .split(",")
700
+ .map((value) => value.trim())
701
+ .filter(Boolean);
315
702
  const target = targets.length === 1 ? targets[0] : null;
316
703
  // Coding agents, Grok Bot and Harness live on this machine's board; Bankr, Quotient and Muse live in Harness and the daemon posts there.
317
- const allowed = (name) => AGENTS.includes(name) || ["harness", "grok-bot", "openclaw", "hermes", "muse", "bankr", "quotient"].includes(name);
318
- if (targets.some((name) => !allowed(name))) throw new Error("usage: tryharness board post --for <agent[,agent...]> --objective <text> [--file <brief.md>] [--run --workspace <dir>]");
704
+ const allowed = (name) =>
705
+ AGENTS.includes(name) ||
706
+ [
707
+ "harness",
708
+ "grok-bot",
709
+ "openclaw",
710
+ "hermes",
711
+ "muse",
712
+ "bankr",
713
+ "quotient",
714
+ ].includes(name);
715
+ if (targets.some((name) => !allowed(name)))
716
+ throw new Error(
717
+ "usage: tryharness board post --for <agent[,agent...]> --objective <text> [--file <brief.md>] [--run --workspace <dir>]"
718
+ );
319
719
  const objective = arg("--objective", "");
320
720
  const file = arg("--file");
321
721
  let brief = "";
322
722
  if (file) brief = fs.readFileSync(path.resolve(file), "utf8");
323
- else if (!process.stdin.isTTY) for await (const chunk of process.stdin) brief += chunk;
723
+ else if (!process.stdin.isTTY)
724
+ for await (const chunk of process.stdin) brief += chunk;
324
725
  if (!objective.trim()) throw new Error("board post needs --objective");
325
- const runWorkspace = has("--run") ? path.resolve(arg("--workspace", process.cwd())) : null;
326
- if (runWorkspace && !targets.every((name) => AGENTS.includes(name))) throw new Error("--run needs --for with coding agents installed on this machine");
327
- if (targets.length > 1 && targets.some((name) => ["bankr", "quotient", "muse"].includes(name))) throw new Error("a task for several agents at once is for agents on this machine; post a task for bankr, quotient or muse on its own");
328
- const response = await daemonRequest({ command: "board.post", agent, objective, brief, targetAgent: target, ...(targets.length > 1 ? { targetAgents: targets } : {}), runWorkspace });
726
+ const runWorkspace = has("--run")
727
+ ? path.resolve(arg("--workspace", process.cwd()))
728
+ : null;
729
+ if (runWorkspace && !targets.every((name) => AGENTS.includes(name)))
730
+ throw new Error(
731
+ "--run needs --for with coding agents installed on this machine"
732
+ );
733
+ if (
734
+ targets.length > 1 &&
735
+ targets.some((name) => ["bankr", "quotient", "muse"].includes(name))
736
+ )
737
+ throw new Error(
738
+ "a task for several agents at once is for agents on this machine; post a task for bankr, quotient or muse on its own"
739
+ );
740
+ const response = await daemonRequest({
741
+ command: "board.post",
742
+ agent,
743
+ objective,
744
+ brief,
745
+ targetAgent: target,
746
+ ...(targets.length > 1 ? { targetAgents: targets } : {}),
747
+ runWorkspace,
748
+ });
329
749
  if (!response.ok) throw new Error(response.error || "post failed");
330
- if (targets.length > 1) console.log(`Task ${response.task.id} is on the board for ${targets.join(", ")} at once${runWorkspace ? `, each running in ${runWorkspace}` : ""}. It closes with every result when the last of them finishes.`);
331
- else if (runWorkspace) console.log(`Task ${response.task.id} is on the board and ${target} will run it in ${runWorkspace}. Follow it: npx tryharness task ${response.task.id} --follow`);
332
- else console.log(`Task ${response.task.id} is on the board${target ? ` for ${target}` : ""}. Any agent's list_tasks tool shows it.`);
750
+ if (targets.length > 1)
751
+ console.log(
752
+ `Task ${response.task.id} is on the board for ${targets.join(", ")} at once${runWorkspace ? `, each running in ${runWorkspace}` : ""}. It closes with every result when the last of them finishes.`
753
+ );
754
+ else if (runWorkspace)
755
+ console.log(
756
+ `Task ${response.task.id} is on the board and ${target} will run it in ${runWorkspace}. Follow it: npx tryharness task ${response.task.id} --follow`
757
+ );
758
+ else
759
+ console.log(
760
+ `Task ${response.task.id} is on the board${target ? ` for ${target}` : ""}. Any agent's list_tasks tool shows it.`
761
+ );
333
762
  return;
334
763
  }
335
764
  if (sub === "send" || sub === "discard") {
336
765
  const id = String(argv[argv.indexOf(sub) + 1] || "");
337
766
  const messageId = String(argv[argv.indexOf(sub) + 2] || "");
338
- if (!id || !messageId) throw new Error(`usage: tryharness board ${sub} <task id> <message id>`);
339
- const response = await daemonRequest({ command: sub === "send" ? "board.thread.send" : "board.thread.discard", agent, id, messageId });
767
+ if (!id || !messageId)
768
+ throw new Error(`usage: tryharness board ${sub} <task id> <message id>`);
769
+ const response = await daemonRequest({
770
+ command: sub === "send" ? "board.thread.send" : "board.thread.discard",
771
+ agent,
772
+ id,
773
+ messageId,
774
+ });
340
775
  if (!response.ok) throw new Error(response.error || `${sub} failed`);
341
- console.log(sub === "send" ? `Sent to ${response.message.recipient}.` : "Discarded.");
776
+ console.log(
777
+ sub === "send" ? `Sent to ${response.message.recipient}.` : "Discarded."
778
+ );
779
+ return;
780
+ }
781
+ // Code an agent elsewhere asked to see (snippets.mjs): list the requests waiting, or share or keep one.
782
+ if (sub === "snippet") {
783
+ const [id, choice] = argv.slice(argv.indexOf(sub) + 1);
784
+ if (!id) {
785
+ const response = await daemonRequest({
786
+ command: "board.snippet.list",
787
+ agent: "user",
788
+ });
789
+ if (!response?.ok)
790
+ throw new Error(response?.error || "the daemon did not answer");
791
+ if (!response.snippets.length) {
792
+ console.log("No agent is waiting to see code.");
793
+ return;
794
+ }
795
+ for (const each of response.snippets)
796
+ console.log(
797
+ `${each.id} ${each.agent} asks to see ${each.path} lines ${each.from} to ${each.to} ("${each.about}")\n${each.text
798
+ .split("\n")
799
+ .map((line) => ` ${line}`)
800
+ .join("\n")}\n tryharness board snippet ${each.id} share|keep\n`
801
+ );
802
+ return;
803
+ }
804
+ if (choice !== "share" && choice !== "keep")
805
+ throw new Error("usage: tryharness board snippet <id> share|keep");
806
+ const response = await daemonRequest({
807
+ command: "board.snippet.decide",
808
+ agent: "user",
809
+ id,
810
+ share: choice === "share",
811
+ });
812
+ if (!response?.ok)
813
+ throw new Error(response?.error || "the daemon did not answer");
814
+ console.log(
815
+ choice === "share" ? "Shared in the thread." : "Kept on this computer."
816
+ );
342
817
  return;
343
818
  }
344
819
  if (sub === "auto" || sub === "end") {
345
820
  const id = String(argv[argv.indexOf(sub) + 1] || "");
346
- const response = sub === "auto"
347
- ? await daemonRequest({ command: "board.thread.auto", agent, id, on: argv[argv.indexOf(sub) + 2] !== "off" })
348
- : await daemonRequest({ command: "board.thread.end", agent, id, outcome: arg("--outcome", "") });
821
+ const response =
822
+ sub === "auto"
823
+ ? await daemonRequest({
824
+ command: "board.thread.auto",
825
+ agent,
826
+ id,
827
+ on: argv[argv.indexOf(sub) + 2] !== "off",
828
+ })
829
+ : await daemonRequest({
830
+ command: "board.thread.end",
831
+ agent,
832
+ id,
833
+ outcome: arg("--outcome", ""),
834
+ });
349
835
  if (!response.ok) throw new Error(response.error || `${sub} failed`);
350
- console.log(sub === "auto" ? `Auto-send ${response.thread.auto_send ? "on" : "off"} for ${id.slice(0, 8)}. It turns off again if a reply is flagged.` : `Thread ended; the other side is told.`);
836
+ console.log(
837
+ sub === "auto"
838
+ ? `Auto-send ${response.thread.auto_send ? "on" : "off"} for ${id.slice(0, 8)}. It turns off again if a reply is flagged.`
839
+ : `Thread ended; the other side is told.`
840
+ );
351
841
  return;
352
842
  }
353
843
  if (sub === "context") {
354
844
  const id = String(argv[argv.indexOf(sub) + 1] || "");
355
- const query = argv.slice(argv.indexOf(sub) + 2).filter((value) => !value.startsWith("--")).join(" ");
356
- if (!id || !query) throw new Error("usage: tryharness board context <task id> <what you are looking for>");
357
- const response = await daemonRequest({ command: "board.context.search", agent, id, query, limit: 8 }, 20_000);
845
+ const query = argv
846
+ .slice(argv.indexOf(sub) + 2)
847
+ .filter((value) => !value.startsWith("--"))
848
+ .join(" ");
849
+ if (!id || !query)
850
+ throw new Error(
851
+ "usage: tryharness board context <task id> <what you are looking for>"
852
+ );
853
+ const response = await daemonRequest(
854
+ { command: "board.context.search", agent, id, query, limit: 8 },
855
+ 20_000
856
+ );
358
857
  if (!response.ok) throw new Error(response.error || "search failed");
359
- const how = response.retrieval === "vectors" ? "meaning" : response.embedder === "absent" ? "words (the embedder is not downloaded yet; run npx tryharness)" : `words (${response.context?.embedded ?? 0} of ${response.context?.total ?? 0} entries embedded so far)`;
360
- console.log(`${response.entries.length} result${response.entries.length === 1 ? "" : "s"} by ${how}`);
361
- for (const entry of response.entries) console.log(`\n[${entry.kind} · ${entry.author}${entry.source ? ` · ${entry.source}` : ""} · ${entry.score}]\n${entry.text.slice(0, 400)}`);
858
+ const how =
859
+ response.retrieval === "vectors"
860
+ ? "meaning"
861
+ : response.embedder === "absent"
862
+ ? "words (the local models are not downloaded yet; run npx tryharness)"
863
+ : `words (${response.context?.embedded ?? 0} of ${response.context?.total ?? 0} entries embedded so far)`;
864
+ console.log(
865
+ `${response.entries.length} result${response.entries.length === 1 ? "" : "s"} by ${how}`
866
+ );
867
+ for (const entry of response.entries)
868
+ console.log(
869
+ `\n[${entry.kind} · ${entry.author}${entry.source ? ` · ${entry.source}` : ""} · ${entry.score}]\n${entry.text.slice(0, 400)}`
870
+ );
362
871
  return;
363
872
  }
364
873
  // Standing permission: a coding agent starts the tasks you or Harness post for it without a Start. On by default
365
874
  // for the folders you have used it in; name folders and a daily cap instead, turn it off, or back on to the default.
366
875
  // Tasks another agent wrote always wait for you.
367
876
  if (sub === "standing") {
368
- const rest = argv.slice(argv.indexOf(sub) + 1).filter((value, index, all) => !value.startsWith("--") && all[index - 1] !== "--cap" && all[index - 1] !== "--as");
877
+ const rest = argv
878
+ .slice(argv.indexOf(sub) + 1)
879
+ .filter(
880
+ (value, index, all) =>
881
+ !value.startsWith("--") &&
882
+ all[index - 1] !== "--cap" &&
883
+ all[index - 1] !== "--as"
884
+ );
369
885
  const [target, ...dirs] = rest;
370
- const usage = "usage: tryharness board standing [<agent> <folder> [<folder>...] [--cap <runs a day, default 10>] | <agent> off | <agent> on]";
886
+ const usage =
887
+ "usage: tryharness board standing [<agent> <folder> [<folder>...] [--cap <runs a day, default 10>] | <agent> off | <agent> on]";
371
888
  if (target && !AGENTS.includes(target)) throw new Error(usage);
372
- const toggle = dirs.length === 1 && ["on", "off"].includes(dirs[0]) ? dirs[0] : null;
373
- const response = !target ? await daemonRequest({ command: "board.standing", agent })
374
- : toggle ? await daemonRequest({ command: `board.standing.${toggle}`, agent, targetAgent: target })
375
- : dirs.length ? await daemonRequest({ command: "board.standing.set", agent, targetAgent: target, workspaces: dirs.map((dir) => path.resolve(dir)), dailyCap: Number(arg("--cap", "10")) })
376
- : (() => { throw new Error(usage); })();
377
- if (!response.ok) throw new Error(response.error || "standing permission failed");
378
- if (!response.standing.length) { console.log("No coding agent is installed on this machine."); return; }
889
+ const toggle =
890
+ dirs.length === 1 && ["on", "off"].includes(dirs[0]) ? dirs[0] : null;
891
+ const response = !target
892
+ ? await daemonRequest({ command: "board.standing", agent })
893
+ : toggle
894
+ ? await daemonRequest({
895
+ command: `board.standing.${toggle}`,
896
+ agent,
897
+ targetAgent: target,
898
+ })
899
+ : dirs.length
900
+ ? await daemonRequest({
901
+ command: "board.standing.set",
902
+ agent,
903
+ targetAgent: target,
904
+ workspaces: dirs.map((dir) => path.resolve(dir)),
905
+ dailyCap: Number(arg("--cap", "10")),
906
+ })
907
+ : (() => {
908
+ throw new Error(usage);
909
+ })();
910
+ if (!response.ok)
911
+ throw new Error(response.error || "standing permission failed");
912
+ if (!response.standing.length) {
913
+ console.log("No coding agent is installed on this machine.");
914
+ return;
915
+ }
379
916
  for (const rule of response.standing) {
380
- if (rule.mode === "off") { console.log(`${rule.agent} waits for your Start. Turn it on: npx tryharness board standing ${rule.agent} on`); continue; }
381
- const where = rule.mode === "default"
382
- ? (rule.workspaces.length ? `in the ${rule.workspaces.length} folder${rule.workspaces.length === 1 ? "" : "s"} you have used it in (${rule.workspaces.slice(0, 5).join(", ")}${rule.workspaces.length > 5 ? ", ..." : ""})` : "in folders you have used it in (none yet)")
383
- : `in ${rule.workspaces.join(", ")}`;
384
- console.log(`${rule.agent} starts tasks from you or Harness without asking, ${where}; ${rule.runs_today} of ${rule.daily_cap} today. Turn it off: npx tryharness board standing ${rule.agent} off`);
917
+ if (rule.mode === "off") {
918
+ console.log(
919
+ `${rule.agent} waits for your Start. Turn it on: npx tryharness board standing ${rule.agent} on`
920
+ );
921
+ continue;
922
+ }
923
+ const where =
924
+ rule.mode === "default"
925
+ ? rule.workspaces.length
926
+ ? `in the ${rule.workspaces.length} folder${rule.workspaces.length === 1 ? "" : "s"} you have used it in (${rule.workspaces.slice(0, 5).join(", ")}${rule.workspaces.length > 5 ? ", ..." : ""})`
927
+ : "in folders you have used it in (none yet)"
928
+ : `in ${rule.workspaces.join(", ")}`;
929
+ console.log(
930
+ `${rule.agent} starts tasks from you or Harness without asking, ${where}; ${rule.runs_today} of ${rule.daily_cap} today. Turn it off: npx tryharness board standing ${rule.agent} off`
931
+ );
385
932
  }
386
933
  return;
387
934
  }
388
935
  if (sub === "run" || sub === "release") {
389
936
  const id = String(argv[argv.indexOf(sub) + 1] || "");
390
- const response = await daemonRequest({ command: sub === "run" ? "board.run" : "board.release", agent, id });
937
+ const response = await daemonRequest({
938
+ command: sub === "run" ? "board.run" : "board.release",
939
+ agent,
940
+ id,
941
+ });
391
942
  if (!response.ok) throw new Error(response.error || `${sub} failed`);
392
- console.log(sub === "run" ? `Run authorized; ${response.task.target_agent} starts it now. Follow it: npx tryharness task ${id} --follow` : `Task ${id} is open again.`);
943
+ console.log(
944
+ sub === "run"
945
+ ? `Run authorized; ${response.task.target_agent} starts it now. Follow it: npx tryharness task ${id} --follow`
946
+ : `Task ${id} is open again.`
947
+ );
393
948
  return;
394
949
  }
395
950
  if (sub === "watch" || has("--watch")) {
@@ -397,62 +952,147 @@ async function board() {
397
952
  const seen = new Map();
398
953
  let first = true;
399
954
  for (;;) {
400
- const response = await daemonRequest({ command: "board.list", agent, all: true, limit: 100 }).catch(() => null);
955
+ const response = await daemonRequest({
956
+ command: "board.list",
957
+ agent,
958
+ all: true,
959
+ limit: 100,
960
+ }).catch(() => null);
401
961
  if (response?.ok) {
402
962
  for (const t of response.tasks) {
403
963
  const key = `${t.status}:${t.version}:${t.stale ? "stale" : ""}`;
404
964
  if (seen.get(t.id) === key) continue;
405
965
  seen.set(t.id, key);
406
- if (!first || ["open", "claimed", "running", "needs_input"].includes(t.status)) console.log(`${new Date().toLocaleTimeString()} ${t.id.slice(0, 8)} ${(t.stale ? "stale" : t.status).padEnd(11)} ${t.origin_agent} → ${t.target_agent || "anyone"}${t.claimed_by ? ` (${t.claimed_by})` : ""}${t.run_workspace && !t.run_authorized_at ? " [wants to run]" : ""} ${t.objective}`);
966
+ if (
967
+ !first ||
968
+ ["open", "claimed", "running", "needs_input"].includes(t.status)
969
+ )
970
+ console.log(
971
+ `${new Date().toLocaleTimeString()} ${t.id.slice(0, 8)} ${(t.stale ? "stale" : t.status).padEnd(11)} ${t.origin_agent} → ${t.target_agent || "anyone"}${t.claimed_by ? ` (${t.claimed_by})` : ""}${t.run_workspace && !t.run_authorized_at ? " [wants to run]" : ""} ${t.objective}`
972
+ );
407
973
  }
408
974
  if (first) console.log("watching the board (Ctrl-C stops)");
409
975
  first = false;
410
- } else if (first) { console.log("daemon not running; waiting"); first = false; }
976
+ } else if (first) {
977
+ console.log("daemon not running; waiting");
978
+ first = false;
979
+ }
411
980
  await sleep(2_000);
412
981
  }
413
982
  }
414
983
  if (sub === "agents") {
415
984
  const response = await daemonRequest({ command: "board.agents", agent });
416
985
  if (!response.ok) throw new Error(response.error || "board unavailable");
417
- if (!response.agents.length) { console.log("No agent has read the board yet."); return; }
418
- for (const row of response.agents) console.log(`${row.agent.padEnd(14)} last seen ${row.last_seen_at}${response.running.some((run) => run.agent === row.agent) ? " · running a task" : ""}`);
986
+ if (!response.agents.length) {
987
+ console.log("No agent has read the board yet.");
988
+ return;
989
+ }
990
+ for (const row of response.agents)
991
+ console.log(
992
+ `${row.agent.padEnd(14)} last seen ${row.last_seen_at}${response.running.some((run) => run.agent === row.agent) ? " · running a task" : ""}`
993
+ );
419
994
  return;
420
995
  }
421
996
  if (sub === "show") {
422
- const response = await daemonRequest({ command: "board.get", agent, id: String(argv[argv.indexOf("show") + 1] || "") });
997
+ const response = await daemonRequest({
998
+ command: "board.get",
999
+ agent,
1000
+ id: String(argv[argv.indexOf("show") + 1] || ""),
1001
+ });
423
1002
  if (!response.ok) throw new Error(response.error || "task not found");
424
1003
  const { task: t } = response;
425
- console.log(`${t.id} · ${t.status}${t.stale ? " (stale claim)" : ""} · from ${t.origin_agent}${t.target_agent ? ` for ${t.target_agent}` : ""}${t.claimed_by ? ` · claimed by ${t.claimed_by}` : ""} · held by ${t.holder || "nobody"} · version ${t.version}\n${t.objective}\n`);
426
- if (t.kind === "group") for (const part of t.children || []) console.log(` part for ${part.target_agent}: ${part.status}${part.claimed_by ? ` (${part.claimed_by})` : ""} · ${part.id}${part.result ? `\n ${String(part.result).slice(0, 200)}` : ""}`);
427
- if (t.run_workspace) console.log(`run: ${t.run_authorized_at ? `authorized by ${t.run_authorized_by}` : "requested, waiting for: npx tryharness board run " + t.id} · ${t.run_workspace}${t.session_id ? `\nagent session: ${t.session_id}\nopen live session: ${resumeCommand(t.target_agent, t.session_id, t.run_workspace)}` : ""}\n`);
1004
+ console.log(
1005
+ `${t.id} · ${t.status}${t.stale ? " (stale claim)" : ""} · from ${t.origin_agent}${t.target_agent ? ` for ${t.target_agent}` : ""}${t.claimed_by ? ` · claimed by ${t.claimed_by}` : ""} · held by ${t.holder || "nobody"} · version ${t.version}\n${t.objective}\n`
1006
+ );
1007
+ if (t.kind === "group")
1008
+ for (const part of t.children || [])
1009
+ console.log(
1010
+ ` part for ${part.target_agent}: ${part.status}${part.claimed_by ? ` (${part.claimed_by})` : ""} · ${part.id}${part.result ? `\n ${String(part.result).slice(0, 200)}` : ""}`
1011
+ );
1012
+ if (t.run_workspace)
1013
+ console.log(
1014
+ `run: ${t.run_authorized_at ? `authorized by ${t.run_authorized_by}` : "requested, waiting for: npx tryharness board run " + t.id} · ${t.run_workspace}${t.session_id ? `\nagent session: ${t.session_id}\nopen live session: ${resumeCommand(t.target_agent, t.session_id, t.run_workspace)}` : ""}\n`
1015
+ );
428
1016
  if (t.brief) console.log(t.brief + "\n");
429
- if (t.result) console.log(`${t.status === "needs_input" ? "question" : "result"}: ${t.result}`);
430
- const thread = await daemonRequest({ command: "board.thread.read", agent, id: t.id });
1017
+ if (t.result)
1018
+ console.log(
1019
+ `${t.status === "needs_input" ? "question" : "result"}: ${t.result}`
1020
+ );
1021
+ const thread = await daemonRequest({
1022
+ command: "board.thread.read",
1023
+ agent,
1024
+ id: t.id,
1025
+ });
431
1026
  if (thread.ok) {
432
- console.log(`\nthread with ${thread.thread.with_agent}${thread.thread.closed_at ? " (ended)" : thread.thread.auto_send ? " (auto-send on)" : " (replies held for you)"}`);
433
- for (const m of thread.messages) console.log(` ${m.direction === "in" ? "←" : "→"} ${m.sender} [${m.state}${m.flag_reason ? `: ${m.flag_reason}` : ""}] ${m.text.slice(0, 300)}${m.state === "held" || m.state === "flagged" ? `\n send: tryharness board send ${t.id} ${m.id}` : ""}`);
1027
+ console.log(
1028
+ `\nthread with ${thread.thread.with_agent}${thread.thread.closed_at ? " (ended)" : thread.thread.auto_send ? " (auto-send on)" : " (replies held for you)"}`
1029
+ );
1030
+ for (const m of thread.messages)
1031
+ console.log(
1032
+ ` ${m.direction === "in" ? "←" : "→"} ${m.sender} [${m.state}${m.flag_reason ? `: ${m.flag_reason}` : ""}] ${m.text.slice(0, 300)}${m.state === "held" || m.state === "flagged" ? `\n send: tryharness board send ${t.id} ${m.id}` : ""}`
1033
+ );
434
1034
  }
435
1035
  return;
436
1036
  }
437
1037
  if (sub === "trace") {
438
- const response = await daemonRequest({ command: "board.trace", agent, id: String(argv[argv.indexOf("trace") + 1] || "") }, 30_000);
1038
+ const response = await daemonRequest(
1039
+ {
1040
+ command: "board.trace",
1041
+ agent,
1042
+ id: String(argv[argv.indexOf("trace") + 1] || ""),
1043
+ },
1044
+ 30_000
1045
+ );
439
1046
  if (!response.ok) throw new Error(response.error || "trace unavailable");
440
1047
  const t = response.trace;
441
- const time = (value) => (value == null ? "-" : value < 1_000 ? `${value}ms` : value < 120_000 ? `${(value / 1_000).toFixed(1)}s` : `${(value / 60_000).toFixed(1)}m`);
442
- console.log(`${t.hops} hop${t.hops === 1 ? "" : "s"} · ${t.status} · ${time(t.wall_ms)} end to end${t.finished_at ? "" : " so far"} · ${time(t.person_ms)} waiting on you · agents: ${t.agents.join(", ")}`);
443
- for (const span of t.spans) console.log(`${" ".repeat(span.depth)}${span.id.slice(0, 8)} ${span.origin_agent} → ${span.worked_by || span.target_agent || "anyone"} [${span.status}${span.trust === "agent" ? `, written by ${span.origin_chain.join(" → ")}` : ""}] you ${time(span.person_ms)} · queue ${time(span.queue_ms)} · work ${time(span.work_ms)} · ${span.objective.slice(0, 60)}`);
444
- if (t.slowest) console.log(`slowest: ${t.slowest.id.slice(0, 8)} (${t.slowest.worked_by}) ${time(t.slowest.work_ms)}`);
1048
+ const time = (value) =>
1049
+ value == null
1050
+ ? "-"
1051
+ : value < 1_000
1052
+ ? `${value}ms`
1053
+ : value < 120_000
1054
+ ? `${(value / 1_000).toFixed(1)}s`
1055
+ : `${(value / 60_000).toFixed(1)}m`;
1056
+ console.log(
1057
+ `${t.hops} hop${t.hops === 1 ? "" : "s"} · ${t.status} · ${time(t.wall_ms)} end to end${t.finished_at ? "" : " so far"} · ${time(t.person_ms)} waiting on you · agents: ${t.agents.join(", ")}`
1058
+ );
1059
+ for (const span of t.spans)
1060
+ console.log(
1061
+ `${" ".repeat(span.depth)}${span.id.slice(0, 8)} ${span.origin_agent} → ${span.worked_by || span.target_agent || "anyone"} [${span.status}${span.trust === "agent" ? `, written by ${span.origin_chain.join(" → ")}` : ""}] you ${time(span.person_ms)} · queue ${time(span.queue_ms)} · work ${time(span.work_ms)} · ${span.objective.slice(0, 60)}`
1062
+ );
1063
+ if (t.slowest)
1064
+ console.log(
1065
+ `slowest: ${t.slowest.id.slice(0, 8)} (${t.slowest.worked_by}) ${time(t.slowest.work_ms)}`
1066
+ );
445
1067
  const c = t.cost;
446
1068
  const usd = (microCents) => `$${(microCents / 100_000_000).toFixed(4)}`;
447
- console.log(`cost: ${usd(c.charged_micro_cents)} charged by Harness${c.held_micro_cents ? `, ${usd(c.held_micro_cents)} held` : ""}; agents reported $${c.agent_reported_usd.toFixed(4)} and ${c.tokens.input + c.tokens.output} tokens${c.unrecorded.length ? `; no figure for ${c.unrecorded.map((id) => id.slice(0, 8)).join(", ")}` : ""}`);
448
- for (const wait of t.waiting_on) console.log(`waiting: ${wait.id.slice(0, 8)} is ${wait.status}${wait.holder ? ` with ${wait.holder}` : ""}`);
1069
+ console.log(
1070
+ `cost: ${usd(c.charged_micro_cents)} charged by Harness${c.held_micro_cents ? `, ${usd(c.held_micro_cents)} held` : ""}; agents reported $${c.agent_reported_usd.toFixed(4)} and ${c.tokens.input + c.tokens.output} tokens${c.unrecorded.length ? `; no figure for ${c.unrecorded.map((id) => id.slice(0, 8)).join(", ")}` : ""}`
1071
+ );
1072
+ for (const wait of t.waiting_on)
1073
+ console.log(
1074
+ `waiting: ${wait.id.slice(0, 8)} is ${wait.status}${wait.holder ? ` with ${wait.holder}` : ""}`
1075
+ );
449
1076
  return;
450
1077
  }
451
- if (sub !== "list") throw new Error("usage: tryharness board [list --all | show <id> | trace <id> | post --for <agent> --objective <text> [--file <brief.md>] [--run --workspace <dir>] | run <id> | release <id> | standing [<agent> <folder>... --cap <n> | <agent> off | <agent> on] | agents]");
452
- const response = await daemonRequest({ command: "board.list", agent, all: has("--all") });
1078
+ if (sub !== "list")
1079
+ throw new Error(
1080
+ "usage: tryharness board [list --all | show <id> | trace <id> | post --for <agent> --objective <text> [--file <brief.md>] [--run --workspace <dir>] | run <id> | release <id> | standing [<agent> <folder>... --cap <n> | <agent> off | <agent> on] | agents]"
1081
+ );
1082
+ const response = await daemonRequest({
1083
+ command: "board.list",
1084
+ agent,
1085
+ all: has("--all"),
1086
+ });
453
1087
  if (!response.ok) throw new Error(response.error || "board unavailable");
454
- if (!response.tasks.length) { console.log("The board is empty."); return; }
455
- for (const t of response.tasks) console.log(`${t.id} ${(t.stale ? "stale" : t.status).padEnd(11)} ${t.origin_agent} → ${t.kind === "group" ? `${t.child_agents} (${t.child_done} of ${t.child_count} done)` : t.target_agent || "anyone"}${t.claimed_by ? ` (${t.claimed_by})` : ""}${t.run_workspace && !t.run_authorized_at ? " [wants to run]" : ""} ${t.objective}`);
1088
+ if (!response.tasks.length) {
1089
+ console.log("The board is empty.");
1090
+ return;
1091
+ }
1092
+ for (const t of response.tasks)
1093
+ console.log(
1094
+ `${t.id} ${(t.stale ? "stale" : t.status).padEnd(11)} ${t.origin_agent} → ${t.kind === "group" ? `${t.child_agents} (${t.child_done} of ${t.child_count} done)` : t.target_agent || "anyone"}${t.claimed_by ? ` (${t.claimed_by})` : ""}${t.run_workspace && !t.run_authorized_at ? " [wants to run]" : ""} ${t.objective}`
1095
+ );
456
1096
  }
457
1097
 
458
1098
  // Goals: one objective for a team of agents, worked in a room. Set from here (or by Harness in chat); agents post,
@@ -461,45 +1101,108 @@ async function goal() {
461
1101
  const sub = argv[0] || "list";
462
1102
  const agent = arg("--as", "user");
463
1103
  const id = String(argv[1] || "");
464
- const intFlag = (flag) => { const value = arg(flag); return value == null ? undefined : Number(value); };
1104
+ const intFlag = (flag) => {
1105
+ const value = arg(flag);
1106
+ return value == null ? undefined : Number(value);
1107
+ };
465
1108
  if (sub === "set") {
466
- const response = await daemonRequest({ command: "board.goal.create", agent, objective: arg("--objective", ""), brief: arg("--file") ? fs.readFileSync(path.resolve(arg("--file")), "utf8") : arg("--brief", ""),
467
- team: arg("--team") ? String(arg("--team")).split(",").map((name) => name.trim()).filter(Boolean) : undefined, facilitator: arg("--facilitator") || undefined,
468
- workspace: arg("--workspace") ? path.resolve(arg("--workspace")) : undefined, maxPosts: intFlag("--max-posts"), maxWakes: intFlag("--max-wakes") });
1109
+ const response = await daemonRequest({
1110
+ command: "board.goal.create",
1111
+ agent,
1112
+ objective: arg("--objective", ""),
1113
+ brief: arg("--file")
1114
+ ? fs.readFileSync(path.resolve(arg("--file")), "utf8")
1115
+ : arg("--brief", ""),
1116
+ team: arg("--team")
1117
+ ? String(arg("--team"))
1118
+ .split(",")
1119
+ .map((name) => name.trim())
1120
+ .filter(Boolean)
1121
+ : undefined,
1122
+ facilitator: arg("--facilitator") || undefined,
1123
+ workspace: arg("--workspace")
1124
+ ? path.resolve(arg("--workspace"))
1125
+ : undefined,
1126
+ maxPosts: intFlag("--max-posts"),
1127
+ maxWakes: intFlag("--max-wakes"),
1128
+ });
469
1129
  if (!response.ok) throw new Error(response.error || "the goal was not set");
470
1130
  const g = response.task;
471
- console.log(`Goal ${g.id} is set. ${g.goal.facilitator} facilitates; the team is ${g.goal.team.join(", ")}${g.goal.workspace ? `; coding agents work in ${g.goal.workspace}` : ""}. Follow it: npx tryharness goal show ${g.id}`);
1131
+ console.log(
1132
+ `Goal ${g.id} is set. ${g.goal.facilitator} facilitates; the team is ${g.goal.team.join(", ")}${g.goal.workspace ? `; coding agents work in ${g.goal.workspace}` : ""}. Follow it: npx tryharness goal show ${g.id}`
1133
+ );
472
1134
  return;
473
1135
  }
474
1136
  if (sub === "list") {
475
- const response = await daemonRequest({ command: "board.goal.list", agent, all: has("--all") });
1137
+ const response = await daemonRequest({
1138
+ command: "board.goal.list",
1139
+ agent,
1140
+ all: has("--all"),
1141
+ });
476
1142
  if (!response.ok) throw new Error(response.error || "goals unavailable");
477
- if (!response.goals.length) { console.log("No goals."); return; }
478
- for (const g of response.goals) console.log(`${g.id} ${g.status.padEnd(7)} ${g.goal.posts} posts, ${g.goal.spend.runs} runs ${g.objective}`);
1143
+ if (!response.goals.length) {
1144
+ console.log("No goals.");
1145
+ return;
1146
+ }
1147
+ for (const g of response.goals)
1148
+ console.log(
1149
+ `${g.id} ${g.status.padEnd(7)} ${g.goal.posts} posts, ${g.goal.spend.runs} runs ${g.objective}`
1150
+ );
479
1151
  return;
480
1152
  }
481
1153
  if (sub === "show") {
482
- const response = await daemonRequest({ command: "board.goal.get", agent, id });
1154
+ const response = await daemonRequest({
1155
+ command: "board.goal.get",
1156
+ agent,
1157
+ id,
1158
+ });
483
1159
  if (!response.ok) throw new Error(response.error || "goal not found");
484
- const g = response.task, spend = g.goal.spend;
485
- console.log(`${g.objective}\n${g.status}${g.goal.paused_reason ? ` (${g.goal.paused_reason})` : ""} · facilitator ${g.goal.facilitator} · team ${g.goal.team.join(", ")} · ${g.goal.posts} posts · ${spend.runs} runs, ${spend.input_tokens + spend.output_tokens} tokens${spend.cost_usd ? `, $${spend.cost_usd.toFixed(4)} reported` : ""}${g.result ? `\nanswer: ${g.result}` : ""}\n`);
1160
+ const g = response.task,
1161
+ spend = g.goal.spend;
1162
+ console.log(
1163
+ `${g.objective}\n${g.status}${g.goal.paused_reason ? ` (${g.goal.paused_reason})` : ""} · facilitator ${g.goal.facilitator} · team ${g.goal.team.join(", ")} · ${g.goal.posts} posts · ${spend.runs} runs, ${spend.input_tokens + spend.output_tokens} tokens${spend.cost_usd ? `, $${spend.cost_usd.toFixed(4)} reported` : ""}${g.result ? `\nanswer: ${g.result}` : ""}\n`
1164
+ );
486
1165
  const room = await daemonRequest({ command: "board.room.read", agent, id });
487
- for (const post of room.posts || []) console.log(`[${post.seq}] ${post.author}${post.kind !== "post" ? ` (${post.kind})` : ""}: ${post.text.slice(0, 400)}`);
1166
+ for (const post of room.posts || [])
1167
+ console.log(
1168
+ `[${post.seq}] ${post.author}${post.kind !== "post" ? ` (${post.kind})` : ""}: ${post.text.slice(0, 400)}`
1169
+ );
488
1170
  return;
489
1171
  }
490
1172
  if (sub === "invite") {
491
- const response = await daemonRequest({ command: "board.goal.invite", agent, id, to: String(argv[2] || "") }, 20_000);
492
- if (!response.ok) throw new Error(response.error || "the invitation was not sent");
493
- console.log(`Invited ${argv[2]} as a guest (thread ${String(response.threadId || "").slice(0, 8)}, ${response.status || "requested"}). They see only posts shared with guests, and their replies land in the room.`);
1173
+ const response = await daemonRequest(
1174
+ { command: "board.goal.invite", agent, id, to: String(argv[2] || "") },
1175
+ 20_000
1176
+ );
1177
+ if (!response.ok)
1178
+ throw new Error(response.error || "the invitation was not sent");
1179
+ console.log(
1180
+ `Invited ${argv[2]} as a guest (thread ${String(response.threadId || "").slice(0, 8)}, ${response.status || "requested"}). They see only posts shared with guests, and their replies land in the room.`
1181
+ );
494
1182
  return;
495
1183
  }
496
1184
  if (sub === "stop" || sub === "resume" || sub === "folder") {
497
- const response = await daemonRequest({ command: `board.goal.${sub}`, agent, id, maxPosts: intFlag("--max-posts"), workspace: sub === "folder" ? path.resolve(String(argv[2] || "")) : undefined });
1185
+ const response = await daemonRequest({
1186
+ command: `board.goal.${sub}`,
1187
+ agent,
1188
+ id,
1189
+ maxPosts: intFlag("--max-posts"),
1190
+ workspace:
1191
+ sub === "folder" ? path.resolve(String(argv[2] || "")) : undefined,
1192
+ });
498
1193
  if (!response.ok) throw new Error(response.error || `${sub} failed`);
499
- console.log(sub === "stop" ? "Stopped. Open jobs closed and running agents stopped." : sub === "resume" ? "Resumed." : `Coding agents on this goal now work in ${response.task.goal.workspace}.`);
1194
+ console.log(
1195
+ sub === "stop"
1196
+ ? "Stopped. Open jobs closed and running agents stopped."
1197
+ : sub === "resume"
1198
+ ? "Resumed."
1199
+ : `Coding agents on this goal now work in ${response.task.goal.workspace}.`
1200
+ );
500
1201
  return;
501
1202
  }
502
- throw new Error("usage: tryharness goal [list | set --objective <text> [--brief <text>] [--team a,b] [--facilitator <agent>] [--workspace <dir>] [--max-posts n] [--max-wakes n] | show <id> | stop <id> | resume <id> [--max-posts n] | folder <id> <dir> | invite <id> <handle/agent>]");
1203
+ throw new Error(
1204
+ "usage: tryharness goal [list | set --objective <text> [--brief <text>] [--team a,b] [--facilitator <agent>] [--workspace <dir>] [--max-posts n] [--max-wakes n] | show <id> | stop <id> | resume <id> [--max-posts n] | folder <id> <dir> | invite <id> <handle/agent>]"
1205
+ );
503
1206
  }
504
1207
 
505
1208
  // The swarm's coordinator: who coordinates, and the record of every round it ran.
@@ -507,61 +1210,134 @@ async function swarm() {
507
1210
  const sub = argv[0] || "rounds";
508
1211
  if (sub === "coordinator") {
509
1212
  const mode = argv[1];
510
- if (mode && mode !== "harness" && mode !== "local") throw new Error("usage: tryharness swarm coordinator [harness|local]");
511
- const response = await daemonRequest({ command: "board.swarm.coordinator", agent: "user", ...(mode ? { mode } : {}) });
512
- if (!response.ok) throw new Error(response.error || "the coordinator setting was not changed");
513
- console.log(response.mode === "local" ? "Threads are coordinated by an agent on this computer with its tools off; nothing in them goes to Harness." : "Threads are coordinated by Harness: each round sends the thread's newest posts and its options to Harness, which decides and words the next step.");
1213
+ if (mode && mode !== "harness" && mode !== "local")
1214
+ throw new Error("usage: tryharness swarm coordinator [harness|local]");
1215
+ const response = await daemonRequest({
1216
+ command: "board.swarm.coordinator",
1217
+ agent: "user",
1218
+ ...(mode ? { mode } : {}),
1219
+ });
1220
+ if (!response.ok)
1221
+ throw new Error(
1222
+ response.error || "the coordinator setting was not changed"
1223
+ );
1224
+ console.log(
1225
+ response.mode === "local"
1226
+ ? "Threads are coordinated by an agent on this computer with its tools off; nothing in them goes to Harness."
1227
+ : "Threads are coordinated by Harness: each round sends the thread's newest posts and its options to Harness, which decides and words the next step."
1228
+ );
514
1229
  return;
515
1230
  }
516
1231
  if (sub === "rounds") {
517
- const response = await daemonRequest({ command: "board.swarm.rounds", agent: "user", ...(argv[1] ? { id: argv[1] } : {}), limit: Number(arg("--limit", "30")) });
1232
+ const response = await daemonRequest({
1233
+ command: "board.swarm.rounds",
1234
+ agent: "user",
1235
+ ...(argv[1] ? { id: argv[1] } : {}),
1236
+ limit: Number(arg("--limit", "30")),
1237
+ });
518
1238
  if (!response.ok) throw new Error(response.error || "rounds unavailable");
519
- if (!response.rounds.length) { console.log("No rounds yet."); return; }
1239
+ if (!response.rounds.length) {
1240
+ console.log("No rounds yet.");
1241
+ return;
1242
+ }
520
1243
  for (const r of response.rounds) {
521
- const cost = r.estimated_micro_usd == null ? "cost unknown" : `$${(r.estimated_micro_usd / 1e6).toFixed(4)} est.`;
1244
+ const cost =
1245
+ r.estimated_micro_usd == null
1246
+ ? "cost unknown"
1247
+ : `$${(r.estimated_micro_usd / 1e6).toFixed(4)} est.`;
522
1248
  const progress = `${r.progress.runsFinished} runs done, ${r.progress.newResults} new, ${r.progress.roundsWithoutProgress} rounds idle`;
523
- console.log(`${r.at} ${r.goal_id.slice(0, 8)} ${r.coordinator.padEnd(11)} ${(r.error ? "error" : r.action ?? "-").padEnd(10)} ${(r.agent ?? "").padEnd(11)} ${(r.loop ?? "").padEnd(13)} ${progress} ${r.elapsed_ms}ms ${cost}${r.error ? ` ${r.error}` : ""}`);
1249
+ console.log(
1250
+ `${r.at} ${r.goal_id.slice(0, 8)} ${r.coordinator.padEnd(11)} ${(r.error ? "error" : (r.action ?? "-")).padEnd(10)} ${(r.agent ?? "").padEnd(11)} ${(r.loop ?? "").padEnd(13)} ${progress} ${r.elapsed_ms}ms ${cost}${r.error ? ` ${r.error}` : ""}`
1251
+ );
524
1252
  }
525
1253
  return;
526
1254
  }
527
- throw new Error("usage: tryharness swarm [rounds [<thread>] [--limit n] | coordinator [harness|local]]");
1255
+ throw new Error(
1256
+ "usage: tryharness swarm [rounds [<thread>] [--limit n] | coordinator [harness|local]]"
1257
+ );
528
1258
  }
529
1259
 
530
1260
  async function room() {
531
1261
  const agent = arg("--as", "user");
532
- const words = argv.filter((value, index, all) => !value.startsWith("--") && !["--as", "--since", "--ask"].includes(all[index - 1]));
1262
+ const words = argv.filter(
1263
+ (value, index, all) =>
1264
+ !value.startsWith("--") &&
1265
+ !["--as", "--since", "--ask"].includes(all[index - 1])
1266
+ );
533
1267
  if (words[0] === "post") {
534
- const ask = String(arg("--ask", "")).split(",").map((name) => name.trim()).filter(Boolean);
535
- const response = await daemonRequest({ command: "board.room.post", agent, id: String(words[1] || ""), text: words.slice(2).join(" "), ask, shareWithGuests: has("--guests") });
1268
+ const ask = String(arg("--ask", ""))
1269
+ .split(",")
1270
+ .map((name) => name.trim())
1271
+ .filter(Boolean);
1272
+ const response = await daemonRequest({
1273
+ command: "board.room.post",
1274
+ agent,
1275
+ id: String(words[1] || ""),
1276
+ text: words.slice(2).join(" "),
1277
+ ask,
1278
+ shareWithGuests: has("--guests"),
1279
+ });
536
1280
  if (!response.ok) throw new Error(response.error || "post failed");
537
- console.log(response.held ? `Held: it looks like ${response.held}.` : `Posted [${response.post.seq}].${response.note ? ` ${response.note}` : ""}`);
1281
+ console.log(
1282
+ response.held
1283
+ ? `Held: it looks like ${response.held}.`
1284
+ : `Posted [${response.post.seq}].${response.note ? ` ${response.note}` : ""}`
1285
+ );
538
1286
  return;
539
1287
  }
540
- const response = await daemonRequest({ command: "board.room.read", agent, id: String(words[0] || ""), since: Number(arg("--since", "0")) });
541
- if (!response.ok) throw new Error(response.error || "usage: tryharness room <goal> [--since n] | room post <goal> <text>");
542
- for (const post of response.posts) console.log(`[${post.seq}] ${post.author}${post.kind !== "post" ? ` (${post.kind})` : ""}: ${post.text}`);
1288
+ const response = await daemonRequest({
1289
+ command: "board.room.read",
1290
+ agent,
1291
+ id: String(words[0] || ""),
1292
+ since: Number(arg("--since", "0")),
1293
+ });
1294
+ if (!response.ok)
1295
+ throw new Error(
1296
+ response.error ||
1297
+ "usage: tryharness room <goal> [--since n] | room post <goal> <text>"
1298
+ );
1299
+ for (const post of response.posts)
1300
+ console.log(
1301
+ `[${post.seq}] ${post.author}${post.kind !== "post" ? ` (${post.kind})` : ""}: ${post.text}`
1302
+ );
543
1303
  }
544
1304
 
545
1305
  async function task() {
546
1306
  const runId = String(argv[0] || "").trim();
547
- if (!/^[a-zA-Z0-9._-]{1,128}$/.test(runId)) throw new Error("usage: tryharness task <run-id> [--follow]");
1307
+ if (!/^[a-zA-Z0-9._-]{1,128}$/.test(runId))
1308
+ throw new Error("usage: tryharness task <run-id> [--follow]");
548
1309
  const follow = has("--follow") || has("-f");
549
1310
  let offset = Math.max(0, Math.floor(Number(arg("--offset", "0")) || 0));
550
1311
  let announced = false;
551
1312
  let sessionShown = null;
552
1313
  let misses = 0;
553
1314
  for (;;) {
554
- const response = await daemonRequest({ command: "tasks.get", runId, offset });
1315
+ const response = await daemonRequest({
1316
+ command: "tasks.get",
1317
+ runId,
1318
+ offset,
1319
+ });
555
1320
  if (!response.ok) {
556
- if (follow && misses < 10) { misses += 1; await sleep(500); continue; }
557
- throw new Error(response.error || `task ${runId} was not found on this machine`);
1321
+ if (follow && misses < 10) {
1322
+ misses += 1;
1323
+ await sleep(500);
1324
+ continue;
1325
+ }
1326
+ throw new Error(
1327
+ response.error || `task ${runId} was not found on this machine`
1328
+ );
558
1329
  }
559
1330
  misses = 0;
560
1331
  const current = response.task;
561
1332
  if (!announced) {
562
- console.log(`Harness task ${runId} · ${current.agent} · ${current.status}`);
1333
+ console.log(
1334
+ `Harness task ${runId} · ${current.agent} · ${current.status}`
1335
+ );
563
1336
  if (current.cwd) console.log(`directory: ${current.cwd}`);
564
- if (follow) console.log("following machine-local agent output (Ctrl-C stops following, not the task)\n");
1337
+ if (follow)
1338
+ console.log(
1339
+ "following machine-local agent output (Ctrl-C stops following, not the task)\n"
1340
+ );
565
1341
  announced = true;
566
1342
  }
567
1343
  if (current.log) process.stdout.write(current.log);
@@ -569,13 +1345,21 @@ async function task() {
569
1345
  if (current.sessionId && current.sessionId !== sessionShown) {
570
1346
  sessionShown = current.sessionId;
571
1347
  console.log(`\nagent session: ${current.sessionId}`);
572
- console.log(`open live session: ${resumeCommand(current.agent, current.sessionId, current.cwd)}`);
1348
+ console.log(
1349
+ `open live session: ${resumeCommand(current.agent, current.sessionId, current.cwd)}`
1350
+ );
573
1351
  }
574
- const terminal = current.status === "done" || current.status === "failed" || current.status === "interrupted";
1352
+ const terminal =
1353
+ current.status === "done" ||
1354
+ current.status === "failed" ||
1355
+ current.status === "interrupted";
575
1356
  if (!follow || (terminal && offset >= current.logLength)) {
576
1357
  console.log(`\nstatus: ${current.status}`);
577
1358
  if (current.message) console.log(current.message);
578
- if (current.status === "interrupted") console.log("The daemon restarted during this run; its outcome is unknown. Check the repository.");
1359
+ if (current.status === "interrupted")
1360
+ console.log(
1361
+ "The daemon restarted during this run; its outcome is unknown. Check the repository."
1362
+ );
579
1363
  if (current.status !== "done") process.exitCode = 1;
580
1364
  return;
581
1365
  }
@@ -584,21 +1368,40 @@ async function task() {
584
1368
  }
585
1369
 
586
1370
  function logs() {
587
- if (!fs.existsSync(paths.log)) { console.log("No daemon log yet."); return; }
1371
+ if (!fs.existsSync(paths.log)) {
1372
+ console.log("No daemon log yet.");
1373
+ return;
1374
+ }
588
1375
  const lines = fs.readFileSync(paths.log, "utf8").split("\n");
589
- process.stdout.write(lines.slice(-Math.max(1, Number(arg("--lines", "100")))).join("\n"));
590
- if (has("--follow")) fs.watchFile(paths.log, { interval: 500 }, () => process.stdout.write(fs.readFileSync(paths.log, "utf8").split("\n").slice(-20).join("\n")));
1376
+ process.stdout.write(
1377
+ lines.slice(-Math.max(1, Number(arg("--lines", "100")))).join("\n")
1378
+ );
1379
+ if (has("--follow"))
1380
+ fs.watchFile(paths.log, { interval: 500 }, () =>
1381
+ process.stdout.write(
1382
+ fs.readFileSync(paths.log, "utf8").split("\n").slice(-20).join("\n")
1383
+ )
1384
+ );
591
1385
  }
592
1386
 
593
1387
  async function disconnect() {
594
1388
  uninstallService();
595
1389
  retireDesktop({});
596
- try { await daemonRequest({ command: "shutdown" }, 2_000); } catch {}
1390
+ try {
1391
+ await daemonRequest({ command: "shutdown" }, 2_000);
1392
+ } catch {}
597
1393
  removeAgentConfigurations();
598
1394
  removeLocalWakes();
599
- try { fs.renameSync(paths.config, paths.config + `.disconnected-${Date.now()}`); } catch {}
600
- for (const target of [paths.status, paths.socket]) try { fs.unlinkSync(target); } catch {}
601
- console.log("Harness disconnected. The local history database was preserved.");
1395
+ try {
1396
+ fs.renameSync(paths.config, paths.config + `.disconnected-${Date.now()}`);
1397
+ } catch {}
1398
+ for (const target of [paths.status, paths.socket])
1399
+ try {
1400
+ fs.unlinkSync(target);
1401
+ } catch {}
1402
+ console.log(
1403
+ "Harness disconnected. The local history database was preserved."
1404
+ );
602
1405
  }
603
1406
 
604
1407
  // How Harness reaches each agent: `agents` walks the whole swarm, `agents setup <agent>` one agent, `agents test <agent>`
@@ -606,55 +1409,144 @@ async function disconnect() {
606
1409
  async function agents() {
607
1410
  const config = readJson(paths.config, {});
608
1411
  const [sub, agent] = argv;
609
- if (sub === "test" && agent) { process.exitCode = (await testAgent({ agent, daemonRequest })) ? 0 : 1; return; }
1412
+ if (sub === "test" && agent) {
1413
+ process.exitCode = (await testAgent({ agent, daemonRequest })) ? 0 : 1;
1414
+ return;
1415
+ }
610
1416
  // What every agent is told about the others, and the person's own note on one agent's card.
611
1417
  if (sub === "cards") {
612
- const response = await daemonRequest({ command: "board.swarm.agents", agent: "user" });
613
- if (!response.ok) throw new Error(response.error || "the daemon could not list your agents");
614
- console.log(response.text || "No agents in your swarm yet. Set them up: npx tryharness agents");
1418
+ const response = await daemonRequest({
1419
+ command: "board.swarm.agents",
1420
+ agent: "user",
1421
+ });
1422
+ if (!response.ok)
1423
+ throw new Error(
1424
+ response.error || "the daemon could not list your agents"
1425
+ );
1426
+ console.log(
1427
+ response.text ||
1428
+ "No agents in your swarm yet. Set them up: npx tryharness agents"
1429
+ );
615
1430
  return;
616
1431
  }
617
1432
  if (sub === "note") {
618
- const text = argv.slice(2).filter((word) => word !== "--clear").join(" ").trim();
619
- if (!agent || (!text && !has("--clear"))) throw new Error('usage: tryharness agents note <agent> "<what every other agent should know about it>" | --clear');
620
- const response = await daemonRequest({ command: "board.swarm.note", agent: "user", target: agent, text: has("--clear") ? "" : text });
1433
+ const text = argv
1434
+ .slice(2)
1435
+ .filter((word) => word !== "--clear")
1436
+ .join(" ")
1437
+ .trim();
1438
+ if (!agent || (!text && !has("--clear")))
1439
+ throw new Error(
1440
+ 'usage: tryharness agents note <agent> "<what every other agent should know about it>" | --clear'
1441
+ );
1442
+ const response = await daemonRequest({
1443
+ command: "board.swarm.note",
1444
+ agent: "user",
1445
+ target: agent,
1446
+ text: has("--clear") ? "" : text,
1447
+ });
621
1448
  if (!response.ok) throw new Error(response.error);
622
- console.log(response.note ? `Every agent now reads this on ${response.agent}'s card: ${response.note}` : `${response.agent}'s card has no note now.`);
1449
+ console.log(
1450
+ response.note
1451
+ ? `Every agent now reads this on ${response.agent}'s card: ${response.note}`
1452
+ : `${response.agent}'s card has no note now.`
1453
+ );
623
1454
  return;
624
1455
  }
625
1456
  // Each coding agent's models as it lists them, your choice for it, and your words on how Auto picks (models.mjs).
626
1457
  if (sub === "models") {
627
- const response = await daemonRequest({ command: "board.swarm.models", agent: "user", ...(agent ? { target: agent } : {}), refresh: has("--refresh") });
1458
+ const response = await daemonRequest({
1459
+ command: "board.swarm.models",
1460
+ agent: "user",
1461
+ ...(agent ? { target: agent } : {}),
1462
+ refresh: has("--refresh"),
1463
+ });
628
1464
  if (!response.ok) throw new Error(response.error);
629
1465
  for (const entry of response.agents) {
630
- const choice = entry.choice ? (entry.choice.auto ? "Auto" : [entry.choice.model, entry.choice.effort && `effort ${entry.choice.effort}`].filter(Boolean).join(", ")) : `its own settings${entry.ownModel ? ` (${entry.ownModel})` : ""}`;
1466
+ const choice = entry.choice
1467
+ ? entry.choice.auto
1468
+ ? "Auto"
1469
+ : [
1470
+ entry.choice.model,
1471
+ entry.choice.effort && `effort ${entry.choice.effort}`,
1472
+ ]
1473
+ .filter(Boolean)
1474
+ .join(", ")
1475
+ : `its own settings${entry.ownModel ? ` (${entry.ownModel})` : ""}`;
631
1476
  console.log(`${entry.name}: runs on ${choice}`);
632
- if (entry.error) console.log(` its list could not be read: ${entry.error}`);
633
- if (agent) for (const model of entry.models) console.log(` ${model.id}${model.efforts.length ? ` effort ${model.efforts.join(", ")}` : ""}${model.about ? ` ${model.about}` : ""}`);
634
- else console.log(` ${entry.models.length} models; list them with: npx tryharness agents models ${entry.id}`);
1477
+ if (entry.error)
1478
+ console.log(` its list could not be read: ${entry.error}`);
1479
+ if (agent)
1480
+ for (const model of entry.models)
1481
+ console.log(
1482
+ ` ${model.id}${model.efforts.length ? ` effort ${model.efforts.join(", ")}` : ""}${model.about ? ` ${model.about}` : ""}`
1483
+ );
1484
+ else
1485
+ console.log(
1486
+ ` ${entry.models.length} models; list them with: npx tryharness agents models ${entry.id}`
1487
+ );
635
1488
  }
636
- console.log(`\nAuto picks by: ${response.routing ?? `${response.defaultRouting} (the default; change it with: npx tryharness agents routing "<your words>")`}`);
1489
+ console.log(
1490
+ `\nAuto picks by: ${response.routing ?? `${response.defaultRouting} (the default; change it with: npx tryharness agents routing "<your words>")`}`
1491
+ );
637
1492
  return;
638
1493
  }
639
1494
  if (sub === "model") {
640
1495
  const model = argv[2];
641
- if (!agent || !model) throw new Error("usage: tryharness agents model <agent> <model from its list> [--effort <level>] | auto | default");
642
- const response = await daemonRequest({ command: "board.swarm.model", agent: "user", target: agent, model, effort: arg("--effort") });
1496
+ if (!agent || !model)
1497
+ throw new Error(
1498
+ "usage: tryharness agents model <agent> <model from its list> [--effort <level>] | auto | default"
1499
+ );
1500
+ const response = await daemonRequest({
1501
+ command: "board.swarm.model",
1502
+ agent: "user",
1503
+ target: agent,
1504
+ model,
1505
+ effort: arg("--effort"),
1506
+ });
643
1507
  if (!response.ok) throw new Error(response.error);
644
- console.log(!response.choice ? `${response.agent} runs on its own settings.` : response.choice.auto ? `Auto picks ${response.agent}'s model for each run.` : `${response.agent} runs on ${response.choice.model}${response.choice.effort ? `, effort ${response.choice.effort}` : ""}.`);
1508
+ console.log(
1509
+ !response.choice
1510
+ ? `${response.agent} runs on its own settings.`
1511
+ : response.choice.auto
1512
+ ? `Auto picks ${response.agent}'s model for each run.`
1513
+ : `${response.agent} runs on ${response.choice.model}${response.choice.effort ? `, effort ${response.choice.effort}` : ""}.`
1514
+ );
645
1515
  return;
646
1516
  }
647
1517
  if (sub === "routing") {
648
- const text = argv.slice(1).filter((word) => word !== "--clear").join(" ").trim();
649
- if (!text && !has("--clear")) throw new Error('usage: tryharness agents routing "<how Auto should pick models for your swarm>" | --clear');
650
- const response = await daemonRequest({ command: "board.swarm.routing", agent: "user", text: has("--clear") ? "" : text });
1518
+ const text = argv
1519
+ .slice(1)
1520
+ .filter((word) => word !== "--clear")
1521
+ .join(" ")
1522
+ .trim();
1523
+ if (!text && !has("--clear"))
1524
+ throw new Error(
1525
+ 'usage: tryharness agents routing "<how Auto should pick models for your swarm>" | --clear'
1526
+ );
1527
+ const response = await daemonRequest({
1528
+ command: "board.swarm.routing",
1529
+ agent: "user",
1530
+ text: has("--clear") ? "" : text,
1531
+ });
651
1532
  if (!response.ok) throw new Error(response.error);
652
- console.log(`Auto picks by: ${response.routing ?? `${response.defaultRouting} (the default)`}`);
1533
+ console.log(
1534
+ `Auto picks by: ${response.routing ?? `${response.defaultRouting} (the default)`}`
1535
+ );
653
1536
  return;
654
1537
  }
655
- const ui = createUi(), only = sub === "setup" ? agent || null : null;
656
- ui.intro(`${pc.bgCyan(pc.black(" tryharness "))} ${only ? `Setting up ${only}` : "Your swarm"}`);
657
- await setupSwarm({ runtimeRoot: config.runtimeRoot, states: Object.values(config.agentStates || {}), daemonRequest, ui, only });
1538
+ const ui = createUi(),
1539
+ only = sub === "setup" ? agent || null : null;
1540
+ ui.intro(
1541
+ `${pc.bgCyan(pc.black(" tryharness "))} ${only ? `Setting up ${only}` : "Your swarm"}`
1542
+ );
1543
+ await setupSwarm({
1544
+ runtimeRoot: config.runtimeRoot,
1545
+ states: Object.values(config.agentStates || {}),
1546
+ daemonRequest,
1547
+ ui,
1548
+ only,
1549
+ });
658
1550
  ui.outro("Done.");
659
1551
  }
660
1552
 
@@ -662,87 +1554,182 @@ async function phone() {
662
1554
  const sub = argv[0] || "pair";
663
1555
  if (sub === "pair") {
664
1556
  const response = await daemonRequest({ command: "phone.offer" });
665
- if (!response.ok) throw new Error(response.error || "the daemon could not make a pairing code");
1557
+ if (!response.ok)
1558
+ throw new Error(
1559
+ response.error || "the daemon could not make a pairing code"
1560
+ );
666
1561
  const { default: encodeQR } = await import("qr");
667
- console.log("Scan this with your phone's camera. The code works once, for 10 minutes.\n");
1562
+ console.log(
1563
+ "Scan this with your phone's camera. The code works once, for 10 minutes.\n"
1564
+ );
668
1565
  console.log(encodeQR(response.url, "term"));
669
- if (has("--link")) console.log(`\n${response.url}\n\nThis link is the pairing secret: open it only on your own phone.`);
670
- console.log("\nThe phone talks to this computer through Harness, and Harness only relays sealed messages it cannot read.");
1566
+ if (has("--link"))
1567
+ console.log(
1568
+ `\n${response.url}\n\nThis link is the pairing secret: open it only on your own phone.`
1569
+ );
1570
+ console.log(
1571
+ "\nThe phone talks to this computer through Harness, and Harness only relays sealed messages it cannot read."
1572
+ );
671
1573
  return;
672
1574
  }
673
1575
  if (sub === "list") {
674
1576
  const response = await daemonRequest({ command: "phone.list" });
675
- if (!response.ok) throw new Error(response.error || "the daemon could not list phones");
676
- if (!response.phones.length) { console.log("Nothing paired. Pair a phone: npx tryharness phone pair; connect a browser: npx tryharness link"); return; }
677
- for (const p of response.phones) console.log(`${p.id.slice(0, 8)} ${(p.kind || "phone").padEnd(8)} ${p.label.padEnd(20)} paired ${p.paired_at.slice(0, 10)} ${p.revoked_at ? `disconnected ${p.revoked_at.slice(0, 10)}` : p.last_seen_at ? `last seen ${p.last_seen_at.slice(0, 16).replace("T", " ")}` : "not used yet"}`);
1577
+ if (!response.ok)
1578
+ throw new Error(response.error || "the daemon could not list phones");
1579
+ if (!response.phones.length) {
1580
+ console.log(
1581
+ "Nothing paired. Pair a phone: npx tryharness phone pair; connect a browser: npx tryharness link"
1582
+ );
1583
+ return;
1584
+ }
1585
+ for (const p of response.phones)
1586
+ console.log(
1587
+ `${p.id.slice(0, 8)} ${(p.kind || "phone").padEnd(8)} ${p.label.padEnd(20)} paired ${p.paired_at.slice(0, 10)} ${p.revoked_at ? `disconnected ${p.revoked_at.slice(0, 10)}` : p.last_seen_at ? `last seen ${p.last_seen_at.slice(0, 16).replace("T", " ")}` : "not used yet"}`
1588
+ );
678
1589
  return;
679
1590
  }
680
1591
  if (sub === "revoke") {
681
1592
  const prefix = String(argv[1] || "");
682
1593
  if (!prefix) throw new Error("usage: tryharness phone revoke <phone id>");
683
1594
  const listed = await daemonRequest({ command: "phone.list" });
684
- const matches = (listed.phones || []).filter((p) => p.id.startsWith(prefix));
685
- if (matches.length !== 1) throw new Error(matches.length ? "that id matches more than one phone; type more of it" : "no phone with that id (see: tryharness phone list)");
686
- const response = await daemonRequest({ command: "phone.revoke", id: matches[0].id });
1595
+ const matches = (listed.phones || []).filter((p) =>
1596
+ p.id.startsWith(prefix)
1597
+ );
1598
+ if (matches.length !== 1)
1599
+ throw new Error(
1600
+ matches.length
1601
+ ? "that id matches more than one phone; type more of it"
1602
+ : "no phone with that id (see: tryharness phone list)"
1603
+ );
1604
+ const response = await daemonRequest({
1605
+ command: "phone.revoke",
1606
+ id: matches[0].id,
1607
+ });
687
1608
  if (!response.ok) throw new Error(response.error);
688
- console.log(`${matches[0].label} is disconnected. It can no longer reach this computer.`);
1609
+ console.log(
1610
+ `${matches[0].label} is disconnected. It can no longer reach this computer.`
1611
+ );
689
1612
  return;
690
1613
  }
691
- throw new Error("usage: tryharness phone [pair [--link] | list | revoke <phone id>]");
1614
+ throw new Error(
1615
+ "usage: tryharness phone [pair [--link] | list | revoke <phone id>]"
1616
+ );
692
1617
  }
693
1618
 
694
1619
  // A browser joins the way a phone does, by a one-time link this machine makes: its secret and this
695
1620
  // machine's key ride in the part of the address no server receives. Once one browser is paired,
696
1621
  // this machine answers Harness's calls for the person's board, sessions and runs only sealed.
697
1622
  function openUrl(url) {
698
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
699
- try { spawn(command, args, { detached: true, stdio: "ignore" }).on("error", () => {}).unref(); return true; } catch { return false; }
1623
+ const [command, args] =
1624
+ process.platform === "darwin"
1625
+ ? ["open", [url]]
1626
+ : process.platform === "win32"
1627
+ ? ["cmd", ["/c", "start", "", url]]
1628
+ : ["xdg-open", [url]];
1629
+ try {
1630
+ spawn(command, args, { detached: true, stdio: "ignore" })
1631
+ .on("error", () => {})
1632
+ .unref();
1633
+ return true;
1634
+ } catch {
1635
+ return false;
1636
+ }
700
1637
  }
701
1638
  async function browserLink() {
702
- const response = await daemonRequest({ command: "phone.offer", kind: "browser" });
703
- if (!response.ok) throw new Error(response.error || "the daemon could not make a link");
704
- return { url: response.url, opened: !has("--print") && openUrl(response.url) };
1639
+ const response = await daemonRequest({
1640
+ command: "phone.offer",
1641
+ kind: "browser",
1642
+ });
1643
+ if (!response.ok)
1644
+ throw new Error(response.error || "the daemon could not make a link");
1645
+ return {
1646
+ url: response.url,
1647
+ opened: !has("--print") && openUrl(response.url),
1648
+ };
705
1649
  }
706
1650
  async function link() {
707
1651
  const { url, opened } = await browserLink();
708
- console.log(opened ? "Opened Harness in your browser to connect it to this computer end to end. The link works once, for 10 minutes." : `Open this in the browser you use Harness in. It works once, for 10 minutes, and is the pairing secret:\n\n${url}`);
709
- console.log("Harness then carries what your browser and this computer say to each other sealed, and cannot read it.");
1652
+ console.log(
1653
+ opened
1654
+ ? "Opened Harness in your browser to connect it to this computer end to end. The link works once, for 10 minutes."
1655
+ : `Open this in the browser you use Harness in. It works once, for 10 minutes, and is the pairing secret:\n\n${url}`
1656
+ );
1657
+ console.log(
1658
+ "Harness then carries what your browser and this computer say to each other sealed, and cannot read it."
1659
+ );
710
1660
  }
711
1661
 
712
1662
  // Approving Harness in T3 Code (lib/t3.mjs): then what you send your T3 Code sessions from Harness, in the desktop app or
713
1663
  // on your phone, goes straight into their threads. No T3 account; T3's page asks for a pairing code from its settings.
714
- const t3Words = (access) => !access ? "Harness is not approved in T3 Code. Messages to its sessions wait for them to pick them up (run: tryharness t3)."
715
- : access.state === "send" ? `Harness sends your messages into T3 Code's threads, until ${access.until.slice(0, 10)}.`
716
- : access.state === "read" ? "Harness was approved in T3 Code to read only, so messages still wait for its sessions to pick them up. Approve it again with the access your threads run with: tryharness t3"
717
- : "Harness's approval in T3 Code ran out. Approve it again: tryharness t3";
1664
+ const t3Words = (access) =>
1665
+ !access
1666
+ ? "Harness is not approved in T3 Code. Messages to its sessions wait for them to pick them up (run: tryharness t3)."
1667
+ : access.state === "send"
1668
+ ? `Harness sends your messages into T3 Code's threads, until ${access.until.slice(0, 10)}.`
1669
+ : access.state === "read"
1670
+ ? "Harness was approved in T3 Code to read only, so messages still wait for its sessions to pick them up. Approve it again with the access your threads run with: tryharness t3"
1671
+ : "Harness's approval in T3 Code ran out. Approve it again: tryharness t3";
718
1672
  async function t3() {
719
1673
  const sub = argv[0] || "connect";
720
- if (sub === "status") { console.log(t3Words((await daemonRequest({ command: "t3.status" })).t3)); return; }
721
- if (sub === "forget") { await daemonRequest({ command: "t3.forget" }); console.log("Harness forgot its approval. To end it in T3 Code as well: Settings → Connections."); return; }
722
- if (sub !== "connect") throw new Error("usage: tryharness t3 [connect [--print] | status | forget]");
1674
+ if (sub === "status") {
1675
+ console.log(t3Words((await daemonRequest({ command: "t3.status" })).t3));
1676
+ return;
1677
+ }
1678
+ if (sub === "forget") {
1679
+ await daemonRequest({ command: "t3.forget" });
1680
+ console.log(
1681
+ "Harness forgot its approval. To end it in T3 Code as well: Settings → Connections."
1682
+ );
1683
+ return;
1684
+ }
1685
+ if (sub !== "connect")
1686
+ throw new Error(
1687
+ "usage: tryharness t3 [connect [--print] | status | forget]"
1688
+ );
723
1689
  const before = (await daemonRequest({ command: "t3.status" })).t3;
724
1690
  const started = await daemonRequest({ command: "t3.connect" }, 20_000);
725
- if (!started.ok) throw new Error(started.error || "the daemon could not start the approval");
1691
+ if (!started.ok)
1692
+ throw new Error(started.error || "the daemon could not start the approval");
726
1693
  const opened = !has("--print") && openUrl(started.url);
727
- console.log(opened ? "T3 Code's approval page is open in your browser." : `Open this in a browser on this computer:\n\n${started.url}\n`);
728
- console.log("There, enter a pairing code from T3 Code (Settings → Connections), then choose what Harness may do. To message threads that run in Full access, choose Full access.");
729
- console.log("Waiting for your approval (Ctrl-C stops waiting; an approval still counts).");
1694
+ console.log(
1695
+ opened
1696
+ ? "T3 Code's approval page is open in your browser."
1697
+ : `Open this in a browser on this computer:\n\n${started.url}\n`
1698
+ );
1699
+ console.log(
1700
+ "There, enter a pairing code from T3 Code (Settings → Connections), then choose what Harness may do. To message threads that run in Full access, choose Full access."
1701
+ );
1702
+ console.log(
1703
+ "Waiting for your approval (Ctrl-C stops waiting; an approval still counts)."
1704
+ );
730
1705
  for (;;) {
731
1706
  await sleep(1_000);
732
1707
  const now = (await daemonRequest({ command: "t3.status" })).t3;
733
- if (now && JSON.stringify(now) !== JSON.stringify(before)) { console.log(t3Words(now)); return; }
1708
+ if (now && JSON.stringify(now) !== JSON.stringify(before)) {
1709
+ console.log(t3Words(now));
1710
+ return;
1711
+ }
734
1712
  }
735
1713
  }
736
1714
 
737
1715
  function help() {
738
- console.log(`tryharness ${VERSION}\n\nCommands:\n tryharness set up everything and start the daemon\n tryharness desktop open the Harness desktop app (setup opens it too; --no-desktop skips it)\n tryharness status show daemon, bridge, agent, and index health\n tryharness doctor diagnose the complete connection\n tryharness sync [--agent] rescan local coding history in the background\n tryharness task <id> --follow\n follow a delegated agent run in this terminal\n tryharness board [--all] list open work on the machine's task board\n tryharness board post --for <agent> --objective <text> [--file <brief.md>]\n tryharness board show <id>\n tryharness board post --for codex --objective <text> --run --workspace <dir>\n the daemon runs it and reports back on the task\n tryharness board run <id> authorize a run an agent asked for\n tryharness board release <id>\n tryharness board agents when each agent last read the board\n tryharness board --watch follow the board in this terminal\n tryharness notify on|off desktop notifications for what waits on you\n tryharness board send <task> <message> send a held reply to another person's agent\n tryharness board auto <task> on|off auto-send replies on that thread\n tryharness board end <task> --outcome <text>\n tryharness board context <task> <query> search everything written about a task\n tryharness goal set --objective <text> [--team a,b] [--workspace <dir>] a goal your agents work together in one room\n tryharness goal [list | show <id> | stop <id> | resume <id>]\n tryharness room <goal> | room post <goal> <text> [--ask agent,agent]\n tryharness swarm rounds [<thread>] what the coordinator saw, chose and cost each round\n tryharness swarm coordinator [harness|local] who coordinates your threads\n tryharness connect <handle/agent> --about <text> ask another person's agent to work an issue with yours, sealed to their computer\n tryharness peers the keys of other people's computers this one has worked with, to compare\n tryharness agents how Harness wakes each of your agents, and set up the rest\n tryharness agents setup <agent> | agents test <agent>\n tryharness agents cards what each of your agents is told about the others\n tryharness agents note <agent> <text> [--clear] your own note on an agent's card, told to every other agent\n tryharness agents models [<agent>] [--refresh] the models each coding agent lists and the one it runs on\n tryharness agents model <agent> <model> [--effort <level>] | auto | default the model it runs on; auto lets Harness pick per run\n tryharness agents routing "<text>" [--clear] how Auto picks models for your swarm\n tryharness mcp grok-bot print the MCP snippet that puts Grok Bot on the board\n tryharness link connect the browser you use Harness in to this computer, end to end (--print shows the link)\n tryharness phone pair show a QR code that pairs your phone with this computer\n tryharness phone list | revoke <id>\n tryharness t3 approve Harness in T3 Code once, so your messages go into its threads (t3 status | t3 forget)\n tryharness logs [--follow]\n tryharness disconnect\n`);
1716
+ console.log(
1717
+ `tryharness ${VERSION}\n\nCommands:\n tryharness set up everything and start the daemon\n tryharness desktop open the Harness desktop app (setup opens it too; --no-desktop skips it)\n tryharness status show daemon, bridge, agent, and index health\n tryharness doctor diagnose the complete connection\n tryharness local [learn] the relay's local models, where they run, what is indexed and the last learning\n tryharness local add|remove <folder> a folder of recordings and screenshots agents can search by what was said or shown\n tryharness watch [add <name> --run <command> [--every 5m] [--field f | --age f | --count text] [--above n] [--below n] | start <id> | remove <id>]\n watches the relay checks and says out loud only when they are off\n tryharness sync [--agent] rescan local coding history in the background\n tryharness task <id> --follow\n follow a delegated agent run in this terminal\n tryharness board [--all] list open work on the machine's task board\n tryharness board post --for <agent> --objective <text> [--file <brief.md>]\n tryharness board show <id>\n tryharness board post --for codex --objective <text> --run --workspace <dir>\n the daemon runs it and reports back on the task\n tryharness board run <id> authorize a run an agent asked for\n tryharness board release <id>\n tryharness board agents when each agent last read the board\n tryharness board --watch follow the board in this terminal\n tryharness notify on|off desktop notifications for what waits on you\n tryharness board send <task> <message> send a held reply to another person's agent\n tryharness board snippet [<id> share|keep] code an agent elsewhere asked to see; nothing leaves until you share it\n tryharness board auto <task> on|off auto-send replies on that thread\n tryharness board end <task> --outcome <text>\n tryharness board context <task> <query> search everything written about a task\n tryharness goal set --objective <text> [--team a,b] [--workspace <dir>] a goal your agents work together in one room\n tryharness goal [list | show <id> | stop <id> | resume <id>]\n tryharness room <goal> | room post <goal> <text> [--ask agent,agent]\n tryharness swarm rounds [<thread>] what the coordinator saw, chose and cost each round\n tryharness swarm coordinator [harness|local] who coordinates your threads\n tryharness connect <handle/agent> --about <text> ask another person's agent to work an issue with yours, sealed to their computer\n tryharness peers the keys of other people's computers this one has worked with, to compare\n tryharness agents how Harness wakes each of your agents, and set up the rest\n tryharness agents setup <agent> | agents test <agent>\n tryharness agents cards what each of your agents is told about the others\n tryharness agents note <agent> <text> [--clear] your own note on an agent's card, told to every other agent\n tryharness agents models [<agent>] [--refresh] the models each coding agent lists and the one it runs on\n tryharness agents model <agent> <model> [--effort <level>] | auto | default the model it runs on; auto lets Harness pick per run\n tryharness agents routing "<text>" [--clear] how Auto picks models for your swarm\n tryharness mcp grok-bot print the MCP snippet that puts Grok Bot on the board\n tryharness link connect the browser you use Harness in to this computer, end to end (--print shows the link)\n tryharness phone pair show a QR code that pairs your phone with this computer\n tryharness phone list | revoke <id>\n tryharness t3 approve Harness in T3 Code once, so your messages go into its threads (t3 status | t3 forget)\n tryharness logs [--follow]\n tryharness disconnect\n`
1718
+ );
739
1719
  }
740
1720
 
741
1721
  try {
742
- if (command === "setup" || (command === "connect" && (!argv[0] || argv[0].startsWith("-")))) await connect();
1722
+ if (
1723
+ command === "setup" ||
1724
+ (command === "connect" && (!argv[0] || argv[0].startsWith("-")))
1725
+ )
1726
+ await connect();
743
1727
  else if (command === "status" || command === "bridge") await status();
744
1728
  else if (command === "doctor") await doctor();
745
- else if (command === "sync" || command === "backfill" || command === "drain") await sync();
1729
+ else if (command === "local") await local();
1730
+ else if (command === "watch") await watch();
1731
+ else if (command === "sync" || command === "backfill" || command === "drain")
1732
+ await sync();
746
1733
  else if (command === "task") await task();
747
1734
  else if (command === "board") await board();
748
1735
  else if (command === "goal") await goal();
@@ -756,11 +1743,16 @@ try {
756
1743
  else if (command === "t3") await t3();
757
1744
  else if (command === "peers") await peers();
758
1745
  else if (command === "connect") await connectAgent();
759
- else if (command === "desktop") { const shown = await desktop(); console.log(shown.text); if (!shown.ok) process.exitCode = 1; }
760
- else if (command === "logs") logs();
1746
+ else if (command === "desktop") {
1747
+ const shown = await desktop();
1748
+ console.log(shown.text);
1749
+ if (!shown.ok) process.exitCode = 1;
1750
+ } else if (command === "logs") logs();
761
1751
  else if (command === "disconnect") await disconnect();
762
- else if (command === "daemon") { const { startDaemon } = await import("./lib/daemon.mjs"); await startDaemon(); }
763
- else if (command === "version") console.log(VERSION);
1752
+ else if (command === "daemon") {
1753
+ const { startDaemon } = await import("./lib/daemon.mjs");
1754
+ await startDaemon();
1755
+ } else if (command === "version") console.log(VERSION);
764
1756
  else if (["help", "--help", "-h"].includes(command)) help();
765
1757
  else throw new Error(`unknown command '${command}' (run: tryharness help)`);
766
1758
  } catch (error) {