pi-herdr-agents 1.4.0 → 1.4.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/CHANGELOG.md +10 -1
- package/package.json +3 -2
- package/pi-extension/subagents/activity.ts +23 -19
- package/pi-extension/subagents/completion.ts +8 -14
- package/pi-extension/subagents/herdr.ts +56 -122
- package/pi-extension/subagents/index.ts +283 -163
- package/pi-extension/subagents/launch.ts +25 -9
- package/pi-extension/subagents/lifecycle.ts +20 -22
- package/pi-extension/subagents/model-config.ts +14 -12
- package/pi-extension/subagents/runtime-routing.ts +18 -13
- package/pi-extension/subagents/session.ts +29 -28
- package/pi-extension/subagents/status.ts +26 -15
- package/pi-extension/subagents/subagent-done.ts +12 -11
- package/pi-extension/subagents/type-guards.ts +39 -0
- package/pi-extension/subagents/workflow-worker.js +11 -5
- package/pi-extension/subagents/workflow.ts +62 -88
package/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
9
9
|
|
|
10
|
-
## [v1.4.
|
|
10
|
+
## [v1.4.1](https://github.com/giuseppecrj/pi-herdr-agents/compare/v1.4.0...v1.4.1)
|
|
11
|
+
|
|
12
|
+
### Commits
|
|
13
|
+
|
|
14
|
+
- chore: install anti-slop lint rules [`633766b`](https://github.com/giuseppecrj/pi-herdr-agents/commit/633766b434e9558df67650b458a7d4536ac9ffd6)
|
|
15
|
+
- refactor: satisfy anti-slop lint rules [`d9cbf1d`](https://github.com/giuseppecrj/pi-herdr-agents/commit/d9cbf1dbc9442971260414fd52ff20a49a2cde88)
|
|
16
|
+
- test: stop asserting a deterministic reviewer start order [`edd848e`](https://github.com/giuseppecrj/pi-herdr-agents/commit/edd848ee8057290a7d47f7260b65ba29d832bec5)
|
|
17
|
+
|
|
18
|
+
## [v1.4.0](https://github.com/giuseppecrj/pi-herdr-agents/compare/v1.3.1...v1.4.0) - 2026-08-31
|
|
11
19
|
|
|
12
20
|
### Commits
|
|
13
21
|
|
|
14
22
|
- test: disable commit signing in git fixtures [`c87e20d`](https://github.com/giuseppecrj/pi-herdr-agents/commit/c87e20dd3d4d7ff058b555759904de36bf881875)
|
|
23
|
+
- chore: release v1.4.0 [`0480689`](https://github.com/giuseppecrj/pi-herdr-agents/commit/0480689cc6914738406d0dab9c260744c7666df3)
|
|
15
24
|
- feat: state the coordinator contract in always-visible guidelines [`08b1dc6`](https://github.com/giuseppecrj/pi-herdr-agents/commit/08b1dc6ffdfbb4ef03289b0871cc4fbc48883032)
|
|
16
25
|
- fix: stop implying every writing subagent needs a worktree [`114c239`](https://github.com/giuseppecrj/pi-herdr-agents/commit/114c239f71666a8a9c9a2c8705197b70c5f4e7a2)
|
|
17
26
|
- fix: point reviewers of worktree results at the retained worktree path [`4d84718`](https://github.com/giuseppecrj/pi-herdr-agents/commit/4d84718ada823eba830195e865c327155f37a8dd)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-herdr-agents",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Asynchronous Pi subagents and approved review workflows in Herdr, with optional isolated Git worktrees",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -60,8 +60,9 @@
|
|
|
60
60
|
"@earendil-works/pi-ai": "^0.84.0",
|
|
61
61
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
62
62
|
"@earendil-works/pi-tui": "^0.84.0",
|
|
63
|
+
"@oxlint/plugins": "^1.80.0",
|
|
63
64
|
"@sinclair/typebox": "^0.34.52",
|
|
64
65
|
"auto-changelog": "^2.6.0",
|
|
65
|
-
"oxlint": "^1.
|
|
66
|
+
"oxlint": "^1.80.0"
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { isBoolean, isFiniteNumber, isPlainObject, isString } from "./type-guards.ts";
|
|
3
4
|
|
|
4
5
|
export type SubagentActivityPhase = "starting" | "active" | "waiting" | "done";
|
|
5
6
|
export type SubagentActivityScope = "agent" | "turn" | "provider" | "streaming" | "tool";
|
|
@@ -105,37 +106,40 @@ export function getSubagentActivityFile(artifactDir: string, runningChildId: str
|
|
|
105
106
|
return join(artifactDir, "subagent-activity", `${runningChildId}.json`);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
return value as Record<string, unknown>;
|
|
109
|
+
export function isSubagentActivityScope(value: any): value is SubagentActivityScope {
|
|
110
|
+
return isString(value) && KNOWN_SCOPES.has(value);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function
|
|
114
|
-
return
|
|
113
|
+
function requireObject(value: any) {
|
|
114
|
+
return isPlainObject(value) ? value : null;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function
|
|
117
|
+
function validateFiniteNumber(object: any, fieldName: string): string | null {
|
|
118
|
+
return isFiniteNumber(object[fieldName]) ? null : `${fieldName} must be finite`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateOptionalFiniteNumber(object: any, fieldName: string): string | null {
|
|
118
122
|
const value = object[fieldName];
|
|
119
|
-
return value == null ||
|
|
123
|
+
return value == null || isFiniteNumber(value) ? null : `${fieldName} must be finite when present`;
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
function validateInteger(object:
|
|
126
|
+
function validateInteger(object: any, fieldName: string): string | null {
|
|
123
127
|
return Number.isInteger(object[fieldName]) ? null : `${fieldName} must be an integer`;
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
function validateOptionalInteger(object:
|
|
130
|
+
function validateOptionalInteger(object: any, fieldName: string): string | null {
|
|
127
131
|
const value = object[fieldName];
|
|
128
132
|
return value == null || Number.isInteger(value) ? null : `${fieldName} must be an integer when present`;
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
function validateBoolean(object:
|
|
132
|
-
return
|
|
135
|
+
function validateBoolean(object: any, fieldName: string): string | null {
|
|
136
|
+
return isBoolean(object[fieldName]) ? null : `${fieldName} must be a boolean`;
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
function validateOptionalActivityString(object:
|
|
139
|
+
function validateOptionalActivityString(object: any, fieldName: string): string | null {
|
|
136
140
|
const value = object[fieldName];
|
|
137
141
|
if (value == null) return null;
|
|
138
|
-
if (
|
|
142
|
+
if (!isString(value)) return `${fieldName} must be a string when present`;
|
|
139
143
|
if (/\r|\n/.test(value)) return `${fieldName} must not contain newlines`;
|
|
140
144
|
return value.length <= MAX_ACTIVITY_STRING_LENGTH ? null : `${fieldName} is too long`;
|
|
141
145
|
}
|
|
@@ -144,21 +148,21 @@ function invalidActivity(error: string): ActivityReadResult {
|
|
|
144
148
|
return { ok: false, reason: "invalid", error };
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
function validateActivity(value:
|
|
151
|
+
function validateActivity(value: any, expectedRunningChildId: string): ActivityReadResult {
|
|
148
152
|
const object = requireObject(value);
|
|
149
153
|
if (!object) return invalidActivity("activity must be an object");
|
|
150
154
|
if (object.version !== 1) return invalidActivity("unsupported activity version");
|
|
151
|
-
if (
|
|
155
|
+
if (!isString(object.runningChildId)) return invalidActivity("runningChildId must be a string");
|
|
152
156
|
if (object.runningChildId !== expectedRunningChildId) return { ok: false, reason: "wrong-id" };
|
|
153
|
-
if (
|
|
157
|
+
if (!isString(object.latestEvent) || !KNOWN_EVENTS.has(object.latestEvent)) {
|
|
154
158
|
return invalidActivity("unknown latestEvent");
|
|
155
159
|
}
|
|
156
|
-
if (
|
|
160
|
+
if (!isString(object.phase) || !KNOWN_PHASES.has(object.phase)) {
|
|
157
161
|
return invalidActivity("unknown activity phase");
|
|
158
162
|
}
|
|
159
163
|
if (
|
|
160
164
|
object.activeScope != null &&
|
|
161
|
-
(
|
|
165
|
+
(!isString(object.activeScope) || !KNOWN_SCOPES.has(object.activeScope))
|
|
162
166
|
) {
|
|
163
167
|
return invalidActivity("unknown activeScope");
|
|
164
168
|
}
|
|
@@ -182,7 +186,7 @@ function validateActivity(value: unknown, expectedRunningChildId: string): Activ
|
|
|
182
186
|
].find((error) => error != null);
|
|
183
187
|
if (validationError) return invalidActivity(validationError);
|
|
184
188
|
|
|
185
|
-
return { ok: true, activity: object
|
|
189
|
+
return { ok: true, activity: object };
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
export function readSubagentActivityFile(
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
|
|
3
|
+
import { isNonEmptyString, isString } from "./type-guards.ts";
|
|
4
|
+
|
|
3
5
|
const ABORT_MESSAGE = "Aborted while waiting for subagent to finish";
|
|
4
6
|
const TERMINAL_SENTINEL = /__SUBAGENT_DONE_(\d+)__/;
|
|
5
7
|
|
|
@@ -24,30 +26,22 @@ export interface CompletionOptions {
|
|
|
24
26
|
onTick?: (elapsedSeconds: number) => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export function interpretExitSidecar(
|
|
28
|
-
const payload = data as {
|
|
29
|
-
type?: unknown;
|
|
30
|
-
name?: unknown;
|
|
31
|
-
message?: unknown;
|
|
32
|
-
errorMessage?: unknown;
|
|
33
|
-
};
|
|
34
|
-
|
|
29
|
+
export function interpretExitSidecar(payload: any): CompletionResult {
|
|
35
30
|
if (payload?.type === "ping") {
|
|
36
31
|
return {
|
|
37
32
|
reason: "ping",
|
|
38
33
|
exitCode: 0,
|
|
39
34
|
ping: {
|
|
40
|
-
name:
|
|
41
|
-
message:
|
|
35
|
+
name: isString(payload.name) ? payload.name : "subagent",
|
|
36
|
+
message: isString(payload.message) ? payload.message : "",
|
|
42
37
|
},
|
|
43
38
|
};
|
|
44
39
|
}
|
|
45
40
|
|
|
46
41
|
if (payload?.type === "error") {
|
|
47
|
-
const errorMessage =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
: "Subagent exited with stopReason=error (no errorMessage in sidecar).";
|
|
42
|
+
const errorMessage = isNonEmptyString(payload.errorMessage)
|
|
43
|
+
? payload.errorMessage
|
|
44
|
+
: "Subagent exited with stopReason=error (no errorMessage in sidecar).";
|
|
51
45
|
return { reason: "error", exitCode: 1, errorMessage };
|
|
52
46
|
}
|
|
53
47
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile, execSync, execFileSync } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { isFiniteNumber, isPlainObject, isString } from "./type-guards.ts";
|
|
3
4
|
|
|
4
5
|
const execFileAsync = promisify(execFile);
|
|
5
6
|
|
|
@@ -40,7 +41,7 @@ export function isHerdrAvailable(): boolean {
|
|
|
40
41
|
return process.env.HERDR_ENV === "1" && hasCommand("herdr");
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function parseHerdrJson(value: string)
|
|
44
|
+
function parseHerdrJson(value: string) {
|
|
44
45
|
try {
|
|
45
46
|
return JSON.parse(value);
|
|
46
47
|
} catch {
|
|
@@ -50,9 +51,8 @@ function parseHerdrJson(value: string): unknown {
|
|
|
50
51
|
|
|
51
52
|
function extractHerdrPaneId(output: string, context: string): string {
|
|
52
53
|
const parsed = parseHerdrJson(output);
|
|
53
|
-
const paneId =
|
|
54
|
-
|
|
55
|
-
if (typeof paneId !== "string" || !paneId) {
|
|
54
|
+
const paneId = parsed?.result?.pane?.pane_id;
|
|
55
|
+
if (!isString(paneId) || !paneId) {
|
|
56
56
|
throw new Error(
|
|
57
57
|
`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
|
|
58
58
|
);
|
|
@@ -62,9 +62,8 @@ function extractHerdrPaneId(output: string, context: string): string {
|
|
|
62
62
|
|
|
63
63
|
function extractHerdrRootPaneId(output: string, context: string): string {
|
|
64
64
|
const parsed = parseHerdrJson(output);
|
|
65
|
-
const paneId =
|
|
66
|
-
|
|
67
|
-
if (typeof paneId !== "string" || !paneId) {
|
|
65
|
+
const paneId = parsed?.result?.root_pane?.pane_id;
|
|
66
|
+
if (!isString(paneId) || !paneId) {
|
|
68
67
|
throw new Error(
|
|
69
68
|
`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
|
|
70
69
|
);
|
|
@@ -80,24 +79,17 @@ export interface HerdrWorktreeSurface {
|
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
function extractHerdrWorktree(output: string): HerdrWorktreeSurface {
|
|
83
|
-
const parsed = parseHerdrJson(output)
|
|
84
|
-
result?: {
|
|
85
|
-
type?: unknown;
|
|
86
|
-
workspace?: { workspace_id?: unknown };
|
|
87
|
-
root_pane?: { pane_id?: unknown };
|
|
88
|
-
worktree?: { path?: unknown; branch?: unknown };
|
|
89
|
-
};
|
|
90
|
-
} | null;
|
|
82
|
+
const parsed = parseHerdrJson(output);
|
|
91
83
|
const result = parsed?.result;
|
|
92
84
|
if (
|
|
93
85
|
result?.type !== "worktree_created" ||
|
|
94
|
-
|
|
86
|
+
!isString(result.workspace?.workspace_id) ||
|
|
95
87
|
!result.workspace.workspace_id ||
|
|
96
|
-
|
|
88
|
+
!isString(result.root_pane?.pane_id) ||
|
|
97
89
|
!result.root_pane.pane_id ||
|
|
98
|
-
|
|
90
|
+
!isString(result.worktree?.path) ||
|
|
99
91
|
!result.worktree.path ||
|
|
100
|
-
|
|
92
|
+
!isString(result.worktree.branch) ||
|
|
101
93
|
!result.worktree.branch
|
|
102
94
|
) {
|
|
103
95
|
throw new Error(
|
|
@@ -133,11 +125,13 @@ function buildCurrentPaneArgs(): string[] {
|
|
|
133
125
|
return ["pane", "current", "--current"];
|
|
134
126
|
}
|
|
135
127
|
|
|
136
|
-
|
|
128
|
+
interface HerdrCurrentPaneInfo {
|
|
137
129
|
pane_id: string;
|
|
138
130
|
tab_id: string;
|
|
139
131
|
workspace_id: string;
|
|
140
|
-
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getHerdrCurrentPaneInfo(): HerdrCurrentPaneInfo {
|
|
141
135
|
const paneId = process.env.HERDR_PANE_ID;
|
|
142
136
|
const tabId = process.env.HERDR_TAB_ID;
|
|
143
137
|
const workspaceId = process.env.HERDR_WORKSPACE_ID;
|
|
@@ -147,11 +141,8 @@ function getHerdrCurrentPaneInfo(): {
|
|
|
147
141
|
if (!paneId || !tabId || !workspaceId) {
|
|
148
142
|
const output = herdrExec(buildCurrentPaneArgs());
|
|
149
143
|
const parsed = parseHerdrJson(output);
|
|
150
|
-
const pane =
|
|
151
|
-
|
|
152
|
-
| { pane_id?: string; tab_id?: string; workspace_id?: string }
|
|
153
|
-
| undefined;
|
|
154
|
-
if (!pane?.pane_id || !pane?.tab_id || !pane?.workspace_id) {
|
|
144
|
+
const pane = parsed?.result?.pane;
|
|
145
|
+
if (!isString(pane?.pane_id) || !isString(pane?.tab_id) || !isString(pane?.workspace_id)) {
|
|
155
146
|
throw new Error(
|
|
156
147
|
`Unexpected herdr pane current output: ${output.trim() || "(empty)"}`,
|
|
157
148
|
);
|
|
@@ -251,38 +242,23 @@ export class HerdrWorktreeCreateError extends Error {
|
|
|
251
242
|
}
|
|
252
243
|
|
|
253
244
|
export function parseHerdrWorktreeList(output: string): HerdrWorktreeInfo[] {
|
|
254
|
-
const parsed = parseHerdrJson(output)
|
|
255
|
-
result?: {
|
|
256
|
-
type?: unknown;
|
|
257
|
-
worktrees?: Array<{
|
|
258
|
-
branch?: unknown;
|
|
259
|
-
path?: unknown;
|
|
260
|
-
label?: unknown;
|
|
261
|
-
open_workspace_id?: unknown;
|
|
262
|
-
is_linked_worktree?: unknown;
|
|
263
|
-
}>;
|
|
264
|
-
};
|
|
265
|
-
} | null;
|
|
245
|
+
const parsed = parseHerdrJson(output);
|
|
266
246
|
const worktrees = parsed?.result?.worktrees;
|
|
267
247
|
if (parsed?.result?.type !== "worktree_list" || !Array.isArray(worktrees)) {
|
|
268
248
|
throw new Error("Unexpected herdr worktree list output");
|
|
269
249
|
}
|
|
270
250
|
return worktrees.map((worktree) => {
|
|
271
|
-
if (
|
|
272
|
-
typeof worktree.branch !== "string" ||
|
|
273
|
-
typeof worktree.path !== "string"
|
|
274
|
-
) {
|
|
251
|
+
if (!isString(worktree.branch) || !isString(worktree.path)) {
|
|
275
252
|
throw new Error("Unexpected herdr worktree list entry");
|
|
276
253
|
}
|
|
277
|
-
|
|
254
|
+
const info: HerdrWorktreeInfo = {
|
|
278
255
|
branch: worktree.branch,
|
|
279
256
|
path: worktree.path,
|
|
280
|
-
...(typeof worktree.label === "string" ? { label: worktree.label } : {}),
|
|
281
|
-
...(typeof worktree.open_workspace_id === "string"
|
|
282
|
-
? { workspaceId: worktree.open_workspace_id }
|
|
283
|
-
: {}),
|
|
284
257
|
isLinkedWorktree: worktree.is_linked_worktree === true,
|
|
285
258
|
};
|
|
259
|
+
if (isString(worktree.label)) info.label = worktree.label;
|
|
260
|
+
if (isString(worktree.open_workspace_id)) info.workspaceId = worktree.open_workspace_id;
|
|
261
|
+
return info;
|
|
286
262
|
});
|
|
287
263
|
}
|
|
288
264
|
|
|
@@ -293,12 +269,7 @@ export function listHerdrWorktrees(cwd?: string): HerdrWorktreeInfo[] {
|
|
|
293
269
|
}
|
|
294
270
|
|
|
295
271
|
function parseHerdrPaneList(output: string, workspaceId: string): string[] {
|
|
296
|
-
const parsed = parseHerdrJson(output)
|
|
297
|
-
result?: {
|
|
298
|
-
type?: unknown;
|
|
299
|
-
panes?: Array<{ pane_id?: unknown; workspace_id?: unknown }>;
|
|
300
|
-
};
|
|
301
|
-
} | null;
|
|
272
|
+
const parsed = parseHerdrJson(output);
|
|
302
273
|
if (
|
|
303
274
|
parsed?.result?.type !== "pane_list" ||
|
|
304
275
|
!Array.isArray(parsed.result.panes)
|
|
@@ -308,7 +279,7 @@ function parseHerdrPaneList(output: string, workspaceId: string): string[] {
|
|
|
308
279
|
return parsed.result.panes
|
|
309
280
|
.filter((pane) => pane.workspace_id === workspaceId)
|
|
310
281
|
.map((pane) => pane.pane_id)
|
|
311
|
-
.filter(
|
|
282
|
+
.filter(isString);
|
|
312
283
|
}
|
|
313
284
|
|
|
314
285
|
function recoverHerdrWorktree(
|
|
@@ -429,33 +400,21 @@ function parsePaneGetOutput(
|
|
|
429
400
|
output: string,
|
|
430
401
|
surface: string,
|
|
431
402
|
): PaneInspectionResult {
|
|
432
|
-
const parsed = parseHerdrJson(output)
|
|
433
|
-
result?: { pane?: unknown };
|
|
434
|
-
error?: { code?: unknown; message?: unknown };
|
|
435
|
-
} | null;
|
|
403
|
+
const parsed = parseHerdrJson(output);
|
|
436
404
|
const errorObj = parsed?.error;
|
|
437
405
|
if (errorObj?.code === "pane_not_found" || errorObj?.code === "not_found") {
|
|
438
406
|
return {
|
|
439
407
|
kind: "missing",
|
|
440
|
-
error:
|
|
441
|
-
typeof errorObj.message === "string"
|
|
442
|
-
? errorObj.message
|
|
443
|
-
: "pane not found",
|
|
408
|
+
error: isString(errorObj.message) ? errorObj.message : "pane not found",
|
|
444
409
|
};
|
|
445
410
|
}
|
|
446
|
-
const
|
|
447
|
-
if (!
|
|
411
|
+
const record = parsed?.result?.pane;
|
|
412
|
+
if (!isPlainObject(record))
|
|
448
413
|
return { kind: "unavailable", error: "pane get returned no pane record" };
|
|
449
|
-
const record = pane as {
|
|
450
|
-
pane_id?: unknown;
|
|
451
|
-
agent?: unknown;
|
|
452
|
-
agent_status?: unknown;
|
|
453
|
-
};
|
|
454
414
|
if (record.pane_id !== surface)
|
|
455
415
|
return { kind: "unavailable", error: "pane id mismatch" };
|
|
456
|
-
const agent =
|
|
457
|
-
const rawStatus =
|
|
458
|
-
typeof record.agent_status === "string" ? record.agent_status : "unknown";
|
|
416
|
+
const agent = isString(record.agent) ? record.agent : undefined;
|
|
417
|
+
const rawStatus = isString(record.agent_status) ? record.agent_status : "unknown";
|
|
459
418
|
const agentStatus =
|
|
460
419
|
rawStatus === "idle" ||
|
|
461
420
|
rawStatus === "working" ||
|
|
@@ -464,12 +423,14 @@ function parsePaneGetOutput(
|
|
|
464
423
|
rawStatus === "unknown"
|
|
465
424
|
? rawStatus
|
|
466
425
|
: "unknown";
|
|
467
|
-
|
|
426
|
+
const result: PaneInspectionResult = { kind: "present", agentStatus };
|
|
427
|
+
if (agent) result.agent = agent;
|
|
428
|
+
return result;
|
|
468
429
|
}
|
|
469
430
|
|
|
470
431
|
function parsePaneGetError(error: any): PaneInspectionResult {
|
|
471
432
|
for (const raw of [error?.stderr, error?.stdout]) {
|
|
472
|
-
if (
|
|
433
|
+
if (!isString(raw) || !raw.trim()) continue;
|
|
473
434
|
try {
|
|
474
435
|
const parsed = parsePaneGetOutput(raw, "");
|
|
475
436
|
if (parsed.kind === "missing") return parsed;
|
|
@@ -529,43 +490,23 @@ export function parsePaneProcessInfo(
|
|
|
529
490
|
output: string,
|
|
530
491
|
paneId: string,
|
|
531
492
|
): HerdrPaneProcessInfo {
|
|
532
|
-
const parsed = parseHerdrJson(output)
|
|
533
|
-
result?: {
|
|
534
|
-
process_info?: {
|
|
535
|
-
pane_id?: unknown;
|
|
536
|
-
shell_pid?: unknown;
|
|
537
|
-
foreground_process_group_id?: unknown;
|
|
538
|
-
foreground_processes?: Array<{
|
|
539
|
-
pid?: unknown;
|
|
540
|
-
name?: unknown;
|
|
541
|
-
argv0?: unknown;
|
|
542
|
-
argv?: unknown;
|
|
543
|
-
cwd?: unknown;
|
|
544
|
-
}>;
|
|
545
|
-
};
|
|
546
|
-
};
|
|
547
|
-
} | null;
|
|
493
|
+
const parsed = parseHerdrJson(output);
|
|
548
494
|
const info = parsed?.result?.process_info;
|
|
549
|
-
if (!info
|
|
495
|
+
if (!isPlainObject(info)) {
|
|
550
496
|
throw new Error(
|
|
551
497
|
`Unexpected herdr pane process-info output: ${output.trim() || "(empty)"}`,
|
|
552
498
|
);
|
|
553
499
|
}
|
|
554
|
-
if (
|
|
500
|
+
if (isString(info.pane_id) && info.pane_id !== paneId) {
|
|
555
501
|
throw new Error(
|
|
556
502
|
`herdr pane process-info pane id mismatch: ${info.pane_id} != ${paneId}`,
|
|
557
503
|
);
|
|
558
504
|
}
|
|
559
505
|
const pids = new Set<number>();
|
|
560
|
-
if (
|
|
561
|
-
typeof info.shell_pid === "number" &&
|
|
562
|
-
Number.isInteger(info.shell_pid) &&
|
|
563
|
-
info.shell_pid > 0
|
|
564
|
-
) {
|
|
506
|
+
if (Number.isInteger(info.shell_pid) && info.shell_pid > 0) {
|
|
565
507
|
pids.add(info.shell_pid);
|
|
566
508
|
}
|
|
567
509
|
if (
|
|
568
|
-
typeof info.foreground_process_group_id === "number" &&
|
|
569
510
|
Number.isInteger(info.foreground_process_group_id) &&
|
|
570
511
|
info.foreground_process_group_id > 0
|
|
571
512
|
) {
|
|
@@ -573,33 +514,24 @@ export function parsePaneProcessInfo(
|
|
|
573
514
|
}
|
|
574
515
|
const foregroundProcesses: HerdrForegroundProcess[] = [];
|
|
575
516
|
for (const process of info.foreground_processes ?? []) {
|
|
576
|
-
if (
|
|
577
|
-
typeof process?.pid === "number" &&
|
|
578
|
-
Number.isInteger(process.pid) &&
|
|
579
|
-
process.pid > 0
|
|
580
|
-
) {
|
|
517
|
+
if (Number.isInteger(process?.pid) && process.pid > 0) {
|
|
581
518
|
pids.add(process.pid);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
...(typeof process.cwd === "string" ? { cwd: process.cwd } : {}),
|
|
591
|
-
});
|
|
519
|
+
const entry: HerdrForegroundProcess = { pid: process.pid };
|
|
520
|
+
if (isString(process.name)) entry.name = process.name;
|
|
521
|
+
if (isString(process.argv0)) entry.argv0 = process.argv0;
|
|
522
|
+
if (Array.isArray(process.argv) && process.argv.every(isString)) {
|
|
523
|
+
entry.argv = process.argv;
|
|
524
|
+
}
|
|
525
|
+
if (isString(process.cwd)) entry.cwd = process.cwd;
|
|
526
|
+
foregroundProcesses.push(entry);
|
|
592
527
|
}
|
|
593
528
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
pids: [...pids],
|
|
601
|
-
foregroundProcesses,
|
|
602
|
-
};
|
|
529
|
+
const result: HerdrPaneProcessInfo = { paneId, pids: [...pids], foregroundProcesses };
|
|
530
|
+
if (isFiniteNumber(info.shell_pid)) result.shellPid = info.shell_pid;
|
|
531
|
+
if (isFiniteNumber(info.foreground_process_group_id)) {
|
|
532
|
+
result.foregroundProcessGroupId = info.foreground_process_group_id;
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
603
535
|
}
|
|
604
536
|
|
|
605
537
|
export function getHerdrPaneProcessInfo(surface: string): HerdrPaneProcessInfo {
|
|
@@ -705,6 +637,8 @@ export function isProcessAlive(pid: number): boolean {
|
|
|
705
637
|
process.kill(pid, 0);
|
|
706
638
|
return true;
|
|
707
639
|
} catch (error) {
|
|
640
|
+
// SAFETY: process.kill only throws Node's fs/process errors here, which
|
|
641
|
+
// are always Error instances carrying an ErrnoException `code`.
|
|
708
642
|
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
709
643
|
}
|
|
710
644
|
}
|