harnery 0.31.0 → 0.31.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 +124 -14
- package/dist/commands/grep.d.ts +1 -1
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +19 -2
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +20 -4
- package/dist/core/agents/canonical-emit.d.ts +5 -10
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +10 -39
- package/dist/core/agents/coord-bin.d.ts +33 -0
- package/dist/core/agents/coord-bin.d.ts.map +1 -0
- package/dist/core/agents/coord-bin.js +89 -0
- package/dist/core/agents/coord-client.d.ts +57 -6
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +181 -48
- package/dist/core/agents/paths.d.ts +5 -2
- package/dist/core/agents/paths.d.ts.map +1 -1
- package/dist/core/agents/paths.js +7 -13
- package/dist/core/hooks/adapter/detect.d.ts +4 -2
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +9 -4
- package/dist/core/hooks/cli.js +14 -13
- package/dist/core/hooks/effects/index.d.ts.map +1 -1
- package/dist/core/hooks/effects/index.js +2 -2
- package/dist/core/hooks/resolve/coord-root.d.ts +16 -11
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
- package/dist/core/hooks/resolve/coord-root.js +18 -43
- package/dist/lib/context/index.js +9 -2
- package/package.json +1 -1
- package/src/commands/agents.ts +129 -14
- package/src/commands/grep.ts +19 -2
- package/src/commands/tunnel.ts +19 -4
- package/src/core/agents/canonical-emit.ts +10 -37
- package/src/core/agents/coord-bin.ts +92 -0
- package/src/core/agents/coord-client.ts +183 -50
- package/src/core/agents/paths.ts +7 -11
- package/src/core/hooks/adapter/detect.ts +9 -4
- package/src/core/hooks/cli.ts +14 -13
- package/src/core/hooks/effects/index.ts +2 -2
- package/src/core/hooks/resolve/coord-root.ts +18 -40
- package/src/lib/context/index.ts +11 -1
|
@@ -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,qBAAqB,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,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AA2E1E;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;CAAE,CAErF;AAsED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,cAAc,CAAC,EAAE,qBAAqB,GACrC,IAAI,CA+PN;AAyJD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAyO7D;AA2sBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,EAAE,GACzB;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKvD"}
|
package/dist/commands/agents.js
CHANGED
|
@@ -16,6 +16,7 @@ import { spawnSync } from "node:child_process";
|
|
|
16
16
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
17
17
|
import { homedir, tmpdir } from "node:os";
|
|
18
18
|
import { basename, join, resolve } from "node:path";
|
|
19
|
+
import { coordBinPath } from "../core/agents/coord-bin.js";
|
|
19
20
|
import { readStreamTailBounded } from "../core/agents/events/consume.js";
|
|
20
21
|
import { emitCanonical, monorepoRoot, normalizeAdapter, readHeartbeat, resolveOwner, resolveOwnerBySessionEnv, resolveOwnerWithSource, } from "../core/agents/index.js";
|
|
21
22
|
import { coordFreshnessSeconds, resolveBinName } from "../core/config.js";
|
|
@@ -56,6 +57,41 @@ const SUBAGENT_NOTE = "Bash identity is process-level in v1; if you're running i
|
|
|
56
57
|
export function coordHelperOpts(root) {
|
|
57
58
|
return { cwd: root, env: { ...process.env, HARNERY_COORD_ROOT_OVERRIDE: root } };
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the bundled `agent-coord` helper, or exit with an error that names the
|
|
62
|
+
* problem. Every heartbeat mutation goes through that binary, so a caller who
|
|
63
|
+
* cannot find it has nothing useful to do — but it has to SAY so, because the
|
|
64
|
+
* alternative was spawning a path that did not exist and then crashing on the
|
|
65
|
+
* result (see `spawnFailureMessage`).
|
|
66
|
+
*/
|
|
67
|
+
function agentCoordOrExit(root) {
|
|
68
|
+
const binary = coordBinPath("agent-coord", root);
|
|
69
|
+
if (!binary) {
|
|
70
|
+
emit.error({
|
|
71
|
+
code: "coord_helper_missing",
|
|
72
|
+
message: `agent-coord helper not found for coord root ${root}; reinstall or rebuild harnery`,
|
|
73
|
+
});
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
return binary;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Describe a failed `spawnSync` without assuming it ran.
|
|
80
|
+
*
|
|
81
|
+
* When the binary cannot be executed at all (ENOENT, EACCES) node reports
|
|
82
|
+
* `status: null` AND `stderr: null`, so the obvious `result.stderr.trim()`
|
|
83
|
+
* threw "null is not an object" — replacing a legible "helper not found" with a
|
|
84
|
+
* crash that pointed at the wrong layer entirely. `error` carries the real
|
|
85
|
+
* cause on that path, so prefer it.
|
|
86
|
+
*/
|
|
87
|
+
function spawnFailureMessage(result, what) {
|
|
88
|
+
if (result.error)
|
|
89
|
+
return `${what}: ${result.error.message}`;
|
|
90
|
+
const stderr = result.stderr?.trim();
|
|
91
|
+
if (stderr)
|
|
92
|
+
return stderr;
|
|
93
|
+
return result.status === null ? `${what}: did not run` : `${what} exited ${result.status}`;
|
|
94
|
+
}
|
|
59
95
|
function formatPlatformLabel(platform) {
|
|
60
96
|
if (platform === "cursor")
|
|
61
97
|
return "Cursor";
|
|
@@ -537,7 +573,7 @@ function runWhoami(opts) {
|
|
|
537
573
|
if (!hb) {
|
|
538
574
|
emit.error({
|
|
539
575
|
code: "no_heartbeat",
|
|
540
|
-
message:
|
|
576
|
+
message: noHeartbeatMessage(myOwner),
|
|
541
577
|
});
|
|
542
578
|
process.exit(1);
|
|
543
579
|
}
|
|
@@ -721,6 +757,42 @@ function normalizeKind(kind) {
|
|
|
721
757
|
return "unknown";
|
|
722
758
|
return kind;
|
|
723
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* The `no_heartbeat` diagnostic, quoting the owner id in FULL.
|
|
762
|
+
*
|
|
763
|
+
* This id is actionable: the reader's next move is `agents heal --kind
|
|
764
|
+
* heartbeat --owner <id>`, and heartbeats are keyed by the whole
|
|
765
|
+
* `instance_id` (`.harnery/active/<instance_id>.json`). An abbreviated id
|
|
766
|
+
* here reads as complete, so it gets copy-pasted into `--owner` and mints a
|
|
767
|
+
* heartbeat at a filename no reader resolves — an orphan that leaves the
|
|
768
|
+
* session looking unhealable. Abbreviate ids for display elsewhere, never in
|
|
769
|
+
* a message whose whole purpose is to hand the reader an id to pass back.
|
|
770
|
+
*/
|
|
771
|
+
function noHeartbeatMessage(owner) {
|
|
772
|
+
return `resolved owner=${owner} but no heartbeat exists at .harnery/active/${owner}.json`;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* The instance_id of a live heartbeat that `prefix` strictly prefixes, or null.
|
|
776
|
+
*
|
|
777
|
+
* Used to recognize an abbreviated id handed back by a reader when the caller
|
|
778
|
+
* supplied no canonical id of their own. Ambiguity is treated as "no answer":
|
|
779
|
+
* two live sessions sharing the prefix means we cannot name the one intended,
|
|
780
|
+
* and refusing with the wrong id in the message would be worse than the orphan.
|
|
781
|
+
*/
|
|
782
|
+
function liveIdWithPrefix(root, prefix) {
|
|
783
|
+
const activeDir = resolve(root, ".harnery", "active");
|
|
784
|
+
if (!existsSync(activeDir))
|
|
785
|
+
return null;
|
|
786
|
+
const matches = new Set();
|
|
787
|
+
for (const file of readdirSync(activeDir)) {
|
|
788
|
+
if (!file.endsWith(".json"))
|
|
789
|
+
continue;
|
|
790
|
+
const id = file.slice(0, -".json".length);
|
|
791
|
+
if (id !== prefix && id.startsWith(prefix))
|
|
792
|
+
matches.add(id);
|
|
793
|
+
}
|
|
794
|
+
return matches.size === 1 ? matches.values().next().value : null;
|
|
795
|
+
}
|
|
724
796
|
async function runWatch(pollMs) {
|
|
725
797
|
const root = monorepoRoot();
|
|
726
798
|
if (!root) {
|
|
@@ -1012,8 +1084,8 @@ function ensureCursorSession(root) {
|
|
|
1012
1084
|
const sessionId = cursorEnvSessionId();
|
|
1013
1085
|
if (!sessionId)
|
|
1014
1086
|
return;
|
|
1015
|
-
const agentHook =
|
|
1016
|
-
if (!
|
|
1087
|
+
const agentHook = coordBinPath("agent-hook", root);
|
|
1088
|
+
if (!agentHook)
|
|
1017
1089
|
return;
|
|
1018
1090
|
const payload = JSON.stringify({
|
|
1019
1091
|
conversation_id: sessionId,
|
|
@@ -1060,7 +1132,7 @@ function runReleaseClaim(path) {
|
|
|
1060
1132
|
let canonical = path;
|
|
1061
1133
|
if (path.startsWith(`${root}/`))
|
|
1062
1134
|
canonical = path.slice(root.length + 1);
|
|
1063
|
-
const helper =
|
|
1135
|
+
const helper = agentCoordOrExit(root);
|
|
1064
1136
|
const result = spawnSync(helper, ["release-claim", myOwner, canonical], {
|
|
1065
1137
|
encoding: "utf8",
|
|
1066
1138
|
...coordHelperOpts(root),
|
|
@@ -1068,7 +1140,7 @@ function runReleaseClaim(path) {
|
|
|
1068
1140
|
if (result.status !== 0) {
|
|
1069
1141
|
emit.error({
|
|
1070
1142
|
code: "release_claim_failed",
|
|
1071
|
-
message: result
|
|
1143
|
+
message: spawnFailureMessage(result, "agent-coord"),
|
|
1072
1144
|
});
|
|
1073
1145
|
process.exit(1);
|
|
1074
1146
|
}
|
|
@@ -1106,7 +1178,7 @@ function runSetTask(task, opts) {
|
|
|
1106
1178
|
const priorHb = readHeartbeat(myOwner);
|
|
1107
1179
|
const firstOfSession = !priorHb?.task_updated_at;
|
|
1108
1180
|
// Heartbeat mutation goes through agent-coord (atomic temp+rename).
|
|
1109
|
-
const helper =
|
|
1181
|
+
const helper = agentCoordOrExit(root);
|
|
1110
1182
|
const result = spawnSync(helper, ["set-task", myOwner, task], {
|
|
1111
1183
|
encoding: "utf8",
|
|
1112
1184
|
...coordHelperOpts(root),
|
|
@@ -1114,7 +1186,7 @@ function runSetTask(task, opts) {
|
|
|
1114
1186
|
if (result.status !== 0) {
|
|
1115
1187
|
emit.error({
|
|
1116
1188
|
code: "set_task_failed",
|
|
1117
|
-
message: result
|
|
1189
|
+
message: spawnFailureMessage(result, "agent-coord"),
|
|
1118
1190
|
});
|
|
1119
1191
|
process.exit(1);
|
|
1120
1192
|
}
|
|
@@ -1232,7 +1304,7 @@ function runStatus(opts) {
|
|
|
1232
1304
|
if (!hb) {
|
|
1233
1305
|
emit.error({
|
|
1234
1306
|
code: "no_heartbeat",
|
|
1235
|
-
message:
|
|
1307
|
+
message: noHeartbeatMessage(myOwner),
|
|
1236
1308
|
});
|
|
1237
1309
|
process.exit(1);
|
|
1238
1310
|
}
|
|
@@ -1241,7 +1313,7 @@ function runStatus(opts) {
|
|
|
1241
1313
|
// populated for back-compat with consumers reading v1 directly. The stamp
|
|
1242
1314
|
// goes through agent-coord (atomic write).
|
|
1243
1315
|
try {
|
|
1244
|
-
const helper =
|
|
1316
|
+
const helper = agentCoordOrExit(root);
|
|
1245
1317
|
spawnSync(helper, ["stamp-status-call", myOwner], {
|
|
1246
1318
|
encoding: "utf8",
|
|
1247
1319
|
timeout: 2000,
|
|
@@ -3025,6 +3097,41 @@ function runHeal(opts) {
|
|
|
3025
3097
|
process.exit(1);
|
|
3026
3098
|
}
|
|
3027
3099
|
const action = kind === "pidmap" ? "heal-pidmap" : kind === "heartbeat" ? "heal-heartbeat" : "kill-heartbeat";
|
|
3100
|
+
// Refuse to mint a heartbeat at a TRUNCATED owner id.
|
|
3101
|
+
//
|
|
3102
|
+
// Heartbeats are keyed by the whole instance_id, so healing at an
|
|
3103
|
+
// abbreviated id writes `.harnery/active/<prefix>.json` while every reader
|
|
3104
|
+
// (`status`, `set-task`, `whoami`) resolves `<instance_id>.json`. The heal
|
|
3105
|
+
// reports success, the session stays broken, and an orphan file is left
|
|
3106
|
+
// behind that the singleton fallback can then mis-resolve other callers to.
|
|
3107
|
+
//
|
|
3108
|
+
// A distinct instance_id (subagent, workflow child) is never a prefix of its
|
|
3109
|
+
// own session_id, so a strict prefix is unambiguously an abbreviated id
|
|
3110
|
+
// rather than a legitimately different owner. Only guard the create path: an
|
|
3111
|
+
// existing heartbeat at `owner` means the id is real, whatever its shape.
|
|
3112
|
+
//
|
|
3113
|
+
// Two sources for the canonical id, because the reported failure arrives
|
|
3114
|
+
// without `--session-id`: someone copies a truncated id out of a diagnostic
|
|
3115
|
+
// and passes it as `--owner` alone. Comparing against `--session-id` alone
|
|
3116
|
+
// therefore misses the exact path that produced the orphan; a live heartbeat
|
|
3117
|
+
// whose instance_id this id is a prefix of settles it just as well, and is
|
|
3118
|
+
// present in precisely the case that matters (the session the reader was
|
|
3119
|
+
// trying to heal is registered, just not under the abbreviated name).
|
|
3120
|
+
if (kind === "heartbeat" && !readHeartbeat(owner)) {
|
|
3121
|
+
const canonical = opts.sessionId && opts.sessionId.trim() !== owner && opts.sessionId.trim().startsWith(owner)
|
|
3122
|
+
? opts.sessionId.trim()
|
|
3123
|
+
: liveIdWithPrefix(root, owner);
|
|
3124
|
+
if (canonical) {
|
|
3125
|
+
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live heartbeat";
|
|
3126
|
+
emit.error({
|
|
3127
|
+
code: "truncated_owner",
|
|
3128
|
+
message: `--owner ${owner} is a prefix of ${canonical} (${source}), and no heartbeat exists ` +
|
|
3129
|
+
`at .harnery/active/${owner}.json. Healing here would create a heartbeat no reader ` +
|
|
3130
|
+
`resolves. Re-run with --owner ${canonical}.`,
|
|
3131
|
+
});
|
|
3132
|
+
process.exit(1);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3028
3135
|
// Build positional args. agent-coord's arg layout:
|
|
3029
3136
|
// heal-pidmap <instance_id> [<pid>]
|
|
3030
3137
|
// heal-heartbeat <instance_id> [<session_id>]
|
|
@@ -3035,8 +3142,8 @@ function runHeal(opts) {
|
|
|
3035
3142
|
if (kind === "heartbeat" && opts.sessionId)
|
|
3036
3143
|
helperArgs.push(opts.sessionId);
|
|
3037
3144
|
// heal-pidmap / heal-heartbeat / kill-heartbeat are handled by the
|
|
3038
|
-
// agent-coord binary
|
|
3039
|
-
const helper =
|
|
3145
|
+
// bundled agent-coord binary.
|
|
3146
|
+
const helper = agentCoordOrExit(root);
|
|
3040
3147
|
const proc = spawnSync(helper, helperArgs, {
|
|
3041
3148
|
encoding: "utf8",
|
|
3042
3149
|
...coordHelperOpts(root),
|
|
@@ -3044,7 +3151,7 @@ function runHeal(opts) {
|
|
|
3044
3151
|
if (proc.status !== 0) {
|
|
3045
3152
|
emit.error({
|
|
3046
3153
|
code: "heal_failed",
|
|
3047
|
-
message: proc
|
|
3154
|
+
message: spawnFailureMessage(proc, `agent-coord ${action}`),
|
|
3048
3155
|
});
|
|
3049
3156
|
process.exit(1);
|
|
3050
3157
|
}
|
|
@@ -3079,7 +3186,10 @@ function runHeal(opts) {
|
|
|
3079
3186
|
],
|
|
3080
3187
|
meta: {
|
|
3081
3188
|
kind,
|
|
3082
|
-
|
|
3189
|
+
// The path actually spawned, not a guess at the layout: the helper is
|
|
3190
|
+
// resolved from harnery's own package location, which differs between a
|
|
3191
|
+
// submodule, an installed dependency, and a standalone checkout.
|
|
3192
|
+
helper,
|
|
3083
3193
|
},
|
|
3084
3194
|
});
|
|
3085
3195
|
if (!opts.json) {
|
|
@@ -3793,7 +3903,7 @@ function runCouncilContribute(id, opts) {
|
|
|
3793
3903
|
if (!myHb?.name) {
|
|
3794
3904
|
emit.error({
|
|
3795
3905
|
code: "no_self_name",
|
|
3796
|
-
message: `resolved owner ${myOwner
|
|
3906
|
+
message: `resolved owner ${myOwner} has no name on heartbeat (pass --as <member> to override)`,
|
|
3797
3907
|
});
|
|
3798
3908
|
process.exit(1);
|
|
3799
3909
|
}
|
package/dist/commands/grep.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ type DecodeMode = "content" | "count" | "filesOnly" | "plainLines";
|
|
|
60
60
|
* chunk-boundary tests. Record shapes (both engines, pinned by the parity
|
|
61
61
|
* suite):
|
|
62
62
|
* content: <path>NUL<line>:<text>LF
|
|
63
|
-
* count: <path>NUL<count>LF
|
|
63
|
+
* count: <path>NUL<count>LF (BSD grep ignores --null here: <path>:<count>LF)
|
|
64
64
|
* filesOnly: <path>NUL (NUL is the record terminator)
|
|
65
65
|
* plainLines: <path>LF (files mode: rg --files / find)
|
|
66
66
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../src/commands/grep.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../src/commands/grep.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/grep.js
CHANGED
|
@@ -546,7 +546,7 @@ function buildFindArgs(pattern, paths, opts, extraExcludeDirs) {
|
|
|
546
546
|
* chunk-boundary tests. Record shapes (both engines, pinned by the parity
|
|
547
547
|
* suite):
|
|
548
548
|
* content: <path>NUL<line>:<text>LF
|
|
549
|
-
* count: <path>NUL<count>LF
|
|
549
|
+
* count: <path>NUL<count>LF (BSD grep ignores --null here: <path>:<count>LF)
|
|
550
550
|
* filesOnly: <path>NUL (NUL is the record terminator)
|
|
551
551
|
* plainLines: <path>LF (files mode: rg --files / find)
|
|
552
552
|
*
|
|
@@ -610,8 +610,25 @@ export class NulDecoder {
|
|
|
610
610
|
return { file: normalizeFile(path), line: 0, text: "" };
|
|
611
611
|
}
|
|
612
612
|
const nul = record.indexOf(0);
|
|
613
|
-
if (nul < 0)
|
|
613
|
+
if (nul < 0) {
|
|
614
|
+
// BSD grep honours --null for content and -l records but IGNORES it under
|
|
615
|
+
// -c, emitting `<path>:<count>` where GNU grep emits `<path>NUL<count>`.
|
|
616
|
+
// Without this fallback every count row is dropped as unframed chatter, so
|
|
617
|
+
// `grep -c` returns nothing on a host whose only engine is BSD grep. The
|
|
618
|
+
// count is always a terminal run of digits, so a greedy match on the LAST
|
|
619
|
+
// colon recovers the path even when the path itself contains colons.
|
|
620
|
+
if (this.mode === "count") {
|
|
621
|
+
const bsd = /^(.*):(\d+)$/.exec(record.toString("utf8"));
|
|
622
|
+
if (!bsd)
|
|
623
|
+
return null;
|
|
624
|
+
return {
|
|
625
|
+
file: normalizeFile(bsd[1]),
|
|
626
|
+
line: Number.parseInt(bsd[2], 10),
|
|
627
|
+
text: "",
|
|
628
|
+
};
|
|
629
|
+
}
|
|
614
630
|
return null; // not a NUL-framed record (stray engine chatter)
|
|
631
|
+
}
|
|
615
632
|
const file = normalizeFile(record.subarray(0, nul).toString("utf8"));
|
|
616
633
|
const rest = record.subarray(nul + 1).toString("utf8");
|
|
617
634
|
if (this.mode === "count") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/commands/tunnel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAa1E,OAAO,EAYL,KAAK,WAAW,EAGjB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/commands/tunnel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAa1E,OAAO,EAYL,KAAK,WAAW,EAGjB,MAAM,wBAAwB,CAAC;AA+rBhC;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAsE7F;AAgND,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,YAAY,CAAC,EAAE,qBAAqB,GACnC,IAAI,CA4EN"}
|
package/dist/commands/tunnel.js
CHANGED
|
@@ -146,12 +146,28 @@ function sweepStrays(gatePort, alreadyKilled) {
|
|
|
146
146
|
return (killByPattern(`gate\\.ts.*--port ${gatePort}( |$)`, alreadyKilled) +
|
|
147
147
|
killByPattern(`--url http://localhost:${gatePort}( |$)`, alreadyKilled));
|
|
148
148
|
}
|
|
149
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* Ports currently bound by a LISTEN socket (best-effort, platform-dependent).
|
|
151
|
+
*
|
|
152
|
+
* `ss` (iproute2) is Linux-only. Without the lsof fallback this returns an empty
|
|
153
|
+
* set on macOS/BSD. That does not fail loudly. It reports every port as free, so
|
|
154
|
+
* `allocateGatePort` can hand out an occupied port and the reload port-release
|
|
155
|
+
* check becomes inert.
|
|
156
|
+
*/
|
|
150
157
|
function listeningPorts() {
|
|
151
158
|
const ports = new Set();
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
for (const m of
|
|
159
|
+
const ss = spawnSync("ss", ["-tlnH"], { encoding: "utf-8" });
|
|
160
|
+
if (ss.status === 0 && typeof ss.stdout === "string") {
|
|
161
|
+
for (const m of ss.stdout.matchAll(/:(\d+)\s/g))
|
|
162
|
+
ports.add(Number(m[1]));
|
|
163
|
+
return ports;
|
|
164
|
+
}
|
|
165
|
+
// Rows look like: `bun 123 user 4u IPv4 0x… 0t0 TCP 127.0.0.1:58055 (LISTEN)`.
|
|
166
|
+
// Status can be nonzero while stdout still holds usable rows (lsof reports a
|
|
167
|
+
// failure when any single process is unreadable), so judge it on the output.
|
|
168
|
+
const lsof = spawnSync("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"], { encoding: "utf-8" });
|
|
169
|
+
if (typeof lsof.stdout === "string") {
|
|
170
|
+
for (const m of lsof.stdout.matchAll(/:(\d+) \(LISTEN\)$/gm))
|
|
155
171
|
ports.add(Number(m[1]));
|
|
156
172
|
}
|
|
157
173
|
return ports;
|
|
@@ -21,16 +21,11 @@ export interface CanonicalEmitInput {
|
|
|
21
21
|
/**
|
|
22
22
|
* Resolve the coord root for a canonical emit.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `<root>/harnery/bin/agent-coord` doesn't exist — so `agents status` /
|
|
30
|
-
* `set-task` emits silently vanished and the Stop-hook's rule 1/3
|
|
31
|
-
* (`state.status_checked` in-turn) blocked turns that had done the ritual.
|
|
32
|
-
* Same bug class as the coordHelperOpts root-pin fix; this closes the
|
|
33
|
-
* emitCanonical instance of it.
|
|
24
|
+
* Delegates to `resolveCoordRoot()`, the single resolution the hooks and the
|
|
25
|
+
* CLI's reads also use. An emit resolved any other way is an emit the Stop hook
|
|
26
|
+
* cannot see: the hook reads `state.status_checked` from the stream under ITS
|
|
27
|
+
* root, so a divergent emit root blocks rule 1/3 on a turn that did run
|
|
28
|
+
* `agents status`, and no sequence of CLI commands can satisfy it.
|
|
34
29
|
*/
|
|
35
30
|
export declare function resolveEmitRoot(start?: string): string | null;
|
|
36
31
|
export declare function emitCanonical(input: CanonicalEmitInput): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canonical-emit.d.ts","sourceRoot":"","sources":["../../../src/core/agents/canonical-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"canonical-emit.d.ts","sourceRoot":"","sources":["../../../src/core/agents/canonical-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAE5E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAsC7D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,GAAG,OAAO,CAIjG"}
|
|
@@ -9,44 +9,26 @@
|
|
|
9
9
|
* latency budget per call.
|
|
10
10
|
*/
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { monorepoRoot } from "./coord-client.js";
|
|
12
|
+
import { coordBinPath } from "./coord-bin.js";
|
|
13
|
+
import { resolveCoordRoot } from "./coord-client.js";
|
|
15
14
|
/**
|
|
16
15
|
* Resolve the coord root for a canonical emit.
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* `<root>/harnery/bin/agent-coord` doesn't exist — so `agents status` /
|
|
24
|
-
* `set-task` emits silently vanished and the Stop-hook's rule 1/3
|
|
25
|
-
* (`state.status_checked` in-turn) blocked turns that had done the ritual.
|
|
26
|
-
* Same bug class as the coordHelperOpts root-pin fix; this closes the
|
|
27
|
-
* emitCanonical instance of it.
|
|
17
|
+
* Delegates to `resolveCoordRoot()`, the single resolution the hooks and the
|
|
18
|
+
* CLI's reads also use. An emit resolved any other way is an emit the Stop hook
|
|
19
|
+
* cannot see: the hook reads `state.status_checked` from the stream under ITS
|
|
20
|
+
* root, so a divergent emit root blocks rule 1/3 on a turn that did run
|
|
21
|
+
* `agents status`, and no sequence of CLI commands can satisfy it.
|
|
28
22
|
*/
|
|
29
23
|
export function resolveEmitRoot(start = process.cwd()) {
|
|
30
|
-
|
|
31
|
-
if (override)
|
|
32
|
-
return override;
|
|
33
|
-
// Memoized per start-dir: the session-tee middleware resolves once per
|
|
34
|
-
// emitted event, and monorepoRoot() spawns git — cache so a streaming
|
|
35
|
-
// command doesn't pay 1-3 subprocess spawns per output line.
|
|
36
|
-
if (cachedRoot && cachedRoot.start === start)
|
|
37
|
-
return cachedRoot.root;
|
|
38
|
-
const gitRoot = monorepoRoot();
|
|
39
|
-
const root = gitRoot && existsSync(join(gitRoot, ".harnery")) ? gitRoot : findRepoRoot(start);
|
|
40
|
-
cachedRoot = { start, root };
|
|
41
|
-
return root;
|
|
24
|
+
return resolveCoordRoot(start);
|
|
42
25
|
}
|
|
43
|
-
let cachedRoot;
|
|
44
26
|
export function emitCanonical(input) {
|
|
45
27
|
const root = resolveEmitRoot();
|
|
46
28
|
if (!root)
|
|
47
29
|
return;
|
|
48
|
-
const binary =
|
|
49
|
-
if (!
|
|
30
|
+
const binary = coordBinPath("agent-coord", root);
|
|
31
|
+
if (!binary)
|
|
50
32
|
return;
|
|
51
33
|
try {
|
|
52
34
|
const args = [
|
|
@@ -97,14 +79,3 @@ export function normalizeAdapter(platform) {
|
|
|
97
79
|
return "codex";
|
|
98
80
|
return "claude-code";
|
|
99
81
|
}
|
|
100
|
-
function findRepoRoot(start) {
|
|
101
|
-
let dir = resolve(start);
|
|
102
|
-
while (true) {
|
|
103
|
-
if (existsSync(join(dir, ".harnery")))
|
|
104
|
-
return dir;
|
|
105
|
-
const parent = dirname(dir);
|
|
106
|
-
if (parent === dir)
|
|
107
|
-
return null;
|
|
108
|
-
dir = parent;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locate harnery's own bundled coordination binaries (`agent-coord`,
|
|
3
|
+
* `agent-hook`, `harn`).
|
|
4
|
+
*
|
|
5
|
+
* Every coord-helper spawn used to build its path as
|
|
6
|
+
* `<coordRoot>/harnery/bin/<name>`, which only exists in one host layout: a
|
|
7
|
+
* superproject carrying harnery as a submodule at `harnery/`. That assumption
|
|
8
|
+
* was load-bearing in the worst way — because the path missed whenever the
|
|
9
|
+
* resolved root was anything else, root resolution itself was bent toward the
|
|
10
|
+
* git superproject to keep the spawns working, which is what split the CLI's
|
|
11
|
+
* root from the hook's and blocked end-of-turn rule 1/3. The same missing path
|
|
12
|
+
* also surfaced as a crash rather than an error: `spawnSync` on a nonexistent
|
|
13
|
+
* binary reports `status: null` and `stderr: null`, so a caller checking
|
|
14
|
+
* `status !== 0` then reading `stderr.trim()` threw "null is not an object".
|
|
15
|
+
*
|
|
16
|
+
* The binaries ship inside the harnery package, so the package is what knows
|
|
17
|
+
* where they are. Resolution walks up from this module to the package root,
|
|
18
|
+
* which covers every install shape: `src/` under Bun, `dist/` on Node, and
|
|
19
|
+
* `node_modules/harnery/`. The layout-specific paths remain as fallbacks so a
|
|
20
|
+
* host whose harnery copy is not the one executing (a vendored tree, a shim)
|
|
21
|
+
* still resolves.
|
|
22
|
+
*/
|
|
23
|
+
export type CoordBinName = "agent-coord" | "agent-hook" | "harn";
|
|
24
|
+
/**
|
|
25
|
+
* Absolute path to one of harnery's bundled binaries, or null when no candidate
|
|
26
|
+
* exists on disk. Callers must handle null — a missing helper is a real state
|
|
27
|
+
* (a partially installed package) and silently spawning a nonexistent path is
|
|
28
|
+
* what produced the null-deref crash above.
|
|
29
|
+
*/
|
|
30
|
+
export declare function coordBinPath(name: CoordBinName, coordRoot?: string | null): string | null;
|
|
31
|
+
/** Candidate paths for `name`, in resolution order. Exported for tests. */
|
|
32
|
+
export declare function coordBinCandidates(name: CoordBinName, coordRoot?: string | null): string[];
|
|
33
|
+
//# sourceMappingURL=coord-bin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coord-bin.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-bin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAOH,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,MAAM,CAAC;AAEjE;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAKzF;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAuB1F"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locate harnery's own bundled coordination binaries (`agent-coord`,
|
|
3
|
+
* `agent-hook`, `harn`).
|
|
4
|
+
*
|
|
5
|
+
* Every coord-helper spawn used to build its path as
|
|
6
|
+
* `<coordRoot>/harnery/bin/<name>`, which only exists in one host layout: a
|
|
7
|
+
* superproject carrying harnery as a submodule at `harnery/`. That assumption
|
|
8
|
+
* was load-bearing in the worst way — because the path missed whenever the
|
|
9
|
+
* resolved root was anything else, root resolution itself was bent toward the
|
|
10
|
+
* git superproject to keep the spawns working, which is what split the CLI's
|
|
11
|
+
* root from the hook's and blocked end-of-turn rule 1/3. The same missing path
|
|
12
|
+
* also surfaced as a crash rather than an error: `spawnSync` on a nonexistent
|
|
13
|
+
* binary reports `status: null` and `stderr: null`, so a caller checking
|
|
14
|
+
* `status !== 0` then reading `stderr.trim()` threw "null is not an object".
|
|
15
|
+
*
|
|
16
|
+
* The binaries ship inside the harnery package, so the package is what knows
|
|
17
|
+
* where they are. Resolution walks up from this module to the package root,
|
|
18
|
+
* which covers every install shape: `src/` under Bun, `dist/` on Node, and
|
|
19
|
+
* `node_modules/harnery/`. The layout-specific paths remain as fallbacks so a
|
|
20
|
+
* host whose harnery copy is not the one executing (a vendored tree, a shim)
|
|
21
|
+
* still resolves.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync } from "node:fs";
|
|
24
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { coordEnv } from "../../lib/env.js";
|
|
27
|
+
/**
|
|
28
|
+
* Absolute path to one of harnery's bundled binaries, or null when no candidate
|
|
29
|
+
* exists on disk. Callers must handle null — a missing helper is a real state
|
|
30
|
+
* (a partially installed package) and silently spawning a nonexistent path is
|
|
31
|
+
* what produced the null-deref crash above.
|
|
32
|
+
*/
|
|
33
|
+
export function coordBinPath(name, coordRoot) {
|
|
34
|
+
for (const candidate of coordBinCandidates(name, coordRoot)) {
|
|
35
|
+
if (existsSync(candidate))
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Candidate paths for `name`, in resolution order. Exported for tests. */
|
|
41
|
+
export function coordBinCandidates(name, coordRoot) {
|
|
42
|
+
const candidates = [];
|
|
43
|
+
const push = (value) => {
|
|
44
|
+
if (value && !candidates.includes(value))
|
|
45
|
+
candidates.push(value);
|
|
46
|
+
};
|
|
47
|
+
// Explicit pin, for a host that relocates the binaries.
|
|
48
|
+
const pinned = coordEnv("BIN_DIR");
|
|
49
|
+
if (pinned)
|
|
50
|
+
push(join(pinned, name));
|
|
51
|
+
// The package this code is executing from.
|
|
52
|
+
const pkgRoot = packageRoot(name);
|
|
53
|
+
if (pkgRoot)
|
|
54
|
+
push(join(pkgRoot, "bin", name));
|
|
55
|
+
if (coordRoot) {
|
|
56
|
+
// Host layouts, in decreasing specificity: harnery as a submodule, as an
|
|
57
|
+
// installed dependency, or the coord root being harnery's own checkout.
|
|
58
|
+
push(resolve(coordRoot, "harnery", "bin", name));
|
|
59
|
+
push(resolve(coordRoot, "node_modules", "harnery", "bin", name));
|
|
60
|
+
push(resolve(coordRoot, "bin", name));
|
|
61
|
+
}
|
|
62
|
+
return candidates;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Walk up from this module to the directory holding `bin/<name>`.
|
|
66
|
+
*
|
|
67
|
+
* The walk keys on the binary itself rather than on `package.json`, because
|
|
68
|
+
* both `src/core/agents/` and the built `dist/core/agents/` sit under the same
|
|
69
|
+
* package root and only the binary's presence proves we found the right level.
|
|
70
|
+
*/
|
|
71
|
+
function packageRoot(name) {
|
|
72
|
+
let dir;
|
|
73
|
+
try {
|
|
74
|
+
dir = dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// No import.meta (a CJS transpile); the coordRoot fallbacks still apply.
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
81
|
+
if (existsSync(join(dir, "bin", name)))
|
|
82
|
+
return dir;
|
|
83
|
+
const parent = dirname(dir);
|
|
84
|
+
if (parent === dir)
|
|
85
|
+
break;
|
|
86
|
+
dir = parent;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|