tryharness 0.7.23 → 0.7.26

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 (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5268 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +384 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
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 { MODELS, ensureLlama, ensureModels, modelsPresent } from "./lib/localmodels.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();
@@ -176,40 +304,87 @@ async function connect() {
176
304
  // The relay's core local models (localmodels.mjs) and the server that runs them come down with setup, so agents get
177
305
  // context packs, code search, the handoff firewall and images checked before they leave from the first task. Each file
178
306
  // is checked against its pinned hash.
179
- const core = MODELS.filter((entry) => entry.tier === "core").map((entry) => entry.id);
307
+ const core = MODELS.filter((entry) => entry.tier === "core").map(
308
+ (entry) => entry.id
309
+ );
180
310
  const localRoot = path.join(paths.home, "local");
181
311
  if (modelsPresent(localRoot, core)) ui.success("Local models ready");
182
312
  else {
183
313
  const download = ui.spinner();
184
- const size = Math.round(MODELS.filter((entry) => core.includes(entry.id)).flatMap((entry) => entry.files).reduce((sum, file) => sum + file.size, 0) / 1e6);
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
+ );
185
319
  download.start(`Downloading the local models (about ${size} MB, once)`);
186
320
  // The classifiers run without the model server, so one that cannot start here does not hold them back.
187
321
  const failures = [];
188
- for (const step of [() => ensureLlama({ root: localRoot, log: (line) => download.message(`Downloading the local models · ${line.slice(0, 40)}`) }), () => ensureModels({ root: localRoot, ids: core, log: (line) => download.message(`Downloading the local models · ${line.slice(0, 40)}`) })]) {
189
- try { await step(); } catch (error) { failures.push(error.message); }
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
+ }
190
346
  }
191
- if (failures.length) download.stop(`Local models not ready (${failures.join("; ")}). Agents work as before until the next setup run`, 2);
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
+ );
192
352
  else download.stop("Local models ready");
193
353
  }
194
- 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
+ );
195
358
  if (failed.length) {
196
- 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
+ );
197
362
  }
198
363
  ui.info("History indexes locally in the background");
199
- 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
+ );
200
367
  ui.outro("Done. You can close this terminal.");
201
368
  }
202
369
 
203
370
  // The desktop app: the newest build for this machine, installed and opened. Never fatal; the
204
371
  // daemon works without it.
205
372
  async function desktop() {
206
- 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)" };
207
375
  try {
208
376
  const installed = await setupDesktop({ sourceRoot: SOURCE_ROOT });
209
- 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
+ };
210
382
  const { target, updated } = installed;
211
383
  openDesktop({ target });
212
- 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
+ };
213
388
  } catch (error) {
214
389
  return { ok: false, text: `Desktop app not opened (${error.message})` };
215
390
  }
@@ -218,21 +393,34 @@ async function desktop() {
218
393
  async function status() {
219
394
  const fallback = readJson(paths.status, null);
220
395
  let current;
221
- try { current = await daemonRequest({ command: "status" }); }
222
- 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
+ }
223
401
  const service = serviceInfo();
224
402
  if (!current) {
225
403
  console.log("Harness daemon: not running");
226
- console.log(`service: ${service.installed ? "installed" : "not installed"}`);
404
+ console.log(
405
+ `service: ${service.installed ? "installed" : "not installed"}`
406
+ );
227
407
  process.exitCode = 1;
228
408
  return;
229
409
  }
230
- console.log(`Harness daemon ${current.version || "unknown"}: ${current.ok === false ? "not responding" : "running"} (pid ${current.pid || "?"})`);
231
- console.log(`Harness bridge: ${current.connected ? "connected" : "disconnected"}`);
232
- 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
+ );
233
419
  for (const state of current.agents || []) {
234
420
  const count = Number(current.counts?.[state.agent] || 0);
235
- 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
+ );
236
424
  }
237
425
  }
238
426
 
@@ -242,59 +430,143 @@ async function status() {
242
430
  async function local() {
243
431
  const root = path.join(paths.home, "local");
244
432
  if (argv[0] === "add" || argv[0] === "remove") {
245
- const response = await daemonRequest({ command: `local.${argv[0]}`, folder: path.resolve(argv[1] || process.cwd()) });
246
- if (!response?.ok) throw new Error(response?.error || "the daemon did not answer");
247
- console.log(argv[0] === "add" ? `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)" : ""}.` : "Removed.");
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
+ );
248
444
  return;
249
445
  }
250
446
  for (const entry of MODELS) {
251
447
  const here = modelsPresent(root, [entry.id]);
252
- const mb = Math.round(entry.files.reduce((sum, file) => sum + file.size, 0) / 1e6);
253
- console.log(`${here ? "ready " : "missing"} ${entry.name} (${entry.tier}, ${entry.licence}, ${mb} MB): ${entry.purpose}`);
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
+ );
254
454
  }
255
455
  let current = null;
256
- try { current = await daemonRequest({ command: argv[0] === "learn" ? "local.learn" : "local.status" }); }
257
- catch (error) { console.log(`daemon: ${error.message}`); process.exitCode = 1; return; }
258
- if (!current?.ok) { console.log(current?.error || "the daemon did not answer"); process.exitCode = 1; return; }
259
- if (argv[0] === "learn") { console.log("Learning from your agents' runs now; tryharness local shows the result when it is done."); return; }
260
- console.log(`runs on: ${current.device ? (current.device.gpu ? "GPU" : "CPU") : "not checked yet"}`);
261
- for (const indexed of current.roots || []) console.log(`indexed: ${indexed.root} (${indexed.files} files)`);
262
- if (current.pieces?.waiting) console.log(`waiting to be embedded: ${current.pieces.waiting} pieces`);
263
- console.log(current.model ? `ranking: learned ${current.model.trainedAt}, held-out recall at 10 ${JSON.stringify(current.model.heldOut)}` : "ranking: the default until the learner has enough runs");
264
- for (const folder of current.media?.folders || []) console.log(`recordings and screenshots: ${folder}`);
265
- if (current.media?.folders?.length) console.log(`media files read: ${current.media.files}`);
266
- if (current.learning) console.log(`last learning: ${current.learning.at} · ${current.learning.kept ? "kept a new model" : `kept the old one${current.learning.why ? ` (${current.learning.why})` : ""}`}`);
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
+ );
267
496
  }
268
497
 
269
498
  // Watches (watches.mjs): `tryharness watch` lists them and what each last read; `watch add` starts one; agents' suggestions
270
499
  // wait for `watch start`.
271
500
  async function watch() {
272
501
  const sub = argv[0] || "list";
273
- const minutes = (text) => { const found = String(text || "").match(/^(\d+(?:\.\d+)?)\s*(s|m|h)?$/); return found ? Number(found[1]) * ({ s: 1, m: 60, h: 3600 }[found[2] || "m"]) : null; };
274
- const response = sub === "add" ? await daemonRequest({ command: "board.watch.add", agent: "user", name: argv[1], watchCommand: arg("--run", ""), folder: path.resolve(arg("--in", process.cwd())),
275
- everySeconds: minutes(arg("--every", "5m")) ?? 300, field: arg("--field", undefined), age: arg("--age", undefined), count: arg("--count", undefined), above: arg("--above", undefined), below: arg("--below", undefined) })
276
- : sub === "start" || sub === "remove" ? await daemonRequest({ command: `board.watch.${sub}`, agent: "user", id: argv[1] })
277
- : await daemonRequest({ command: "board.watch.list", agent: "user" });
278
- if (!response?.ok) throw new Error(response?.error || "the daemon did not answer");
279
- if (sub === "add") { console.log(`Watching ${response.watch.name} every ${response.watch.everySeconds} s (${response.watch.id}).`); return; }
280
- if (sub !== "list") { console.log("Done."); return; }
281
- if (!response.watches.length) console.log("No watches. Add one: tryharness watch add \"keeper heartbeat\" --run \"<command>\" --age completedAt --above 900");
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
+ );
282
546
  for (const entry of response.watches) {
283
547
  const last = entry.samples.at(-1);
284
- console.log(`${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}` : ""}`);
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
+ );
285
551
  }
286
552
  }
287
553
 
288
554
  async function doctor() {
289
555
  console.log(`tryharness ${VERSION}`);
290
556
  const config = readJson(paths.config, {});
291
- console.log(`pairing: ${await validateConnection(config) ? "valid" : "missing or invalid"}`);
557
+ console.log(
558
+ `pairing: ${(await validateConnection(config)) ? "valid" : "missing or invalid"}`
559
+ );
292
560
  const service = serviceInfo();
293
- 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
+ );
294
564
  try {
295
565
  const report = await daemonRequest({ command: "doctor" });
296
566
  console.log(`daemon: running (pid ${report.status.pid})`);
297
- 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
+ );
298
570
  console.log(`database: ${report.database}`);
299
571
  console.log(`control socket: ${report.socket}`);
300
572
  console.log(`log: ${report.log}`);
@@ -302,66 +574,120 @@ async function doctor() {
302
574
  console.log(`daemon: unavailable — ${error.message}`);
303
575
  process.exitCode = 1;
304
576
  }
305
- 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"}`);
306
579
  }
307
580
 
308
581
  async function sync() {
309
582
  const agent = arg("--agent", "all");
310
- 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}'`);
311
585
  const response = await daemonRequest({ command: "sync", agent });
312
586
  if (!response.ok) throw new Error(response.error || "sync rejected");
313
- 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
+ );
314
590
  }
315
591
 
316
592
  async function notify() {
317
593
  const mode = argv[0];
318
- 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
+ }
319
601
  const config = readJson(paths.config, {});
320
602
  writeJsonAtomic(paths.config, { ...config, notifications: mode === "on" });
321
- 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
+ );
322
606
  }
323
607
 
324
608
  async function connectAgent() {
325
609
  // Ask another person's listed agent to work an issue with one of ours: a request their Harness shows them.
326
610
  const to = String(argv[0] || "");
327
- 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
+ );
328
615
  const fromAgent = arg("--agent", "claude-code");
329
- 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(", ")}`);
330
618
  const about = arg("--about", "");
331
619
  if (!about.trim()) throw new Error("connect needs --about");
332
620
  const file = arg("--file");
333
621
  let brief = "";
334
622
  if (file) brief = fs.readFileSync(path.resolve(file), "utf8");
335
- else if (!process.stdin.isTTY) for await (const chunk of process.stdin) brief += chunk;
336
- 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
+ );
337
636
  if (!response.ok) throw new Error(response.error || "request failed");
338
- 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.`);
339
- console.log(response.sealed
340
- ? `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`
341
- : `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
+ );
342
645
  }
343
646
 
344
647
  // The keys of other people's computers this one has worked with, pinned the first time, and its own.
345
648
  async function peers() {
346
649
  const response = await daemonRequest({ command: "relay.peers" });
347
- 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");
348
652
  console.log(`This computer: ${response.mine ?? "no key yet"}`);
349
- if (!response.peers.length) { console.log("No other computers yet. A key is pinned the first time you connect with someone."); return; }
350
- for (const peer of response.peers) console.log(`${peer.handle.padEnd(20)} ${peer.fingerprint} pinned ${peer.pinned_at.slice(0, 10)}`);
351
- 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
+ );
352
666
  }
353
667
 
354
668
  async function mcpSnippet() {
355
669
  const client = argv[0];
356
- 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("|")}>`);
357
672
  const config = readJson(paths.config, {});
358
673
  const runtimeRoot = config.runtimeRoot || currentRuntime();
359
- 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
+ );
360
678
  const found = detectMcpClients().find((entry) => entry.client === client);
361
- console.log(`${client}: ${found?.path ? `found at ${found.path}` : "not found on this machine; the snippet still works once it is installed"}`);
362
- console.log(`In ${client === "grok-bot" ? "Grok Bot, open Settings, MCP, add a server" : client} and paste this:\n`);
363
- console.log(JSON.stringify(mcpClientSnippet({ runtimeRoot, client }), null, 2));
364
- 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
+ );
365
691
  }
366
692
 
367
693
  async function board() {
@@ -369,101 +695,256 @@ async function board() {
369
695
  const agent = arg("--as", "user");
370
696
  if (sub === "post") {
371
697
  // --for codex,claude-code posts one task for several agents at once.
372
- 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);
373
702
  const target = targets.length === 1 ? targets[0] : null;
374
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.
375
- const allowed = (name) => AGENTS.includes(name) || ["harness", "grok-bot", "openclaw", "hermes", "muse", "bankr", "quotient"].includes(name);
376
- 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
+ );
377
719
  const objective = arg("--objective", "");
378
720
  const file = arg("--file");
379
721
  let brief = "";
380
722
  if (file) brief = fs.readFileSync(path.resolve(file), "utf8");
381
- 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;
382
725
  if (!objective.trim()) throw new Error("board post needs --objective");
383
- const runWorkspace = has("--run") ? path.resolve(arg("--workspace", process.cwd())) : null;
384
- if (runWorkspace && !targets.every((name) => AGENTS.includes(name))) throw new Error("--run needs --for with coding agents installed on this machine");
385
- 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");
386
- 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
+ });
387
749
  if (!response.ok) throw new Error(response.error || "post failed");
388
- 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.`);
389
- 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`);
390
- 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
+ );
391
762
  return;
392
763
  }
393
764
  if (sub === "send" || sub === "discard") {
394
765
  const id = String(argv[argv.indexOf(sub) + 1] || "");
395
766
  const messageId = String(argv[argv.indexOf(sub) + 2] || "");
396
- if (!id || !messageId) throw new Error(`usage: tryharness board ${sub} <task id> <message id>`);
397
- 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
+ });
398
775
  if (!response.ok) throw new Error(response.error || `${sub} failed`);
399
- console.log(sub === "send" ? `Sent to ${response.message.recipient}.` : "Discarded.");
776
+ console.log(
777
+ sub === "send" ? `Sent to ${response.message.recipient}.` : "Discarded."
778
+ );
400
779
  return;
401
780
  }
402
781
  // Code an agent elsewhere asked to see (snippets.mjs): list the requests waiting, or share or keep one.
403
782
  if (sub === "snippet") {
404
783
  const [id, choice] = argv.slice(argv.indexOf(sub) + 1);
405
784
  if (!id) {
406
- const response = await daemonRequest({ command: "board.snippet.list", agent: "user" });
407
- if (!response?.ok) throw new Error(response?.error || "the daemon did not answer");
408
- if (!response.snippets.length) { console.log("No agent is waiting to see code."); return; }
409
- for (const each of response.snippets) console.log(`${each.id} ${each.agent} asks to see ${each.path} lines ${each.from} to ${each.to} ("${each.about}")\n${each.text.split("\n").map((line) => ` ${line}`).join("\n")}\n tryharness board snippet ${each.id} share|keep\n`);
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
+ );
410
802
  return;
411
803
  }
412
- if (choice !== "share" && choice !== "keep") throw new Error("usage: tryharness board snippet <id> share|keep");
413
- const response = await daemonRequest({ command: "board.snippet.decide", agent: "user", id, share: choice === "share" });
414
- if (!response?.ok) throw new Error(response?.error || "the daemon did not answer");
415
- console.log(choice === "share" ? "Shared in the thread." : "Kept on this computer.");
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
+ );
416
817
  return;
417
818
  }
418
819
  if (sub === "auto" || sub === "end") {
419
820
  const id = String(argv[argv.indexOf(sub) + 1] || "");
420
- const response = sub === "auto"
421
- ? await daemonRequest({ command: "board.thread.auto", agent, id, on: argv[argv.indexOf(sub) + 2] !== "off" })
422
- : 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
+ });
423
835
  if (!response.ok) throw new Error(response.error || `${sub} failed`);
424
- 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
+ );
425
841
  return;
426
842
  }
427
843
  if (sub === "context") {
428
844
  const id = String(argv[argv.indexOf(sub) + 1] || "");
429
- const query = argv.slice(argv.indexOf(sub) + 2).filter((value) => !value.startsWith("--")).join(" ");
430
- if (!id || !query) throw new Error("usage: tryharness board context <task id> <what you are looking for>");
431
- 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
+ );
432
857
  if (!response.ok) throw new Error(response.error || "search failed");
433
- const how = response.retrieval === "vectors" ? "meaning" : response.embedder === "absent" ? "words (the local models are not downloaded yet; run npx tryharness)" : `words (${response.context?.embedded ?? 0} of ${response.context?.total ?? 0} entries embedded so far)`;
434
- console.log(`${response.entries.length} result${response.entries.length === 1 ? "" : "s"} by ${how}`);
435
- 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
+ );
436
871
  return;
437
872
  }
438
873
  // Standing permission: a coding agent starts the tasks you or Harness post for it without a Start. On by default
439
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.
440
875
  // Tasks another agent wrote always wait for you.
441
876
  if (sub === "standing") {
442
- 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
+ );
443
885
  const [target, ...dirs] = rest;
444
- 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]";
445
888
  if (target && !AGENTS.includes(target)) throw new Error(usage);
446
- const toggle = dirs.length === 1 && ["on", "off"].includes(dirs[0]) ? dirs[0] : null;
447
- const response = !target ? await daemonRequest({ command: "board.standing", agent })
448
- : toggle ? await daemonRequest({ command: `board.standing.${toggle}`, agent, targetAgent: target })
449
- : dirs.length ? await daemonRequest({ command: "board.standing.set", agent, targetAgent: target, workspaces: dirs.map((dir) => path.resolve(dir)), dailyCap: Number(arg("--cap", "10")) })
450
- : (() => { throw new Error(usage); })();
451
- if (!response.ok) throw new Error(response.error || "standing permission failed");
452
- 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
+ }
453
916
  for (const rule of response.standing) {
454
- if (rule.mode === "off") { console.log(`${rule.agent} waits for your Start. Turn it on: npx tryharness board standing ${rule.agent} on`); continue; }
455
- const where = rule.mode === "default"
456
- ? (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)")
457
- : `in ${rule.workspaces.join(", ")}`;
458
- 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
+ );
459
932
  }
460
933
  return;
461
934
  }
462
935
  if (sub === "run" || sub === "release") {
463
936
  const id = String(argv[argv.indexOf(sub) + 1] || "");
464
- 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
+ });
465
942
  if (!response.ok) throw new Error(response.error || `${sub} failed`);
466
- 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
+ );
467
948
  return;
468
949
  }
469
950
  if (sub === "watch" || has("--watch")) {
@@ -471,62 +952,147 @@ async function board() {
471
952
  const seen = new Map();
472
953
  let first = true;
473
954
  for (;;) {
474
- 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);
475
961
  if (response?.ok) {
476
962
  for (const t of response.tasks) {
477
963
  const key = `${t.status}:${t.version}:${t.stale ? "stale" : ""}`;
478
964
  if (seen.get(t.id) === key) continue;
479
965
  seen.set(t.id, key);
480
- 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
+ );
481
973
  }
482
974
  if (first) console.log("watching the board (Ctrl-C stops)");
483
975
  first = false;
484
- } 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
+ }
485
980
  await sleep(2_000);
486
981
  }
487
982
  }
488
983
  if (sub === "agents") {
489
984
  const response = await daemonRequest({ command: "board.agents", agent });
490
985
  if (!response.ok) throw new Error(response.error || "board unavailable");
491
- if (!response.agents.length) { console.log("No agent has read the board yet."); return; }
492
- 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
+ );
493
994
  return;
494
995
  }
495
996
  if (sub === "show") {
496
- 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
+ });
497
1002
  if (!response.ok) throw new Error(response.error || "task not found");
498
1003
  const { task: t } = response;
499
- 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`);
500
- 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)}` : ""}`);
501
- 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
+ );
502
1016
  if (t.brief) console.log(t.brief + "\n");
503
- if (t.result) console.log(`${t.status === "needs_input" ? "question" : "result"}: ${t.result}`);
504
- 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
+ });
505
1026
  if (thread.ok) {
506
- console.log(`\nthread with ${thread.thread.with_agent}${thread.thread.closed_at ? " (ended)" : thread.thread.auto_send ? " (auto-send on)" : " (replies held for you)"}`);
507
- 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
+ );
508
1034
  }
509
1035
  return;
510
1036
  }
511
1037
  if (sub === "trace") {
512
- 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
+ );
513
1046
  if (!response.ok) throw new Error(response.error || "trace unavailable");
514
1047
  const t = response.trace;
515
- 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`);
516
- 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(", ")}`);
517
- 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)}`);
518
- 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
+ );
519
1067
  const c = t.cost;
520
1068
  const usd = (microCents) => `$${(microCents / 100_000_000).toFixed(4)}`;
521
- 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(", ")}` : ""}`);
522
- 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
+ );
523
1076
  return;
524
1077
  }
525
- 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]");
526
- 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
+ });
527
1087
  if (!response.ok) throw new Error(response.error || "board unavailable");
528
- if (!response.tasks.length) { console.log("The board is empty."); return; }
529
- 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
+ );
530
1096
  }
531
1097
 
532
1098
  // Goals: one objective for a team of agents, worked in a room. Set from here (or by Harness in chat); agents post,
@@ -535,45 +1101,108 @@ async function goal() {
535
1101
  const sub = argv[0] || "list";
536
1102
  const agent = arg("--as", "user");
537
1103
  const id = String(argv[1] || "");
538
- 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
+ };
539
1108
  if (sub === "set") {
540
- const response = await daemonRequest({ command: "board.goal.create", agent, objective: arg("--objective", ""), brief: arg("--file") ? fs.readFileSync(path.resolve(arg("--file")), "utf8") : arg("--brief", ""),
541
- team: arg("--team") ? String(arg("--team")).split(",").map((name) => name.trim()).filter(Boolean) : undefined, facilitator: arg("--facilitator") || undefined,
542
- 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
+ });
543
1129
  if (!response.ok) throw new Error(response.error || "the goal was not set");
544
1130
  const g = response.task;
545
- 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
+ );
546
1134
  return;
547
1135
  }
548
1136
  if (sub === "list") {
549
- 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
+ });
550
1142
  if (!response.ok) throw new Error(response.error || "goals unavailable");
551
- if (!response.goals.length) { console.log("No goals."); return; }
552
- 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
+ );
553
1151
  return;
554
1152
  }
555
1153
  if (sub === "show") {
556
- 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
+ });
557
1159
  if (!response.ok) throw new Error(response.error || "goal not found");
558
- const g = response.task, spend = g.goal.spend;
559
- 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
+ );
560
1165
  const room = await daemonRequest({ command: "board.room.read", agent, id });
561
- 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
+ );
562
1170
  return;
563
1171
  }
564
1172
  if (sub === "invite") {
565
- const response = await daemonRequest({ command: "board.goal.invite", agent, id, to: String(argv[2] || "") }, 20_000);
566
- if (!response.ok) throw new Error(response.error || "the invitation was not sent");
567
- 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
+ );
568
1182
  return;
569
1183
  }
570
1184
  if (sub === "stop" || sub === "resume" || sub === "folder") {
571
- 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
+ });
572
1193
  if (!response.ok) throw new Error(response.error || `${sub} failed`);
573
- 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
+ );
574
1201
  return;
575
1202
  }
576
- 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
+ );
577
1206
  }
578
1207
 
579
1208
  // The swarm's coordinator: who coordinates, and the record of every round it ran.
@@ -581,61 +1210,134 @@ async function swarm() {
581
1210
  const sub = argv[0] || "rounds";
582
1211
  if (sub === "coordinator") {
583
1212
  const mode = argv[1];
584
- if (mode && mode !== "harness" && mode !== "local") throw new Error("usage: tryharness swarm coordinator [harness|local]");
585
- const response = await daemonRequest({ command: "board.swarm.coordinator", agent: "user", ...(mode ? { mode } : {}) });
586
- if (!response.ok) throw new Error(response.error || "the coordinator setting was not changed");
587
- 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
+ );
588
1229
  return;
589
1230
  }
590
1231
  if (sub === "rounds") {
591
- 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
+ });
592
1238
  if (!response.ok) throw new Error(response.error || "rounds unavailable");
593
- if (!response.rounds.length) { console.log("No rounds yet."); return; }
1239
+ if (!response.rounds.length) {
1240
+ console.log("No rounds yet.");
1241
+ return;
1242
+ }
594
1243
  for (const r of response.rounds) {
595
- 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.`;
596
1248
  const progress = `${r.progress.runsFinished} runs done, ${r.progress.newResults} new, ${r.progress.roundsWithoutProgress} rounds idle`;
597
- 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
+ );
598
1252
  }
599
1253
  return;
600
1254
  }
601
- 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
+ );
602
1258
  }
603
1259
 
604
1260
  async function room() {
605
1261
  const agent = arg("--as", "user");
606
- 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
+ );
607
1267
  if (words[0] === "post") {
608
- const ask = String(arg("--ask", "")).split(",").map((name) => name.trim()).filter(Boolean);
609
- 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
+ });
610
1280
  if (!response.ok) throw new Error(response.error || "post failed");
611
- 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
+ );
612
1286
  return;
613
1287
  }
614
- const response = await daemonRequest({ command: "board.room.read", agent, id: String(words[0] || ""), since: Number(arg("--since", "0")) });
615
- if (!response.ok) throw new Error(response.error || "usage: tryharness room <goal> [--since n] | room post <goal> <text>");
616
- 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
+ );
617
1303
  }
618
1304
 
619
1305
  async function task() {
620
1306
  const runId = String(argv[0] || "").trim();
621
- 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]");
622
1309
  const follow = has("--follow") || has("-f");
623
1310
  let offset = Math.max(0, Math.floor(Number(arg("--offset", "0")) || 0));
624
1311
  let announced = false;
625
1312
  let sessionShown = null;
626
1313
  let misses = 0;
627
1314
  for (;;) {
628
- const response = await daemonRequest({ command: "tasks.get", runId, offset });
1315
+ const response = await daemonRequest({
1316
+ command: "tasks.get",
1317
+ runId,
1318
+ offset,
1319
+ });
629
1320
  if (!response.ok) {
630
- if (follow && misses < 10) { misses += 1; await sleep(500); continue; }
631
- 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
+ );
632
1329
  }
633
1330
  misses = 0;
634
1331
  const current = response.task;
635
1332
  if (!announced) {
636
- console.log(`Harness task ${runId} · ${current.agent} · ${current.status}`);
1333
+ console.log(
1334
+ `Harness task ${runId} · ${current.agent} · ${current.status}`
1335
+ );
637
1336
  if (current.cwd) console.log(`directory: ${current.cwd}`);
638
- 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
+ );
639
1341
  announced = true;
640
1342
  }
641
1343
  if (current.log) process.stdout.write(current.log);
@@ -643,13 +1345,21 @@ async function task() {
643
1345
  if (current.sessionId && current.sessionId !== sessionShown) {
644
1346
  sessionShown = current.sessionId;
645
1347
  console.log(`\nagent session: ${current.sessionId}`);
646
- 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
+ );
647
1351
  }
648
- 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";
649
1356
  if (!follow || (terminal && offset >= current.logLength)) {
650
1357
  console.log(`\nstatus: ${current.status}`);
651
1358
  if (current.message) console.log(current.message);
652
- 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
+ );
653
1363
  if (current.status !== "done") process.exitCode = 1;
654
1364
  return;
655
1365
  }
@@ -658,21 +1368,40 @@ async function task() {
658
1368
  }
659
1369
 
660
1370
  function logs() {
661
- 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
+ }
662
1375
  const lines = fs.readFileSync(paths.log, "utf8").split("\n");
663
- process.stdout.write(lines.slice(-Math.max(1, Number(arg("--lines", "100")))).join("\n"));
664
- 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
+ );
665
1385
  }
666
1386
 
667
1387
  async function disconnect() {
668
1388
  uninstallService();
669
1389
  retireDesktop({});
670
- try { await daemonRequest({ command: "shutdown" }, 2_000); } catch {}
1390
+ try {
1391
+ await daemonRequest({ command: "shutdown" }, 2_000);
1392
+ } catch {}
671
1393
  removeAgentConfigurations();
672
1394
  removeLocalWakes();
673
- try { fs.renameSync(paths.config, paths.config + `.disconnected-${Date.now()}`); } catch {}
674
- for (const target of [paths.status, paths.socket]) try { fs.unlinkSync(target); } catch {}
675
- 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
+ );
676
1405
  }
677
1406
 
678
1407
  // How Harness reaches each agent: `agents` walks the whole swarm, `agents setup <agent>` one agent, `agents test <agent>`
@@ -680,55 +1409,144 @@ async function disconnect() {
680
1409
  async function agents() {
681
1410
  const config = readJson(paths.config, {});
682
1411
  const [sub, agent] = argv;
683
- 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
+ }
684
1416
  // What every agent is told about the others, and the person's own note on one agent's card.
685
1417
  if (sub === "cards") {
686
- const response = await daemonRequest({ command: "board.swarm.agents", agent: "user" });
687
- if (!response.ok) throw new Error(response.error || "the daemon could not list your agents");
688
- 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
+ );
689
1430
  return;
690
1431
  }
691
1432
  if (sub === "note") {
692
- const text = argv.slice(2).filter((word) => word !== "--clear").join(" ").trim();
693
- if (!agent || (!text && !has("--clear"))) throw new Error('usage: tryharness agents note <agent> "<what every other agent should know about it>" | --clear');
694
- 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
+ });
695
1448
  if (!response.ok) throw new Error(response.error);
696
- 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
+ );
697
1454
  return;
698
1455
  }
699
1456
  // Each coding agent's models as it lists them, your choice for it, and your words on how Auto picks (models.mjs).
700
1457
  if (sub === "models") {
701
- 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
+ });
702
1464
  if (!response.ok) throw new Error(response.error);
703
1465
  for (const entry of response.agents) {
704
- 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})` : ""}`;
705
1476
  console.log(`${entry.name}: runs on ${choice}`);
706
- if (entry.error) console.log(` its list could not be read: ${entry.error}`);
707
- if (agent) for (const model of entry.models) console.log(` ${model.id}${model.efforts.length ? ` effort ${model.efforts.join(", ")}` : ""}${model.about ? ` ${model.about}` : ""}`);
708
- 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
+ );
709
1488
  }
710
- 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
+ );
711
1492
  return;
712
1493
  }
713
1494
  if (sub === "model") {
714
1495
  const model = argv[2];
715
- if (!agent || !model) throw new Error("usage: tryharness agents model <agent> <model from its list> [--effort <level>] | auto | default");
716
- 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
+ });
717
1507
  if (!response.ok) throw new Error(response.error);
718
- 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
+ );
719
1515
  return;
720
1516
  }
721
1517
  if (sub === "routing") {
722
- const text = argv.slice(1).filter((word) => word !== "--clear").join(" ").trim();
723
- if (!text && !has("--clear")) throw new Error('usage: tryharness agents routing "<how Auto should pick models for your swarm>" | --clear');
724
- 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
+ });
725
1532
  if (!response.ok) throw new Error(response.error);
726
- 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
+ );
727
1536
  return;
728
1537
  }
729
- const ui = createUi(), only = sub === "setup" ? agent || null : null;
730
- ui.intro(`${pc.bgCyan(pc.black(" tryharness "))} ${only ? `Setting up ${only}` : "Your swarm"}`);
731
- 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
+ });
732
1550
  ui.outro("Done.");
733
1551
  }
734
1552
 
@@ -736,89 +1554,182 @@ async function phone() {
736
1554
  const sub = argv[0] || "pair";
737
1555
  if (sub === "pair") {
738
1556
  const response = await daemonRequest({ command: "phone.offer" });
739
- 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
+ );
740
1561
  const { default: encodeQR } = await import("qr");
741
- 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
+ );
742
1565
  console.log(encodeQR(response.url, "term"));
743
- if (has("--link")) console.log(`\n${response.url}\n\nThis link is the pairing secret: open it only on your own phone.`);
744
- 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
+ );
745
1573
  return;
746
1574
  }
747
1575
  if (sub === "list") {
748
1576
  const response = await daemonRequest({ command: "phone.list" });
749
- if (!response.ok) throw new Error(response.error || "the daemon could not list phones");
750
- if (!response.phones.length) { console.log("Nothing paired. Pair a phone: npx tryharness phone pair; connect a browser: npx tryharness link"); return; }
751
- 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
+ );
752
1589
  return;
753
1590
  }
754
1591
  if (sub === "revoke") {
755
1592
  const prefix = String(argv[1] || "");
756
1593
  if (!prefix) throw new Error("usage: tryharness phone revoke <phone id>");
757
1594
  const listed = await daemonRequest({ command: "phone.list" });
758
- const matches = (listed.phones || []).filter((p) => p.id.startsWith(prefix));
759
- 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)");
760
- 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
+ });
761
1608
  if (!response.ok) throw new Error(response.error);
762
- 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
+ );
763
1612
  return;
764
1613
  }
765
- 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
+ );
766
1617
  }
767
1618
 
768
1619
  // A browser joins the way a phone does, by a one-time link this machine makes: its secret and this
769
1620
  // machine's key ride in the part of the address no server receives. Once one browser is paired,
770
1621
  // this machine answers Harness's calls for the person's board, sessions and runs only sealed.
771
1622
  function openUrl(url) {
772
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
773
- 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
+ }
774
1637
  }
775
1638
  async function browserLink() {
776
- const response = await daemonRequest({ command: "phone.offer", kind: "browser" });
777
- if (!response.ok) throw new Error(response.error || "the daemon could not make a link");
778
- 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
+ };
779
1649
  }
780
1650
  async function link() {
781
1651
  const { url, opened } = await browserLink();
782
- 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}`);
783
- 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
+ );
784
1660
  }
785
1661
 
786
1662
  // Approving Harness in T3 Code (lib/t3.mjs): then what you send your T3 Code sessions from Harness, in the desktop app or
787
1663
  // on your phone, goes straight into their threads. No T3 account; T3's page asks for a pairing code from its settings.
788
- 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)."
789
- : access.state === "send" ? `Harness sends your messages into T3 Code's threads, until ${access.until.slice(0, 10)}.`
790
- : 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"
791
- : "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";
792
1672
  async function t3() {
793
1673
  const sub = argv[0] || "connect";
794
- if (sub === "status") { console.log(t3Words((await daemonRequest({ command: "t3.status" })).t3)); return; }
795
- 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; }
796
- 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
+ );
797
1689
  const before = (await daemonRequest({ command: "t3.status" })).t3;
798
1690
  const started = await daemonRequest({ command: "t3.connect" }, 20_000);
799
- 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");
800
1693
  const opened = !has("--print") && openUrl(started.url);
801
- 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`);
802
- 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.");
803
- 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
+ );
804
1705
  for (;;) {
805
1706
  await sleep(1_000);
806
1707
  const now = (await daemonRequest({ command: "t3.status" })).t3;
807
- 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
+ }
808
1712
  }
809
1713
  }
810
1714
 
811
1715
  function help() {
812
- 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 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`);
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
+ );
813
1719
  }
814
1720
 
815
1721
  try {
816
- 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();
817
1727
  else if (command === "status" || command === "bridge") await status();
818
1728
  else if (command === "doctor") await doctor();
819
1729
  else if (command === "local") await local();
820
1730
  else if (command === "watch") await watch();
821
- else if (command === "sync" || command === "backfill" || command === "drain") await sync();
1731
+ else if (command === "sync" || command === "backfill" || command === "drain")
1732
+ await sync();
822
1733
  else if (command === "task") await task();
823
1734
  else if (command === "board") await board();
824
1735
  else if (command === "goal") await goal();
@@ -832,11 +1743,16 @@ try {
832
1743
  else if (command === "t3") await t3();
833
1744
  else if (command === "peers") await peers();
834
1745
  else if (command === "connect") await connectAgent();
835
- else if (command === "desktop") { const shown = await desktop(); console.log(shown.text); if (!shown.ok) process.exitCode = 1; }
836
- 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();
837
1751
  else if (command === "disconnect") await disconnect();
838
- else if (command === "daemon") { const { startDaemon } = await import("./lib/daemon.mjs"); await startDaemon(); }
839
- 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);
840
1756
  else if (["help", "--help", "-h"].includes(command)) help();
841
1757
  else throw new Error(`unknown command '${command}' (run: tryharness help)`);
842
1758
  } catch (error) {