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