harnery 0.3.1 → 0.3.2
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/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +14 -1
- package/dist/core/agents/coord-client.js +20 -5
- package/dist/core/hooks/cli.js +25 -20
- package/dist/core/hooks/resolve/anchor.d.ts +15 -0
- package/dist/core/hooks/resolve/anchor.d.ts.map +1 -1
- package/dist/core/hooks/resolve/anchor.js +22 -0
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +20 -8
- package/package.json +1 -1
- package/src/commands/agents.ts +14 -1
- package/src/core/agents/coord-client.ts +17 -4
- package/src/core/hooks/cli.ts +24 -20
- package/src/core/hooks/resolve/anchor.ts +21 -0
- package/src/core/hooks/resolve/owner.ts +17 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAgFnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,GAAG,IAAI,CA6OpF"}
|
package/dist/commands/agents.js
CHANGED
|
@@ -18,6 +18,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
18
18
|
import { basename, join, resolve } from "node:path";
|
|
19
19
|
import { emitCanonical, monorepoRoot, normalizeHarness, readHeartbeat, resolveOwner, resolveOwnerWithSource, } from "../core/agents/index.js";
|
|
20
20
|
import { resolveBinName } from "../core/config.js";
|
|
21
|
+
import { parsePsChainLine } from "../core/hooks/resolve/anchor.js";
|
|
21
22
|
import { buildCouncilId, buildInviteMarkdown, COUNCIL_SCHEMA_VERSION, contributorsInRound, councilBodyDir, councilsArchiveDir, deleteArchivedCouncil, effectiveSteward, findManifestByPartialId, listKnownAgents, listManifests, moveFromArchive, moveToArchive, normalizeAgentName, pendingCouncilsForMember, readArchivedManifest, readManifest, readRoundPrompts, roundDir, setCouncilSteward, writeContribution, writeManifest, writePrompt, } from "../lib/council/index.js";
|
|
22
23
|
import { displayName as displayAgentName, ensureIdentity, listIdentities, lookupById as lookupIdentityById, lookupByName as lookupIdentityByName, } from "../lib/identities/index.js";
|
|
23
24
|
import { appendEntry, resolveOwnerByName } from "../lib/scratch/index.js";
|
|
@@ -2251,14 +2252,26 @@ function runHarnessProbe(id, opts) {
|
|
|
2251
2252
|
for (let hops = 0; hops < 20; hops++) {
|
|
2252
2253
|
let comm = "?";
|
|
2253
2254
|
let ppid = 0;
|
|
2255
|
+
let got = false;
|
|
2254
2256
|
try {
|
|
2255
2257
|
comm = readFileSync(`/proc/${walkPid}/comm`, "utf8").trim() || "?";
|
|
2256
2258
|
const status = readFileSync(`/proc/${walkPid}/status`, "utf8");
|
|
2257
2259
|
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
2258
2260
|
ppid = m ? Number(m[1]) : 0;
|
|
2261
|
+
got = true;
|
|
2259
2262
|
}
|
|
2260
2263
|
catch {
|
|
2261
|
-
// non-Linux (no /proc)
|
|
2264
|
+
// non-Linux (no /proc): fall through to the portable ps walk below.
|
|
2265
|
+
}
|
|
2266
|
+
if (!got) {
|
|
2267
|
+
const out = spawnSync("ps", ["-o", "ppid=,comm=", "-p", String(walkPid)], {
|
|
2268
|
+
encoding: "utf8",
|
|
2269
|
+
});
|
|
2270
|
+
const parsed = out.status === 0 ? parsePsChainLine(out.stdout) : null;
|
|
2271
|
+
if (parsed) {
|
|
2272
|
+
comm = parsed.comm || "?";
|
|
2273
|
+
ppid = parsed.ppid;
|
|
2274
|
+
}
|
|
2262
2275
|
}
|
|
2263
2276
|
chainParts.push(`${walkPid}:${comm}`);
|
|
2264
2277
|
if (!anchorPid && anchorTokens.has(comm))
|
|
@@ -330,15 +330,30 @@ export function selfDisplayName() {
|
|
|
330
330
|
return displayName(owner);
|
|
331
331
|
}
|
|
332
332
|
function readPpid(pid) {
|
|
333
|
+
// Linux/WSL fast path: /proc/<pid>/status carries `PPid:`.
|
|
333
334
|
try {
|
|
334
335
|
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
335
336
|
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
if (m) {
|
|
338
|
+
const parsed = Number.parseInt(m[1], 10);
|
|
339
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
340
|
+
return parsed;
|
|
341
|
+
}
|
|
340
342
|
}
|
|
341
343
|
catch {
|
|
342
|
-
|
|
344
|
+
// no /proc (macOS/BSD) — fall through to ps
|
|
345
|
+
}
|
|
346
|
+
// Portable fallback: `ps -o ppid= -p <pid>` works on macOS/BSD/Linux.
|
|
347
|
+
try {
|
|
348
|
+
const out = spawnSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" });
|
|
349
|
+
if (out.status === 0) {
|
|
350
|
+
const parsed = Number.parseInt(out.stdout.trim(), 10);
|
|
351
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
352
|
+
return parsed;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// ps unavailable — give up
|
|
343
357
|
}
|
|
358
|
+
return null;
|
|
344
359
|
}
|
package/dist/core/hooks/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ import { captureImages, detectPresence, imageJanitor, playSound, resetSoundCount
|
|
|
30
30
|
import { emit } from "./events/emit.js";
|
|
31
31
|
import { detectHarness } from "./harness/detect.js";
|
|
32
32
|
import { extractBashCommand, extractToolDescription, normalizeEventName, parsePayload, } from "./harness/parse.js";
|
|
33
|
-
import { selectAnchorPid } from "./resolve/anchor.js";
|
|
33
|
+
import { parsePsChainLine, selectAnchorPid } from "./resolve/anchor.js";
|
|
34
34
|
import { findCoordRoot } from "./resolve/coord-root.js";
|
|
35
35
|
import { extractIntentComment, resolveIntent } from "./resolve/intent.js";
|
|
36
36
|
import { resolveOwner } from "./resolve/owner.js";
|
|
@@ -820,10 +820,10 @@ function healHeartbeatViaCli(coordRoot, instanceId, sessionId, harness) {
|
|
|
820
820
|
* the next tool call rather than going invisible until SessionStart fires
|
|
821
821
|
* again, which it may never do.
|
|
822
822
|
*
|
|
823
|
-
* Returns undefined
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
823
|
+
* Returns undefined only when no anchor is found; callers fall back to
|
|
824
|
+
* `process.ppid` (the bash wrapper's parent, which is usually the harness binary
|
|
825
|
+
* itself). `HARNERY_AGENT_COORD_TEST_ANCHOR_PID` overrides everything so the
|
|
826
|
+
* test sandbox can pin a deterministic PID.
|
|
827
827
|
*/
|
|
828
828
|
function findHarnessAnchorPid(harness) {
|
|
829
829
|
const override = coordEnv("AGENT_COORD_TEST_ANCHOR_PID");
|
|
@@ -832,30 +832,35 @@ function findHarnessAnchorPid(harness) {
|
|
|
832
832
|
if (Number.isFinite(n) && n > 0)
|
|
833
833
|
return n;
|
|
834
834
|
}
|
|
835
|
-
// Build the ppid chain (nearest → root, up to 20 hops)
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
835
|
+
// Build the ppid chain (nearest → root, up to 20 hops), then hand it to the
|
|
836
|
+
// pure selector. Linux/WSL reads /proc; macOS/BSD (no /proc) falls back to
|
|
837
|
+
// `ps -o ppid=,comm=` parsed by the unit-tested `parsePsChainLine`. Splitting
|
|
838
|
+
// the walk (untestable off a live box) from the comm-matching keeps the
|
|
839
|
+
// cursor `node`-fallback logic verifiable.
|
|
839
840
|
const chain = [];
|
|
840
841
|
let pid = process.pid;
|
|
841
842
|
for (let hops = 0; hops < 20; hops++) {
|
|
842
|
-
let
|
|
843
|
-
let status;
|
|
843
|
+
let hop = null;
|
|
844
844
|
try {
|
|
845
|
-
comm = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
|
|
846
|
-
status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
845
|
+
const comm = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
|
|
846
|
+
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
847
|
+
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
848
|
+
hop = { comm, ppid: m ? Number(m[1]) : 0 };
|
|
847
849
|
}
|
|
848
850
|
catch {
|
|
849
|
-
|
|
851
|
+
// no /proc (macOS/BSD) — fall through to ps
|
|
852
|
+
}
|
|
853
|
+
if (!hop) {
|
|
854
|
+
const out = spawnSync("ps", ["-o", "ppid=,comm=", "-p", String(pid)], { encoding: "utf8" });
|
|
855
|
+
if (out.status === 0)
|
|
856
|
+
hop = parsePsChainLine(out.stdout);
|
|
850
857
|
}
|
|
851
|
-
|
|
852
|
-
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
853
|
-
if (!m)
|
|
858
|
+
if (!hop)
|
|
854
859
|
break;
|
|
855
|
-
|
|
856
|
-
if (!Number.isFinite(ppid) || ppid === 0 || ppid === 1)
|
|
860
|
+
chain.push({ pid, comm: hop.comm });
|
|
861
|
+
if (!Number.isFinite(hop.ppid) || hop.ppid === 0 || hop.ppid === 1)
|
|
857
862
|
break;
|
|
858
|
-
pid = ppid;
|
|
863
|
+
pid = hop.ppid;
|
|
859
864
|
}
|
|
860
865
|
return selectAnchorPid(chain, harness);
|
|
861
866
|
}
|
|
@@ -34,4 +34,19 @@ export declare function selectAnchorPid(chain: ReadonlyArray<{
|
|
|
34
34
|
pid: number;
|
|
35
35
|
comm: string;
|
|
36
36
|
}>, harness?: string): number | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Parse one `ps -o ppid=,comm= -p <pid>` line into the same `{ ppid, comm }`
|
|
39
|
+
* shape the `/proc` fast path produces, for the macOS/BSD branch of the anchor
|
|
40
|
+
* walk. `ps` prints `comm` as a full executable path (and some Apple helper
|
|
41
|
+
* names contain spaces, e.g. `Code Helper (Plugin)`), so the comm is reduced to
|
|
42
|
+
* its basename to match the harness comm tokens the way Linux's `/proc/<pid>/comm`
|
|
43
|
+
* basename does. Returns null when the line has no leading numeric ppid.
|
|
44
|
+
*
|
|
45
|
+
* Pure (no I/O) so the parsing — the error-prone part — is unit-testable
|
|
46
|
+
* without a live process tree; the caller owns the `ps` spawn.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parsePsChainLine(line: string): {
|
|
49
|
+
ppid: number;
|
|
50
|
+
comm: string;
|
|
51
|
+
} | null;
|
|
37
52
|
//# sourceMappingURL=anchor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anchor.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/anchor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAMH;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACnD,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAUpB"}
|
|
1
|
+
{"version":3,"file":"anchor.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/anchor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAMH;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACnD,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAUpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQpF"}
|
|
@@ -46,3 +46,25 @@ export function selectAnchorPid(chain, harness) {
|
|
|
46
46
|
}
|
|
47
47
|
return undefined;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse one `ps -o ppid=,comm= -p <pid>` line into the same `{ ppid, comm }`
|
|
51
|
+
* shape the `/proc` fast path produces, for the macOS/BSD branch of the anchor
|
|
52
|
+
* walk. `ps` prints `comm` as a full executable path (and some Apple helper
|
|
53
|
+
* names contain spaces, e.g. `Code Helper (Plugin)`), so the comm is reduced to
|
|
54
|
+
* its basename to match the harness comm tokens the way Linux's `/proc/<pid>/comm`
|
|
55
|
+
* basename does. Returns null when the line has no leading numeric ppid.
|
|
56
|
+
*
|
|
57
|
+
* Pure (no I/O) so the parsing — the error-prone part — is unit-testable
|
|
58
|
+
* without a live process tree; the caller owns the `ps` spawn.
|
|
59
|
+
*/
|
|
60
|
+
export function parsePsChainLine(line) {
|
|
61
|
+
const m = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
62
|
+
if (!m)
|
|
63
|
+
return null;
|
|
64
|
+
const ppid = Number.parseInt(m[1], 10);
|
|
65
|
+
if (!Number.isFinite(ppid))
|
|
66
|
+
return null;
|
|
67
|
+
const commPath = m[2].trim();
|
|
68
|
+
const comm = commPath.split("/").pop() || commPath;
|
|
69
|
+
return { ppid, comm };
|
|
70
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"owner.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/owner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"owner.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/resolve/owner.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,KAAK,GAAG,SAAS,GAAG,aAAa,GAAG,iBAAiB,CAAA;CAAE,GAAG,IAAI,CA+ChG;AA2BD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ7E;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUxF;AAED,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgBnF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { coordEnv } from "../../../lib/env.js";
|
|
@@ -67,19 +68,30 @@ export function resolveOwner(opts) {
|
|
|
67
68
|
return null;
|
|
68
69
|
}
|
|
69
70
|
function readPpid(pid) {
|
|
70
|
-
// Linux/WSL: /proc/<pid>/status carries `PPid:`.
|
|
71
|
-
// macOS or any read failure; ancestor walk just terminates.
|
|
71
|
+
// Linux/WSL fast path: /proc/<pid>/status carries `PPid:`.
|
|
72
72
|
try {
|
|
73
73
|
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
75
|
+
if (m) {
|
|
76
|
+
const n = Number.parseInt(m[1], 10);
|
|
77
|
+
if (Number.isFinite(n) && n > 0)
|
|
78
|
+
return n;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* no /proc (macOS/BSD) — fall through to ps */
|
|
83
|
+
}
|
|
84
|
+
// Portable fallback: `ps -o ppid= -p <pid>` works on macOS/BSD/Linux.
|
|
85
|
+
try {
|
|
86
|
+
const out = spawnSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" });
|
|
87
|
+
if (out.status === 0) {
|
|
88
|
+
const n = Number.parseInt(out.stdout.trim(), 10);
|
|
89
|
+
if (Number.isFinite(n) && n > 0)
|
|
90
|
+
return n;
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
93
|
catch {
|
|
82
|
-
/*
|
|
94
|
+
/* ps unavailable — give up */
|
|
83
95
|
}
|
|
84
96
|
return null;
|
|
85
97
|
}
|
package/package.json
CHANGED
package/src/commands/agents.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
resolveOwnerWithSource,
|
|
39
39
|
} from "../core/agents/index.ts";
|
|
40
40
|
import { resolveBinName } from "../core/config.ts";
|
|
41
|
+
import { parsePsChainLine } from "../core/hooks/resolve/anchor.ts";
|
|
41
42
|
import {
|
|
42
43
|
buildCouncilId,
|
|
43
44
|
buildInviteMarkdown,
|
|
@@ -2692,13 +2693,25 @@ function runHarnessProbe(
|
|
|
2692
2693
|
for (let hops = 0; hops < 20; hops++) {
|
|
2693
2694
|
let comm = "?";
|
|
2694
2695
|
let ppid = 0;
|
|
2696
|
+
let got = false;
|
|
2695
2697
|
try {
|
|
2696
2698
|
comm = readFileSync(`/proc/${walkPid}/comm`, "utf8").trim() || "?";
|
|
2697
2699
|
const status = readFileSync(`/proc/${walkPid}/status`, "utf8");
|
|
2698
2700
|
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
2699
2701
|
ppid = m ? Number(m[1]) : 0;
|
|
2702
|
+
got = true;
|
|
2700
2703
|
} catch {
|
|
2701
|
-
// non-Linux (no /proc)
|
|
2704
|
+
// non-Linux (no /proc): fall through to the portable ps walk below.
|
|
2705
|
+
}
|
|
2706
|
+
if (!got) {
|
|
2707
|
+
const out = spawnSync("ps", ["-o", "ppid=,comm=", "-p", String(walkPid)], {
|
|
2708
|
+
encoding: "utf8",
|
|
2709
|
+
});
|
|
2710
|
+
const parsed = out.status === 0 ? parsePsChainLine(out.stdout) : null;
|
|
2711
|
+
if (parsed) {
|
|
2712
|
+
comm = parsed.comm || "?";
|
|
2713
|
+
ppid = parsed.ppid;
|
|
2714
|
+
}
|
|
2702
2715
|
}
|
|
2703
2716
|
chainParts.push(`${walkPid}:${comm}`);
|
|
2704
2717
|
if (!anchorPid && anchorTokens.has(comm)) anchorPid = String(walkPid);
|
|
@@ -361,13 +361,26 @@ export function selfDisplayName(): string {
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
function readPpid(pid: number): number | null {
|
|
364
|
+
// Linux/WSL fast path: /proc/<pid>/status carries `PPid:`.
|
|
364
365
|
try {
|
|
365
366
|
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
366
367
|
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
|
|
368
|
+
if (m) {
|
|
369
|
+
const parsed = Number.parseInt(m[1]!, 10);
|
|
370
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
371
|
+
}
|
|
370
372
|
} catch {
|
|
371
|
-
|
|
373
|
+
// no /proc (macOS/BSD) — fall through to ps
|
|
372
374
|
}
|
|
375
|
+
// Portable fallback: `ps -o ppid= -p <pid>` works on macOS/BSD/Linux.
|
|
376
|
+
try {
|
|
377
|
+
const out = spawnSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" });
|
|
378
|
+
if (out.status === 0) {
|
|
379
|
+
const parsed = Number.parseInt(out.stdout.trim(), 10);
|
|
380
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
// ps unavailable — give up
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
373
386
|
}
|
package/src/core/hooks/cli.ts
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
type ParsedPayload,
|
|
52
52
|
parsePayload,
|
|
53
53
|
} from "./harness/parse.ts";
|
|
54
|
-
import { selectAnchorPid } from "./resolve/anchor.ts";
|
|
54
|
+
import { parsePsChainLine, selectAnchorPid } from "./resolve/anchor.ts";
|
|
55
55
|
import { findCoordRoot } from "./resolve/coord-root.ts";
|
|
56
56
|
import { extractIntentComment, resolveIntent } from "./resolve/intent.ts";
|
|
57
57
|
import { resolveOwner } from "./resolve/owner.ts";
|
|
@@ -908,10 +908,10 @@ function healHeartbeatViaCli(
|
|
|
908
908
|
* the next tool call rather than going invisible until SessionStart fires
|
|
909
909
|
* again, which it may never do.
|
|
910
910
|
*
|
|
911
|
-
* Returns undefined
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
911
|
+
* Returns undefined only when no anchor is found; callers fall back to
|
|
912
|
+
* `process.ppid` (the bash wrapper's parent, which is usually the harness binary
|
|
913
|
+
* itself). `HARNERY_AGENT_COORD_TEST_ANCHOR_PID` overrides everything so the
|
|
914
|
+
* test sandbox can pin a deterministic PID.
|
|
915
915
|
*/
|
|
916
916
|
function findHarnessAnchorPid(harness?: Harness): number | undefined {
|
|
917
917
|
const override = coordEnv("AGENT_COORD_TEST_ANCHOR_PID");
|
|
@@ -919,27 +919,31 @@ function findHarnessAnchorPid(harness?: Harness): number | undefined {
|
|
|
919
919
|
const n = Number(override);
|
|
920
920
|
if (Number.isFinite(n) && n > 0) return n;
|
|
921
921
|
}
|
|
922
|
-
// Build the ppid chain (nearest → root, up to 20 hops)
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
922
|
+
// Build the ppid chain (nearest → root, up to 20 hops), then hand it to the
|
|
923
|
+
// pure selector. Linux/WSL reads /proc; macOS/BSD (no /proc) falls back to
|
|
924
|
+
// `ps -o ppid=,comm=` parsed by the unit-tested `parsePsChainLine`. Splitting
|
|
925
|
+
// the walk (untestable off a live box) from the comm-matching keeps the
|
|
926
|
+
// cursor `node`-fallback logic verifiable.
|
|
926
927
|
const chain: Array<{ pid: number; comm: string }> = [];
|
|
927
928
|
let pid = process.pid;
|
|
928
929
|
for (let hops = 0; hops < 20; hops++) {
|
|
929
|
-
let comm: string;
|
|
930
|
-
let status: string;
|
|
930
|
+
let hop: { comm: string; ppid: number } | null = null;
|
|
931
931
|
try {
|
|
932
|
-
comm = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
|
|
933
|
-
status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
932
|
+
const comm = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
|
|
933
|
+
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
934
|
+
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
935
|
+
hop = { comm, ppid: m ? Number(m[1]) : 0 };
|
|
934
936
|
} catch {
|
|
935
|
-
|
|
937
|
+
// no /proc (macOS/BSD) — fall through to ps
|
|
936
938
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if (!
|
|
942
|
-
pid
|
|
939
|
+
if (!hop) {
|
|
940
|
+
const out = spawnSync("ps", ["-o", "ppid=,comm=", "-p", String(pid)], { encoding: "utf8" });
|
|
941
|
+
if (out.status === 0) hop = parsePsChainLine(out.stdout);
|
|
942
|
+
}
|
|
943
|
+
if (!hop) break;
|
|
944
|
+
chain.push({ pid, comm: hop.comm });
|
|
945
|
+
if (!Number.isFinite(hop.ppid) || hop.ppid === 0 || hop.ppid === 1) break;
|
|
946
|
+
pid = hop.ppid;
|
|
943
947
|
}
|
|
944
948
|
return selectAnchorPid(chain, harness);
|
|
945
949
|
}
|
|
@@ -49,3 +49,24 @@ export function selectAnchorPid(
|
|
|
49
49
|
}
|
|
50
50
|
return undefined;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse one `ps -o ppid=,comm= -p <pid>` line into the same `{ ppid, comm }`
|
|
55
|
+
* shape the `/proc` fast path produces, for the macOS/BSD branch of the anchor
|
|
56
|
+
* walk. `ps` prints `comm` as a full executable path (and some Apple helper
|
|
57
|
+
* names contain spaces, e.g. `Code Helper (Plugin)`), so the comm is reduced to
|
|
58
|
+
* its basename to match the harness comm tokens the way Linux's `/proc/<pid>/comm`
|
|
59
|
+
* basename does. Returns null when the line has no leading numeric ppid.
|
|
60
|
+
*
|
|
61
|
+
* Pure (no I/O) so the parsing — the error-prone part — is unit-testable
|
|
62
|
+
* without a live process tree; the caller owns the `ps` spawn.
|
|
63
|
+
*/
|
|
64
|
+
export function parsePsChainLine(line: string): { ppid: number; comm: string } | null {
|
|
65
|
+
const m = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
66
|
+
if (!m) return null;
|
|
67
|
+
const ppid = Number.parseInt(m[1]!, 10);
|
|
68
|
+
if (!Number.isFinite(ppid)) return null;
|
|
69
|
+
const commPath = m[2]!.trim();
|
|
70
|
+
const comm = commPath.split("/").pop() || commPath;
|
|
71
|
+
return { ppid, comm };
|
|
72
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { coordEnv } from "../../../lib/env.ts";
|
|
@@ -73,18 +74,26 @@ export function resolveOwner(opts: {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
function readPpid(pid: number): number | null {
|
|
76
|
-
// Linux/WSL: /proc/<pid>/status carries `PPid:`.
|
|
77
|
-
// macOS or any read failure; ancestor walk just terminates.
|
|
77
|
+
// Linux/WSL fast path: /proc/<pid>/status carries `PPid:`.
|
|
78
78
|
try {
|
|
79
79
|
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
81
|
+
if (m) {
|
|
82
|
+
const n = Number.parseInt(m[1]!, 10);
|
|
83
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
/* no /proc (macOS/BSD) — fall through to ps */
|
|
87
|
+
}
|
|
88
|
+
// Portable fallback: `ps -o ppid= -p <pid>` works on macOS/BSD/Linux.
|
|
89
|
+
try {
|
|
90
|
+
const out = spawnSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" });
|
|
91
|
+
if (out.status === 0) {
|
|
92
|
+
const n = Number.parseInt(out.stdout.trim(), 10);
|
|
93
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
85
94
|
}
|
|
86
95
|
} catch {
|
|
87
|
-
/*
|
|
96
|
+
/* ps unavailable — give up */
|
|
88
97
|
}
|
|
89
98
|
return null;
|
|
90
99
|
}
|