tryharness 0.7.25 → 0.7.28

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/cli.mjs CHANGED
@@ -1247,13 +1247,51 @@ async function swarm() {
1247
1247
  : `$${(r.estimated_micro_usd / 1e6).toFixed(4)} est.`;
1248
1248
  const progress = `${r.progress.runsFinished} runs done, ${r.progress.newResults} new, ${r.progress.roundsWithoutProgress} rounds idle`;
1249
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}` : ""}`
1250
+ `${r.at} ${r.goal_id.slice(0, 8)} ${r.coordinator.padEnd(11)} ${(r.action ?? "error").padEnd(10)} ${(r.agent ?? "").padEnd(11)} ${(r.loop ?? "").padEnd(13)} ${progress} ${r.elapsed_ms}ms ${cost}${r.error ? ` ${r.action ? "after " : ""}${r.error}` : ""}`
1251
1251
  );
1252
1252
  }
1253
1253
  return;
1254
1254
  }
1255
+ // What a thread may spend of the person's Harness credits: dollars, or off.
1256
+ if (sub === "ceiling") {
1257
+ if (!argv[1] || argv[2] === undefined)
1258
+ throw new Error("usage: tryharness swarm ceiling <thread> <dollars|off>");
1259
+ const response = await daemonRequest({
1260
+ command: "board.swarm.ceiling",
1261
+ agent: "user",
1262
+ id: argv[1],
1263
+ usd: argv[2],
1264
+ });
1265
+ if (!response.ok)
1266
+ throw new Error(response.error || "the limit was not set");
1267
+ console.log(
1268
+ response.thread.ceilingUsd === null
1269
+ ? "This thread has no spending limit of its own."
1270
+ : `This thread may spend $${response.thread.ceilingUsd.toFixed(2)} of your Harness credits.`
1271
+ );
1272
+ return;
1273
+ }
1274
+ // How the coordinator held up, a day at a time: who decided each round, and how many no one could.
1275
+ if (sub === "health") {
1276
+ const response = await daemonRequest({
1277
+ command: "board.swarm.health",
1278
+ agent: "user",
1279
+ ...(argv[1] ? { id: argv[1] } : {}),
1280
+ days: Number(arg("--days", "14")),
1281
+ });
1282
+ if (!response.ok) throw new Error(response.error || "health unavailable");
1283
+ if (!response.days.length) {
1284
+ console.log("No rounds yet.");
1285
+ return;
1286
+ }
1287
+ for (const d of response.days)
1288
+ console.log(
1289
+ `${d.day} ${String(d.rounds).padStart(4)} rounds: Harness ${d.harness}, backup ${d.backup}, this computer ${d.local}, undecided ${d.undecided}`
1290
+ );
1291
+ return;
1292
+ }
1255
1293
  throw new Error(
1256
- "usage: tryharness swarm [rounds [<thread>] [--limit n] | coordinator [harness|local]]"
1294
+ "usage: tryharness swarm [rounds [<thread>] [--limit n] | health [<thread>] [--days n] | ceiling <thread> <dollars|off> | coordinator [harness|local]]"
1257
1295
  );
1258
1296
  }
1259
1297
 
@@ -1714,7 +1752,7 @@ async function t3() {
1714
1752
 
1715
1753
  function help() {
1716
1754
  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`
1755
+ `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 health [<thread>] who decided each day's rounds: Harness, a backup, or no one\n tryharness swarm ceiling <thread> <dollars|off> what a thread may spend of your Harness credits\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
1756
  );
1719
1757
  }
1720
1758
 
package/lib/asks.mjs ADDED
@@ -0,0 +1,260 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { nameOf } from "./members.mjs";
3
+
4
+ // An ask in a thread for an agent that is not started on this computer (Harness, Grok Bot, Muse, Bankr, Quotient,
5
+ // OpenClaw, Hermes) becomes a task in the thread, in the asker's name. No ask is dropped without a word: each is asked
6
+ // at once, or kept on the board (so a restart keeps it) with why said in the thread and to its asker, and asked when
7
+ // that clears: the agent's earlier ask in the thread ends (one at a time, so the agent reads its own answer first),
8
+ // the thread is resumed, or this computer reaches Harness again. One that cannot be asked is said in the thread, wakes
9
+ // the agent that asked with the reason, and reaches the coordinator's round as a failed job.
10
+ //
11
+ // Agents on Harness's server (Bankr, Quotient, Muse) keep their tasks there and take asks side by side. A send that may
12
+ // have reached Harness is looked up by its id before it goes again, so a retry never makes a second task.
13
+
14
+ const OPEN_RUN = ["open", "claimed", "running", "needs_input"];
15
+ // Sends lost on the way to Harness before the ask is given up and said as failed.
16
+ const TRIES = 5;
17
+
18
+ export function createAsks({
19
+ store,
20
+ deliver,
21
+ find,
22
+ connected,
23
+ onServer,
24
+ earlierWaits,
25
+ say,
26
+ }) {
27
+ const read = (key) => {
28
+ try {
29
+ return JSON.parse(store.meta(key) || "[]");
30
+ } catch {
31
+ return [];
32
+ }
33
+ };
34
+ const kept = (threadId) => read(`asks:${threadId}`);
35
+ const keep = (threadId, asks) =>
36
+ store.meta(`asks:${threadId}`, JSON.stringify(asks));
37
+ const change = (threadId, id, fields) =>
38
+ keep(
39
+ threadId,
40
+ kept(threadId).map((each) =>
41
+ each.id === id ? { ...each, ...fields } : each
42
+ )
43
+ );
44
+ const forget = (threadId, id) =>
45
+ keep(
46
+ threadId,
47
+ kept(threadId).filter((each) => each.id !== id)
48
+ );
49
+ const failures = (threadId) => read(`asks-failed:${threadId}`);
50
+ const sending = new Set();
51
+ // The ask being made right now: its asker hears what happens to it in its own call, so it is not woken for it.
52
+ let asking = null;
53
+
54
+ const busy = (threadId, agent) =>
55
+ store
56
+ .threadRuns(threadId)
57
+ .some(
58
+ (run) => run.target_agent === agent && OPEN_RUN.includes(run.status)
59
+ );
60
+ const objectiveOf = (thread, ask) =>
61
+ `Asked in "${thread.objective}": ${ask.reason}`.slice(0, 500);
62
+
63
+ // The ask as the thread names it, and as its asker is told.
64
+ const whose = (ask, toAsker) =>
65
+ toAsker || ask.asker === "user" ? "your ask" : `${nameOf(ask.asker)}'s ask`;
66
+ const WHY = {
67
+ turn: (thread, ask, toAsker) =>
68
+ [
69
+ `${nameOf(ask.agent)} has an earlier ask open in this thread; ${whose(ask, toAsker)} goes to it when that one ends.`,
70
+ earlierWaits(thread.id, ask.agent),
71
+ ]
72
+ .filter(Boolean)
73
+ .join(" "),
74
+ paused: (thread, ask, toAsker) =>
75
+ `This thread is paused; ${whose(ask, toAsker)} goes to ${nameOf(ask.agent)} when it is resumed.`,
76
+ offline: (thread, ask, toAsker) =>
77
+ `${nameOf(ask.agent)} runs on Harness's server, and this computer is not connected to it right now; ${whose(ask, toAsker)} goes when it reconnects.`,
78
+ lost: (thread, ask, toAsker) => {
79
+ const text = `${whose(ask, toAsker)} to ${nameOf(ask.agent)} may not have reached Harness (${ask.lost}); it is sent again within a minute or when this computer reconnects.`;
80
+ return text[0].toUpperCase() + text.slice(1);
81
+ },
82
+ };
83
+ // The thread hears why an ask waits once for each reason, however often the reason comes back. Offline after a send
84
+ // that may not have reached Harness is said already: it goes again on reconnect.
85
+ const said = (thread, snapshot, why) => {
86
+ const ask =
87
+ kept(thread.id).find((each) => each.id === snapshot.id) ?? snapshot;
88
+ const before = ask.said ?? [];
89
+ if (ask.told !== why) change(thread.id, ask.id, { told: why });
90
+ if (before.includes(why) || (why === "offline" && ask.lost)) return;
91
+ change(thread.id, ask.id, { said: [...before, why] });
92
+ say(thread, WHY[why](thread, ask, false));
93
+ };
94
+
95
+ const fail = (thread, ask, why) => {
96
+ const reason = String(why || "the board refused it").replace(/\.$/, "");
97
+ store.meta(
98
+ `asks-failed:${thread.id}`,
99
+ JSON.stringify(
100
+ [
101
+ ...failures(thread.id),
102
+ {
103
+ agent: ask.agent,
104
+ objective: objectiveOf(thread, ask),
105
+ result: `Could not be asked: ${reason}`.slice(0, 4_000),
106
+ },
107
+ ].slice(-5)
108
+ )
109
+ );
110
+ say(thread, `${nameOf(ask.agent)} could not be asked: ${reason}.`, {
111
+ wake: ask.asker === "user" || ask.id === asking ? null : ask.asker,
112
+ });
113
+ return `${nameOf(ask.agent)} could not be asked: ${reason}.`;
114
+ };
115
+
116
+ // Why an ask is not sent now, or null to send it.
117
+ const waitOf = (thread, ask, ahead) => {
118
+ if (thread.status === "paused") return "paused";
119
+ if (onServer(ask.agent)) return connected() ? null : "offline";
120
+ return ahead || busy(thread.id, ask.agent) ? "turn" : null;
121
+ };
122
+
123
+ // Returns why the ask could not be asked, when that is known at once.
124
+ const send = (thread, ask) => {
125
+ if (!onServer(ask.agent)) {
126
+ forget(thread.id, ask.id);
127
+ const sent = deliver(thread, ask);
128
+ if (sent?.pending)
129
+ sent.pending.catch((error) => fail(thread, ask, error?.message));
130
+ else if (!sent?.ok) return fail(thread, ask, sent?.error);
131
+ return null;
132
+ }
133
+ sending.add(ask.id);
134
+ void sendToServer(thread, ask).finally(() => sending.delete(ask.id));
135
+ return null;
136
+ };
137
+ const sendToServer = async (thread, ask) => {
138
+ try {
139
+ if (ask.sent && (await find(ask.id))) return forget(thread.id, ask.id);
140
+ change(thread.id, ask.id, { sent: true });
141
+ const sent = deliver(thread, { ...ask, resend: Boolean(ask.sent) });
142
+ if (sent?.pending) await sent.pending;
143
+ else if (!sent?.ok)
144
+ throw Object.assign(new Error(sent?.error), { refused: connected() });
145
+ forget(thread.id, ask.id);
146
+ } catch (error) {
147
+ const tries = (ask.tries ?? 0) + 1;
148
+ if (error?.refused || tries >= TRIES) {
149
+ forget(thread.id, ask.id);
150
+ return fail(thread, ask, error?.message);
151
+ }
152
+ const lost = String(error?.message || "no answer").replace(/\.$/, "");
153
+ change(thread.id, ask.id, { tries, lost });
154
+ said(thread, ask, "lost");
155
+ }
156
+ };
157
+
158
+ // Sends what can go now of the asks kept for one agent in a thread, oldest first, and says why the rest wait. Returns why
159
+ // `fresh` could not be asked, when it failed here.
160
+ const pump = (threadId, agent, fresh = null) => {
161
+ const thread = store.getTask(threadId);
162
+ if (!thread || thread.kind !== "goal") return null;
163
+ let refused = null;
164
+ let ahead = false;
165
+ for (const ask of kept(threadId).filter((each) => each.agent === agent)) {
166
+ if (sending.has(ask.id)) {
167
+ ahead = true;
168
+ continue;
169
+ }
170
+ if (!["open", "paused"].includes(thread.status)) {
171
+ forget(threadId, ask.id);
172
+ const told = fail(thread, ask, `the thread is ${thread.status}`);
173
+ if (ask.id === fresh) refused = told;
174
+ continue;
175
+ }
176
+ const why = waitOf(thread, ask, ahead);
177
+ if (why) {
178
+ ahead = true;
179
+ said(thread, ask, why);
180
+ continue;
181
+ }
182
+ const failed = send(thread, ask);
183
+ if (ask.id === fresh && failed) refused = failed;
184
+ if (kept(threadId).some((each) => each.id === ask.id)) ahead = true;
185
+ }
186
+ return refused;
187
+ };
188
+ const agentsWaiting = (threadId) => [
189
+ ...new Set(kept(threadId).map((each) => each.agent)),
190
+ ];
191
+
192
+ return {
193
+ // Returns what the asker is told when its ask does not go at once, else null.
194
+ ask(threadId, agent, { reason, asker, words }) {
195
+ const ask = {
196
+ id: randomUUID(),
197
+ agent,
198
+ reason,
199
+ asker,
200
+ words: String(words ?? reason),
201
+ at: new Date().toISOString(),
202
+ };
203
+ keep(threadId, [...kept(threadId), ask]);
204
+ asking = ask.id;
205
+ let refused = null;
206
+ try {
207
+ refused = pump(threadId, agent, ask.id);
208
+ } finally {
209
+ asking = null;
210
+ }
211
+ const waiting = kept(threadId).find((each) => each.id === ask.id);
212
+ const thread = store.getTask(threadId);
213
+ return (
214
+ refused ??
215
+ (waiting?.told && thread
216
+ ? WHY[waiting.told](thread, waiting, true)
217
+ : null)
218
+ );
219
+ },
220
+ // Refused before it became an ask (a coding agent not installed here): said like any ask that could not go.
221
+ refuse(threadId, agent, { reason, asker }, why) {
222
+ const thread = store.getTask(threadId);
223
+ if (!thread) return null;
224
+ const ask = { id: randomUUID(), agent, reason, asker };
225
+ asking = ask.id;
226
+ try {
227
+ return fail(thread, ask, why);
228
+ } finally {
229
+ asking = null;
230
+ }
231
+ },
232
+ // The agent's ask in the thread ended, or the thread changed: what waited for it goes now.
233
+ changed(threadId, agent = null) {
234
+ for (const each of agent ? [agent] : agentsWaiting(threadId))
235
+ pump(threadId, each);
236
+ },
237
+ // This computer reached Harness again, or a minute passed: everything kept goes now if it can.
238
+ retry() {
239
+ for (const thread of store.goals())
240
+ for (const agent of agentsWaiting(thread.id)) pump(thread.id, agent);
241
+ },
242
+ // The person stopped the thread: what waited stops with it.
243
+ stopped(threadId) {
244
+ keep(threadId, []);
245
+ },
246
+ // Asks kept for the agent, as the coordinator's round lists work that is already the agent's.
247
+ waitingFor(threadId, agent) {
248
+ const thread = store.getTask(threadId);
249
+ return thread
250
+ ? kept(threadId)
251
+ .filter((each) => each.agent === agent)
252
+ .map((each) => ({ objective: objectiveOf(thread, each) }))
253
+ : [];
254
+ },
255
+ // Asks that could not be asked, as the round's failed jobs.
256
+ failed(threadId) {
257
+ return failures(threadId);
258
+ },
259
+ };
260
+ }
package/lib/cards.mjs CHANGED
@@ -214,7 +214,8 @@ export function cardLine(card, now = Date.now()) {
214
214
  parts.push(
215
215
  `Its own sessions show its changes tested after the last edit ${checks.tested} of ${checks.tested + checks.untested + checks.failing} times${checks.failing ? ` (${checks.failing} with tests failing)` : ""}.`
216
216
  );
217
- else if (member(card.id)?.home === "machine")
217
+ // Said only of an agent with no work at all: one with finished work and no receipts has its count above.
218
+ else if (!done && !failed && member(card.id)?.home === "machine")
218
219
  parts.push("Has finished no work on this board yet.");
219
220
  if (card.working || card.waiting)
220
221
  parts.push(`Now: ${card.working} running, ${card.waiting} waiting for it.`);