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.
- package/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- 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 {
|
|
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 {
|
|
18
|
-
|
|
19
|
-
|
|
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"), {
|
|
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(
|
|
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("-")
|
|
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) {
|
|
41
|
-
|
|
42
|
-
|
|
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 =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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(
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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)
|
|
131
|
+
if (!response.ok)
|
|
132
|
+
throw new Error(`pairing unavailable (HTTP ${response.status})`);
|
|
79
133
|
const { pairId, code } = await response.json();
|
|
80
|
-
ui.note(
|
|
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, {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
buffer
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
}).then(
|
|
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(
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
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(
|
|
134
|
-
|
|
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 =
|
|
139
|
-
|
|
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({
|
|
251
|
+
const runtimeRoot = installRuntime({
|
|
252
|
+
sourceRoot: SOURCE_ROOT,
|
|
253
|
+
version: VERSION,
|
|
254
|
+
});
|
|
151
255
|
const states = configureAgents({ runtimeRoot, selected });
|
|
152
|
-
const stateMap = Object.fromEntries(
|
|
153
|
-
|
|
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 {
|
|
158
|
-
|
|
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 (
|
|
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(
|
|
165
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 [
|
|
189
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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({}))
|
|
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)
|
|
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 {
|
|
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 {
|
|
222
|
-
|
|
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(
|
|
404
|
+
console.log(
|
|
405
|
+
`service: ${service.installed ? "installed" : "not installed"}`
|
|
406
|
+
);
|
|
227
407
|
process.exitCode = 1;
|
|
228
408
|
return;
|
|
229
409
|
}
|
|
230
|
-
console.log(
|
|
231
|
-
|
|
232
|
-
|
|
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(
|
|
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({
|
|
246
|
-
|
|
247
|
-
|
|
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(
|
|
253
|
-
|
|
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 {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (current
|
|
266
|
-
|
|
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) => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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(
|
|
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(
|
|
557
|
+
console.log(
|
|
558
|
+
`pairing: ${(await validateConnection(config)) ? "valid" : "missing or invalid"}`
|
|
559
|
+
);
|
|
292
560
|
const service = serviceInfo();
|
|
293
|
-
console.log(
|
|
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(
|
|
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())
|
|
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))
|
|
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(
|
|
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)) {
|
|
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(
|
|
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))
|
|
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))
|
|
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)
|
|
336
|
-
|
|
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(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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)
|
|
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) {
|
|
350
|
-
|
|
351
|
-
|
|
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))
|
|
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)
|
|
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(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
console.log(
|
|
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", ""))
|
|
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) =>
|
|
376
|
-
|
|
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)
|
|
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")
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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)
|
|
389
|
-
|
|
390
|
-
|
|
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)
|
|
397
|
-
|
|
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(
|
|
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({
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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")
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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 =
|
|
421
|
-
|
|
422
|
-
|
|
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(
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
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 =
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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") {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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({
|
|
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(
|
|
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({
|
|
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 (
|
|
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) {
|
|
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) {
|
|
492
|
-
|
|
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({
|
|
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(
|
|
500
|
-
|
|
501
|
-
|
|
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)
|
|
504
|
-
|
|
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(
|
|
507
|
-
|
|
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(
|
|
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) =>
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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(
|
|
522
|
-
|
|
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")
|
|
526
|
-
|
|
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) {
|
|
529
|
-
|
|
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) => {
|
|
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({
|
|
541
|
-
|
|
542
|
-
|
|
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(
|
|
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({
|
|
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) {
|
|
552
|
-
|
|
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({
|
|
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,
|
|
559
|
-
|
|
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 || [])
|
|
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(
|
|
566
|
-
|
|
567
|
-
|
|
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({
|
|
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(
|
|
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(
|
|
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")
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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({
|
|
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) {
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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", ""))
|
|
609
|
-
|
|
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(
|
|
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({
|
|
615
|
-
|
|
616
|
-
|
|
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))
|
|
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({
|
|
1315
|
+
const response = await daemonRequest({
|
|
1316
|
+
command: "tasks.get",
|
|
1317
|
+
runId,
|
|
1318
|
+
offset,
|
|
1319
|
+
});
|
|
629
1320
|
if (!response.ok) {
|
|
630
|
-
if (follow && misses < 10) {
|
|
631
|
-
|
|
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(
|
|
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)
|
|
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(
|
|
1348
|
+
console.log(
|
|
1349
|
+
`open live session: ${resumeCommand(current.agent, current.sessionId, current.cwd)}`
|
|
1350
|
+
);
|
|
647
1351
|
}
|
|
648
|
-
const terminal =
|
|
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")
|
|
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)) {
|
|
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(
|
|
664
|
-
|
|
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 {
|
|
1390
|
+
try {
|
|
1391
|
+
await daemonRequest({ command: "shutdown" }, 2_000);
|
|
1392
|
+
} catch {}
|
|
671
1393
|
removeAgentConfigurations();
|
|
672
1394
|
removeLocalWakes();
|
|
673
|
-
try {
|
|
674
|
-
|
|
675
|
-
|
|
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) {
|
|
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({
|
|
687
|
-
|
|
688
|
-
|
|
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
|
|
693
|
-
|
|
694
|
-
|
|
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(
|
|
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({
|
|
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
|
|
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)
|
|
707
|
-
|
|
708
|
-
|
|
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(
|
|
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)
|
|
716
|
-
|
|
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(
|
|
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
|
|
723
|
-
|
|
724
|
-
|
|
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(
|
|
1533
|
+
console.log(
|
|
1534
|
+
`Auto picks by: ${response.routing ?? `${response.defaultRouting} (the default)`}`
|
|
1535
|
+
);
|
|
727
1536
|
return;
|
|
728
1537
|
}
|
|
729
|
-
const ui = createUi(),
|
|
730
|
-
|
|
731
|
-
|
|
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)
|
|
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(
|
|
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"))
|
|
744
|
-
|
|
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)
|
|
750
|
-
|
|
751
|
-
|
|
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) =>
|
|
759
|
-
|
|
760
|
-
|
|
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(
|
|
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(
|
|
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] =
|
|
773
|
-
|
|
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({
|
|
777
|
-
|
|
778
|
-
|
|
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(
|
|
783
|
-
|
|
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) =>
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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") {
|
|
795
|
-
|
|
796
|
-
|
|
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)
|
|
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(
|
|
802
|
-
|
|
803
|
-
|
|
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)) {
|
|
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(
|
|
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 (
|
|
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")
|
|
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") {
|
|
836
|
-
|
|
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") {
|
|
839
|
-
|
|
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) {
|