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/lib/onboard.mjs CHANGED
@@ -1,6 +1,11 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import pc from "picocolors";
3
- import { detectLocalAgents, setupHermes, setupOpenClaw, localWakes } from "./wakes.mjs";
3
+ import {
4
+ detectLocalAgents,
5
+ setupHermes,
6
+ setupOpenClaw,
7
+ localWakes,
8
+ } from "./wakes.mjs";
4
9
  import { columns, createUi, wrap } from "./ui.mjs";
5
10
 
6
11
  // "Your swarm" in setup: every agent class with how Harness reaches it, the recommended way already chosen, and a real
@@ -8,13 +13,19 @@ import { columns, createUi, wrap } from "./ui.mjs";
8
13
  // `npx tryharness agents`. Without a terminal to answer in, it only says what is set up and how to finish.
9
14
 
10
15
  const SETUP = { openclaw: setupOpenClaw, hermes: setupHermes };
11
- const LABEL = { openclaw: "OpenClaw", hermes: "Hermes", "grok-bot": "Grok Bot" };
12
- const later = (agent) => `Later: ${pc.cyan(`npx tryharness agents setup ${agent}`)}`;
16
+ const LABEL = {
17
+ openclaw: "OpenClaw",
18
+ hermes: "Hermes",
19
+ "grok-bot": "Grok Bot",
20
+ };
21
+ const later = (agent) =>
22
+ `Later: ${pc.cyan(`npx tryharness agents setup ${agent}`)}`;
13
23
 
14
24
  // The Harness mark (src/main/home/landing/walkthrough/BrandMark.tsx) in box characters: dim corners, the bright one in
15
25
  // Harness teal, the wordmark beside it. Plain when the output is not a terminal or NO_COLOR is set.
16
26
  export function banner(version, { color = pc.isColorSupported } = {}) {
17
- const c = pc.createColors(color), teal = (text) => (color ? `\x1b[38;5;36m${text}\x1b[39m` : text);
27
+ const c = pc.createColors(color),
28
+ teal = (text) => (color ? `\x1b[38;5;36m${text}\x1b[39m` : text);
18
29
  const rows = [
19
30
  [c.dim("┌───"), " ", teal("───┐")],
20
31
  [c.dim("│"), " ", teal("│")],
@@ -26,34 +37,90 @@ export function banner(version, { color = pc.isColorSupported } = {}) {
26
37
  [c.dim("│"), " ", c.dim("│")],
27
38
  [c.dim("└───"), " ", c.dim("───┘")],
28
39
  ];
29
- const side = { 3: c.bold("harness relay"), 4: "your personal swarm", 5: c.dim(`setup ${version}`) };
30
- return rows.map((parts, index) => {
31
- const line = parts.join("");
32
- const width = line.replace(/\x1b\[[0-9;]*m/g, "").length;
33
- return side[index] ? ` ${line}${" ".repeat(23 - width)}${side[index]}` : ` ${line}`.trimEnd();
34
- }).join("\n");
40
+ const side = {
41
+ 3: c.bold("harness relay"),
42
+ 4: "your personal swarm",
43
+ 5: c.dim(`setup ${version}`),
44
+ };
45
+ return rows
46
+ .map((parts, index) => {
47
+ const line = parts.join("");
48
+ const width = line.replace(/\x1b\[[0-9;]*m/g, "").length;
49
+ return side[index]
50
+ ? ` ${line}${" ".repeat(23 - width)}${side[index]}`
51
+ : ` ${line}`.trimEnd();
52
+ })
53
+ .join("\n");
35
54
  }
36
55
 
37
56
  const copy = (text) => {
38
- const tool = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip";
39
- const result = spawnSync(tool, tool === "xclip" ? ["-selection", "clipboard"] : [], { input: text });
57
+ const tool =
58
+ process.platform === "darwin"
59
+ ? "pbcopy"
60
+ : process.platform === "win32"
61
+ ? "clip"
62
+ : "xclip";
63
+ const result = spawnSync(
64
+ tool,
65
+ tool === "xclip" ? ["-selection", "clipboard"] : [],
66
+ { input: text }
67
+ );
40
68
  return result.status === 0;
41
69
  };
42
70
 
43
71
  /** Post a setup check for the agent and wait for its report: the same path real work takes. */
44
- export async function testAgent({ agent, daemonRequest, ui = createUi(), waitMs = 10 * 60_000, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
72
+ export async function testAgent({
73
+ agent,
74
+ daemonRequest,
75
+ ui = createUi(),
76
+ waitMs = 10 * 60_000,
77
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
78
+ }) {
45
79
  const name = LABEL[agent] || agent;
46
- const posted = await daemonRequest({ command: "board.post", agent: "user", objective: "Setup check from Harness: reply ready", brief: "Harness is checking that it can wake you. Claim this task and report it done with the single word: ready. Do nothing else.", targetAgent: agent });
47
- if (!posted?.ok) { ui.warn(`The test task could not be posted: ${posted?.error || "no answer"}`); return false; }
48
- const started = Date.now(), spin = ui.spinner();
80
+ const posted = await daemonRequest({
81
+ command: "board.post",
82
+ agent: "user",
83
+ objective: "Setup check from Harness: reply ready",
84
+ brief:
85
+ "Harness is checking that it can wake you. Claim this task and report it done with the single word: ready. Do nothing else.",
86
+ targetAgent: agent,
87
+ });
88
+ if (!posted?.ok) {
89
+ ui.warn(
90
+ `The test task could not be posted: ${posted?.error || "no answer"}`
91
+ );
92
+ return false;
93
+ }
94
+ const started = Date.now(),
95
+ spin = ui.spinner();
49
96
  spin.start(`Waiting for ${name} to answer a test task`);
50
97
  while (Date.now() - started < waitMs) {
51
98
  await sleep(2_000);
52
- const task = (await daemonRequest({ command: "board.get", agent: "user", id: posted.task.id }).catch(() => null))?.task;
53
- if (task?.status === "done") { spin.stop(`${name} answered in ${Math.round((Date.now() - started) / 1000)}s. Ready.`); return true; }
54
- if (task && ["failed", "needs_input"].includes(task.status)) { spin.stop(`${name} answered, but the task is ${task.status}${task.result ? `: ${task.result}` : ""}`, 2); return false; }
99
+ const task = (
100
+ await daemonRequest({
101
+ command: "board.get",
102
+ agent: "user",
103
+ id: posted.task.id,
104
+ }).catch(() => null)
105
+ )?.task;
106
+ if (task?.status === "done") {
107
+ spin.stop(
108
+ `${name} answered in ${Math.round((Date.now() - started) / 1000)}s. Ready.`
109
+ );
110
+ return true;
111
+ }
112
+ if (task && ["failed", "needs_input"].includes(task.status)) {
113
+ spin.stop(
114
+ `${name} answered, but the task is ${task.status}${task.result ? `: ${task.result}` : ""}`,
115
+ 2
116
+ );
117
+ return false;
118
+ }
55
119
  }
56
- spin.stop(`No answer after ${Math.round(waitMs / 60_000)} minutes. Check that ${name} is running, then: npx tryharness agents test ${agent}`, 2);
120
+ spin.stop(
121
+ `No answer after ${Math.round(waitMs / 60_000)} minutes. Check that ${name} is running, then: npx tryharness agents test ${agent}`,
122
+ 2
123
+ );
57
124
  return false;
58
125
  }
59
126
 
@@ -61,21 +128,50 @@ async function setupLocal({ entry, runtimeRoot, ui, daemonRequest }) {
61
128
  const name = LABEL[entry.agent];
62
129
  const run = async () => {
63
130
  const result = SETUP[entry.agent]({ runtimeRoot });
64
- if (!result.ok) { ui.warn(result.detail); return; }
131
+ if (!result.ok) {
132
+ ui.warn(result.detail);
133
+ return;
134
+ }
65
135
  ui.success(result.detail);
66
136
  await testAgent({ agent: entry.agent, daemonRequest, ui });
67
137
  };
68
138
  if (localWakes()[entry.agent]) {
69
- const health = (await daemonRequest({ command: "wake.local" }).catch(() => null))?.agents?.[entry.agent];
70
- if (!health || health.answering) { ui.success(`${name} is woken on this computer by its own webhook`); return; }
71
- ui.warn(`${name} is not answering wakes (${health.lastFailure}). Check that it is running.`);
72
- if (!ui.interactive || !(await ui.confirm({ message: `Set ${name} up again?`, initialValue: true }))) { ui.info(later(entry.agent)); return; }
139
+ const health = (
140
+ await daemonRequest({ command: "wake.local" }).catch(() => null)
141
+ )?.agents?.[entry.agent];
142
+ if (!health || health.answering) {
143
+ ui.success(`${name} is woken on this computer by its own webhook`);
144
+ return;
145
+ }
146
+ ui.warn(
147
+ `${name} is not answering wakes (${health.lastFailure}). Check that it is running.`
148
+ );
149
+ if (
150
+ !ui.interactive ||
151
+ !(await ui.confirm({
152
+ message: `Set ${name} up again?`,
153
+ initialValue: true,
154
+ }))
155
+ ) {
156
+ ui.info(later(entry.agent));
157
+ return;
158
+ }
73
159
  await run();
74
160
  return;
75
161
  }
76
- if (!ui.interactive) { ui.info(`${name} found. ${later(entry.agent)}`); return; }
77
- ui.step(`${name} found on this computer\n${pc.dim(`Harness adds itself to ${name}'s MCP servers and turns on its webhook with a key made here${entry.agent === "openclaw" ? ", then restarts its gateway" : ""}. Nothing to copy.`)}`);
78
- if (!(await ui.confirm({ message: `Set up ${name} now?`, initialValue: true }))) { ui.info(`Skipped. ${later(entry.agent)}`); return; }
162
+ if (!ui.interactive) {
163
+ ui.info(`${name} found. ${later(entry.agent)}`);
164
+ return;
165
+ }
166
+ ui.step(
167
+ `${name} found on this computer\n${pc.dim(`Harness adds itself to ${name}'s MCP servers and turns on its webhook with a key made here${entry.agent === "openclaw" ? ", then restarts its gateway" : ""}. Nothing to copy.`)}`
168
+ );
169
+ if (
170
+ !(await ui.confirm({ message: `Set up ${name} now?`, initialValue: true }))
171
+ ) {
172
+ ui.info(`Skipped. ${later(entry.agent)}`);
173
+ return;
174
+ }
79
175
  await run();
80
176
  }
81
177
 
@@ -89,64 +185,190 @@ async function waitFor(check, waitMs = 10 * 60_000, everyMs = 3_000) {
89
185
  }
90
186
 
91
187
  async function setupGrok({ ui, daemonRequest, clipboard }) {
92
- const list = () => daemonRequest({ command: "wake.server", input: { action: "list" } });
93
- const listed = await list().catch((error) => ({ ok: false, error: error.message }));
94
- if (!listed.ok) { ui.warn(`Harness could not be reached (${listed.error}). ${later("grok-bot")}`); return; }
95
- const current = (listed.wakes || []).find((wake) => wake.agent === "grok-bot");
188
+ const list = () =>
189
+ daemonRequest({ command: "wake.server", input: { action: "list" } });
190
+ const listed = await list().catch((error) => ({
191
+ ok: false,
192
+ error: error.message,
193
+ }));
194
+ if (!listed.ok) {
195
+ ui.warn(
196
+ `Harness could not be reached (${listed.error}). ${later("grok-bot")}`
197
+ );
198
+ return;
199
+ }
200
+ const current = (listed.wakes || []).find(
201
+ (wake) => wake.agent === "grok-bot"
202
+ );
96
203
  if (current) {
97
- const how = current.channel === "email" ? `email at ${current.address}` : "its webhook";
98
- if (current.answering) { ui.success(`Grok Bot is woken by ${how}`); return; }
99
- ui.warn(`Grok Bot is not answering wakes by ${how} (${current.last_failure})`);
100
- if (!ui.interactive || !(await ui.confirm({ message: "Set it up again?", initialValue: true }))) { ui.info(later("grok-bot")); return; }
204
+ const how =
205
+ current.channel === "email"
206
+ ? `email at ${current.address}`
207
+ : "its webhook";
208
+ if (current.answering) {
209
+ ui.success(`Grok Bot is woken by ${how}`);
210
+ return;
211
+ }
212
+ ui.warn(
213
+ `Grok Bot is not answering wakes by ${how} (${current.last_failure})`
214
+ );
215
+ if (
216
+ !ui.interactive ||
217
+ !(await ui.confirm({ message: "Set it up again?", initialValue: true }))
218
+ ) {
219
+ ui.info(later("grok-bot"));
220
+ return;
221
+ }
222
+ }
223
+ if (!ui.interactive) {
224
+ ui.info(`Grok Bot is not set up. ${later("grok-bot")}`);
225
+ return;
101
226
  }
102
- if (!ui.interactive) { ui.info(`Grok Bot is not set up. ${later("grok-bot")}`); return; }
103
227
  const channel = await ui.select({
104
228
  message: "How should Harness wake Grok Bot?",
105
229
  initialValue: "email",
106
230
  options: [
107
- { value: "email", label: "Email", hint: "recommended · about 23s · nothing to copy back" },
108
- { value: "webhook", label: "Webhook", hint: "about 17s · you paste its URL and key here" },
231
+ {
232
+ value: "email",
233
+ label: "Email",
234
+ hint: "recommended · about 23s · nothing to copy back",
235
+ },
236
+ {
237
+ value: "webhook",
238
+ label: "Webhook",
239
+ hint: "about 17s · you paste its URL and key here",
240
+ },
109
241
  { value: "skip", label: "Not now" },
110
242
  ],
111
243
  });
112
- if (!channel || channel === "skip") { ui.info(`Skipped. ${later("grok-bot")}`); return; }
244
+ if (!channel || channel === "skip") {
245
+ ui.info(`Skipped. ${later("grok-bot")}`);
246
+ return;
247
+ }
113
248
  const message = listed.messages?.["grok-bot"]?.[channel];
114
- if (!message) { ui.warn("Harness did not send the setup message. Update tryharness and try again."); return; }
115
- const setup = (listed.setup || []).find((entry) => entry.agent === "grok-bot");
249
+ if (!message) {
250
+ ui.warn(
251
+ "Harness did not send the setup message. Update tryharness and try again."
252
+ );
253
+ return;
254
+ }
255
+ const setup = (listed.setup || []).find(
256
+ (entry) => entry.agent === "grok-bot"
257
+ );
116
258
  const connected = Boolean(setup) && !setup.missing?.includes("connection");
117
259
  const copied = clipboard(message);
118
- ui.note(wrap(message, Math.max(40, columns() - 8)).join("\n"), copied ? "Paste this into Grok · it is on your clipboard" : "Paste this into Grok");
119
- if (connected) ui.success(`Grok is already connected to Harness as Grok Bot${setup.old_approval ? `\n${pc.dim("approved before it could register its own address, so it gives you its address instead")}` : ""}`);
120
- else ui.info("Grok opens Harness's approval page. Name the connection Grok Bot and approve it.");
260
+ ui.note(
261
+ wrap(message, Math.max(40, columns() - 8)).join("\n"),
262
+ copied
263
+ ? "Paste this into Grok · it is on your clipboard"
264
+ : "Paste this into Grok"
265
+ );
266
+ if (connected)
267
+ ui.success(
268
+ `Grok is already connected to Harness as Grok Bot${setup.old_approval ? `\n${pc.dim("approved before it could register its own address, so it gives you its address instead")}` : ""}`
269
+ );
270
+ else
271
+ ui.info(
272
+ "Grok opens Harness's approval page. Name the connection Grok Bot and approve it."
273
+ );
121
274
 
122
275
  const save = async (input, done) => {
123
- const saved = await daemonRequest({ command: "wake.server", input: { action: "save", agent: "grok-bot", ...input } });
124
- if (!saved.ok) { ui.warn(`${saved.error}. Try again: npx tryharness agents setup grok-bot`); return false; }
276
+ const saved = await daemonRequest({
277
+ command: "wake.server",
278
+ input: { action: "save", agent: "grok-bot", ...input },
279
+ });
280
+ if (!saved.ok) {
281
+ ui.warn(
282
+ `${saved.error}. Try again: npx tryharness agents setup grok-bot`
283
+ );
284
+ return false;
285
+ }
125
286
  ui.success(done(saved));
126
287
  return true;
127
288
  };
128
289
  if (channel === "webhook") {
129
- ui.info("In Grok, open Routines, then Harness wake, and copy its webhook URL and key.");
130
- const address = await ui.text({ message: "Grok's webhook URL", placeholder: "https://", validate: (value) => (/^https:\/\/\S+$/.test(value.trim()) ? undefined : "Paste the https URL from Grok's routine") });
131
- if (!address) { ui.info(`Skipped. ${later("grok-bot")}`); return; }
132
- const key = await ui.password({ message: "Its key", validate: (value) => (value.trim() ? undefined : "Paste the key from Grok's routine") });
133
- if (!key) { ui.info(`Skipped. ${later("grok-bot")}`); return; }
134
- if (!(await save({ channel: "webhook", address: address.trim(), key }, () => "Saved. The key is stored sealed on Harness and never shown again."))) return;
290
+ ui.info(
291
+ "In Grok, open Routines, then Harness wake, and copy its webhook URL and key."
292
+ );
293
+ const address = await ui.text({
294
+ message: "Grok's webhook URL",
295
+ placeholder: "https://",
296
+ validate: (value) =>
297
+ /^https:\/\/\S+$/.test(value.trim())
298
+ ? undefined
299
+ : "Paste the https URL from Grok's routine",
300
+ });
301
+ if (!address) {
302
+ ui.info(`Skipped. ${later("grok-bot")}`);
303
+ return;
304
+ }
305
+ const key = await ui.password({
306
+ message: "Its key",
307
+ validate: (value) =>
308
+ value.trim() ? undefined : "Paste the key from Grok's routine",
309
+ });
310
+ if (!key) {
311
+ ui.info(`Skipped. ${later("grok-bot")}`);
312
+ return;
313
+ }
314
+ if (
315
+ !(await save(
316
+ { channel: "webhook", address: address.trim(), key },
317
+ () =>
318
+ "Saved. The key is stored sealed on Harness and never shown again."
319
+ ))
320
+ )
321
+ return;
135
322
  } else {
136
- const ownAddress = (value) => (/^[^\s@]+@mail\.grokbot\.com$/i.test(value.trim()) ? undefined : "Grok's address ends in @mail.grokbot.com");
323
+ const ownAddress = (value) =>
324
+ /^[^\s@]+@mail\.grokbot\.com$/i.test(value.trim())
325
+ ? undefined
326
+ : "Grok's address ends in @mail.grokbot.com";
137
327
  let address = "";
138
328
  if (setup?.old_approval) {
139
- address = await ui.text({ message: "Grok's email address", placeholder: "name@mail.grokbot.com", validate: ownAddress }) ?? "";
140
- if (!address) { ui.info(`Skipped. ${later("grok-bot")}`); return; }
329
+ address =
330
+ (await ui.text({
331
+ message: "Grok's email address",
332
+ placeholder: "name@mail.grokbot.com",
333
+ validate: ownAddress,
334
+ })) ?? "";
335
+ if (!address) {
336
+ ui.info(`Skipped. ${later("grok-bot")}`);
337
+ return;
338
+ }
141
339
  } else {
142
340
  // Grok registers its own address; the person may type the one Grok gives instead, and the prompt closes when it registers.
143
341
  let settled = false;
144
- const registered = waitFor(async () => settled || ((await list().catch(() => null))?.wakes || []).some((wake) => wake.agent === "grok-bot"))
145
- .then((ok) => (ok ? { ok, line: "Grok registered its address" } : { ok, line: "Grok has not registered yet. When it has: npx tryharness agents test grok-bot" }));
146
- const answer = await ui.waitOrType({ message: "Waiting for Grok to register its address", placeholder: "or type the address Grok gives you", until: registered });
342
+ const registered = waitFor(
343
+ async () =>
344
+ settled ||
345
+ ((await list().catch(() => null))?.wakes || []).some(
346
+ (wake) => wake.agent === "grok-bot"
347
+ )
348
+ ).then((ok) =>
349
+ ok
350
+ ? { ok, line: "Grok registered its address" }
351
+ : {
352
+ ok,
353
+ line: "Grok has not registered yet. When it has: npx tryharness agents test grok-bot",
354
+ }
355
+ );
356
+ const answer = await ui.waitOrType({
357
+ message: "Waiting for Grok to register its address",
358
+ placeholder: "or type the address Grok gives you",
359
+ until: registered,
360
+ });
147
361
  let outcome = answer.done;
148
- if (answer.cancelled) { settled = true; ui.info(`Stopped waiting. ${later("grok-bot")}`); return; }
149
- if (answer.value && ownAddress(answer.value)) { settled = true; ui.warn(`${ownAddress(answer.value)}. ${later("grok-bot")}`); return; }
362
+ if (answer.cancelled) {
363
+ settled = true;
364
+ ui.info(`Stopped waiting. ${later("grok-bot")}`);
365
+ return;
366
+ }
367
+ if (answer.value && ownAddress(answer.value)) {
368
+ settled = true;
369
+ ui.warn(`${ownAddress(answer.value)}. ${later("grok-bot")}`);
370
+ return;
371
+ }
150
372
  if (answer.value) address = answer.value;
151
373
  else if (!outcome) {
152
374
  const spin = ui.spinner();
@@ -157,25 +379,54 @@ async function setupGrok({ ui, daemonRequest, clipboard }) {
157
379
  settled = true;
158
380
  if (outcome && !outcome.ok) return;
159
381
  }
160
- if (address && !(await save({ channel: "email", address: address.trim() }, (saved) => `Saved. Harness wakes Grok at ${saved.wake?.address ?? address.trim()}`))) return;
382
+ if (
383
+ address &&
384
+ !(await save(
385
+ { channel: "email", address: address.trim() },
386
+ (saved) =>
387
+ `Saved. Harness wakes Grok at ${saved.wake?.address ?? address.trim()}`
388
+ ))
389
+ )
390
+ return;
161
391
  }
162
392
  await testAgent({ agent: "grok-bot", daemonRequest, ui });
163
393
  }
164
394
 
165
395
  /** The whole "Your swarm" step. `only` limits it to one agent (npx tryharness agents setup <agent>). */
166
- export async function setupSwarm({ runtimeRoot, states = [], daemonRequest, ui = createUi(), only = null, clipboard = copy }) {
396
+ export async function setupSwarm({
397
+ runtimeRoot,
398
+ states = [],
399
+ daemonRequest,
400
+ ui = createUi(),
401
+ only = null,
402
+ clipboard = copy,
403
+ }) {
167
404
  const want = (agent) => !only || only === agent;
168
405
  if (!only && states.length) {
169
406
  const width = Math.max(...states.map((state) => state.agent.length));
170
- const rows = states.map((state) => `${!state.installed ? pc.dim("–") : state.configured ? pc.green("✓") : pc.yellow("!")} ${state.agent.padEnd(width)} ${pc.dim(state.detail)}`);
171
- ui.step(`Coding agents on this computer ${pc.dim("· Harness starts them when work is posted")}\n${rows.join("\n")}`);
407
+ const rows = states.map(
408
+ (state) =>
409
+ `${!state.installed ? pc.dim("–") : state.configured ? pc.green("✓") : pc.yellow("!")} ${state.agent.padEnd(width)} ${pc.dim(state.detail)}`
410
+ );
411
+ ui.step(
412
+ `Coding agents on this computer ${pc.dim("· Harness starts them when work is posted")}\n${rows.join("\n")}`
413
+ );
172
414
  }
173
- const local = detectLocalAgents().filter((entry) => entry.found && want(entry.agent));
174
- for (const entry of local) await setupLocal({ entry, runtimeRoot, ui, daemonRequest });
175
- if (only && LABEL[only] && only !== "grok-bot" && !local.length) ui.warn(`${LABEL[only]} was not found on this computer.`);
415
+ const local = detectLocalAgents().filter(
416
+ (entry) => entry.found && want(entry.agent)
417
+ );
418
+ for (const entry of local)
419
+ await setupLocal({ entry, runtimeRoot, ui, daemonRequest });
420
+ if (only && LABEL[only] && only !== "grok-bot" && !local.length)
421
+ ui.warn(`${LABEL[only]} was not found on this computer.`);
176
422
  if (want("grok-bot")) {
177
- ui.step(`Grok Bot ${pc.dim("· runs in its own cloud, woken by email or webhook")}`);
423
+ ui.step(
424
+ `Grok Bot ${pc.dim("· runs in its own cloud, woken by email or webhook")}`
425
+ );
178
426
  await setupGrok({ ui, daemonRequest, clipboard });
179
427
  }
180
- if (!only) ui.info(`Bankr and Quotient run on Harness ${pc.dim("· nothing to set up")}`);
428
+ if (!only)
429
+ ui.info(
430
+ `Bankr and Quotient run on Harness ${pc.dim("· nothing to set up")}`
431
+ );
181
432
  }