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.
- package/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5248 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +382 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- package/package.json +3 -3
package/lib/network.mjs
CHANGED
|
@@ -7,14 +7,25 @@
|
|
|
7
7
|
const SEEN = (id) => `relay_seen:${id}`;
|
|
8
8
|
const AUTO_SEND_DEFAULT = "relay_auto_send_default";
|
|
9
9
|
const DECISIONS = new Set(["accept", "decline", "block"]);
|
|
10
|
-
const handleOf = (identity) =>
|
|
10
|
+
const handleOf = (identity) =>
|
|
11
|
+
String(identity || "")
|
|
12
|
+
.split("/")[0]
|
|
13
|
+
.toLowerCase();
|
|
11
14
|
const agentOf = (identity) => String(identity || "").split("/")[1] || "";
|
|
12
15
|
|
|
13
|
-
export function createNetwork({
|
|
16
|
+
export function createNetwork({
|
|
17
|
+
store,
|
|
18
|
+
askHarness,
|
|
19
|
+
relayRequest,
|
|
20
|
+
openRelayRequest,
|
|
21
|
+
fingerprint,
|
|
22
|
+
myKey,
|
|
23
|
+
}) {
|
|
14
24
|
// Harness answers for the server's half; a refusal comes back as a plain error the person can read.
|
|
15
25
|
const harness = async (input) => {
|
|
16
26
|
const answer = await askHarness("relay_network", input);
|
|
17
|
-
if (!answer.ok)
|
|
27
|
+
if (!answer.ok)
|
|
28
|
+
throw new Error(answer.error || "Harness refused the request");
|
|
18
29
|
const { ok: _ok, ...rest } = answer;
|
|
19
30
|
return rest;
|
|
20
31
|
};
|
|
@@ -23,7 +34,9 @@ export function createNetwork({ store, askHarness, relayRequest, openRelayReques
|
|
|
23
34
|
// a message from a person), nothing (open), the other side (a request not answered yet), or nobody (ended).
|
|
24
35
|
const conversations = async () => {
|
|
25
36
|
const server = await harness({ op: "threads" }).catch(() => null);
|
|
26
|
-
const local = new Map(
|
|
37
|
+
const local = new Map(
|
|
38
|
+
store.relayThreads().map((row) => [row.task_id, row])
|
|
39
|
+
);
|
|
27
40
|
const items = [];
|
|
28
41
|
for (const thread of server?.threads ?? []) {
|
|
29
42
|
const here = local.get(thread.id) || null;
|
|
@@ -33,45 +46,150 @@ export function createNetwork({ store, askHarness, relayRequest, openRelayReques
|
|
|
33
46
|
let keyPrint = null;
|
|
34
47
|
// A sealed request's subject: opened on this computer, with the other computer's key to compare.
|
|
35
48
|
if (thread.sealed && !about) {
|
|
36
|
-
const opened = await openRelayRequest({
|
|
37
|
-
|
|
49
|
+
const opened = await openRelayRequest({
|
|
50
|
+
sealed: thread.sealed,
|
|
51
|
+
from: `${peer.handle}/${peer.agent}`,
|
|
52
|
+
}).catch(() => null);
|
|
53
|
+
if (opened) {
|
|
54
|
+
about = opened.about;
|
|
55
|
+
keyPrint = opened.fingerprint;
|
|
56
|
+
}
|
|
38
57
|
}
|
|
39
|
-
items.push(
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
items.push(
|
|
59
|
+
item({
|
|
60
|
+
id: thread.id,
|
|
61
|
+
role: thread.role,
|
|
62
|
+
kind:
|
|
63
|
+
thread.kind ||
|
|
64
|
+
(thread.target?.agent === "person" ? "person" : "agent"),
|
|
65
|
+
status: thread.status,
|
|
66
|
+
about,
|
|
67
|
+
peer: { handle: peer.handle, agent: peer.agent },
|
|
68
|
+
mine:
|
|
69
|
+
thread.role === "asker" ? thread.asker.agent : thread.target.agent,
|
|
70
|
+
askerSince: thread.role === "target" ? thread.asker.since : null,
|
|
71
|
+
sealed: Boolean(thread.sealed),
|
|
72
|
+
fingerprint: keyPrint,
|
|
73
|
+
createdAt: thread.createdAt,
|
|
74
|
+
here,
|
|
75
|
+
})
|
|
76
|
+
);
|
|
42
77
|
}
|
|
43
78
|
// Threads this computer holds that Harness no longer lists (older ones, or Harness out of reach).
|
|
44
79
|
for (const here of local.values()) {
|
|
45
|
-
items.push(
|
|
46
|
-
|
|
47
|
-
|
|
80
|
+
items.push(
|
|
81
|
+
item({
|
|
82
|
+
id: here.task_id,
|
|
83
|
+
role: here.role,
|
|
84
|
+
kind: agentOf(here.with_agent) === "person" ? "person" : "agent",
|
|
85
|
+
status: here.closed_at ? "closed" : "accepted",
|
|
86
|
+
about: here.objective,
|
|
87
|
+
peer: {
|
|
88
|
+
handle: handleOf(here.with_agent),
|
|
89
|
+
agent: agentOf(here.with_agent),
|
|
90
|
+
},
|
|
91
|
+
mine: here.target_agent === "user" ? "person" : here.target_agent,
|
|
92
|
+
askerSince: null,
|
|
93
|
+
sealed: false,
|
|
94
|
+
fingerprint: null,
|
|
95
|
+
createdAt: here.opened_at,
|
|
96
|
+
here,
|
|
97
|
+
})
|
|
98
|
+
);
|
|
48
99
|
}
|
|
49
|
-
items.sort((a, b) =>
|
|
50
|
-
|
|
100
|
+
items.sort((a, b) =>
|
|
101
|
+
String(b.updatedAt).localeCompare(String(a.updatedAt))
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
connected: Boolean(server),
|
|
106
|
+
items,
|
|
107
|
+
needsYou: items.filter((entry) => entry.group === "needs_you").length,
|
|
108
|
+
};
|
|
51
109
|
};
|
|
52
110
|
|
|
53
|
-
const item = ({
|
|
111
|
+
const item = ({
|
|
112
|
+
id,
|
|
113
|
+
role,
|
|
114
|
+
kind,
|
|
115
|
+
status,
|
|
116
|
+
about,
|
|
117
|
+
peer,
|
|
118
|
+
mine,
|
|
119
|
+
askerSince,
|
|
120
|
+
sealed,
|
|
121
|
+
fingerprint: keyPrint,
|
|
122
|
+
createdAt,
|
|
123
|
+
here,
|
|
124
|
+
}) => {
|
|
54
125
|
const seen = Number(store.meta(SEEN(id)) || 0);
|
|
55
|
-
const thread = here
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
126
|
+
const thread = here
|
|
127
|
+
? {
|
|
128
|
+
open: !here.closed_at,
|
|
129
|
+
autoSend: here.auto_send,
|
|
130
|
+
held: here.held,
|
|
131
|
+
unread: here.last_in_seq > seen,
|
|
132
|
+
messages: here.messages,
|
|
133
|
+
outcome: here.outcome,
|
|
134
|
+
last: here.last
|
|
135
|
+
? {
|
|
136
|
+
direction: here.last.direction,
|
|
137
|
+
sender: here.last.sender,
|
|
138
|
+
text: String(here.last.text || "").slice(0, 160),
|
|
139
|
+
state: here.last.state,
|
|
140
|
+
at: here.last.created_at,
|
|
141
|
+
}
|
|
142
|
+
: null,
|
|
143
|
+
}
|
|
62
144
|
: null;
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
145
|
+
const reason =
|
|
146
|
+
role === "target" && status === "requested"
|
|
147
|
+
? "request"
|
|
148
|
+
: thread?.open && thread.held
|
|
149
|
+
? "held"
|
|
150
|
+
: thread?.open && kind === "person" && thread.unread
|
|
151
|
+
? "message"
|
|
152
|
+
: null;
|
|
153
|
+
const group = reason
|
|
154
|
+
? "needs_you"
|
|
155
|
+
: thread
|
|
156
|
+
? thread.open
|
|
157
|
+
? "open"
|
|
158
|
+
: "ended"
|
|
159
|
+
: status === "requested"
|
|
160
|
+
? "waiting"
|
|
161
|
+
: status === "accepted" || status === "accepted_pending"
|
|
162
|
+
? "open"
|
|
163
|
+
: "ended";
|
|
164
|
+
return {
|
|
165
|
+
id,
|
|
166
|
+
role,
|
|
167
|
+
kind,
|
|
168
|
+
status,
|
|
169
|
+
about,
|
|
170
|
+
peer,
|
|
171
|
+
mine,
|
|
172
|
+
askerSince,
|
|
173
|
+
sealed,
|
|
174
|
+
fingerprint: keyPrint,
|
|
175
|
+
group,
|
|
176
|
+
reason,
|
|
177
|
+
thread,
|
|
178
|
+
createdAt,
|
|
179
|
+
updatedAt: here?.updated_at || createdAt,
|
|
180
|
+
};
|
|
69
181
|
};
|
|
70
182
|
|
|
71
183
|
// A refusal from Harness or the other computer comes back as the answer, in its own words.
|
|
72
|
-
const answered = (promise) => ({
|
|
184
|
+
const answered = (promise) => ({
|
|
185
|
+
pending: promise.catch((error) => ({ ok: false, error: error.message })),
|
|
186
|
+
});
|
|
73
187
|
const command = (request, agent) => {
|
|
74
|
-
if (agent !== "user")
|
|
188
|
+
if (agent !== "user")
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
error: "only the person talks to other swarms from here",
|
|
192
|
+
};
|
|
75
193
|
const id = String(request.id || "");
|
|
76
194
|
switch (request.command) {
|
|
77
195
|
case "board.relay.list":
|
|
@@ -79,40 +197,81 @@ export function createNetwork({ store, askHarness, relayRequest, openRelayReques
|
|
|
79
197
|
// The server's half of Swarms (card, search, contacts, blocks): what the dashboard asks Harness directly.
|
|
80
198
|
case "board.relay.network": {
|
|
81
199
|
const { command: _command, agent: _agent, ...input } = request;
|
|
82
|
-
if (input.op === "decide" || input.op === "threads")
|
|
83
|
-
|
|
200
|
+
if (input.op === "decide" || input.op === "threads")
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: "use board.relay.decide and board.relay.list",
|
|
204
|
+
};
|
|
205
|
+
return answered(
|
|
206
|
+
harness(input).then((answer) => ({ ok: true, ...answer }))
|
|
207
|
+
);
|
|
84
208
|
}
|
|
85
209
|
case "board.relay.decide":
|
|
86
|
-
if (!DECISIONS.has(String(request.decision)))
|
|
87
|
-
|
|
210
|
+
if (!DECISIONS.has(String(request.decision)))
|
|
211
|
+
return { ok: false, error: "decision is accept, decline or block" };
|
|
212
|
+
return answered(
|
|
213
|
+
harness({
|
|
214
|
+
op: "decide",
|
|
215
|
+
id,
|
|
216
|
+
decision: String(request.decision),
|
|
217
|
+
}).then((answer) => ({ ok: true, ...answer }))
|
|
218
|
+
);
|
|
88
219
|
// A request from this computer: to one of their listed agents from one of the person's, or to the person themselves.
|
|
89
220
|
case "board.relay.request": {
|
|
90
|
-
const handle = String(request.handle || "")
|
|
221
|
+
const handle = String(request.handle || "")
|
|
222
|
+
.toLowerCase()
|
|
223
|
+
.replace(/^@/, "");
|
|
91
224
|
const toAgent = String(request.toAgent || "person");
|
|
92
|
-
const fromAgent =
|
|
93
|
-
|
|
225
|
+
const fromAgent =
|
|
226
|
+
toAgent === "person" ? "person" : String(request.fromAgent || "");
|
|
227
|
+
return answered(
|
|
228
|
+
relayRequest({
|
|
229
|
+
to: `${handle}/${toAgent}`,
|
|
230
|
+
fromAgent,
|
|
231
|
+
about: request.about,
|
|
232
|
+
brief: request.brief,
|
|
233
|
+
refs: request.refs,
|
|
234
|
+
}).then((made) => ({ ok: true, ...made }))
|
|
235
|
+
);
|
|
94
236
|
}
|
|
95
237
|
case "board.relay.seen": {
|
|
96
238
|
const thread = store.getThread(id);
|
|
97
239
|
if (!thread) return { ok: false, error: "no thread on this task" };
|
|
98
|
-
const last = store
|
|
240
|
+
const last = store
|
|
241
|
+
.threadMessages(id)
|
|
242
|
+
.filter((message) => message.direction === "in")
|
|
243
|
+
.at(-1);
|
|
99
244
|
store.meta(SEEN(id), String(last?.seq ?? 0));
|
|
100
245
|
return { ok: true };
|
|
101
246
|
}
|
|
102
247
|
// Whether replies the person's agents write wait for the person on a new thread (the default) or send on their own.
|
|
103
248
|
case "board.relay.defaults":
|
|
104
|
-
if (typeof request.autoSend === "boolean")
|
|
249
|
+
if (typeof request.autoSend === "boolean")
|
|
250
|
+
store.meta(AUTO_SEND_DEFAULT, request.autoSend ? "1" : "0");
|
|
105
251
|
return { ok: true, autoSend: store.meta(AUTO_SEND_DEFAULT) === "1" };
|
|
106
252
|
case "board.relay.peers":
|
|
107
|
-
return answered(
|
|
108
|
-
.
|
|
253
|
+
return answered(
|
|
254
|
+
Promise.all(
|
|
255
|
+
store.relayPeers().map(async (peer) => ({
|
|
256
|
+
handle: peer.handle,
|
|
257
|
+
pinnedAt: peer.pinned_at,
|
|
258
|
+
fingerprint: await fingerprint(peer.public_key),
|
|
259
|
+
}))
|
|
260
|
+
).then(async (peers) => ({
|
|
261
|
+
ok: true,
|
|
262
|
+
peers,
|
|
263
|
+
mine: myKey() ? await fingerprint(myKey()) : null,
|
|
264
|
+
}))
|
|
265
|
+
);
|
|
109
266
|
default:
|
|
110
267
|
return { ok: false, error: `unknown command ${request.command}` };
|
|
111
268
|
}
|
|
112
269
|
};
|
|
113
270
|
|
|
114
271
|
/** A thread just opened on this computer starts with the person's default for their agents' replies. */
|
|
115
|
-
const opened = (taskId) => {
|
|
272
|
+
const opened = (taskId) => {
|
|
273
|
+
if (store.meta(AUTO_SEND_DEFAULT) === "1") store.setAutoSend(taskId, true);
|
|
274
|
+
};
|
|
116
275
|
|
|
117
276
|
return { command, opened };
|
|
118
277
|
}
|
package/lib/notify.mjs
CHANGED
|
@@ -9,24 +9,46 @@ import { spawn } from "node:child_process";
|
|
|
9
9
|
const TITLE = "Harness";
|
|
10
10
|
|
|
11
11
|
function command(platform, title, body, env) {
|
|
12
|
-
if (env.HARNESS_NOTIFY_COMMAND)
|
|
12
|
+
if (env.HARNESS_NOTIFY_COMMAND)
|
|
13
|
+
return { command: env.HARNESS_NOTIFY_COMMAND, args: [title, body] };
|
|
13
14
|
if (platform === "darwin") {
|
|
14
|
-
const quote = (value) =>
|
|
15
|
-
|
|
15
|
+
const quote = (value) =>
|
|
16
|
+
String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
17
|
+
return {
|
|
18
|
+
command: "osascript",
|
|
19
|
+
args: [
|
|
20
|
+
"-e",
|
|
21
|
+
`display notification "${quote(body)}" with title "${quote(title)}"`,
|
|
22
|
+
],
|
|
23
|
+
};
|
|
16
24
|
}
|
|
17
|
-
if (platform === "linux")
|
|
25
|
+
if (platform === "linux")
|
|
26
|
+
return { command: "notify-send", args: [title, body] };
|
|
18
27
|
if (platform === "win32") {
|
|
19
28
|
const quote = (value) => String(value).replaceAll("'", "''");
|
|
20
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
command: "powershell",
|
|
31
|
+
args: [
|
|
32
|
+
"-NoProfile",
|
|
33
|
+
"-Command",
|
|
34
|
+
`[void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(8000, '${quote(title)}', '${quote(body)}', 'Info'); Start-Sleep -Seconds 9; $n.Dispose()`,
|
|
35
|
+
],
|
|
36
|
+
};
|
|
21
37
|
}
|
|
22
38
|
return null;
|
|
23
39
|
}
|
|
24
40
|
|
|
25
|
-
export function notifyDesktop(
|
|
41
|
+
export function notifyDesktop(
|
|
42
|
+
body,
|
|
43
|
+
{ title = TITLE, platform = process.platform, env = process.env } = {}
|
|
44
|
+
) {
|
|
26
45
|
const call = command(platform, title, String(body).slice(0, 240), env);
|
|
27
46
|
if (!call) return false;
|
|
28
47
|
try {
|
|
29
|
-
const child = spawn(call.command, call.args, {
|
|
48
|
+
const child = spawn(call.command, call.args, {
|
|
49
|
+
stdio: "ignore",
|
|
50
|
+
detached: true,
|
|
51
|
+
});
|
|
30
52
|
child.on("error", () => {});
|
|
31
53
|
child.unref();
|
|
32
54
|
return true;
|
|
@@ -39,14 +61,29 @@ export function notifyDesktop(body, { title = TITLE, platform = process.platform
|
|
|
39
61
|
export function notificationFor(task, { previous = null } = {}) {
|
|
40
62
|
if (!task) return null;
|
|
41
63
|
const objective = String(task.objective || "").slice(0, 80);
|
|
42
|
-
const wantsRun =
|
|
43
|
-
|
|
64
|
+
const wantsRun =
|
|
65
|
+
task.status === "open" && task.run_workspace && !task.run_authorized_at;
|
|
66
|
+
if (
|
|
67
|
+
wantsRun &&
|
|
68
|
+
!(
|
|
69
|
+
previous &&
|
|
70
|
+
previous.status === "open" &&
|
|
71
|
+
previous.run_workspace &&
|
|
72
|
+
!previous.run_authorized_at
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
44
75
|
return `${task.origin_agent} wants ${task.target_agent} to run: ${objective}. Authorize with tryharness board run ${String(task.id).slice(0, 8)}`;
|
|
45
76
|
}
|
|
46
|
-
if (
|
|
77
|
+
if (
|
|
78
|
+
task.status === "needs_input" &&
|
|
79
|
+
(task.holder === "user" || task.origin_agent === "user") &&
|
|
80
|
+
previous?.status !== "needs_input"
|
|
81
|
+
) {
|
|
47
82
|
return `${task.claimed_by} has a question on: ${objective}`;
|
|
48
83
|
}
|
|
49
|
-
if (task.status === "unknown" && previous?.status !== "unknown")
|
|
50
|
-
|
|
84
|
+
if (task.status === "unknown" && previous?.status !== "unknown")
|
|
85
|
+
return `The outcome of ${task.target_agent}'s run is unknown: ${objective}`;
|
|
86
|
+
if (task.status === "open" && task.target_agent === "harness" && !previous)
|
|
87
|
+
return `${task.origin_agent} posted for Harness: ${objective}`;
|
|
51
88
|
return null;
|
|
52
89
|
}
|