harnery 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +6 -0
- package/dist/commands/agents.d.ts +24 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +244 -13
- package/dist/commands/backup.d.ts +5 -4
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +15 -14
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +27 -1
- package/dist/commands/claude-desktop.d.ts +19 -0
- package/dist/commands/claude-desktop.d.ts.map +1 -0
- package/dist/commands/claude-desktop.js +168 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +149 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -3
- package/dist/commands/eml.d.ts +32 -0
- package/dist/commands/eml.d.ts.map +1 -1
- package/dist/commands/eml.js +16 -3
- package/dist/commands/harness.d.ts +11 -0
- package/dist/commands/harness.d.ts.map +1 -0
- package/dist/commands/harness.js +118 -0
- package/dist/commands/presence.d.ts +9 -4
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +88 -5
- package/dist/commands/relay.d.ts +9 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +143 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +53 -17
- package/dist/core/agents/canonical-emit.d.ts +15 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +41 -3
- package/dist/core/agents/cli.js +4 -2
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +44 -3
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +26 -1
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +4 -37
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +16 -43
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +11 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +3 -3
- package/dist/core/agents/state/names.d.ts +38 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +46 -7
- package/dist/core/agents/state/pidmap.d.ts +5 -1
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +58 -2
- package/dist/core/agents/state/stale-sweep.d.ts +3 -2
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +6 -6
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +191 -25
- package/dist/core/context/index.d.ts +144 -0
- package/dist/core/context/index.d.ts.map +1 -0
- package/dist/core/context/index.js +380 -0
- package/dist/core/harnesses/bench.d.ts +29 -0
- package/dist/core/harnesses/bench.d.ts.map +1 -0
- package/dist/core/harnesses/bench.js +151 -0
- package/dist/core/harnesses/index.d.ts +9 -0
- package/dist/core/harnesses/index.d.ts.map +1 -0
- package/dist/core/harnesses/index.js +4 -0
- package/dist/core/harnesses/profiles.d.ts +62 -0
- package/dist/core/harnesses/profiles.d.ts.map +1 -0
- package/dist/core/harnesses/profiles.js +115 -0
- package/dist/core/harnesses/registry.d.ts +15 -0
- package/dist/core/harnesses/registry.d.ts.map +1 -0
- package/dist/core/harnesses/registry.js +113 -0
- package/dist/core/harnesses/types.d.ts +64 -0
- package/dist/core/harnesses/types.d.ts.map +1 -0
- package/dist/core/harnesses/types.js +20 -0
- package/dist/core/hooks/cli.js +280 -63
- package/dist/core/hooks/events/schema.d.ts +38 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +3 -0
- package/dist/core/hooks/harness/parse.d.ts +1 -1
- package/dist/core/hooks/harness/parse.d.ts.map +1 -1
- package/dist/core/hooks/harness/parse.js +4 -0
- package/dist/core/presence/blob.d.ts +40 -0
- package/dist/core/presence/blob.d.ts.map +1 -0
- package/dist/core/presence/blob.js +91 -0
- package/dist/core/presence/git.d.ts +68 -0
- package/dist/core/presence/git.d.ts.map +1 -0
- package/dist/core/presence/git.js +173 -0
- package/dist/core/presence/index.d.ts +74 -0
- package/dist/core/presence/index.d.ts.map +1 -0
- package/dist/core/presence/index.js +224 -0
- package/dist/core/presence/relay-client.d.ts +39 -0
- package/dist/core/presence/relay-client.d.ts.map +1 -0
- package/dist/core/presence/relay-client.js +295 -0
- package/dist/core/presence/relay-protocol.d.ts +94 -0
- package/dist/core/presence/relay-protocol.d.ts.map +1 -0
- package/dist/core/presence/relay-protocol.js +169 -0
- package/dist/core/workflow/billing.d.ts +1 -1
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +7 -1
- package/dist/core/workflow/engine.d.ts +5 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +164 -16
- package/dist/core/workflow/harnesses.d.ts +3 -3
- package/dist/core/workflow/harnesses.d.ts.map +1 -1
- package/dist/core/workflow/harnesses.js +8 -17
- package/dist/core/workflow/index.d.ts +5 -0
- package/dist/core/workflow/index.d.ts.map +1 -0
- package/dist/core/workflow/index.js +3 -0
- package/dist/core/workflow/proof.d.ts +42 -0
- package/dist/core/workflow/proof.d.ts.map +1 -0
- package/dist/core/workflow/proof.js +342 -0
- package/dist/core/workflow/spawn-claude.d.ts +4 -1
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +39 -18
- package/dist/core/workflow/spawn-codex.d.ts +4 -1
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +44 -22
- package/dist/core/workflow/spawn-cursor.d.ts +4 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +35 -16
- package/dist/core/workflow/types.d.ts +156 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +1 -1
- package/dist/lib/browser/client.d.ts +16 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +22 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -0
- package/dist/lib/browser/launch-args.d.ts +21 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -0
- package/dist/lib/browser/launch-args.js +32 -0
- package/dist/lib/claude-desktop.d.ts +105 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -0
- package/dist/lib/claude-desktop.js +217 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +5 -2
- package/dist/lib/identities/assume.d.ts +51 -0
- package/dist/lib/identities/assume.d.ts.map +1 -0
- package/dist/lib/identities/assume.js +274 -0
- package/dist/lib/identities/index.d.ts +7 -7
- package/dist/lib/identities/index.d.ts.map +1 -1
- package/dist/lib/identities/index.js +24 -24
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +15 -2
- package/package.json +22 -6
- package/schemas/config.schema.json +88 -19
- package/src/commander.ts +6 -0
- package/src/commands/agents.ts +280 -14
- package/src/commands/backup.ts +17 -16
- package/src/commands/browse.ts +32 -0
- package/src/commands/claude-desktop.ts +215 -0
- package/src/commands/context.ts +174 -1
- package/src/commands/doctor.ts +2 -3
- package/src/commands/eml.ts +22 -4
- package/src/commands/harness.ts +147 -0
- package/src/commands/presence.ts +111 -5
- package/src/commands/relay.ts +160 -0
- package/src/commands/sync.ts +4 -0
- package/src/commands/workflow.ts +62 -19
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +4 -2
- package/src/core/agents/render/prompt-context.ts +48 -3
- package/src/core/agents/render/session-context.ts +27 -1
- package/src/core/agents/rules/commit-conflict.ts +4 -33
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +10 -1
- package/src/core/agents/state/heartbeat-writer.ts +3 -2
- package/src/core/agents/state/names.ts +68 -9
- package/src/core/agents/state/pidmap.ts +59 -2
- package/src/core/agents/state/stale-sweep.ts +6 -11
- package/src/core/config.ts +248 -24
- package/src/core/context/index.ts +575 -0
- package/src/core/harnesses/bench.ts +214 -0
- package/src/core/harnesses/index.ts +31 -0
- package/src/core/harnesses/profiles.ts +141 -0
- package/src/core/harnesses/registry.ts +140 -0
- package/src/core/harnesses/types.ts +90 -0
- package/src/core/hooks/cli.ts +331 -62
- package/src/core/hooks/events/schema.ts +68 -0
- package/src/core/hooks/harness/events.ts +3 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- package/src/core/presence/blob.ts +125 -0
- package/src/core/presence/git.ts +191 -0
- package/src/core/presence/index.ts +274 -0
- package/src/core/presence/relay-client.ts +322 -0
- package/src/core/presence/relay-protocol.ts +246 -0
- package/src/core/workflow/billing.ts +8 -2
- package/src/core/workflow/engine.ts +182 -16
- package/src/core/workflow/harnesses.ts +14 -19
- package/src/core/workflow/index.ts +43 -0
- package/src/core/workflow/proof.ts +434 -0
- package/src/core/workflow/spawn-claude.ts +39 -20
- package/src/core/workflow/spawn-codex.ts +47 -28
- package/src/core/workflow/spawn-cursor.ts +37 -18
- package/src/core/workflow/types.ts +176 -2
- package/src/lib/browser/client.ts +37 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/launch-args.ts +33 -0
- package/src/lib/claude-desktop.ts +293 -0
- package/src/lib/docs-lint.ts +5 -2
- package/src/lib/identities/assume.ts +363 -0
- package/src/lib/identities/index.ts +28 -24
- package/src/lib/instructions/templates.ts +16 -2
|
@@ -12,6 +12,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { resolveBinName, resolveHooksSetupHint } from "../../config.ts";
|
|
14
14
|
import { harneryVersion, loadHarnessWiring } from "../../hooks/harness/wiring.ts";
|
|
15
|
+
import { readRemoteMachines } from "../../presence/index.ts";
|
|
15
16
|
|
|
16
17
|
interface HeartbeatRow {
|
|
17
18
|
instance_id?: string;
|
|
@@ -48,7 +49,9 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
48
49
|
|
|
49
50
|
// 1. Self-name line + peer table (folded if peers present)
|
|
50
51
|
const peers = readActivePeers(coordRoot, instanceId);
|
|
51
|
-
const
|
|
52
|
+
const localTable = formatPeerTable(peers, sessionId);
|
|
53
|
+
// Cross-machine presence (ADR 0016): sessions on other machines (advisory).
|
|
54
|
+
const peerTable = [localTable, formatRemoteMachines(coordRoot)].filter(Boolean).join("\n\n");
|
|
52
55
|
if (agentName) {
|
|
53
56
|
const suffix = platformLabel ? ` (${platformLabel})` : "";
|
|
54
57
|
const selfLine = `You are agent-${agentName}${suffix}.`;
|
|
@@ -107,6 +110,29 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
107
110
|
return messages.join("\n\n");
|
|
108
111
|
}
|
|
109
112
|
|
|
113
|
+
/** One-line-per-agent view of sessions on other machines (presence refs). */
|
|
114
|
+
function formatRemoteMachines(coordRoot: string): string {
|
|
115
|
+
try {
|
|
116
|
+
const remote = readRemoteMachines(coordRoot);
|
|
117
|
+
if (remote.length === 0) return "";
|
|
118
|
+
const lines: string[] = [];
|
|
119
|
+
for (const m of remote) {
|
|
120
|
+
for (const a of m.agents.slice(0, 10)) {
|
|
121
|
+
const task = a.task ? ` "${a.task.slice(0, 60)}"` : "";
|
|
122
|
+
const files = a.files_touched?.length
|
|
123
|
+
? `holds: ${a.files_touched.slice(0, 3).join(", ")}${a.files_touched.length > 3 ? `, +${a.files_touched.length - 3} more` : ""}`
|
|
124
|
+
: "nothing held";
|
|
125
|
+
lines.push(
|
|
126
|
+
` - agent-${a.name ?? a.instance_id.slice(0, 8)} @${m.machine}${task} (${files})`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return `Sessions on other machines (advisory, via presence refs):\n${lines.join("\n")}`;
|
|
131
|
+
} catch {
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
110
136
|
/** Read all peer heartbeats from .harnery/active/, excluding self. */
|
|
111
137
|
function readActivePeers(coordRoot: string, selfInstanceId: string): HeartbeatRow[] {
|
|
112
138
|
const out: HeartbeatRow[] = [];
|
|
@@ -23,9 +23,8 @@
|
|
|
23
23
|
import { spawnSync } from "node:child_process";
|
|
24
24
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
25
25
|
import { join } from "node:path";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
const DEFAULT_FRESHNESS_SECS = 600;
|
|
26
|
+
import { coordFreshnessSeconds } from "../../config.ts";
|
|
27
|
+
import { instanceHasLivePid } from "../state/pidmap.ts";
|
|
29
28
|
|
|
30
29
|
interface PeerHeartbeat {
|
|
31
30
|
instance_id: string;
|
|
@@ -86,7 +85,7 @@ export function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): Co
|
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
const peers = readActivePeers(coordRoot);
|
|
89
|
-
const cutoffMs = Date.now() -
|
|
88
|
+
const cutoffMs = Date.now() - coordFreshnessSeconds(coordRoot) * 1000;
|
|
90
89
|
const stagedSet = new Set(req.staged_paths);
|
|
91
90
|
const gitlinkSet = new Set(req.staged_gitlinks ?? []);
|
|
92
91
|
|
|
@@ -233,28 +232,7 @@ function isHolderSelfAttributed(
|
|
|
233
232
|
}
|
|
234
233
|
|
|
235
234
|
function holderHasLiveForeignPid(coordRoot: string, holderId: string): boolean {
|
|
236
|
-
|
|
237
|
-
if (!existsSync(dir)) return false;
|
|
238
|
-
for (const f of readdirSync(dir)) {
|
|
239
|
-
let row = "";
|
|
240
|
-
try {
|
|
241
|
-
row = readFileSync(join(dir, f), "utf8").trim();
|
|
242
|
-
} catch {
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const owner = row.split("\t")[0]?.trim() ?? "";
|
|
246
|
-
if (owner !== holderId) continue;
|
|
247
|
-
// Process still alive?
|
|
248
|
-
const pid = Number.parseInt(f, 10);
|
|
249
|
-
if (!Number.isFinite(pid)) continue;
|
|
250
|
-
try {
|
|
251
|
-
process.kill(pid, 0); // signal 0 = liveness probe
|
|
252
|
-
return true; // live foreign pid found
|
|
253
|
-
} catch {
|
|
254
|
-
// ESRCH (no such process): pid-map entry is stale, skip
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return false;
|
|
235
|
+
return instanceHasLivePid(coordRoot, holderId);
|
|
258
236
|
}
|
|
259
237
|
|
|
260
238
|
function isPathCleanInHead(coordRoot: string, relPath: string): boolean {
|
|
@@ -290,13 +268,6 @@ function readActivePeers(coordRoot: string): PeerHeartbeat[] {
|
|
|
290
268
|
return out;
|
|
291
269
|
}
|
|
292
270
|
|
|
293
|
-
function freshnessSecs(): number {
|
|
294
|
-
const raw = coordEnv("AGENT_FRESHNESS");
|
|
295
|
-
if (!raw) return DEFAULT_FRESHNESS_SECS;
|
|
296
|
-
const n = Number.parseInt(raw, 10);
|
|
297
|
-
return Number.isFinite(n) && n > 0 ? n : DEFAULT_FRESHNESS_SECS;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
271
|
function shortName(peer: PeerHeartbeat): string {
|
|
301
272
|
if (peer.name && peer.name.length > 0) return `agent-${peer.name}`;
|
|
302
273
|
return `agent-${peer.instance_id.slice(0, 8)}`;
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { randomBytes } from "node:crypto";
|
|
12
|
-
import { existsSync, readFileSync
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
13
|
import { dirname, resolve } from "node:path";
|
|
14
14
|
// Kept dependency-light: vendored verbatim into a downstream consumer, so no coordEnv import.
|
|
15
|
-
import { normalizeHarness } from "./canonical-emit.ts";
|
|
15
|
+
import { normalizeHarness, resolveEmitRoot } from "./canonical-emit.ts";
|
|
16
16
|
import { emit } from "./events/emit.ts";
|
|
17
17
|
|
|
18
18
|
/** Event types accepted by `writeSessionEvent`. Only the command stream +
|
|
@@ -38,25 +38,14 @@ export function sessionEventsPath(): string {
|
|
|
38
38
|
// Explicit override (tests + non-monorepo invocations).
|
|
39
39
|
const explicit = process.env.HARNERY_OUTPUT_SESSION_EVENTS;
|
|
40
40
|
if (explicit) return explicit;
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
41
|
+
// Superproject-aware root resolution (git first, cwd walk fallback) via
|
|
42
|
+
// resolveEmitRoot. A plain cwd walk here mis-anchored to a NESTED
|
|
43
|
+
// `.harnery/` when the shell sat inside a submodule carrying its own
|
|
44
|
+
// (e.g. an embedded harnery checkout), silently splitting the event
|
|
45
|
+
// stream. Don't reintroduce CLAUDE.md / .git as anchors, which creates
|
|
45
46
|
// stray .harnery/ dirs under submodule subtrees.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const candidate = resolve(dir, ".harnery");
|
|
49
|
-
try {
|
|
50
|
-
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
51
|
-
return resolve(candidate, "session-events.ndjson");
|
|
52
|
-
}
|
|
53
|
-
} catch {
|
|
54
|
-
/* keep walking */
|
|
55
|
-
}
|
|
56
|
-
const parent = dirname(dir);
|
|
57
|
-
if (parent === dir) break;
|
|
58
|
-
dir = parent;
|
|
59
|
-
}
|
|
47
|
+
const root = resolveEmitRoot();
|
|
48
|
+
if (root) return resolve(root, ".harnery", "session-events.ndjson");
|
|
60
49
|
return resolve(process.env.HOME || "/tmp", ".harnery", "session-events.ndjson");
|
|
61
50
|
}
|
|
62
51
|
|
|
@@ -75,25 +64,12 @@ export function newCmdId(): string {
|
|
|
75
64
|
*/
|
|
76
65
|
export function readLastIntent(instanceId?: string): string | null {
|
|
77
66
|
if (!instanceId) return null;
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
86
|
-
agentsDir = candidate;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
/* keep walking */
|
|
91
|
-
}
|
|
92
|
-
const parent = dirname(dir);
|
|
93
|
-
if (parent === dir) break;
|
|
94
|
-
dir = parent;
|
|
95
|
-
}
|
|
96
|
-
if (!agentsDir) return null;
|
|
67
|
+
// Same superproject-aware root resolution as sessionEventsPath(): the
|
|
68
|
+
// intent stamp is written by the PreToolUse hook into the SUPERPROJECT's
|
|
69
|
+
// .harnery/, so a nested-`.harnery/` cwd must not redirect the read.
|
|
70
|
+
const root = resolveEmitRoot();
|
|
71
|
+
if (!root) return null;
|
|
72
|
+
const agentsDir = resolve(root, ".harnery");
|
|
97
73
|
const intentPath = resolve(agentsDir, `.last-intent.${instanceId}`);
|
|
98
74
|
if (!existsSync(intentPath)) return null;
|
|
99
75
|
try {
|
|
@@ -146,7 +146,8 @@ function seed(ev: CanonicalEvent, coordRoot: string): V2Heartbeat {
|
|
|
146
146
|
if (resolved) {
|
|
147
147
|
hb.name = resolved.name;
|
|
148
148
|
hb.kind = resolved.kind;
|
|
149
|
-
|
|
149
|
+
hb.agent_id =
|
|
150
|
+
resolved.agent_id ?? (resolved.kind === "subagent" ? ev.instance_id : undefined);
|
|
150
151
|
}
|
|
151
152
|
} catch {
|
|
152
153
|
/* name-history unavailable: seed stays nameless; sweep + render guards cope */
|
|
@@ -287,6 +288,14 @@ function apply(hb: V2Heartbeat, ev: CanonicalEvent, coordRoot: string): void {
|
|
|
287
288
|
hb.last_status_at = ev.ts;
|
|
288
289
|
break;
|
|
289
290
|
|
|
291
|
+
case "identity.assumed": {
|
|
292
|
+
const name = pickStr(d, "name");
|
|
293
|
+
const agentId = pickStr(d, "agent_id");
|
|
294
|
+
if (name) hb.name = name;
|
|
295
|
+
if (agentId) hb.agent_id = agentId;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
|
|
290
299
|
case "state.presence_change": {
|
|
291
300
|
const to = pickStr(d, "to");
|
|
292
301
|
if (to === "mobile" || to === "office") hb.presence = to;
|
|
@@ -311,8 +311,9 @@ export function healHeartbeat(
|
|
|
311
311
|
if (resolved) {
|
|
312
312
|
name = resolved.name;
|
|
313
313
|
kind = resolved.kind;
|
|
314
|
-
//
|
|
315
|
-
|
|
314
|
+
// An explicitly assumed session carries its durable persona UUID in
|
|
315
|
+
// name-history. Native subagents continue to use instance_id.
|
|
316
|
+
agentId = resolved.agent_id ?? (resolved.kind === "subagent" ? instanceId : "");
|
|
316
317
|
}
|
|
317
318
|
} catch {
|
|
318
319
|
/* names module unavailable, fall back to empty */
|
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
* Durable persistence: `.harnery/.name-history` (JSONL, one row per assignment)
|
|
9
9
|
* + `.harnery/.name-counter` (current counter, atomic temp+rename).
|
|
10
10
|
*
|
|
11
|
-
* Recreation rule
|
|
12
|
-
* 1. Own instance_id in name-history → (
|
|
13
|
-
* 2. session_id in name-history (owner != session) →
|
|
11
|
+
* Recreation rule:
|
|
12
|
+
* 1. Own instance_id in name-history → latest (name, kind, agent_id)
|
|
13
|
+
* 2. session_id in name-history (owner != session) → latest parent's
|
|
14
|
+
* (name, agent_id) with kind="transient"
|
|
15
|
+
*
|
|
16
|
+
* Multiple rows for one instance are intentional. `identity assume` appends a
|
|
17
|
+
* new binding instead of rewriting history; readers therefore resolve from the
|
|
18
|
+
* end of the file. Older one-row histories retain their original behavior.
|
|
14
19
|
* 3. Else: new assignment, consume a counter slot.
|
|
15
20
|
*/
|
|
16
21
|
|
|
@@ -297,11 +302,16 @@ if (COORD_NAMES.length !== 260) {
|
|
|
297
302
|
|
|
298
303
|
export type NameKind = "session" | "subagent" | "transient";
|
|
299
304
|
|
|
300
|
-
interface NameHistoryRow {
|
|
305
|
+
export interface NameHistoryRow {
|
|
301
306
|
instance_id: string;
|
|
302
307
|
name: string;
|
|
303
308
|
kind: NameKind;
|
|
304
309
|
ts: string;
|
|
310
|
+
/** Durable persona UUID. Present after `agents identity assume`. */
|
|
311
|
+
agent_id?: string;
|
|
312
|
+
/** Audit marker distinguishing an explicit role adoption from pool assignment. */
|
|
313
|
+
source?: "pool" | "identity.assume";
|
|
314
|
+
previous_name?: string;
|
|
305
315
|
}
|
|
306
316
|
|
|
307
317
|
function atomicWrite(path: string, content: string): void {
|
|
@@ -351,19 +361,29 @@ export function resolveName(
|
|
|
351
361
|
coordRoot: string,
|
|
352
362
|
instanceId: string,
|
|
353
363
|
sessionId?: string,
|
|
354
|
-
): { name: string; kind: NameKind } | null {
|
|
364
|
+
): { name: string; kind: NameKind; agent_id?: string } | null {
|
|
355
365
|
const history = readHistory(coordRoot);
|
|
356
366
|
|
|
357
|
-
for (
|
|
367
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
368
|
+
const row = history[i]!;
|
|
358
369
|
if (row.instance_id === instanceId) {
|
|
359
|
-
return {
|
|
370
|
+
return {
|
|
371
|
+
name: row.name,
|
|
372
|
+
kind: row.kind,
|
|
373
|
+
...(row.agent_id ? { agent_id: row.agent_id } : {}),
|
|
374
|
+
};
|
|
360
375
|
}
|
|
361
376
|
}
|
|
362
377
|
|
|
363
378
|
if (sessionId && sessionId !== instanceId) {
|
|
364
|
-
for (
|
|
379
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
380
|
+
const row = history[i]!;
|
|
365
381
|
if (row.instance_id === sessionId) {
|
|
366
|
-
return {
|
|
382
|
+
return {
|
|
383
|
+
name: row.name,
|
|
384
|
+
kind: "transient",
|
|
385
|
+
...(row.agent_id ? { agent_id: row.agent_id } : {}),
|
|
386
|
+
};
|
|
367
387
|
}
|
|
368
388
|
}
|
|
369
389
|
}
|
|
@@ -371,6 +391,44 @@ export function resolveName(
|
|
|
371
391
|
return null;
|
|
372
392
|
}
|
|
373
393
|
|
|
394
|
+
export interface NameAssumptionResult {
|
|
395
|
+
changed: boolean;
|
|
396
|
+
previous: { name: string; kind: NameKind; agent_id?: string } | null;
|
|
397
|
+
current: { name: string; kind: NameKind; agent_id: string };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Append an explicit instance → durable-persona binding. Latest-row-wins makes
|
|
402
|
+
* this auditable and retry-safe without mutating prior assignment history.
|
|
403
|
+
*/
|
|
404
|
+
export function recordNameAssumption(
|
|
405
|
+
coordRoot: string,
|
|
406
|
+
instanceId: string,
|
|
407
|
+
name: string,
|
|
408
|
+
agentId: string,
|
|
409
|
+
kind: NameKind = "session",
|
|
410
|
+
): NameAssumptionResult {
|
|
411
|
+
const previous = resolveName(coordRoot, instanceId);
|
|
412
|
+
const current = { name, kind, agent_id: agentId };
|
|
413
|
+
if (
|
|
414
|
+
previous?.name === current.name &&
|
|
415
|
+
previous.kind === current.kind &&
|
|
416
|
+
previous.agent_id === current.agent_id
|
|
417
|
+
) {
|
|
418
|
+
return { changed: false, previous, current };
|
|
419
|
+
}
|
|
420
|
+
appendHistory(coordRoot, {
|
|
421
|
+
instance_id: instanceId,
|
|
422
|
+
name,
|
|
423
|
+
kind,
|
|
424
|
+
agent_id: agentId,
|
|
425
|
+
source: "identity.assume",
|
|
426
|
+
previous_name: previous?.name,
|
|
427
|
+
ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
428
|
+
});
|
|
429
|
+
return { changed: true, previous, current };
|
|
430
|
+
}
|
|
431
|
+
|
|
374
432
|
/**
|
|
375
433
|
* Assign a name to <instanceId> with the given <kind>. Counter-consuming when
|
|
376
434
|
* the owner is new. Idempotent: returns existing name on resume.
|
|
@@ -393,6 +451,7 @@ export function assignName(coordRoot: string, instanceId: string, kind: NameKind
|
|
|
393
451
|
instance_id: instanceId,
|
|
394
452
|
name,
|
|
395
453
|
kind,
|
|
454
|
+
source: "pool",
|
|
396
455
|
ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
397
456
|
});
|
|
398
457
|
return name;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pid-map writer.
|
|
2
|
+
* Pid-map writer + liveness helpers.
|
|
3
3
|
*
|
|
4
4
|
* Per-harness pid-map at `.harnery/pid-map/<pid>` containing
|
|
5
5
|
* `<instance_id>\t<platform>`. `harn agents whoami` walks ppid up 20 hops looking
|
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
* Atomic temp+rename. Idempotent: re-writing the same row is a no-op.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
renameSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from "node:fs";
|
|
12
20
|
import { dirname, join } from "node:path";
|
|
13
21
|
|
|
14
22
|
function atomicWrite(path: string, content: string): void {
|
|
@@ -36,3 +44,52 @@ export function writePidmapRow(
|
|
|
36
44
|
}
|
|
37
45
|
atomicWrite(path, row);
|
|
38
46
|
}
|
|
47
|
+
|
|
48
|
+
/** True when any pid-map row for `instanceId` still belongs to a live process. */
|
|
49
|
+
export function instanceHasLivePid(coordRoot: string, instanceId: string): boolean {
|
|
50
|
+
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
51
|
+
if (!existsSync(dir)) return false;
|
|
52
|
+
for (const f of readdirSync(dir)) {
|
|
53
|
+
let row = "";
|
|
54
|
+
try {
|
|
55
|
+
row = readFileSync(join(dir, f), "utf8").trim();
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const owner = row.split("\t")[0]?.trim() ?? "";
|
|
60
|
+
if (owner !== instanceId) continue;
|
|
61
|
+
const pid = Number.parseInt(f, 10);
|
|
62
|
+
if (!Number.isFinite(pid)) continue;
|
|
63
|
+
try {
|
|
64
|
+
process.kill(pid, 0); // signal 0 = liveness probe
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
// ESRCH (no such process): pid-map entry is stale, keep scanning
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Drop every pid-map row owned by `instanceId`. Returns how many files were removed. */
|
|
74
|
+
export function removePidmapRowsForInstance(coordRoot: string, instanceId: string): number {
|
|
75
|
+
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
76
|
+
if (!existsSync(dir)) return 0;
|
|
77
|
+
let removed = 0;
|
|
78
|
+
for (const f of readdirSync(dir)) {
|
|
79
|
+
let row = "";
|
|
80
|
+
try {
|
|
81
|
+
row = readFileSync(join(dir, f), "utf8").trim();
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const owner = row.split("\t")[0]?.trim() ?? "";
|
|
86
|
+
if (owner !== instanceId) continue;
|
|
87
|
+
try {
|
|
88
|
+
unlinkSync(join(dir, f));
|
|
89
|
+
removed += 1;
|
|
90
|
+
} catch {
|
|
91
|
+
/* best-effort cleanup */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return removed;
|
|
95
|
+
}
|
|
@@ -5,17 +5,16 @@
|
|
|
5
5
|
* Fires at session.start to clean up crashed-peer detritus before the new
|
|
6
6
|
* session's UX layer reads peer state.
|
|
7
7
|
*
|
|
8
|
-
* Freshness threshold defaults to 600s; configurable via
|
|
9
|
-
* HARNERY_AGENT_COORD_FRESHNESS env var.
|
|
8
|
+
* Freshness threshold defaults to 600s; configurable via the
|
|
9
|
+
* HARNERY_AGENT_COORD_FRESHNESS env var or `.harnery/config.jsonc`
|
|
10
|
+
* `coord.freshness_seconds` (see `coordFreshnessSeconds`).
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
13
14
|
import { join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
+
import { coordFreshnessSeconds } from "../../config.ts";
|
|
15
16
|
import { emit } from "../events/emit.ts";
|
|
16
17
|
|
|
17
|
-
const DEFAULT_FRESHNESS_SECS = 600;
|
|
18
|
-
|
|
19
18
|
/** platform → harness, for the swept-event envelope (mirrors heartbeat-writer's harnessOf). */
|
|
20
19
|
function harnessFromPlatform(platform: unknown): "claude-code" | "cursor" | "codex" {
|
|
21
20
|
if (platform === "cursor") return "cursor";
|
|
@@ -65,13 +64,9 @@ export function staleSweep(coordRoot: string): {
|
|
|
65
64
|
let pidmapsRemoved = 0;
|
|
66
65
|
let peerHashesRemoved = 0;
|
|
67
66
|
|
|
68
|
-
const freshness =
|
|
69
|
-
coordEnv("AGENT_COORD_FRESHNESS") ?? String(DEFAULT_FRESHNESS_SECS),
|
|
70
|
-
10,
|
|
71
|
-
);
|
|
67
|
+
const freshness = coordFreshnessSeconds(coordRoot);
|
|
72
68
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
73
|
-
const cutoff =
|
|
74
|
-
nowSec - (Number.isFinite(freshness) && freshness > 0 ? freshness : DEFAULT_FRESHNESS_SECS);
|
|
69
|
+
const cutoff = nowSec - freshness;
|
|
75
70
|
|
|
76
71
|
// 1. Prune stale heartbeats from the canonical `.harnery/active/` dir.
|
|
77
72
|
//
|