pi-herdr-agents 3.0.3 → 3.2.0
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/AGENTS.md +3 -2
- package/CHANGELOG.md +19 -1
- package/CONTEXT.md +33 -5
- package/README.md +166 -14
- package/RELEASING.md +4 -2
- package/docs/README.md +9 -0
- package/docs/worktree-subagents.md +1 -0
- package/maestro/adapters/pi/launch.ts +26 -4
- package/maestro/adapters/pi/pi-harness-adapter.ts +4 -0
- package/maestro/core/config/model-config.ts +83 -10
- package/maestro/core/config/sidebar-config.ts +63 -0
- package/maestro/core/config/task-model-init.ts +208 -13
- package/maestro/core/config/task-model-types.ts +19 -1
- package/maestro/core/config/type-guards.ts +5 -0
- package/maestro/runtime/delegated-marker.ts +206 -0
- package/maestro/runtime/index.ts +2 -1
- package/maestro/runtime/pi-run-session.ts +58 -7
- package/maestro/runtime/task-model-init.ts +106 -9
- package/maestro/surfaces/herdr/herdr-socket.ts +165 -0
- package/package.json +6 -6
- package/pi-extension/subagents/index.ts +159 -13
- package/plugins/sidebar/README.md +242 -0
- package/plugins/sidebar/herdr-plugin.toml +21 -0
- package/plugins/sidebar/sidebar.mjs +238 -0
- package/skills/pi-herdr-agents/SKILL.md +10 -3
|
@@ -66,6 +66,12 @@ import {
|
|
|
66
66
|
type ModelRegistryAdapter,
|
|
67
67
|
} from "../core/routing.ts";
|
|
68
68
|
import { loadPaneConfig, type PaneConfig } from "../core/config/pane-config.ts";
|
|
69
|
+
import {
|
|
70
|
+
DelegatedMarkers,
|
|
71
|
+
herdrMarkerWriter,
|
|
72
|
+
type DelegatedMarker,
|
|
73
|
+
type MarkerWriter,
|
|
74
|
+
} from "./delegated-marker.ts";
|
|
69
75
|
import {
|
|
70
76
|
createRunSession,
|
|
71
77
|
type RunSession,
|
|
@@ -327,6 +333,8 @@ export interface PiRunSessionInfrastructure {
|
|
|
327
333
|
supervision: SupervisionCoordinator;
|
|
328
334
|
/** Kernel process-identity probe for worktree cancel; tests inject it. */
|
|
329
335
|
processProbe?: ProcessIdentityProbe;
|
|
336
|
+
/** Sidebar marker transport; defaults to this pane's Herdr socket. */
|
|
337
|
+
markerWriter?: MarkerWriter;
|
|
330
338
|
}
|
|
331
339
|
// pi-herdr-agents extension
|
|
332
340
|
export interface DefaultRunSessionOptions {
|
|
@@ -340,6 +348,8 @@ export interface DefaultRunSessionOptions {
|
|
|
340
348
|
hooks: PiRunSessionHooks;
|
|
341
349
|
persistent: PiPersistentHostOperations;
|
|
342
350
|
forcePolling: boolean;
|
|
351
|
+
/** Mark delegated children's panes for the Herdr sidebar plugin. */
|
|
352
|
+
sidebarMarkers?: boolean;
|
|
343
353
|
}
|
|
344
354
|
// pi-herdr-agents extension
|
|
345
355
|
export interface PiWorktreeHandoffInput {
|
|
@@ -395,6 +405,7 @@ interface PiEntry {
|
|
|
395
405
|
started: PiStartedMetadata;
|
|
396
406
|
completed?: PiCompletedMetadata;
|
|
397
407
|
readResumeResult?: PiSettlementIO["readResumeResult"];
|
|
408
|
+
marker?: DelegatedMarker;
|
|
398
409
|
}
|
|
399
410
|
// The previous facade carries its live composition explicitly across module replacement.
|
|
400
411
|
// No module-local lookup can accidentally rediscover an owner.
|
|
@@ -413,6 +424,8 @@ interface PiSessionOwner {
|
|
|
413
424
|
"surfaceProvider" | "supervision" | "processProbe"
|
|
414
425
|
>;
|
|
415
426
|
worktreeOperations: WorktreeOperations;
|
|
427
|
+
/** Absent outside Herdr: without a socket nothing can be marked. */
|
|
428
|
+
markers?: DelegatedMarkers;
|
|
416
429
|
}
|
|
417
430
|
|
|
418
431
|
// A detached close can outlive settlement without capturing a child, metadata,
|
|
@@ -426,6 +439,21 @@ function temporarySurfaceRelease(
|
|
|
426
439
|
};
|
|
427
440
|
}
|
|
428
441
|
|
|
442
|
+
function createDelegatedMarkers(
|
|
443
|
+
infrastructure: PiRunSessionInfrastructure | undefined,
|
|
444
|
+
probe: ProcessIdentityProbe | undefined,
|
|
445
|
+
): DelegatedMarkers | undefined {
|
|
446
|
+
// Injected infrastructure never falls back to the ambient Herdr socket.
|
|
447
|
+
if (infrastructure) {
|
|
448
|
+
const write = infrastructure.markerWriter;
|
|
449
|
+
return write ? new DelegatedMarkers({ write, probe }) : undefined;
|
|
450
|
+
}
|
|
451
|
+
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
452
|
+
return socketPath
|
|
453
|
+
? new DelegatedMarkers({ write: herdrMarkerWriter(socketPath), probe })
|
|
454
|
+
: undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
429
457
|
// Capture only resume presentation scalars, not the child or its owning registry.
|
|
430
458
|
function resumeResultReader(
|
|
431
459
|
sessionFile: string,
|
|
@@ -682,6 +710,20 @@ export function createDefaultRunSession(
|
|
|
682
710
|
// Replacements must invoke the original kernel's invocation closure, which
|
|
683
711
|
// reads this shared current binding, not the old facade's options.
|
|
684
712
|
if (adopted) resumePi = state.resumePi;
|
|
713
|
+
// Live markers and their sequence survive a reload with their runs.
|
|
714
|
+
state.markers ??= createDelegatedMarkers(
|
|
715
|
+
options.infrastructure,
|
|
716
|
+
state.infrastructure.processProbe,
|
|
717
|
+
);
|
|
718
|
+
// Turning the setting off retires every marker now, pending ones included;
|
|
719
|
+
// a retired marker never writes live again, even if the setting returns.
|
|
720
|
+
if (!options.sidebarMarkers)
|
|
721
|
+
for (const entry of state.entries.values()) entry.marker?.retire();
|
|
722
|
+
|
|
723
|
+
/** Markers for new launches, only while the sidebar setting is on. */
|
|
724
|
+
function enabledMarkers(): DelegatedMarkers | undefined {
|
|
725
|
+
return state.options.sidebarMarkers ? state.markers : undefined;
|
|
726
|
+
}
|
|
685
727
|
|
|
686
728
|
function prune(id: string) {
|
|
687
729
|
if (!state.kernel.getTask(id)) state.entries.delete(id);
|
|
@@ -712,11 +754,11 @@ export function createDefaultRunSession(
|
|
|
712
754
|
};
|
|
713
755
|
}
|
|
714
756
|
function observed(child: PiRunRecord, observation: RunObservation) {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
)
|
|
719
|
-
|
|
757
|
+
const entry = [...state.entries.values()].find(
|
|
758
|
+
(candidate) => candidate.record === child,
|
|
759
|
+
);
|
|
760
|
+
if (child.lifecycle.delivery === "suppressed" || !entry) return;
|
|
761
|
+
entry.marker?.renew();
|
|
720
762
|
if (observation.kind === "local-evidence" && child.persistent) {
|
|
721
763
|
try {
|
|
722
764
|
state.options.persistent.drain(child, io);
|
|
@@ -745,6 +787,7 @@ export function createDefaultRunSession(
|
|
|
745
787
|
parentRuntime: snapshot.parentRuntime,
|
|
746
788
|
supervision: state.infrastructure.supervision,
|
|
747
789
|
processProbe: state.infrastructure.processProbe,
|
|
790
|
+
recordProcessIdentity: enabledMarkers() !== undefined,
|
|
748
791
|
operations: launchOperations(snapshot),
|
|
749
792
|
onObservation(child, kind) {
|
|
750
793
|
const at = Date.now();
|
|
@@ -794,7 +837,10 @@ export function createDefaultRunSession(
|
|
|
794
837
|
};
|
|
795
838
|
if (suppressed)
|
|
796
839
|
child.lifecycle = markDelivery(child.lifecycle, "suppressed");
|
|
797
|
-
else
|
|
840
|
+
else {
|
|
841
|
+
entry.marker = enabledMarkers()?.attach(child);
|
|
842
|
+
state.entries.set(task.id, entry);
|
|
843
|
+
}
|
|
798
844
|
return {
|
|
799
845
|
handle,
|
|
800
846
|
adapter: owner,
|
|
@@ -805,6 +851,8 @@ export function createDefaultRunSession(
|
|
|
805
851
|
),
|
|
806
852
|
observe: (at) => hydrate(child, at, "refresh"),
|
|
807
853
|
finalize: async (result) => {
|
|
854
|
+
// Clear before delivery; a retained worktree root keeps its pane.
|
|
855
|
+
entry.marker?.retire();
|
|
808
856
|
const at = Date.now();
|
|
809
857
|
let evidence = result.evidence;
|
|
810
858
|
if (evidence)
|
|
@@ -1153,8 +1201,11 @@ export function createDefaultRunSession(
|
|
|
1153
1201
|
suppress(id) {
|
|
1154
1202
|
for (const acquisition of state.pending)
|
|
1155
1203
|
if (acquisition.taskId === id) acquisition.suppressed = true;
|
|
1156
|
-
const
|
|
1204
|
+
const suppressed = live(id);
|
|
1205
|
+
const r = suppressed?.record;
|
|
1157
1206
|
if (r) r.lifecycle = markDelivery(r.lifecycle, "suppressed");
|
|
1207
|
+
// Nothing renews an unsupervised run's marker; clear it now.
|
|
1208
|
+
suppressed?.marker?.retire();
|
|
1158
1209
|
state.kernel.suppress(id);
|
|
1159
1210
|
state.entries.delete(id);
|
|
1160
1211
|
},
|
|
@@ -1,22 +1,119 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildTaskModelBrief,
|
|
3
3
|
buildTaskModelInitPrompt,
|
|
4
|
+
collectApprovalOffers,
|
|
5
|
+
DIRECT_WRITER_DESTINATION,
|
|
6
|
+
type NonEmpty,
|
|
7
|
+
type OfferCallback,
|
|
8
|
+
openApprovalOffer,
|
|
9
|
+
selectApprovalRoute,
|
|
10
|
+
type TaskModelBrief,
|
|
11
|
+
type TaskModelFacts,
|
|
12
|
+
type TaskModelInitDestination,
|
|
4
13
|
type TaskModelRegistryProjector,
|
|
5
14
|
type TaskModelRegistrySnapshot,
|
|
6
15
|
} from "../core/config/task-model-init.ts";
|
|
7
|
-
import type {
|
|
16
|
+
import type { ModelConfigSnapshot } from "../core/config/model-config.ts";
|
|
8
17
|
import { projectTaskModelRegistry } from "../adapters/pi/task-model-init.ts";
|
|
9
18
|
|
|
10
|
-
/**
|
|
11
|
-
export
|
|
19
|
+
/** What one init request did. Only `started` submitted a prompt. */
|
|
20
|
+
export type TaskModelInitOutcome =
|
|
21
|
+
| { kind: "started"; destination: string }
|
|
22
|
+
| { kind: "not-started"; reason: string };
|
|
23
|
+
|
|
24
|
+
export interface TaskModelInitInput {
|
|
25
|
+
preferences: string;
|
|
26
|
+
/** Saved settings and the revision of the same bytes; throws when invalid. */
|
|
27
|
+
readConfig(): ModelConfigSnapshot;
|
|
12
28
|
/** The host supplies its actual active registry to this projector exactly once. */
|
|
13
29
|
projectActiveRegistry(
|
|
14
30
|
project: TaskModelRegistryProjector,
|
|
15
31
|
): TaskModelRegistrySnapshot;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
/** Emits the approval request for this brief and returns when listeners did. */
|
|
33
|
+
emitApprovalRequest(brief: TaskModelBrief, offer: OfferCallback): void;
|
|
34
|
+
isToolActive(name: string): boolean;
|
|
35
|
+
/** Hands the prompt to Pi; a throw means Pi did not take it. */
|
|
36
|
+
submit(prompt: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Listeners share the brief; freezing it keeps the prompt equal to what they saw. */
|
|
40
|
+
function deepFreeze<T>(value: T): T {
|
|
41
|
+
if (value == null || Object(value) !== value) return value;
|
|
42
|
+
// SAFETY: Object(value) === value holds only for objects and functions.
|
|
43
|
+
for (const item of Object.values(value as object)) deepFreeze(item);
|
|
44
|
+
return Object.freeze(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isNonEmpty<T>(values: T[]): values is NonEmpty<T> {
|
|
48
|
+
return values.length > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Capture one brief, pick where its proposal goes, and submit the prompt. It
|
|
53
|
+
* runs synchronously, so nothing can change between selection and submission.
|
|
54
|
+
* An opened offer that is not submitted is cancelled before this returns.
|
|
55
|
+
*/
|
|
56
|
+
export function startTaskModelInit(
|
|
57
|
+
input: TaskModelInitInput,
|
|
58
|
+
): TaskModelInitOutcome {
|
|
59
|
+
const notStarted = (reason: string): TaskModelInitOutcome => ({
|
|
60
|
+
kind: "not-started",
|
|
61
|
+
reason,
|
|
62
|
+
});
|
|
63
|
+
let snapshot: ModelConfigSnapshot;
|
|
64
|
+
try {
|
|
65
|
+
snapshot = input.readConfig();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return notStarted(error instanceof Error ? error.message : String(error));
|
|
68
|
+
}
|
|
69
|
+
const { models } = input.projectActiveRegistry(projectTaskModelRegistry);
|
|
70
|
+
if (!isNonEmpty<TaskModelFacts>(models))
|
|
71
|
+
return notStarted(
|
|
72
|
+
"the active registry has no authenticated models, so there is nothing to rank. Configure a provider, then run /subagents-init again",
|
|
73
|
+
);
|
|
74
|
+
const brief = deepFreeze(
|
|
75
|
+
buildTaskModelBrief({
|
|
76
|
+
models,
|
|
77
|
+
current: snapshot.config,
|
|
78
|
+
configRevision: snapshot.revision,
|
|
79
|
+
preferences: input.preferences,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
const route = selectApprovalRoute(
|
|
83
|
+
collectApprovalOffers((offer) => input.emitApprovalRequest(brief, offer)),
|
|
84
|
+
);
|
|
85
|
+
let destination: TaskModelInitDestination;
|
|
86
|
+
let cancel: () => string | undefined = () => undefined;
|
|
87
|
+
switch (route.kind) {
|
|
88
|
+
case "direct":
|
|
89
|
+
destination = DIRECT_WRITER_DESTINATION;
|
|
90
|
+
break;
|
|
91
|
+
case "refused":
|
|
92
|
+
return notStarted(route.reason);
|
|
93
|
+
case "offer": {
|
|
94
|
+
const opened = openApprovalOffer(route.offer);
|
|
95
|
+
if (opened.kind === "blocked") return notStarted(opened.reason);
|
|
96
|
+
destination = opened.destination;
|
|
97
|
+
cancel = opened.cancel;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
default: {
|
|
101
|
+
const _exhaustive: never = route;
|
|
102
|
+
return _exhaustive;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const cancelled = (reason: string): TaskModelInitOutcome => {
|
|
106
|
+
const problem = cancel();
|
|
107
|
+
return notStarted(problem ? `${reason}; ${problem}` : reason);
|
|
108
|
+
};
|
|
109
|
+
if (!input.isToolActive(destination.toolName))
|
|
110
|
+
return cancelled(`${destination.toolName} is not an active tool`);
|
|
111
|
+
try {
|
|
112
|
+
input.submit(buildTaskModelInitPrompt(brief, destination));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return cancelled(
|
|
115
|
+
`Pi did not accept the init prompt (${error instanceof Error ? error.message : String(error)})`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return { kind: "started", destination: destination.toolName };
|
|
22
119
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
import {
|
|
3
|
+
isNonEmptyString,
|
|
4
|
+
isRecord,
|
|
5
|
+
isString,
|
|
6
|
+
type JsonObject,
|
|
7
|
+
} from "../../core/config/type-guards.ts";
|
|
8
|
+
|
|
9
|
+
export const HERDR_SOCKET_TIMEOUT_MS = 2_000;
|
|
10
|
+
export const HERDR_SOCKET_MAX_REPLY_BYTES = 64 * 1024;
|
|
11
|
+
const MAX_REPORTED_CHARS = 200;
|
|
12
|
+
|
|
13
|
+
export type HerdrSocketReply =
|
|
14
|
+
| { kind: "result"; result: JsonObject }
|
|
15
|
+
/** Herdr answered with an error object. */
|
|
16
|
+
| { kind: "error"; code: string; message: string }
|
|
17
|
+
/** No usable answer: refused, closed, timed out, oversized, malformed, or another id. */
|
|
18
|
+
| { kind: "failed"; reason: string };
|
|
19
|
+
|
|
20
|
+
function bounded(text: string): string {
|
|
21
|
+
return text.replace(/\p{Cc}+/gu, " ").slice(0, MAX_REPORTED_CHARS);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let requestCount = 0;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One request on one connection: Herdr closes ordinary connections after a
|
|
28
|
+
* reply. Settles once with the first newline-terminated reply, or fails on a
|
|
29
|
+
* timeout, an oversized reply, or a connection that ends first. The socket
|
|
30
|
+
* and timer are unreferenced, so a pending request never keeps Pi running.
|
|
31
|
+
*/
|
|
32
|
+
export function requestHerdrSocket(
|
|
33
|
+
socketPath: string,
|
|
34
|
+
method: string,
|
|
35
|
+
params: JsonObject,
|
|
36
|
+
options: { timeoutMs?: number; maxReplyBytes?: number } = {},
|
|
37
|
+
): Promise<HerdrSocketReply> {
|
|
38
|
+
const timeoutMs = options.timeoutMs ?? HERDR_SOCKET_TIMEOUT_MS;
|
|
39
|
+
const maxReplyBytes = options.maxReplyBytes ?? HERDR_SOCKET_MAX_REPLY_BYTES;
|
|
40
|
+
const id = `pi-herdr-agents:${process.pid}:${++requestCount}`;
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let settled = false;
|
|
43
|
+
let received = Buffer.alloc(0);
|
|
44
|
+
const socket = connect(socketPath);
|
|
45
|
+
socket.unref();
|
|
46
|
+
const finish = (reply: HerdrSocketReply) => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
socket.destroy();
|
|
51
|
+
resolve(reply);
|
|
52
|
+
};
|
|
53
|
+
const timer = setTimeout(
|
|
54
|
+
() =>
|
|
55
|
+
finish({
|
|
56
|
+
kind: "failed",
|
|
57
|
+
reason: `${method} got no reply in ${timeoutMs}ms`,
|
|
58
|
+
}),
|
|
59
|
+
timeoutMs,
|
|
60
|
+
);
|
|
61
|
+
timer.unref();
|
|
62
|
+
socket.on("connect", () => {
|
|
63
|
+
socket.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
64
|
+
});
|
|
65
|
+
socket.on("error", (error) =>
|
|
66
|
+
finish({ kind: "failed", reason: bounded(error.message) }),
|
|
67
|
+
);
|
|
68
|
+
socket.on("close", () =>
|
|
69
|
+
finish({
|
|
70
|
+
kind: "failed",
|
|
71
|
+
reason: `${method}: connection closed before a reply`,
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
socket.on("data", (chunk: Buffer) => {
|
|
75
|
+
received = Buffer.concat([received, chunk]);
|
|
76
|
+
const end = received.indexOf(10);
|
|
77
|
+
if ((end < 0 ? received.length : end) > maxReplyBytes) {
|
|
78
|
+
finish({
|
|
79
|
+
kind: "failed",
|
|
80
|
+
reason: `${method} reply exceeds ${maxReplyBytes} bytes`,
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (end >= 0)
|
|
85
|
+
finish(
|
|
86
|
+
parseHerdrSocketReply(received.subarray(0, end).toString("utf8"), id),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Herdr answers `ok` even when it ignores a write whose `seq` is not newer
|
|
94
|
+
* than the last one it applied from this source, so `answered` is not proof
|
|
95
|
+
* that the token changed.
|
|
96
|
+
*/
|
|
97
|
+
export type PaneTokenOutcome = "answered" | "pane-missing" | "failed";
|
|
98
|
+
|
|
99
|
+
export interface PaneTokenWrite {
|
|
100
|
+
paneId: string;
|
|
101
|
+
source: string;
|
|
102
|
+
seq: number;
|
|
103
|
+
token: string;
|
|
104
|
+
/** `null` clears the token. */
|
|
105
|
+
value: string | null;
|
|
106
|
+
ttlMs?: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One sequenced `pane.report_metadata` token patch. */
|
|
110
|
+
export async function reportPaneToken(
|
|
111
|
+
socketPath: string,
|
|
112
|
+
write: PaneTokenWrite,
|
|
113
|
+
options?: { timeoutMs?: number },
|
|
114
|
+
): Promise<PaneTokenOutcome> {
|
|
115
|
+
const params: JsonObject = {
|
|
116
|
+
pane_id: write.paneId,
|
|
117
|
+
source: write.source,
|
|
118
|
+
seq: write.seq,
|
|
119
|
+
tokens: { [write.token]: write.value },
|
|
120
|
+
};
|
|
121
|
+
if (write.ttlMs !== undefined) params.ttl_ms = write.ttlMs;
|
|
122
|
+
const reply = await requestHerdrSocket(
|
|
123
|
+
socketPath,
|
|
124
|
+
"pane.report_metadata",
|
|
125
|
+
params,
|
|
126
|
+
options,
|
|
127
|
+
);
|
|
128
|
+
if (reply.kind === "result") return "answered";
|
|
129
|
+
if (reply.kind === "error" && reply.code === "pane_not_found")
|
|
130
|
+
return "pane-missing";
|
|
131
|
+
return "failed";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Parses one reply line; a reply for another request id is never accepted. */
|
|
135
|
+
export function parseHerdrSocketReply(
|
|
136
|
+
line: string,
|
|
137
|
+
id: string,
|
|
138
|
+
): HerdrSocketReply {
|
|
139
|
+
let reply: JsonObject | undefined;
|
|
140
|
+
try {
|
|
141
|
+
const parsed: unknown = JSON.parse(line);
|
|
142
|
+
if (isRecord(parsed)) reply = parsed;
|
|
143
|
+
} catch {
|
|
144
|
+
// Malformed JSON is the same failure as a non-object reply below.
|
|
145
|
+
}
|
|
146
|
+
if (!reply) return { kind: "failed", reason: "reply is not a JSON object" };
|
|
147
|
+
if (reply.id !== id)
|
|
148
|
+
return {
|
|
149
|
+
kind: "failed",
|
|
150
|
+
reason: `reply id ${bounded(JSON.stringify(reply.id) ?? "missing")} is not ${id}`,
|
|
151
|
+
};
|
|
152
|
+
if (isRecord(reply.error))
|
|
153
|
+
return {
|
|
154
|
+
kind: "error",
|
|
155
|
+
code: isNonEmptyString(reply.error.code)
|
|
156
|
+
? bounded(reply.error.code)
|
|
157
|
+
: "herdr_error",
|
|
158
|
+
message: isString(reply.error.message)
|
|
159
|
+
? bounded(reply.error.message)
|
|
160
|
+
: "",
|
|
161
|
+
};
|
|
162
|
+
if (!isRecord(reply.result))
|
|
163
|
+
return { kind: "failed", reason: "reply has no result" };
|
|
164
|
+
return { kind: "result", result: reply.result };
|
|
165
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-herdr-agents",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Asynchronous Pi subagents in Herdr, with optional isolated Git worktrees",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"type": "module",
|
|
31
31
|
"scripts": {
|
|
32
32
|
"changelog": "auto-changelog",
|
|
33
|
-
"format": "biome format --write pi-extension test maestro",
|
|
34
|
-
"format:check": "biome format pi-extension test maestro",
|
|
35
|
-
"lint": "oxlint pi-extension test maestro",
|
|
36
|
-
"test": "node --experimental-strip-types --test test/test.ts test/worktree-cleanup.test.ts test/launch.test.ts test/runtime-routing.test.ts test/release-workflow.test.ts test/package-manifest.test.js test/maestro/*.test.ts",
|
|
33
|
+
"format": "biome format --write pi-extension test maestro plugins",
|
|
34
|
+
"format:check": "biome format pi-extension test maestro plugins",
|
|
35
|
+
"lint": "oxlint pi-extension test maestro plugins",
|
|
36
|
+
"test": "node --experimental-strip-types --test test/test.ts test/worktree-cleanup.test.ts test/launch.test.ts test/runtime-routing.test.ts test/release-workflow.test.ts test/package-manifest.test.js test/sidebar-plugin.test.js test/maestro/*.test.ts",
|
|
37
37
|
"test:integration": "node --experimental-strip-types --test --test-concurrency=1 test/integration/*.test.ts",
|
|
38
38
|
"test:integration:live": "PI_TEST_LIVE=1 node --experimental-strip-types --test --test-concurrency=1 test/integration/*.test.ts",
|
|
39
39
|
"version": "auto-changelog --package && git add CHANGELOG.md"
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@biomejs/biome": "2.5.11",
|
|
63
63
|
"@earendil-works/pi-ai": "^1.0.0",
|
|
64
|
-
"@earendil-works/pi-coding-agent": "^1.
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
65
65
|
"@earendil-works/pi-tui": "^1.0.0",
|
|
66
66
|
"@oxlint/plugins": "^1.80.0",
|
|
67
67
|
"@sinclair/typebox": "^0.34.52",
|