tryharness 0.7.23 → 0.7.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- package/package.json +3 -3
package/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(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
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
|
-
]
|
|
82
|
+
]
|
|
83
|
+
.sort()
|
|
84
|
+
.join("|");
|
|
43
85
|
}
|
|
44
86
|
|
|
45
87
|
export async function boardQueue(agent) {
|
|
46
88
|
try {
|
|
47
|
-
const response = await daemonRequest(
|
|
48
|
-
|
|
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) {
|
|
57
|
-
|
|
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)
|
|
64
|
-
|
|
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, {
|
|
80
|
-
|
|
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 =
|
|
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 {
|
|
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) => ({
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
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
|
|
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) => {
|
|
135
|
-
|
|
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 {
|
|
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)
|
|
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")
|
|
154
|
-
|
|
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
|
}
|