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 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.0](https://github.com/giuseppecrj/pi-herdr-agents/compare/v1.3.1...v1.4.0)
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.0",
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.73.0"
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 requireObject(value: unknown): Record<string, unknown> | null {
109
- if (value == null || typeof value !== "object" || Array.isArray(value)) return null;
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 validateFiniteNumber(object: Record<string, unknown>, fieldName: string): string | null {
114
- return Number.isFinite(object[fieldName]) ? null : `${fieldName} must be finite`;
113
+ function requireObject(value: any) {
114
+ return isPlainObject(value) ? value : null;
115
115
  }
116
116
 
117
- function validateOptionalFiniteNumber(object: Record<string, unknown>, fieldName: string): string | null {
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 || Number.isFinite(value) ? null : `${fieldName} must be finite when present`;
123
+ return value == null || isFiniteNumber(value) ? null : `${fieldName} must be finite when present`;
120
124
  }
121
125
 
122
- function validateInteger(object: Record<string, unknown>, fieldName: string): string | null {
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: Record<string, unknown>, fieldName: string): string | null {
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: Record<string, unknown>, fieldName: string): string | null {
132
- return typeof object[fieldName] === "boolean" ? null : `${fieldName} must be a boolean`;
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: Record<string, unknown>, fieldName: string): string | null {
139
+ function validateOptionalActivityString(object: any, fieldName: string): string | null {
136
140
  const value = object[fieldName];
137
141
  if (value == null) return null;
138
- if (typeof value !== "string") return `${fieldName} must be a string when present`;
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: unknown, expectedRunningChildId: string): ActivityReadResult {
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 (typeof object.runningChildId !== "string") return invalidActivity("runningChildId must be a string");
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 (typeof object.latestEvent !== "string" || !KNOWN_EVENTS.has(object.latestEvent as SubagentActivityEvent)) {
157
+ if (!isString(object.latestEvent) || !KNOWN_EVENTS.has(object.latestEvent)) {
154
158
  return invalidActivity("unknown latestEvent");
155
159
  }
156
- if (typeof object.phase !== "string" || !KNOWN_PHASES.has(object.phase as SubagentActivityPhase)) {
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
- (typeof object.activeScope !== "string" || !KNOWN_SCOPES.has(object.activeScope as SubagentActivityScope))
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 as unknown as SubagentActivityState };
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(data: unknown): CompletionResult {
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: typeof payload.name === "string" ? payload.name : "subagent",
41
- message: typeof payload.message === "string" ? payload.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
- typeof payload.errorMessage === "string" && payload.errorMessage.trim()
49
- ? payload.errorMessage
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): unknown {
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 = (parsed as { result?: { pane?: { pane_id?: unknown } } })
54
- ?.result?.pane?.pane_id;
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 = (parsed as { result?: { root_pane?: { pane_id?: unknown } } })
66
- ?.result?.root_pane?.pane_id;
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) as {
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
- typeof result.workspace?.workspace_id !== "string" ||
86
+ !isString(result.workspace?.workspace_id) ||
95
87
  !result.workspace.workspace_id ||
96
- typeof result.root_pane?.pane_id !== "string" ||
88
+ !isString(result.root_pane?.pane_id) ||
97
89
  !result.root_pane.pane_id ||
98
- typeof result.worktree?.path !== "string" ||
90
+ !isString(result.worktree?.path) ||
99
91
  !result.worktree.path ||
100
- typeof result.worktree.branch !== "string" ||
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
- function getHerdrCurrentPaneInfo(): {
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 = (parsed as { result?: { pane?: unknown } } | null)?.result
151
- ?.pane as
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) as {
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
- return {
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) as {
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((paneId): paneId is string => typeof paneId === "string");
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) as {
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 pane = parsed?.result?.pane;
447
- if (!pane || typeof pane !== "object")
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 = typeof record.agent === "string" ? record.agent : undefined;
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
- return { kind: "present", ...(agent ? { agent } : {}), agentStatus };
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 (typeof raw !== "string" || !raw.trim()) continue;
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) as {
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 || typeof info !== "object") {
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 (typeof info.pane_id === "string" && info.pane_id !== paneId) {
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
- foregroundProcesses.push({
583
- pid: process.pid,
584
- ...(typeof process.name === "string" ? { name: process.name } : {}),
585
- ...(typeof process.argv0 === "string" ? { argv0: process.argv0 } : {}),
586
- ...(Array.isArray(process.argv) &&
587
- process.argv.every((value) => typeof value === "string")
588
- ? { argv: process.argv as string[] }
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
- return {
595
- paneId,
596
- ...(typeof info.shell_pid === "number" ? { shellPid: info.shell_pid } : {}),
597
- ...(typeof info.foreground_process_group_id === "number"
598
- ? { foregroundProcessGroupId: info.foreground_process_group_id }
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
  }