tryharness 0.7.22 → 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/README.md +48 -8
- package/assets/opencode-plugin.js +48 -10
- package/assets/pi-extension.ts +106 -26
- package/cli.mjs +1281 -289
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +604 -0
- package/lib/bip39-english.txt +2048 -0
- package/lib/cards.mjs +177 -39
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +628 -0
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +281 -56
- package/lib/daemon.mjs +5590 -838
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +81 -0
- package/lib/environment.mjs +180 -0
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +288 -0
- package/lib/folders.mjs +223 -57
- package/lib/frames.mjs +333 -0
- package/lib/inference.mjs +663 -0
- package/lib/learn.mjs +182 -0
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +571 -0
- package/lib/mcp-stdio.mjs +912 -103
- package/lib/media.mjs +228 -0
- 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 +190 -32
- package/lib/ocr.mjs +380 -0
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +77 -0
- package/lib/phones.mjs +398 -51
- package/lib/places.mjs +566 -102
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +82 -0
- package/lib/redact.mjs +730 -0
- package/lib/runner.mjs +347 -55
- package/lib/runtime-install.mjs +59 -17
- package/lib/screens.mjs +212 -0
- package/lib/sealed.mjs +160 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +173 -35
- package/lib/snippets.mjs +140 -0
- package/lib/speech.mjs +993 -0
- package/lib/store.mjs +1678 -301
- package/lib/swarm.mjs +598 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +401 -0
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +362 -0
- package/lib/worktrees.mjs +186 -38
- package/package.json +6 -4
- package/lib/embedder.mjs +0 -156
package/README.md
CHANGED
|
@@ -50,15 +50,51 @@ command as soon as the agent reports its session id, including while the run is
|
|
|
50
50
|
so the user can enter and steer the agent directly rather than interacting through a
|
|
51
51
|
Harness wrapper.
|
|
52
52
|
|
|
53
|
-
##
|
|
53
|
+
## Local models
|
|
54
|
+
|
|
55
|
+
Setup downloads the relay's core local models into `~/.harness/local`, each file checked
|
|
56
|
+
against a pinned SHA-256 and every model under an allowlisted licence (`cli/lib/localmodels.mjs`):
|
|
57
|
+
EmbeddingGemma 2's text part (one search space for code, agent sessions and board posts), an
|
|
58
|
+
two injection guards (one reads long stretches and holds, one reads short ones and flags), a
|
|
59
|
+
credential detector and a screenshot text reader (PP-OCR), plus llama.cpp's own server build
|
|
60
|
+
(about 860 MB of models). Two more come down the first time they are used: speech for the buddy's
|
|
61
|
+
Talk (Silero VAD, 1.3 MB, and on a Mac with macOS 26 Apple's own on-device transcriber through
|
|
62
|
+
the buddy; elsewhere NVIDIA's Parakeet TDT 0.6B v3, 672 MB) and the search model's image and audio part
|
|
63
|
+
(555 MB) when you first add a folder of recordings or screenshots. The search model runs on the
|
|
64
|
+
GPU when this machine's check finds it faster, the CPU otherwise; the classifiers run in native
|
|
65
|
+
ONNX Runtime. Background work runs at the OS's background priority and waits in Low Power Mode,
|
|
66
|
+
on low battery and when the machine is hot.
|
|
67
|
+
|
|
68
|
+
What they do:
|
|
69
|
+
|
|
70
|
+
- **Briefs:** every brief a local coding agent gets carries a context pack (the files in its
|
|
71
|
+
repository most likely needed, earlier sessions on them, and a recording's minute or a
|
|
72
|
+
screenshot from your added folders when one is close) and the thread's task frame: what you
|
|
73
|
+
named resolved to file and line, the repository's state, other threads changing the same
|
|
74
|
+
repository, and each run's receipt. Agents can call `search_code`, `search_media`,
|
|
75
|
+
`compare_screens` (two screenshots of UI work, before and after: what changed and which text)
|
|
76
|
+
and `search_screen` (what you saw in tabs shared with Harness, asked of your own Harness window
|
|
77
|
+
sealed over the `tryharness link` line, so Harness's server carries it unread; your Allow in
|
|
78
|
+
that window comes first).
|
|
79
|
+
- **Checks:** a finished run gets a receipt from its own session file; the coordinator hands an
|
|
80
|
+
untested "tests pass" to an agent to check. An agent's post that reads like orders to the
|
|
81
|
+
swarm is held; a key in anything sent to Harness's server goes as `[SECRET_n]` (and, in requests
|
|
82
|
+
that only decide, emails, phone numbers, wallet addresses and amounts as placeholders too); a
|
|
83
|
+
screenshot a cloud agent reads has keys, addresses and amounts covered; code a cloud agent asks to
|
|
84
|
+
see (`request_snippet`) leaves only when you share it (`tryharness board snippet`).
|
|
85
|
+
- **Watches:** a command on this computer checked on a schedule (a heartbeat's age, a balance, a
|
|
86
|
+
count of error lines), said out loud only when it is off. Agents suggest them with
|
|
87
|
+
`suggest_watch`; nothing runs until you start it.
|
|
88
|
+
- **Learning:** overnight, on the charger, the ranking model retrains on what the agents' runs
|
|
89
|
+
actually changed and is kept only if it beats the current one on the newest runs.
|
|
54
90
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
```sh
|
|
92
|
+
npx tryharness local # what is installed, where it runs, what is indexed, the last learning
|
|
93
|
+
npx tryharness local learn # start the learner now
|
|
94
|
+
npx tryharness local add ~/Recordings # a folder of recordings and screenshots agents can search
|
|
95
|
+
npx tryharness watch # watches and what each last read
|
|
96
|
+
npx tryharness watch add "keeper heartbeat" --run "cat state/heartbeat.json" --age completedAt --above 900 --every 5m
|
|
97
|
+
```
|
|
62
98
|
|
|
63
99
|
## Task board
|
|
64
100
|
|
|
@@ -135,3 +171,7 @@ room. Agents read, post and search the room with `read_room`, `post_to_room` and
|
|
|
135
171
|
Start. Harness facilitates and answers once. `tryharness goal show <id>` prints the room, `goal stop <id>` ends
|
|
136
172
|
everything, `goal resume <id>` reopens a room that paused at a backstop, and `goal invite <id> handle/agent` asks another
|
|
137
173
|
person's agent to join as a guest over a relay thread. `tryharness room post <goal> <text> [--guests]` posts as you.
|
|
174
|
+
|
|
175
|
+
Parakeet TDT 0.6B v3 is by NVIDIA, licensed under CC BY 4.0
|
|
176
|
+
(https://creativecommons.org/licenses/by/4.0/); the relay uses the 8-bit ONNX export by
|
|
177
|
+
istupakov (https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx), unmodified.
|
|
@@ -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,21 +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
|
-
|
|
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
|
+
);
|
|
67
105
|
},
|
|
68
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
|
});
|
|
@@ -149,12 +219,22 @@ export default function tryHarness(pi) {
|
|
|
149
219
|
const board = await harness().catch(() => null);
|
|
150
220
|
const text = board ? await said(board, ctx) : "";
|
|
151
221
|
if (text) pi.sendUserMessage(text);
|
|
222
|
+
else if (board) await board.noteTurn(AGENT, where(ctx), pids);
|
|
152
223
|
});
|
|
153
224
|
// After each turn (pi 0.87 event name turn_end), only when the queue moved.
|
|
154
225
|
pi.on("turn_end", async (_event, ctx) => {
|
|
155
226
|
const board = await harness().catch(() => null);
|
|
156
227
|
if (board) tell(await board.boardNudgeIfChanged(AGENT), ctx);
|
|
157
228
|
});
|
|
158
|
-
pi.on("session_shutdown", () => {
|
|
159
|
-
|
|
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
|
+
});
|
|
160
240
|
}
|