@vzlentin/harness 0.2.3 → 0.3.1
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 +11 -8
- package/dist/answer.js +2 -2
- package/dist/children.d.ts +5 -0
- package/dist/children.js +14 -4
- package/dist/cli.js +38 -16
- package/dist/client.d.ts +11 -7
- package/dist/client.js +14 -9
- package/dist/context.js +2 -1
- package/dist/herdr.d.ts +25 -23
- package/dist/herdr.js +93 -38
- package/dist/host.d.ts +1 -1
- package/dist/host.js +42 -219
- package/dist/load-entries.d.ts +18 -0
- package/dist/load-entries.js +91 -0
- package/dist/mirror.d.ts +95 -0
- package/dist/mirror.js +291 -0
- package/dist/protocol.d.ts +70 -44
- package/dist/protocol.js +35 -11
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +3 -2
- package/dist/targets.d.ts +67 -0
- package/dist/targets.js +140 -0
- package/dist/tool.js +1 -1
- package/dist/view-change.d.ts +38 -0
- package/dist/view-change.js +55 -0
- package/dist/view-state.d.ts +22 -6
- package/dist/view-state.js +45 -7
- package/package.json +2 -1
- package/pi/attachment.ts +303 -0
- package/pi/chat.ts +286 -0
- package/pi/index.ts +10 -8
- package/pi/mirror.ts +218 -0
- package/pi/model-sync.ts +102 -0
- package/pi/package.json +3 -2
- package/pi/program-status.ts +66 -0
- package/pi/usage-line.ts +59 -0
- package/dist/chat.d.ts +0 -48
- package/dist/chat.js +0 -208
- package/dist/chrome.d.ts +0 -18
- package/dist/chrome.js +0 -74
- package/dist/picker.d.ts +0 -22
- package/dist/picker.js +0 -70
- package/dist/theme.d.ts +0 -3
- package/dist/theme.js +0 -19
- package/dist/viewer.d.ts +0 -20
- package/dist/viewer.js +0 -307
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Observable, persistent subagents on [Pi](https://github.com/earendil-works/pi)'s libraries and
|
|
4
4
|
[pi-durable](https://www.npmjs.com/package/@earendil-works/pi-durable). Each subagent is a conversation in a durable
|
|
5
|
-
host process. You can spawn it, check its status, wait for its answer, steer it, and
|
|
5
|
+
host process. You can spawn it, check its status, wait for its answer, steer it, and talk to it live in Pi.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -14,9 +14,9 @@ npm install -g @vzlentin/harness
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
This works with npm 11 and npm 12. It gives you the `harness` command, and a setup runs `pi install` on the `pi/`
|
|
17
|
-
folder of the package, which adds the `subagent` tool, the `/subagent`
|
|
18
|
-
the `harness` command use the same host. npm 12 runs no install script by default, so there the first
|
|
19
|
-
does the setup. When `pi` is not on your `PATH`, or the step fails, the setup prints the command to run later:
|
|
17
|
+
folder of the package, which adds the `subagent` tool, the `/subagent` and `/harness` commands and the subagents widget
|
|
18
|
+
to Pi. Pi and the `harness` command use the same host. npm 12 runs no install script by default, so there the first
|
|
19
|
+
`harness` command does the setup. When `pi` is not on your `PATH`, or the step fails, the setup prints the command to run later:
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
22
|
pi install "$(realpath "$(npm root -g)/@vzlentin/harness/pi")"
|
|
@@ -39,12 +39,15 @@ harness spawn --name review --prompt "Review the current diff"
|
|
|
39
39
|
harness list
|
|
40
40
|
harness wait <handle>
|
|
41
41
|
harness send <handle> "Focus on error handling"
|
|
42
|
-
harness attach <handle> #
|
|
42
|
+
harness attach <handle> # open it in Pi
|
|
43
43
|
harness stop <handle>
|
|
44
|
-
harness main #
|
|
44
|
+
harness main # open the agent of the main project in Pi
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
In Pi, `/harness <project>` opens a session that shows the agent of the project in Pi's own chat, and sends what you
|
|
48
|
+
type to it. Without a project, it asks for one. `pi --harness <project>` starts Pi in such a session.
|
|
49
|
+
|
|
50
|
+
[docs/spec.md](docs/spec.md) describes all the commands, the tool, the Pi extension, projects, the host and its state.
|
|
48
51
|
|
|
49
52
|
## Development
|
|
50
53
|
|
|
@@ -58,5 +61,5 @@ XDG_STATE_HOME=/tmp/harness-dev node src/cli.ts list
|
|
|
58
61
|
stores with the hosts of the installed `harness`.
|
|
59
62
|
|
|
60
63
|
The checkout runs from its TypeScript source. The package contains `dist/`, the Pi extension in `pi/` and its
|
|
61
|
-
`postinstall` script.
|
|
64
|
+
`postinstall` script. To release, merge the release PR `release: <version>` that CI keeps open: CI then publishes that version. `npm run check` builds `dist/` too, before the type check. The extension imports `../dist/`, so to try the checkout's
|
|
62
65
|
extension in Pi, run `npm run build`, then `pi install ./pi`, and build again after each change in `src/`.
|
package/dist/answer.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { copyJson } from "@earendil-works/chord";
|
|
6
6
|
import { Type, validateToolArguments } from "@earendil-works/pi-ai";
|
|
7
7
|
import { Value } from "typebox/value";
|
|
8
|
-
import { Json } from "./protocol.js";
|
|
8
|
+
import { errorMessage, Json } from "./protocol.js";
|
|
9
9
|
/**
|
|
10
10
|
* The user message that asks again for a `respond` call after an answer of a `schema` ask without one. A reminder is
|
|
11
11
|
* known by its exact text, so a user prompt with the same text counts as one.
|
|
@@ -49,7 +49,7 @@ export function createAnswers(durable) {
|
|
|
49
49
|
catch (error) {
|
|
50
50
|
return {
|
|
51
51
|
isError: true,
|
|
52
|
-
output: [{ type: "text", text:
|
|
52
|
+
output: [{ type: "text", text: errorMessage(error) }],
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
await api.commit(async (tx) => {
|
package/dist/children.d.ts
CHANGED
|
@@ -183,6 +183,11 @@ export type Children = {
|
|
|
183
183
|
} | {
|
|
184
184
|
readonly id: string;
|
|
185
185
|
}, context: Context): Promise<Child | undefined>;
|
|
186
|
+
/**
|
|
187
|
+
* The lineage, conversation and state of the child with the conversation ID `id`, of any owner, without its owner
|
|
188
|
+
* operations. Nothing for a root, an unknown ID, or text that is not an ID.
|
|
189
|
+
*/
|
|
190
|
+
inspect(place: Place, id: string, context: Context): Promise<Pick<Child, "lineage" | "conversation" | "state"> | undefined>;
|
|
186
191
|
/** Where the conversation `id` sits in its tree; nothing for a root, or another conversation the library did not open. */
|
|
187
192
|
lineage(store: Pick<Store, "snapshot">, id: string, context: Context): Promise<Lineage | undefined>;
|
|
188
193
|
/**
|
package/dist/children.js
CHANGED
|
@@ -245,7 +245,8 @@ export function createChildren(durable) {
|
|
|
245
245
|
},
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
|
-
|
|
248
|
+
/** The child of a key or a conversation ID, of any owner, with its document. */
|
|
249
|
+
async function load(place, which, context) {
|
|
249
250
|
const id = "key" in which
|
|
250
251
|
? (await place.store.snapshot(Keys, which.key, context))?.conversation
|
|
251
252
|
: conversationIdOf(which.id);
|
|
@@ -255,15 +256,24 @@ export function createChildren(durable) {
|
|
|
255
256
|
const conversation = await place.store.conversation(id, context);
|
|
256
257
|
if (doc === undefined || conversation === undefined)
|
|
257
258
|
return undefined;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
return { child: makeChild(place, id, lineageOf(doc), conversation), owner: doc.owner };
|
|
260
|
+
}
|
|
261
|
+
const find = async (place, owner, which, context) => {
|
|
262
|
+
const loaded = await load(place, which, context);
|
|
263
|
+
if (loaded === undefined)
|
|
264
|
+
return undefined;
|
|
265
|
+
if (!sameOwner(loaded.owner, ownerState(owner))) {
|
|
266
|
+
throw new Error(`The child ${loaded.child.id} belongs to another owner`);
|
|
267
|
+
}
|
|
268
|
+
return loaded.child;
|
|
261
269
|
};
|
|
270
|
+
const inspect = async (place, id, context) => (await load(place, { id }, context))?.child;
|
|
262
271
|
return {
|
|
263
272
|
extension,
|
|
264
273
|
answerExtension: answers.extension,
|
|
265
274
|
lineage,
|
|
266
275
|
find,
|
|
276
|
+
inspect,
|
|
267
277
|
turn,
|
|
268
278
|
async open(place, owner, key, spec, context, setup) {
|
|
269
279
|
const existing = await find(place, owner, { key }, context);
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { request } from "./client.js";
|
|
6
|
+
import { piEnv } from "./herdr.js";
|
|
7
|
+
import { piArgs } from "./mirror.js";
|
|
8
|
+
import { addProject, checkProject, MAIN_PROJECT, projectDir, readProjects, resolveProject } from "./projects.js";
|
|
9
|
+
import { AGENT_HANDLE, errorMessage, statePaths } from "./protocol.js";
|
|
10
|
+
import { setupOnce, userPath } from "./setup.js";
|
|
7
11
|
import { runTool, sendToHost } from "./tool.js";
|
|
8
12
|
function fail(message) {
|
|
9
13
|
throw new Error(message);
|
|
@@ -20,6 +24,7 @@ function usage() {
|
|
|
20
24
|
harness stop <handle>
|
|
21
25
|
harness list
|
|
22
26
|
harness attach <handle>
|
|
27
|
+
harness attach --conversation <id>
|
|
23
28
|
harness create <name> --cwd <dir>
|
|
24
29
|
harness projects
|
|
25
30
|
Any command but harness <project> takes --project <name> before it; without it, the project is HARNESS_PROJECT, then the cwd.`);
|
|
@@ -178,25 +183,42 @@ async function hostCommand(args, project) {
|
|
|
178
183
|
await host.closed;
|
|
179
184
|
process.exit(0);
|
|
180
185
|
}
|
|
181
|
-
/** Opens
|
|
186
|
+
/** Opens a child, or a conversation of the children library with `--conversation <id>`, in Pi in this terminal. */
|
|
182
187
|
async function attachCommand(args) {
|
|
183
|
-
const [
|
|
184
|
-
|
|
188
|
+
const [first, id] = args;
|
|
189
|
+
let ref;
|
|
190
|
+
if (args.length === 1 && first !== undefined && first !== "--conversation")
|
|
191
|
+
ref = { handle: first };
|
|
192
|
+
else if (args.length === 2 && first === "--conversation" && id)
|
|
193
|
+
ref = { conversation: id };
|
|
194
|
+
else
|
|
185
195
|
usage();
|
|
186
|
-
await
|
|
196
|
+
await openInPi({ project: resolveProject(process.env, process.cwd()), ...ref });
|
|
187
197
|
}
|
|
188
|
-
/** Opens the
|
|
198
|
+
/** Opens the agent of the project `name`, which is `main` or a project, in Pi in this terminal. */
|
|
189
199
|
async function agentCommand(name, project) {
|
|
190
200
|
if (project !== undefined)
|
|
191
201
|
fail(`harness ${name} takes no --project`);
|
|
192
|
-
|
|
193
|
-
process.env.HARNESS_PROJECT = name;
|
|
194
|
-
await openViewer(AGENT_HANDLE);
|
|
202
|
+
await openInPi({ project: name, handle: AGENT_HANDLE });
|
|
195
203
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Runs Pi in this terminal with a mirror session of `target` (see the Pi extension), and exits with its code. It starts
|
|
206
|
+
* the host when none runs. A first watch fails before Pi starts for an unknown child, an unknown conversation and a
|
|
207
|
+
* project agent without a model.
|
|
208
|
+
*/
|
|
209
|
+
async function openInPi(target) {
|
|
210
|
+
const ref = target.conversation === undefined ? { handle: target.handle } : { conversation: target.conversation };
|
|
211
|
+
await request({ type: "watch", ...ref }, { start: true, project: target.project });
|
|
212
|
+
const result = spawnSync("pi", piArgs(target), {
|
|
213
|
+
stdio: "inherit",
|
|
214
|
+
env: { ...piEnv(process.env), PATH: userPath() },
|
|
215
|
+
});
|
|
216
|
+
if (result.error !== undefined && "code" in result.error && result.error.code === "ENOENT") {
|
|
217
|
+
fail("pi is not on PATH. Install Pi to open the agent");
|
|
218
|
+
}
|
|
219
|
+
if (result.error !== undefined)
|
|
220
|
+
throw result.error;
|
|
221
|
+
process.exitCode = result.status ?? 1;
|
|
200
222
|
}
|
|
201
223
|
/**
|
|
202
224
|
* Runs a command as a call of the `subagent` tool. The caller is `PI_SESSION_ID`, or `cli` outside Pi, and the defaults
|
|
@@ -246,6 +268,6 @@ async function main() {
|
|
|
246
268
|
process.stdout.write(text);
|
|
247
269
|
}
|
|
248
270
|
main().catch((cause) => {
|
|
249
|
-
process.stderr.write(`${
|
|
271
|
+
process.stderr.write(`${errorMessage(cause)}\n`);
|
|
250
272
|
process.exitCode = 1;
|
|
251
273
|
});
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Socket } from "node:net";
|
|
2
|
-
import { type Replies, type Request, type WatchLine } from "./protocol.ts";
|
|
2
|
+
import { type Replies, type Request, type Target, type WatchLine } from "./protocol.ts";
|
|
3
3
|
/**
|
|
4
4
|
* A connection to the host. Requests are answered in order, one reply line for each request line. After `watch`, the
|
|
5
5
|
* event lines of the watch come between the replies.
|
|
@@ -11,14 +11,16 @@ export declare class Client {
|
|
|
11
11
|
/** The project of the host. `connect` sets it. */
|
|
12
12
|
project: string;
|
|
13
13
|
constructor(socket: Socket);
|
|
14
|
+
/** Whether the connection closed. A request that failed then may have reached the host or not. */
|
|
15
|
+
get ended(): boolean;
|
|
14
16
|
request<T extends Request["type"]>(request: Extract<Request, {
|
|
15
17
|
type: T;
|
|
16
18
|
}>): Promise<Replies[T]>;
|
|
17
19
|
/**
|
|
18
|
-
* Sends the `watch` request of `handle
|
|
19
|
-
* Other requests can follow on this client. Call it once.
|
|
20
|
+
* Sends the `watch` request of `target`, a handle or a conversation ID. Its `lines` are the events of the watch, and
|
|
21
|
+
* end when the connection closes. Other requests can follow on this client. Call it once.
|
|
20
22
|
*/
|
|
21
|
-
watch(
|
|
23
|
+
watch(target: string | Target): Promise<Replies["watch"] & {
|
|
22
24
|
lines: AsyncIterable<WatchLine>;
|
|
23
25
|
}>;
|
|
24
26
|
close(): void;
|
|
@@ -26,14 +28,16 @@ export declare class Client {
|
|
|
26
28
|
abort(cause: unknown): void;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
* A connection to the host of the project of `HARNESS_PROJECT` or the cwd (see `resolveProject`),
|
|
30
|
-
* When no host listens and `start` is true, it starts one, detached, with its output in `host.log`,
|
|
31
|
-
* connect for 10 s, or until `signal` aborts. When no host listens and `start` is false, it is undefined.
|
|
31
|
+
* A connection to the host of `project`, or of the project of `HARNESS_PROJECT` or the cwd (see `resolveProject`),
|
|
32
|
+
* after its `hello`. When no host listens and `start` is true, it starts one, detached, with its output in `host.log`,
|
|
33
|
+
* and tries to connect for 10 s, or until `signal` aborts. When no host listens and `start` is false, it is undefined.
|
|
32
34
|
*/
|
|
33
35
|
export declare function connect(options: ConnectOptions): Promise<Client | undefined>;
|
|
34
36
|
type ConnectOptions = {
|
|
35
37
|
readonly start: boolean;
|
|
36
38
|
readonly signal?: AbortSignal | undefined;
|
|
39
|
+
/** `main` or a project name. Without it, the project comes from `HARNESS_PROJECT` and the cwd. */
|
|
40
|
+
readonly project?: string;
|
|
37
41
|
};
|
|
38
42
|
type OneShot<T extends Request["type"]> = Extract<Request, {
|
|
39
43
|
type: T;
|
package/dist/client.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createConnection } from "node:net";
|
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { Value } from "typebox/value";
|
|
7
|
-
import { MAIN_PROJECT, resolveProject } from "./projects.js";
|
|
7
|
+
import { checkProject, MAIN_PROJECT, resolveProject } from "./projects.js";
|
|
8
8
|
import { cliPath, onLines, PROTOCOL_VERSION, statePaths, } from "./protocol.js";
|
|
9
9
|
const START_TIMEOUT_MS = 10_000;
|
|
10
10
|
/** A line from the host: a reply, or an event of the watch. The value of a reply and the event are not checked here. */
|
|
@@ -50,6 +50,10 @@ export class Client {
|
|
|
50
50
|
socket.on("error", () => this.#end());
|
|
51
51
|
socket.on("close", () => this.#end());
|
|
52
52
|
}
|
|
53
|
+
/** Whether the connection closed. A request that failed then may have reached the host or not. */
|
|
54
|
+
get ended() {
|
|
55
|
+
return this.#ended;
|
|
56
|
+
}
|
|
53
57
|
request(request) {
|
|
54
58
|
if (this.#ended)
|
|
55
59
|
return Promise.reject(new Error("The harness host closed the connection"));
|
|
@@ -61,13 +65,14 @@ export class Client {
|
|
|
61
65
|
return reply.promise;
|
|
62
66
|
}
|
|
63
67
|
/**
|
|
64
|
-
* Sends the `watch` request of `handle
|
|
65
|
-
* Other requests can follow on this client. Call it once.
|
|
68
|
+
* Sends the `watch` request of `target`, a handle or a conversation ID. Its `lines` are the events of the watch, and
|
|
69
|
+
* end when the connection closes. Other requests can follow on this client. Call it once.
|
|
66
70
|
*/
|
|
67
|
-
async watch(
|
|
71
|
+
async watch(target) {
|
|
68
72
|
const lines = new Readable({ objectMode: true, read() { } });
|
|
69
73
|
this.#lines = lines;
|
|
70
|
-
const
|
|
74
|
+
const ref = target instanceof Object ? target : { handle: target };
|
|
75
|
+
const reply = await this.request({ type: "watch", ...ref });
|
|
71
76
|
return { ...reply, lines };
|
|
72
77
|
}
|
|
73
78
|
close() {
|
|
@@ -87,12 +92,12 @@ export class Client {
|
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
/**
|
|
90
|
-
* A connection to the host of the project of `HARNESS_PROJECT` or the cwd (see `resolveProject`),
|
|
91
|
-
* When no host listens and `start` is true, it starts one, detached, with its output in `host.log`,
|
|
92
|
-
* connect for 10 s, or until `signal` aborts. When no host listens and `start` is false, it is undefined.
|
|
95
|
+
* A connection to the host of `project`, or of the project of `HARNESS_PROJECT` or the cwd (see `resolveProject`),
|
|
96
|
+
* after its `hello`. When no host listens and `start` is true, it starts one, detached, with its output in `host.log`,
|
|
97
|
+
* and tries to connect for 10 s, or until `signal` aborts. When no host listens and `start` is false, it is undefined.
|
|
93
98
|
*/
|
|
94
99
|
export async function connect(options) {
|
|
95
|
-
const project = resolveProject(process.env, process.cwd());
|
|
100
|
+
const project = options.project === undefined ? resolveProject(process.env, process.cwd()) : checkProject(options.project);
|
|
96
101
|
const paths = statePaths(project);
|
|
97
102
|
let socket = await tryConnect(paths.socket);
|
|
98
103
|
if (socket === undefined) {
|
package/dist/context.js
CHANGED
|
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import { createFindTool, createGrepTool, createLsTool, DefaultPackageManager, formatSkillsForPrompt, getAgentDir, loadProjectContextFiles, loadSkills, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { defineExtension, defineTool, section, } from "@earendil-works/pi-durable";
|
|
9
9
|
import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-durable/tools";
|
|
10
|
+
import { errorMessage } from "./protocol.js";
|
|
10
11
|
import { savedTrust } from "./trust.js";
|
|
11
12
|
/** A Pi coding agent tool as a durable tool that runs in the calling conversation's directory. */
|
|
12
13
|
function piTool(create) {
|
|
@@ -45,7 +46,7 @@ async function resourcesOf(cwd) {
|
|
|
45
46
|
return await found;
|
|
46
47
|
}
|
|
47
48
|
catch (error) {
|
|
48
|
-
process.stderr.write(`warning: ${cwd}: ${
|
|
49
|
+
process.stderr.write(`warning: ${cwd}: ${errorMessage(error)}; no project instructions or skills\n`);
|
|
49
50
|
return { context: [], skills: [] };
|
|
50
51
|
}
|
|
51
52
|
}
|
package/dist/herdr.d.ts
CHANGED
|
@@ -1,38 +1,40 @@
|
|
|
1
|
+
import { type MirrorTarget } from "./mirror.ts";
|
|
1
2
|
import { type ChildInfo } from "./protocol.ts";
|
|
2
|
-
export type ViewerTarget = {
|
|
3
|
-
title(child: ChildInfo): string;
|
|
4
|
-
project: string[];
|
|
5
|
-
resume: string[];
|
|
6
|
-
};
|
|
7
3
|
export type AgentReporter = {
|
|
8
4
|
report(busy: boolean): void;
|
|
9
5
|
release(): Promise<void>;
|
|
10
6
|
};
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
export declare function viewerTarget(handle: string, project: string): ViewerTarget;
|
|
17
|
-
/**
|
|
18
|
-
* The words of the command that runs the viewer of `handle`, with this Node and this package's CLI, so it does not
|
|
19
|
-
* need the CLI on PATH. Herdr wants a plain command name as the first word of a resume command, so it starts with
|
|
20
|
-
* `env`.
|
|
21
|
-
*/
|
|
22
|
-
export declare function viewerCommand(handle: string, project: string): string[];
|
|
7
|
+
/** Whether this process runs in a Herdr pane, also as the Pi that the CLI starts there. */
|
|
8
|
+
export declare function inHerdr(env: NodeJS.ProcessEnv): boolean;
|
|
9
|
+
/** The environment of the Pi that the CLI starts: in a Herdr pane, `HARNESS_HERDR_PANE` in place of `HERDR_ENV`. */
|
|
10
|
+
export declare function piEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
23
11
|
/**
|
|
24
12
|
* Runs `herdr` with `args` and gives its output. When `herdr` fails, it rejects with the stderr of `herdr`, or with the
|
|
25
13
|
* error of `execFile` when stderr is empty.
|
|
26
14
|
*/
|
|
27
15
|
export declare function herdr(args: readonly string[], env: NodeJS.ProcessEnv): Promise<string>;
|
|
28
16
|
/**
|
|
29
|
-
* Opens a Herdr tab that
|
|
17
|
+
* Opens a Herdr tab that opens `child` in Pi, on the project of `env` and this process's cwd. It rejects when a
|
|
30
18
|
* `herdr` call fails or gives no pane ID.
|
|
31
19
|
*/
|
|
32
|
-
export declare function
|
|
20
|
+
export declare function openChildTab(child: ChildInfo, env: NodeJS.ProcessEnv): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Opens the children library conversation `conversation` in Pi, on the project of `env` and this process's cwd: in a
|
|
23
|
+
* new pane to the right of the Herdr pane `place.pane`, or in a new tab of the Herdr workspace `place.workspace`. The
|
|
24
|
+
* pane or tab gets the key of the conversation as its label. For a conversation, `ChildInfo` gives the same ID in
|
|
25
|
+
* `handle` and in `conversation`. It rejects when a `herdr` call fails or gives no pane ID, and then makes no more
|
|
26
|
+
* calls.
|
|
27
|
+
*/
|
|
28
|
+
export declare function openConversationPane(conversation: ChildInfo, place: {
|
|
29
|
+
pane: string;
|
|
30
|
+
workspace?: never;
|
|
31
|
+
} | {
|
|
32
|
+
workspace: string;
|
|
33
|
+
pane?: never;
|
|
34
|
+
}, env: NodeJS.ProcessEnv): Promise<void>;
|
|
33
35
|
/**
|
|
34
|
-
* Reports the state of the agent `label` to the Herdr pane of
|
|
35
|
-
* the resume command. Both methods do nothing
|
|
36
|
-
* The calls run one after another, so `--seq` reaches Herdr in order.
|
|
36
|
+
* Reports the state of the agent `label` to the Herdr pane of `HARNESS_HERDR_PANE`, with the command that opens
|
|
37
|
+
* `target` as the resume command. Both methods do nothing without that variable, so only a Pi that the CLI
|
|
38
|
+
* started in a Herdr pane reports. The calls run one after another, so `--seq` reaches Herdr in order.
|
|
37
39
|
*/
|
|
38
|
-
export declare function agentReporter(env: NodeJS.ProcessEnv, label: string,
|
|
40
|
+
export declare function agentReporter(env: NodeJS.ProcessEnv, label: string, target: MirrorTarget): AgentReporter;
|
package/dist/herdr.js
CHANGED
|
@@ -1,31 +1,53 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { Value } from "typebox/value";
|
|
4
|
+
import { harnessArgs } from "./mirror.js";
|
|
4
5
|
import { resolveProject } from "./projects.js";
|
|
5
|
-
import {
|
|
6
|
+
import { childLabel, cliPath } from "./protocol.js";
|
|
6
7
|
const HERDR_SOURCE = "custom:harness";
|
|
8
|
+
/**
|
|
9
|
+
* The variable that gives the Herdr pane to the Pi that the CLI starts there, in place of `HERDR_ENV`. Without
|
|
10
|
+
* `HERDR_ENV`, Herdr's own Pi integration does not report, so the pane shows the state of the mirrored agent, which the
|
|
11
|
+
* extension reports, and not the state of Pi's own agent, which does not run.
|
|
12
|
+
*/
|
|
13
|
+
const PANE_VARIABLE = "HARNESS_HERDR_PANE";
|
|
7
14
|
/** The part of the output of `herdr tab create` that has the pane of the tab. */
|
|
8
15
|
const TabCreated = Type.Object({ result: Type.Object({ root_pane: Type.Object({ pane_id: Type.String() }) }) });
|
|
16
|
+
/** The part of the output of `herdr pane split` that has the new pane. */
|
|
17
|
+
const PaneSplit = Type.Object({ result: Type.Object({ pane: Type.Object({ pane_id: Type.String() }) }) });
|
|
9
18
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `harness --project <project> attach <handle>`.
|
|
19
|
+
* The words of the command that opens `target` in Pi, with this Node and this package's CLI, so it does not need the
|
|
20
|
+
* CLI on PATH. Herdr wants a plain command name as the first word of a resume command, so it starts with `env`.
|
|
13
21
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (handle === AGENT_HANDLE)
|
|
17
|
-
return { title: childLabel, project: [], resume: [project] };
|
|
18
|
-
return { title: runDisplayName, project: ["--project", project], resume: ["attach", handle] };
|
|
22
|
+
function openCommand(target) {
|
|
23
|
+
return ["env", process.execPath, cliPath(), ...harnessArgs(target)];
|
|
19
24
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/** The `--env` flags of `herdr` that give `XDG_STATE_HOME` and `XDG_CONFIG_HOME` of `env`, when they are set. */
|
|
26
|
+
function envFlags(env) {
|
|
27
|
+
// The new pane must find the same host and projects as `statePaths()` in protocol.ts and `projectsPath()` in
|
|
28
|
+
// projects.ts.
|
|
29
|
+
const state = env.XDG_STATE_HOME;
|
|
30
|
+
const config = env.XDG_CONFIG_HOME;
|
|
31
|
+
return [
|
|
32
|
+
...(state ? ["--env", `XDG_STATE_HOME=${state}`] : []),
|
|
33
|
+
...(config ? ["--env", `XDG_CONFIG_HOME=${config}`] : []),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
/** Runs the command that opens `target` in Pi in the Herdr pane `pane`, with each word in single quotes. */
|
|
37
|
+
async function runOpen(pane, target, env) {
|
|
38
|
+
const command = openCommand(target)
|
|
39
|
+
.map((word) => `'${word.replaceAll("'", "'\\''")}'`)
|
|
40
|
+
.join(" ");
|
|
41
|
+
await herdr(["pane", "run", pane, command], env);
|
|
42
|
+
}
|
|
43
|
+
/** Whether this process runs in a Herdr pane, also as the Pi that the CLI starts there. */
|
|
44
|
+
export function inHerdr(env) {
|
|
45
|
+
return Boolean(env.HERDR_ENV || env[PANE_VARIABLE]);
|
|
46
|
+
}
|
|
47
|
+
/** The environment of the Pi that the CLI starts: in a Herdr pane, `HARNESS_HERDR_PANE` in place of `HERDR_ENV`. */
|
|
48
|
+
export function piEnv(env) {
|
|
49
|
+
const { HERDR_ENV, ...rest } = env;
|
|
50
|
+
return HERDR_ENV && env.HERDR_PANE_ID ? { ...rest, [PANE_VARIABLE]: env.HERDR_PANE_ID } : env;
|
|
29
51
|
}
|
|
30
52
|
/**
|
|
31
53
|
* Runs `herdr` with `args` and gives its output. When `herdr` fails, it rejects with the stderr of `herdr`, or with the
|
|
@@ -42,24 +64,61 @@ export function herdr(args, env) {
|
|
|
42
64
|
});
|
|
43
65
|
}
|
|
44
66
|
/**
|
|
45
|
-
* Opens a Herdr tab that
|
|
67
|
+
* Opens a Herdr tab that opens `child` in Pi, on the project of `env` and this process's cwd. It rejects when a
|
|
46
68
|
* `herdr` call fails or gives no pane ID.
|
|
47
69
|
*/
|
|
48
|
-
export async function
|
|
70
|
+
export async function openChildTab(child, env) {
|
|
49
71
|
const project = resolveProject(env, process.cwd());
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
const created = await herdr(["tab", "create", "--cwd", child.cwd, "--label", childLabel(child), "--focus", ...envFlags(env)], env);
|
|
73
|
+
let output;
|
|
74
|
+
try {
|
|
75
|
+
output = JSON.parse(created);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
output = undefined;
|
|
79
|
+
}
|
|
80
|
+
if (!Value.Check(TabCreated, output))
|
|
81
|
+
throw new Error(`herdr tab create gave no pane ID: ${created.trim()}`);
|
|
82
|
+
await runOpen(output.result.root_pane.pane_id, { project, handle: child.handle }, env);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Opens the children library conversation `conversation` in Pi, on the project of `env` and this process's cwd: in a
|
|
86
|
+
* new pane to the right of the Herdr pane `place.pane`, or in a new tab of the Herdr workspace `place.workspace`. The
|
|
87
|
+
* pane or tab gets the key of the conversation as its label. For a conversation, `ChildInfo` gives the same ID in
|
|
88
|
+
* `handle` and in `conversation`. It rejects when a `herdr` call fails or gives no pane ID, and then makes no more
|
|
89
|
+
* calls.
|
|
90
|
+
*/
|
|
91
|
+
export async function openConversationPane(conversation, place, env) {
|
|
92
|
+
const project = resolveProject(env, process.cwd());
|
|
93
|
+
const target = { project, conversation: conversation.handle };
|
|
94
|
+
const label = childLabel(conversation);
|
|
95
|
+
if (place.pane !== undefined) {
|
|
96
|
+
const split = await herdr(["pane", "split", place.pane, "--direction", "right", "--cwd", conversation.cwd, ...envFlags(env)], env);
|
|
97
|
+
let output;
|
|
98
|
+
try {
|
|
99
|
+
output = JSON.parse(split);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
output = undefined;
|
|
103
|
+
}
|
|
104
|
+
if (!Value.Check(PaneSplit, output))
|
|
105
|
+
throw new Error(`herdr pane split gave no pane ID: ${split.trim()}`);
|
|
106
|
+
const pane = output.result.pane.pane_id;
|
|
107
|
+
await herdr(["pane", "rename", pane, label], env);
|
|
108
|
+
await runOpen(pane, target, env);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
53
111
|
const created = await herdr([
|
|
54
112
|
"tab",
|
|
55
113
|
"create",
|
|
114
|
+
"--workspace",
|
|
115
|
+
place.workspace,
|
|
56
116
|
"--cwd",
|
|
57
|
-
|
|
117
|
+
conversation.cwd,
|
|
58
118
|
"--label",
|
|
59
|
-
|
|
119
|
+
label,
|
|
60
120
|
"--focus",
|
|
61
|
-
...(
|
|
62
|
-
...(config ? ["--env", `XDG_CONFIG_HOME=${config}`] : []),
|
|
121
|
+
...envFlags(env),
|
|
63
122
|
], env);
|
|
64
123
|
let output;
|
|
65
124
|
try {
|
|
@@ -70,20 +129,16 @@ export async function openViewerTab(child, env) {
|
|
|
70
129
|
}
|
|
71
130
|
if (!Value.Check(TabCreated, output))
|
|
72
131
|
throw new Error(`herdr tab create gave no pane ID: ${created.trim()}`);
|
|
73
|
-
|
|
74
|
-
const viewer = viewerCommand(child.handle, project)
|
|
75
|
-
.map((word) => `'${word.replaceAll("'", "'\\''")}'`)
|
|
76
|
-
.join(" ");
|
|
77
|
-
await herdr(["pane", "run", pane, viewer], env);
|
|
132
|
+
await runOpen(output.result.root_pane.pane_id, target, env);
|
|
78
133
|
}
|
|
79
134
|
/**
|
|
80
|
-
* Reports the state of the agent `label` to the Herdr pane of
|
|
81
|
-
* the resume command. Both methods do nothing
|
|
82
|
-
* The calls run one after another, so `--seq` reaches Herdr in order.
|
|
135
|
+
* Reports the state of the agent `label` to the Herdr pane of `HARNESS_HERDR_PANE`, with the command that opens
|
|
136
|
+
* `target` as the resume command. Both methods do nothing without that variable, so only a Pi that the CLI
|
|
137
|
+
* started in a Herdr pane reports. The calls run one after another, so `--seq` reaches Herdr in order.
|
|
83
138
|
*/
|
|
84
|
-
export function agentReporter(env, label,
|
|
85
|
-
const pane = env
|
|
86
|
-
const resume = pane === undefined ? [] : ["--", ...
|
|
139
|
+
export function agentReporter(env, label, target) {
|
|
140
|
+
const pane = env[PANE_VARIABLE] || undefined;
|
|
141
|
+
const resume = pane === undefined ? [] : ["--", ...openCommand(target)];
|
|
87
142
|
let calls = Promise.resolve();
|
|
88
143
|
let seq = 0;
|
|
89
144
|
let reported;
|
package/dist/host.d.ts
CHANGED