tryharness 0.7.23 → 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.
Files changed (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5248 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +382 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
package/lib/nudge.mjs CHANGED
@@ -18,16 +18,50 @@ export function nudgeText(agent, tasks, unread = [], goals = [], swarm = "") {
18
18
  }
19
19
 
20
20
  function queueText(agent, tasks, unread, goals) {
21
- const waiting = (tasks || []).filter((task) => task.status === "open" && (!task.target_agent || task.target_agent === agent));
22
- const held = (tasks || []).filter((task) => task.claimed_by === agent && (task.status === "claimed" || task.status === "needs_input"));
23
- if (!waiting.length && !held.length && !unread.length && !goals.length) return "";
24
- const line = (task) => `- ${task.id}: ${task.objective}${task.origin_agent ? ` (from ${task.trust === "agent" && task.origin_chain?.length > 1 ? task.origin_chain.join(" → ") : task.origin_agent}${task.trust === "agent" ? ", agent-written" : ""})` : ""}${task.status === "needs_input" ? " · your question was answered" : ""}${task.thread_with ? ` · shared with ${task.thread_with}` : ""}`;
21
+ const waiting = (tasks || []).filter(
22
+ (task) =>
23
+ task.status === "open" &&
24
+ (!task.target_agent || task.target_agent === agent)
25
+ );
26
+ const held = (tasks || []).filter(
27
+ (task) =>
28
+ task.claimed_by === agent &&
29
+ (task.status === "claimed" || task.status === "needs_input")
30
+ );
31
+ if (!waiting.length && !held.length && !unread.length && !goals.length)
32
+ return "";
33
+ const line = (task) =>
34
+ `- ${task.id}: ${task.objective}${task.origin_agent ? ` (from ${task.trust === "agent" && task.origin_chain?.length > 1 ? task.origin_chain.join(" → ") : task.origin_agent}${task.trust === "agent" ? ", agent-written" : ""})` : ""}${task.status === "needs_input" ? " · your question was answered" : ""}${task.thread_with ? ` · shared with ${task.thread_with}` : ""}`;
25
35
  const parts = ["Harness task board:"];
26
- if (waiting.length) parts.push(`${waiting.length} task${waiting.length === 1 ? "" : "s"} waiting for you:`, ...waiting.slice(0, 5).map(line));
27
- if (held.length) parts.push(`${held.length} you already claimed:`, ...held.slice(0, 5).map(line));
28
- if (unread.length) parts.push(...unread.slice(0, 5).map((row) => `- ${row.task_id}: ${row.count} new message${row.count === 1 ? "" : "s"} from ${row.sender} on "${row.objective}" (read_thread)`));
29
- if (goals.length) parts.push(`You are on ${goals.length} goal room${goals.length === 1 ? "" : "s"} (read_room to catch up, post_to_room to contribute):`, ...goals.slice(0, 5).map((goal) => `- ${goal.id}: ${goal.objective} · ${goal.posts} posts`));
30
- parts.push("Read one with get_task, claim it with claim_task before working, and report_task when done or stuck.");
36
+ if (waiting.length)
37
+ parts.push(
38
+ `${waiting.length} task${waiting.length === 1 ? "" : "s"} waiting for you:`,
39
+ ...waiting.slice(0, 5).map(line)
40
+ );
41
+ if (held.length)
42
+ parts.push(
43
+ `${held.length} you already claimed:`,
44
+ ...held.slice(0, 5).map(line)
45
+ );
46
+ if (unread.length)
47
+ parts.push(
48
+ ...unread
49
+ .slice(0, 5)
50
+ .map(
51
+ (row) =>
52
+ `- ${row.task_id}: ${row.count} new message${row.count === 1 ? "" : "s"} from ${row.sender} on "${row.objective}" (read_thread)`
53
+ )
54
+ );
55
+ if (goals.length)
56
+ parts.push(
57
+ `You are on ${goals.length} goal room${goals.length === 1 ? "" : "s"} (read_room to catch up, post_to_room to contribute):`,
58
+ ...goals
59
+ .slice(0, 5)
60
+ .map((goal) => `- ${goal.id}: ${goal.objective} · ${goal.posts} posts`)
61
+ );
62
+ parts.push(
63
+ "Read one with get_task, claim it with claim_task before working, and report_task when done or stuck."
64
+ );
31
65
  return parts.join("\n");
32
66
  }
33
67
 
@@ -35,17 +69,35 @@ function queueText(agent, tasks, unread, goals) {
35
69
  export function queueDigest(agent, tasks, unread = [], goals = []) {
36
70
  return [
37
71
  ...(tasks || [])
38
- .filter((task) => (task.status === "open" && (!task.target_agent || task.target_agent === agent)) || (task.claimed_by === agent && ["claimed", "needs_input"].includes(task.status)))
72
+ .filter(
73
+ (task) =>
74
+ (task.status === "open" &&
75
+ (!task.target_agent || task.target_agent === agent)) ||
76
+ (task.claimed_by === agent &&
77
+ ["claimed", "needs_input"].includes(task.status))
78
+ )
39
79
  .map((task) => `${task.id}:${task.status}:${task.version}`),
40
80
  ...unread.map((row) => `${row.task_id}:unread:${row.count}`),
41
81
  ...goals.map((goal) => `${goal.id}:goal:${goal.posts}`),
42
- ].sort().join("|");
82
+ ]
83
+ .sort()
84
+ .join("|");
43
85
  }
44
86
 
45
87
  export async function boardQueue(agent) {
46
88
  try {
47
- const response = await daemonRequest({ command: "board.list", agent, unread: true, swarm: true }, 1_500);
48
- return response?.ok ? { tasks: response.tasks, unread: response.unread || [], goals: response.goals || [], swarm: response.swarm || null } : null;
89
+ const response = await daemonRequest(
90
+ { command: "board.list", agent, unread: true, swarm: true },
91
+ 1_500
92
+ );
93
+ return response?.ok
94
+ ? {
95
+ tasks: response.tasks,
96
+ unread: response.unread || [],
97
+ goals: response.goals || [],
98
+ swarm: response.swarm || null,
99
+ }
100
+ : null;
49
101
  } catch {
50
102
  return null;
51
103
  }
@@ -53,15 +105,40 @@ export async function boardQueue(agent) {
53
105
 
54
106
  // What this agent was last told, in the Harness home: the queue's digest and the swarm's.
55
107
  const toldFile = (agent, homeDir) => path.join(homeDir, `nudge-${agent}.json`);
56
- function lastTold(agent, homeDir) { try { return JSON.parse(fs.readFileSync(toldFile(agent, homeDir), "utf8")); } catch { return {}; } }
57
- function noteTold(agent, homeDir, told) { try { fs.writeFileSync(toldFile(agent, homeDir), JSON.stringify({ ...lastTold(agent, homeDir), ...told, at: new Date().toISOString() }), { mode: 0o600 }); } catch {} }
108
+ function lastTold(agent, homeDir) {
109
+ try {
110
+ return JSON.parse(fs.readFileSync(toldFile(agent, homeDir), "utf8"));
111
+ } catch {
112
+ return {};
113
+ }
114
+ }
115
+ function noteTold(agent, homeDir, told) {
116
+ try {
117
+ fs.writeFileSync(
118
+ toldFile(agent, homeDir),
119
+ JSON.stringify({
120
+ ...lastTold(agent, homeDir),
121
+ ...told,
122
+ at: new Date().toISOString(),
123
+ }),
124
+ { mode: 0o600 }
125
+ );
126
+ } catch {}
127
+ }
58
128
 
59
129
  /** What a session is told when it starts: its queue and the whole swarm. */
60
130
  export async function boardNudge(agent, homeDir = paths.home) {
61
131
  const queue = await boardQueue(agent);
62
132
  if (!queue) return "";
63
- if (queue.swarm?.digest) noteTold(agent, homeDir, { swarm: queue.swarm.digest });
64
- return nudgeText(agent, queue.tasks, queue.unread, queue.goals, queue.swarm?.text || "");
133
+ if (queue.swarm?.digest)
134
+ noteTold(agent, homeDir, { swarm: queue.swarm.digest });
135
+ return nudgeText(
136
+ agent,
137
+ queue.tasks,
138
+ queue.unread,
139
+ queue.goals,
140
+ queue.swarm?.text || ""
141
+ );
65
142
  }
66
143
 
67
144
  // Only what changed since this agent last saw its queue: a hook that runs on every prompt
@@ -76,46 +153,90 @@ export async function boardNudgeIfChanged(agent, homeDir = paths.home) {
76
153
  const queueMoved = digest !== (last.digest ?? null);
77
154
  const swarmMoved = Boolean(swarm?.digest) && swarm.digest !== last.swarm;
78
155
  if (!queueMoved && !swarmMoved) return "";
79
- noteTold(agent, homeDir, { digest, ...(swarm?.digest ? { swarm: swarm.digest } : {}) });
80
- const told = !queueMoved ? "" : digest ? queueText(agent, tasks, unread, goals) : (last.digest ? "Harness task board: nothing waits for you now." : "");
156
+ noteTold(agent, homeDir, {
157
+ digest,
158
+ ...(swarm?.digest ? { swarm: swarm.digest } : {}),
159
+ });
160
+ const told = !queueMoved
161
+ ? ""
162
+ : digest
163
+ ? queueText(agent, tasks, unread, goals)
164
+ : last.digest
165
+ ? "Harness task board: nothing waits for you now."
166
+ : "";
81
167
  return [told, swarmMoved ? swarm.text : ""].filter(Boolean).join("\n\n");
82
168
  }
83
169
 
84
170
  // The prompt hook also tells the daemon the person typed in this session: the agent process is the hook's parent, or
85
171
  // its grandparent when the hook runs through a shell.
86
- export const PS = ["/bin/ps", "/usr/bin/ps"].find((file) => fs.existsSync(file)) || "ps";
172
+ export const PS =
173
+ ["/bin/ps", "/usr/bin/ps"].find((file) => fs.existsSync(file)) || "ps";
87
174
 
88
175
  function agentPids() {
89
176
  const pids = [process.ppid];
90
- try { const up = Number(spawnSync(PS, ["-o", "ppid=", "-p", String(process.ppid)], { encoding: "utf8" }).stdout.trim()); if (up > 1) pids.push(up); } catch {}
177
+ try {
178
+ const up = Number(
179
+ spawnSync(PS, ["-o", "ppid=", "-p", String(process.ppid)], {
180
+ encoding: "utf8",
181
+ }).stdout.trim()
182
+ );
183
+ if (up > 1) pids.push(up);
184
+ } catch {}
91
185
  return pids;
92
186
  }
93
187
 
94
188
  // Where the session runs, as its own environment says (the agent's, which the hook shares): places.mjs reads it.
95
- const placeOf = (input) => ({ sessionId: input?.session_id, folder: input?.cwd || process.cwd(), env: placeEnv(process.env) });
189
+ const placeOf = (input) => ({
190
+ sessionId: input?.session_id,
191
+ folder: input?.cwd || process.cwd(),
192
+ env: placeEnv(process.env),
193
+ });
96
194
 
97
195
  // An extension or plugin running inside the agent passes its own process ids.
98
196
  export async function notePrompt(agent, input = null, pids = agentPids()) {
99
- try { await daemonRequest({ command: "board.prompted", agent, pids, ...(input?.session_id ? placeOf(input) : {}) }, 1_500); } catch {}
197
+ try {
198
+ await daemonRequest(
199
+ {
200
+ command: "board.prompted",
201
+ agent,
202
+ pids,
203
+ ...(input?.session_id ? placeOf(input) : {}),
204
+ },
205
+ 1_500
206
+ );
207
+ } catch {}
100
208
  }
101
209
 
102
210
  /** A session starting says where it runs. */
103
211
  export async function notePlace(agent, input, pids = agentPids()) {
104
212
  if (!input?.session_id) return;
105
- try { await daemonRequest({ command: "board.live.place", agent, pids, ...placeOf(input) }, 1_500); } catch {}
213
+ try {
214
+ await daemonRequest(
215
+ { command: "board.live.place", agent, pids, ...placeOf(input) },
216
+ 1_500
217
+ );
218
+ } catch {}
106
219
  }
107
220
 
108
221
  /** A turn ended with nothing more to do: the session waits for the person, and the buddy can say so. */
109
222
  export async function noteTurn(agent, input, pids = agentPids()) {
110
223
  if (!input?.session_id) return;
111
- try { await daemonRequest({ command: "board.live.turn", agent, pids, ...placeOf(input) }, 1_500); } catch {}
224
+ try {
225
+ await daemonRequest(
226
+ { command: "board.live.turn", agent, pids, ...placeOf(input) },
227
+ 1_500
228
+ );
229
+ } catch {}
112
230
  }
113
231
 
114
232
  // What the person sent this session from Harness (daemon board.live.say), taken once.
115
233
  export async function takeMessages(agent, sessionId) {
116
234
  if (!sessionId) return [];
117
235
  try {
118
- const response = await daemonRequest({ command: "board.live.take", agent, sessionId }, 1_500);
236
+ const response = await daemonRequest(
237
+ { command: "board.live.take", agent, sessionId },
238
+ 1_500
239
+ );
119
240
  return response?.ok ? response.messages || [] : [];
120
241
  } catch {
121
242
  return [];
@@ -123,7 +244,11 @@ export async function takeMessages(agent, sessionId) {
123
244
  }
124
245
 
125
246
  export function messagesText(messages) {
126
- return messages.map((message) => `The person sent you this from Harness:\n\n${message.text}`).join("\n\n");
247
+ return messages
248
+ .map(
249
+ (message) => `The person sent you this from Harness:\n\n${message.text}`
250
+ )
251
+ .join("\n\n");
127
252
  }
128
253
 
129
254
  // The JSON the agent hands every hook on stdin (session_id among it); null when there is none.
@@ -131,10 +256,23 @@ async function hookInput() {
131
256
  if (process.stdin.isTTY) return null;
132
257
  let text = "";
133
258
  process.stdin.setEncoding("utf8");
134
- const read = new Promise((resolve) => { process.stdin.on("data", (chunk) => { text += chunk; }); process.stdin.on("end", resolve); process.stdin.on("error", resolve); });
135
- await Promise.race([read, new Promise((resolve) => setTimeout(resolve, 1_000).unref())]);
259
+ const read = new Promise((resolve) => {
260
+ process.stdin.on("data", (chunk) => {
261
+ text += chunk;
262
+ });
263
+ process.stdin.on("end", resolve);
264
+ process.stdin.on("error", resolve);
265
+ });
266
+ await Promise.race([
267
+ read,
268
+ new Promise((resolve) => setTimeout(resolve, 1_000).unref()),
269
+ ]);
136
270
  process.stdin.destroy();
137
- try { return JSON.parse(text); } catch { return null; }
271
+ try {
272
+ return JSON.parse(text);
273
+ } catch {
274
+ return null;
275
+ }
138
276
  }
139
277
 
140
278
  if (import.meta.url === `file://${process.argv[1]}`) {
@@ -145,13 +283,26 @@ if (import.meta.url === `file://${process.argv[1]}`) {
145
283
  if (process.argv.includes("--stop")) {
146
284
  // The Stop hook: a message waiting for this session keeps the agent going with it.
147
285
  const messages = await takeMessages(agent, sessionId);
148
- if (messages.length) process.stdout.write(JSON.stringify({ decision: "block", reason: messagesText(messages) }) + "\n");
286
+ if (messages.length)
287
+ process.stdout.write(
288
+ JSON.stringify({ decision: "block", reason: messagesText(messages) }) +
289
+ "\n"
290
+ );
149
291
  else await noteTurn(agent, input);
150
292
  } else {
151
293
  if (process.argv.includes("--if-changed")) await notePrompt(agent, input);
152
294
  else await notePlace(agent, input);
153
- const messages = process.argv.includes("--if-changed") ? await takeMessages(agent, sessionId) : [];
154
- const text = [messages.length ? messagesText(messages) : "", process.argv.includes("--if-changed") ? await boardNudgeIfChanged(agent) : await boardNudge(agent)].filter(Boolean).join("\n\n");
295
+ const messages = process.argv.includes("--if-changed")
296
+ ? await takeMessages(agent, sessionId)
297
+ : [];
298
+ const text = [
299
+ messages.length ? messagesText(messages) : "",
300
+ process.argv.includes("--if-changed")
301
+ ? await boardNudgeIfChanged(agent)
302
+ : await boardNudge(agent),
303
+ ]
304
+ .filter(Boolean)
305
+ .join("\n\n");
155
306
  if (text) process.stdout.write(text + "\n");
156
307
  }
157
308
  }