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
|
@@ -8,8 +8,11 @@ import { pathToFileURL } from "node:url";
|
|
|
8
8
|
// Filled in by setup: the runtime the other agents' Harness server runs from.
|
|
9
9
|
const RUNTIME = "__HARNESS_RUNTIME__";
|
|
10
10
|
const AGENT = "opencode";
|
|
11
|
-
const harness = () =>
|
|
12
|
-
|
|
11
|
+
const harness = () =>
|
|
12
|
+
import(pathToFileURL(path.join(RUNTIME, "lib", "nudge.mjs")).href);
|
|
13
|
+
const socketPath = async () =>
|
|
14
|
+
(await import(pathToFileURL(path.join(RUNTIME, "lib", "paths.mjs")).href))
|
|
15
|
+
.paths.socket;
|
|
13
16
|
|
|
14
17
|
async function notify(message) {
|
|
15
18
|
try {
|
|
@@ -28,8 +31,17 @@ const pids = [process.pid, process.ppid];
|
|
|
28
31
|
let counter = 0;
|
|
29
32
|
const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
30
33
|
const partId = () => {
|
|
31
|
-
const time =
|
|
32
|
-
|
|
34
|
+
const time =
|
|
35
|
+
(BigInt(Date.now()) * 0x1000n + BigInt(++counter % 0x1000)) &
|
|
36
|
+
0xffffffffffffn;
|
|
37
|
+
return (
|
|
38
|
+
"prt_" +
|
|
39
|
+
time.toString(16).padStart(12, "0") +
|
|
40
|
+
Array.from(
|
|
41
|
+
crypto.getRandomValues(new Uint8Array(14)),
|
|
42
|
+
(byte) => BASE62[byte % 62]
|
|
43
|
+
).join("")
|
|
44
|
+
);
|
|
33
45
|
};
|
|
34
46
|
|
|
35
47
|
export const TryHarnessPlugin = async ({ directory, client }) => ({
|
|
@@ -48,22 +60,47 @@ export const TryHarnessPlugin = async ({ directory, client }) => ({
|
|
|
48
60
|
const messageID = output.message?.id || input?.messageID;
|
|
49
61
|
if (!messageID) return;
|
|
50
62
|
const messages = await board.takeMessages(AGENT, sessionId);
|
|
51
|
-
const nudge = first
|
|
63
|
+
const nudge = first
|
|
64
|
+
? await board.boardNudge(AGENT)
|
|
65
|
+
: await board.boardNudgeIfChanged(AGENT);
|
|
52
66
|
// Marked synthetic: the model reads them, and they are not the words typed here.
|
|
53
|
-
for (const text of [
|
|
54
|
-
|
|
67
|
+
for (const text of [
|
|
68
|
+
messages.length ? board.messagesText(messages) : "",
|
|
69
|
+
nudge,
|
|
70
|
+
]) {
|
|
71
|
+
if (text)
|
|
72
|
+
output.parts.push({
|
|
73
|
+
id: partId(),
|
|
74
|
+
sessionID: sessionId,
|
|
75
|
+
messageID,
|
|
76
|
+
type: "text",
|
|
77
|
+
text,
|
|
78
|
+
synthetic: true,
|
|
79
|
+
});
|
|
55
80
|
}
|
|
56
81
|
},
|
|
57
82
|
event: async ({ event }) => {
|
|
58
83
|
if (event?.type !== "session.idle") return;
|
|
59
|
-
const sessionId =
|
|
84
|
+
const sessionId =
|
|
85
|
+
event?.properties?.sessionID || event?.properties?.sessionId;
|
|
60
86
|
if (!sessionId) return;
|
|
61
87
|
await notify({ command: "sync", agent: AGENT, sessionId, cwd: directory });
|
|
62
88
|
// When a turn ends, a message waiting from Harness starts the next one (Claude Code's Stop hook does the same).
|
|
63
89
|
const board = await harness().catch(() => null);
|
|
64
90
|
const messages = board ? await board.takeMessages(AGENT, sessionId) : [];
|
|
65
|
-
const send =
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
const send =
|
|
92
|
+
client?.session?.promptAsync?.bind(client.session) ??
|
|
93
|
+
client?.session?.prompt?.bind(client.session);
|
|
94
|
+
if (messages.length && send)
|
|
95
|
+
await send({
|
|
96
|
+
path: { id: sessionId },
|
|
97
|
+
body: { parts: [{ type: "text", text: board.messagesText(messages) }] },
|
|
98
|
+
}).catch(() => {});
|
|
99
|
+
else if (board && !messages.length)
|
|
100
|
+
await board.noteTurn(
|
|
101
|
+
AGENT,
|
|
102
|
+
{ session_id: sessionId, cwd: directory },
|
|
103
|
+
pids
|
|
104
|
+
);
|
|
68
105
|
},
|
|
69
106
|
});
|
package/assets/pi-extension.ts
CHANGED
|
@@ -11,14 +11,19 @@ import { pathToFileURL } from "node:url";
|
|
|
11
11
|
const RUNTIME = "__HARNESS_RUNTIME__";
|
|
12
12
|
const NODE = "__HARNESS_NODE__";
|
|
13
13
|
const AGENT = "pi";
|
|
14
|
-
const harness = () =>
|
|
14
|
+
const harness = () =>
|
|
15
|
+
import(pathToFileURL(path.join(RUNTIME, "lib", "nudge.mjs")).href);
|
|
15
16
|
|
|
16
17
|
// The server, one per session: JSON-RPC over its stdin and stdout.
|
|
17
18
|
let server = null;
|
|
18
19
|
let nextId = 1;
|
|
19
20
|
function start() {
|
|
20
21
|
if (server) return server;
|
|
21
|
-
const child = spawn(
|
|
22
|
+
const child = spawn(
|
|
23
|
+
NODE,
|
|
24
|
+
[path.join(RUNTIME, "lib", "mcp-stdio.mjs"), "--agent", AGENT],
|
|
25
|
+
{ stdio: ["pipe", "pipe", "ignore"] }
|
|
26
|
+
);
|
|
22
27
|
const pending = new Map();
|
|
23
28
|
let buffer = "";
|
|
24
29
|
child.stdout.setEncoding("utf8");
|
|
@@ -28,14 +33,19 @@ function start() {
|
|
|
28
33
|
buffer = lines.pop();
|
|
29
34
|
for (const line of lines) {
|
|
30
35
|
let message;
|
|
31
|
-
try {
|
|
36
|
+
try {
|
|
37
|
+
message = JSON.parse(line);
|
|
38
|
+
} catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
32
41
|
pending.get(message.id)?.(message);
|
|
33
42
|
pending.delete(message.id);
|
|
34
43
|
}
|
|
35
44
|
});
|
|
36
45
|
const stopped = () => {
|
|
37
46
|
if (server?.child === child) server = null;
|
|
38
|
-
for (const settle of pending.values())
|
|
47
|
+
for (const settle of pending.values())
|
|
48
|
+
settle({ error: { message: "the Harness server stopped" } });
|
|
39
49
|
pending.clear();
|
|
40
50
|
};
|
|
41
51
|
child.on("exit", stopped);
|
|
@@ -47,34 +57,61 @@ function rpc(method, params = {}) {
|
|
|
47
57
|
const { child, pending } = start();
|
|
48
58
|
const id = nextId++;
|
|
49
59
|
return new Promise((resolve, reject) => {
|
|
50
|
-
pending.set(id, (message) =>
|
|
51
|
-
|
|
60
|
+
pending.set(id, (message) =>
|
|
61
|
+
message.error
|
|
62
|
+
? reject(new Error(message.error.message))
|
|
63
|
+
: resolve(message.result)
|
|
64
|
+
);
|
|
65
|
+
child.stdin.write(
|
|
66
|
+
JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n"
|
|
67
|
+
);
|
|
52
68
|
});
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
let typeApi = null;
|
|
56
72
|
async function types() {
|
|
57
73
|
if (typeApi) return typeApi;
|
|
58
|
-
try {
|
|
59
|
-
|
|
74
|
+
try {
|
|
75
|
+
typeApi = (await import("@sinclair/typebox")).Type;
|
|
76
|
+
} catch {
|
|
77
|
+
const loaded = await import("typebox");
|
|
78
|
+
typeApi = loaded.Type || loaded;
|
|
79
|
+
}
|
|
60
80
|
return typeApi;
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
function toType(schema, Type) {
|
|
64
84
|
if (!schema || typeof schema !== "object") return Type.Any();
|
|
65
|
-
const opts =
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
const opts =
|
|
86
|
+
typeof schema.description === "string"
|
|
87
|
+
? { description: schema.description }
|
|
88
|
+
: {};
|
|
89
|
+
if (Array.isArray(schema.enum) && schema.enum.length)
|
|
90
|
+
return Type.Union(
|
|
91
|
+
schema.enum.map((value) => Type.Literal(value)),
|
|
92
|
+
opts
|
|
93
|
+
);
|
|
94
|
+
if (Array.isArray(schema.anyOf || schema.oneOf))
|
|
95
|
+
return Type.Union(
|
|
96
|
+
(schema.anyOf || schema.oneOf).map((entry) => toType(entry, Type)),
|
|
97
|
+
opts
|
|
98
|
+
);
|
|
68
99
|
if (schema.type === "object") {
|
|
69
100
|
const required = new Set(schema.required || []);
|
|
70
101
|
const properties = {};
|
|
71
102
|
for (const [name, child] of Object.entries(schema.properties || {})) {
|
|
72
103
|
const converted = toType(child, Type);
|
|
73
|
-
properties[name] = required.has(name)
|
|
104
|
+
properties[name] = required.has(name)
|
|
105
|
+
? converted
|
|
106
|
+
: Type.Optional(converted);
|
|
74
107
|
}
|
|
75
|
-
return Type.Object(properties, {
|
|
108
|
+
return Type.Object(properties, {
|
|
109
|
+
...opts,
|
|
110
|
+
additionalProperties: schema.additionalProperties === true,
|
|
111
|
+
});
|
|
76
112
|
}
|
|
77
|
-
if (schema.type === "array")
|
|
113
|
+
if (schema.type === "array")
|
|
114
|
+
return Type.Array(toType(schema.items, Type), opts);
|
|
78
115
|
if (schema.type === "string") return Type.String(opts);
|
|
79
116
|
if (schema.type === "integer") return Type.Integer(opts);
|
|
80
117
|
if (schema.type === "number") return Type.Number(opts);
|
|
@@ -91,32 +128,61 @@ export default function tryHarness(pi) {
|
|
|
91
128
|
const Type = await types();
|
|
92
129
|
const listing = await rpc("tools/list");
|
|
93
130
|
for (const def of listing?.tools || []) {
|
|
94
|
-
if (
|
|
131
|
+
if (
|
|
132
|
+
!def?.name ||
|
|
133
|
+
def.name === "report_session" ||
|
|
134
|
+
installed.has(def.name)
|
|
135
|
+
)
|
|
136
|
+
continue;
|
|
95
137
|
pi.registerTool({
|
|
96
138
|
name: def.name,
|
|
97
139
|
label: "Harness: " + def.name.replaceAll("_", " "),
|
|
98
|
-
description:
|
|
140
|
+
description:
|
|
141
|
+
def.description || "Use this connected Harness capability.",
|
|
99
142
|
parameters: toType(def.inputSchema, Type),
|
|
100
143
|
async execute(_id, params) {
|
|
101
|
-
const result = await rpc("tools/call", {
|
|
102
|
-
|
|
144
|
+
const result = await rpc("tools/call", {
|
|
145
|
+
name: def.name,
|
|
146
|
+
arguments: params || {},
|
|
147
|
+
});
|
|
148
|
+
const content = result?.content || [
|
|
149
|
+
{ type: "text", text: "Harness returned no content." },
|
|
150
|
+
];
|
|
103
151
|
// pi marks a tool failed when it throws.
|
|
104
|
-
if (result?.isError)
|
|
152
|
+
if (result?.isError)
|
|
153
|
+
throw new Error(
|
|
154
|
+
content.map((part) => part.text || "").join("\n") ||
|
|
155
|
+
"Harness call failed."
|
|
156
|
+
);
|
|
105
157
|
return { content, details: undefined };
|
|
106
158
|
},
|
|
107
159
|
});
|
|
108
160
|
installed.add(def.name);
|
|
109
161
|
}
|
|
110
162
|
ctx?.ui?.notify?.("Harness connected", "info");
|
|
111
|
-
})().catch((error) => {
|
|
163
|
+
})().catch((error) => {
|
|
164
|
+
connecting = null;
|
|
165
|
+
ctx?.ui?.notify?.("Harness unavailable: " + error?.message, "warning");
|
|
166
|
+
});
|
|
112
167
|
return connecting;
|
|
113
168
|
};
|
|
114
|
-
const where = (ctx) => ({
|
|
169
|
+
const where = (ctx) => ({
|
|
170
|
+
session_id: ctx?.sessionManager?.getSessionId?.(),
|
|
171
|
+
cwd: ctx?.cwd || process.cwd(),
|
|
172
|
+
});
|
|
115
173
|
const pids = [process.pid, process.ppid];
|
|
116
174
|
const tell = (nudge, ctx) => {
|
|
117
175
|
if (!nudge) return;
|
|
118
176
|
// Into the model's context when pi offers it; always visible to the person.
|
|
119
|
-
if (typeof pi.sendMessage === "function") {
|
|
177
|
+
if (typeof pi.sendMessage === "function") {
|
|
178
|
+
try {
|
|
179
|
+
pi.sendMessage({
|
|
180
|
+
customType: "harness-board",
|
|
181
|
+
content: nudge,
|
|
182
|
+
display: true,
|
|
183
|
+
});
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
120
186
|
ctx?.ui?.notify?.(nudge.split("\n").slice(0, 2).join(" "), "info");
|
|
121
187
|
};
|
|
122
188
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -132,7 +198,9 @@ export default function tryHarness(pi) {
|
|
|
132
198
|
// What the person sent this session from Harness (board.live.say), taken once, as Claude Code's hooks take it.
|
|
133
199
|
const said = async (board, ctx) => {
|
|
134
200
|
const sessionId = where(ctx).session_id;
|
|
135
|
-
const messages = sessionId
|
|
201
|
+
const messages = sessionId
|
|
202
|
+
? await board.takeMessages(AGENT, sessionId)
|
|
203
|
+
: [];
|
|
136
204
|
return messages.length ? board.messagesText(messages) : "";
|
|
137
205
|
};
|
|
138
206
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -140,7 +208,9 @@ export default function tryHarness(pi) {
|
|
|
140
208
|
const board = await harness().catch(() => null);
|
|
141
209
|
if (!board) return undefined;
|
|
142
210
|
await board.notePrompt(AGENT, where(ctx), pids);
|
|
143
|
-
const text = [await said(board, ctx), await board.boardNudge(AGENT)]
|
|
211
|
+
const text = [await said(board, ctx), await board.boardNudge(AGENT)]
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
.join("\n\n");
|
|
144
214
|
if (!text || typeof event?.systemPrompt !== "string") return undefined;
|
|
145
215
|
return { systemPrompt: `${event.systemPrompt}\n\n${text}` };
|
|
146
216
|
});
|
|
@@ -156,6 +226,15 @@ export default function tryHarness(pi) {
|
|
|
156
226
|
const board = await harness().catch(() => null);
|
|
157
227
|
if (board) tell(await board.boardNudgeIfChanged(AGENT), ctx);
|
|
158
228
|
});
|
|
159
|
-
pi.on("session_shutdown", () => {
|
|
160
|
-
|
|
229
|
+
pi.on("session_shutdown", () => {
|
|
230
|
+
server?.child.kill();
|
|
231
|
+
server = null;
|
|
232
|
+
});
|
|
233
|
+
pi.registerCommand("harness-reconnect", {
|
|
234
|
+
description: "Reconnect Harness",
|
|
235
|
+
handler: async (_args, ctx) => {
|
|
236
|
+
connecting = null;
|
|
237
|
+
await connect(ctx);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
161
240
|
}
|