harnery 0.8.0 → 0.10.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 +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +8 -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 +37 -0
- 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/grep.d.ts +35 -2
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +427 -139
- 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/init.d.ts +15 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +125 -14
- 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 +4 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +82 -0
- 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 +14 -5
- 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/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.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/rules/stop-hook.d.ts +8 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +8 -0
- 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 +2 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +24 -4
- package/dist/core/agents/state/heartbeat-writer.d.ts +16 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +24 -7
- 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 +77 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +210 -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 +309 -66
- package/dist/core/hooks/events/schema.d.ts +42 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts +7 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +4 -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/hooks/resolve/coord-root.d.ts +11 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -1
- package/dist/core/hooks/resolve/coord-root.js +28 -6
- 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 +48 -0
- package/dist/core/workflow/billing.d.ts.map +1 -0
- package/dist/core/workflow/billing.js +108 -0
- package/dist/core/workflow/child-env.d.ts +30 -0
- package/dist/core/workflow/child-env.d.ts.map +1 -0
- package/dist/core/workflow/child-env.js +43 -0
- package/dist/core/workflow/engine.d.ts +21 -0
- package/dist/core/workflow/engine.d.ts.map +1 -0
- package/dist/core/workflow/engine.js +349 -0
- package/dist/core/workflow/harnesses.d.ts +17 -0
- package/dist/core/workflow/harnesses.d.ts.map +1 -0
- package/dist/core/workflow/harnesses.js +21 -0
- package/dist/core/workflow/spawn-claude.d.ts +25 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -0
- package/dist/core/workflow/spawn-claude.js +103 -0
- package/dist/core/workflow/spawn-codex.d.ts +22 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -0
- package/dist/core/workflow/spawn-codex.js +88 -0
- package/dist/core/workflow/spawn-cursor.d.ts +29 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -0
- package/dist/core/workflow/spawn-cursor.js +91 -0
- package/dist/core/workflow/types.d.ts +152 -0
- package/dist/core/workflow/types.d.ts.map +1 -0
- package/dist/core/workflow/types.js +9 -0
- package/dist/core/workflow/validate.d.ts +15 -0
- package/dist/core/workflow/validate.d.ts.map +1 -0
- package/dist/core/workflow/validate.js +70 -0
- 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 +12 -1
- package/schemas/config.schema.json +88 -19
- package/src/commander.ts +8 -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 +44 -0
- package/src/commands/eml.ts +22 -4
- package/src/commands/grep.ts +535 -142
- package/src/commands/harness.ts +147 -0
- package/src/commands/init.ts +138 -17
- 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 +123 -0
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +15 -6
- 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/claim-conflict.ts +26 -1
- package/src/core/agents/rules/commit-conflict.ts +4 -33
- package/src/core/agents/rules/stop-hook.ts +17 -0
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +23 -2
- package/src/core/agents/state/heartbeat-writer.ts +33 -7
- 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 +271 -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 +361 -65
- package/src/core/hooks/events/schema.ts +72 -0
- package/src/core/hooks/harness/events.ts +11 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- package/src/core/hooks/resolve/coord-root.ts +28 -6
- 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 +152 -0
- package/src/core/workflow/child-env.ts +47 -0
- package/src/core/workflow/engine.ts +403 -0
- package/src/core/workflow/harnesses.ts +33 -0
- package/src/core/workflow/spawn-claude.ts +118 -0
- package/src/core/workflow/spawn-codex.ts +93 -0
- package/src/core/workflow/spawn-cursor.ts +108 -0
- package/src/core/workflow/types.ts +160 -0
- package/src/core/workflow/validate.ts +75 -0
- 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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { MAX_FRAME_BYTES } from "../core/presence/relay-protocol.js";
|
|
2
|
+
/**
|
|
3
|
+
* `relay serve`: the Bun self-host of the presence relay (ADR 0016 phase 2).
|
|
4
|
+
* Same wire protocol as the Cloudflare Durable Objects host in relay/worker/ —
|
|
5
|
+
* one in-memory room per opaque room id, warm-join cache, fan-out, rate
|
|
6
|
+
* limits. For teams who'd rather run their own box than use the public
|
|
7
|
+
* relay.harnery.com; point clients at it via `.harnery/config.jsonc` →
|
|
8
|
+
* `{ "presence": { "relay": "ws://host:port" } }`.
|
|
9
|
+
*
|
|
10
|
+
* Bun-only (Bun.serve WebSockets), like `harn tunnel` — fails fast with a
|
|
11
|
+
* clear message on a Bun-free host.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_SOCKETS_PER_ROOM = 64;
|
|
14
|
+
const RATE_BURST = 10;
|
|
15
|
+
const RATE_REFILL_MS = 2000;
|
|
16
|
+
const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
17
|
+
const ROOM_ID_RE = /^[0-9a-f]{32}$/;
|
|
18
|
+
/** Start the relay server. Exported for in-process integration tests. */
|
|
19
|
+
export function startRelayServer(port) {
|
|
20
|
+
if (typeof Bun === "undefined" || typeof Bun.serve !== "function") {
|
|
21
|
+
throw new Error("relay serve requires Bun (Bun.serve WebSockets)");
|
|
22
|
+
}
|
|
23
|
+
const rooms = new Map();
|
|
24
|
+
const roomOf = (roomId) => {
|
|
25
|
+
let r = rooms.get(roomId);
|
|
26
|
+
if (!r) {
|
|
27
|
+
r = { sockets: new Set(), cache: new Map() };
|
|
28
|
+
rooms.set(roomId, r);
|
|
29
|
+
}
|
|
30
|
+
return r;
|
|
31
|
+
};
|
|
32
|
+
const server = Bun.serve({
|
|
33
|
+
port,
|
|
34
|
+
fetch(req, srv) {
|
|
35
|
+
const url = new URL(req.url);
|
|
36
|
+
if (url.pathname === "/" || url.pathname === "/healthz") {
|
|
37
|
+
return Response.json({ ok: true, service: "harnery-presence-relay", v: 1 });
|
|
38
|
+
}
|
|
39
|
+
const m = /^\/v1\/room\/([0-9a-f]{32})$/.exec(url.pathname);
|
|
40
|
+
if (!m || !ROOM_ID_RE.test(m[1]))
|
|
41
|
+
return new Response("not found", { status: 404 });
|
|
42
|
+
const room = roomOf(m[1]);
|
|
43
|
+
if (room.sockets.size >= MAX_SOCKETS_PER_ROOM) {
|
|
44
|
+
return new Response("room full", { status: 429 });
|
|
45
|
+
}
|
|
46
|
+
const ok = srv.upgrade(req, {
|
|
47
|
+
data: { roomId: m[1], bucketTokens: RATE_BURST, bucketAt: Date.now() },
|
|
48
|
+
});
|
|
49
|
+
return ok ? undefined : new Response("expected websocket", { status: 426 });
|
|
50
|
+
},
|
|
51
|
+
websocket: {
|
|
52
|
+
open(ws) {
|
|
53
|
+
const room = roomOf(ws.data.roomId);
|
|
54
|
+
room.sockets.add(ws);
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
for (const [sender, entry] of room.cache) {
|
|
57
|
+
if (now - entry.at > CACHE_TTL_MS) {
|
|
58
|
+
room.cache.delete(sender);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
ws.send(entry.raw);
|
|
62
|
+
}
|
|
63
|
+
ws.send(JSON.stringify({ t: "hello", peers: room.sockets.size }));
|
|
64
|
+
},
|
|
65
|
+
message(ws, message) {
|
|
66
|
+
if (typeof message !== "string" || message.length > MAX_FRAME_BYTES)
|
|
67
|
+
return;
|
|
68
|
+
// Rate limit (token bucket per socket).
|
|
69
|
+
const d = ws.data;
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const refill = Math.floor((now - d.bucketAt) / RATE_REFILL_MS);
|
|
72
|
+
if (refill > 0) {
|
|
73
|
+
d.bucketTokens = Math.min(RATE_BURST, d.bucketTokens + refill);
|
|
74
|
+
d.bucketAt = now;
|
|
75
|
+
}
|
|
76
|
+
if (d.bucketTokens <= 0)
|
|
77
|
+
return;
|
|
78
|
+
d.bucketTokens -= 1;
|
|
79
|
+
let frame;
|
|
80
|
+
try {
|
|
81
|
+
frame = JSON.parse(message);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (frame.t !== "pub" ||
|
|
87
|
+
typeof frame.sender !== "string" ||
|
|
88
|
+
!/^[0-9a-f]+$/.test(frame.sender) ||
|
|
89
|
+
frame.sender.length > 64) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const room = roomOf(d.roomId);
|
|
93
|
+
room.cache.set(frame.sender, { raw: message, at: now });
|
|
94
|
+
for (const peer of room.sockets) {
|
|
95
|
+
if (peer === ws)
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
peer.send(message);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* dead socket */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
close(ws) {
|
|
106
|
+
const room = rooms.get(ws.data.roomId);
|
|
107
|
+
if (!room)
|
|
108
|
+
return;
|
|
109
|
+
room.sockets.delete(ws);
|
|
110
|
+
if (room.sockets.size === 0 && room.cache.size === 0)
|
|
111
|
+
rooms.delete(ws.data.roomId);
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
port: server.port ?? port,
|
|
117
|
+
stop: () => server.stop(true),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function registerRelayCommand(program, emit) {
|
|
121
|
+
const cmd = program
|
|
122
|
+
.command("relay")
|
|
123
|
+
.description("Self-host the presence relay (see also relay/worker/ for the Cloudflare host)");
|
|
124
|
+
cmd
|
|
125
|
+
.command("serve")
|
|
126
|
+
.description("Run the presence relay on this machine (Bun-only; same protocol as relay/worker/)")
|
|
127
|
+
.option("--port <n>", "Port to listen on", "8787")
|
|
128
|
+
.action((opts) => {
|
|
129
|
+
let server;
|
|
130
|
+
try {
|
|
131
|
+
server = startRelayServer(Number.parseInt(opts.port, 10));
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
emit.error({
|
|
135
|
+
code: "relay_serve_failed",
|
|
136
|
+
message: e instanceof Error ? e.message : String(e),
|
|
137
|
+
});
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
emit.log(`presence relay listening on :${server.port} (ws path: /v1/room/<room-id>)`, "info");
|
|
141
|
+
// Foreground service: run until interrupted.
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAoHnD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAuK7E"}
|
package/dist/commands/sync.js
CHANGED
|
@@ -22,6 +22,7 @@ import { spawnSync } from "node:child_process";
|
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
23
|
import os from "node:os";
|
|
24
24
|
import path from "node:path";
|
|
25
|
+
import { syncJsoncConfig } from "../core/config.js";
|
|
25
26
|
// What we sync. Anything matching one of these (relative to .harnery/) is
|
|
26
27
|
// considered part of the sync set. Everything else stays local-only.
|
|
27
28
|
const SYNC_PATHS = ["identities/", "scratch/archived/", "councils/"];
|
|
@@ -29,10 +30,14 @@ function configPath() {
|
|
|
29
30
|
return path.join(os.homedir(), ".config", "harnery", "sync.json");
|
|
30
31
|
}
|
|
31
32
|
function loadConfig() {
|
|
33
|
+
// Precedence: env → .harnery/config.jsonc `sync` → ~/.config/harnery/sync.json.
|
|
32
34
|
const envRemote = process.env.HARNERY_SYNC_REMOTE;
|
|
33
35
|
const envPrefix = process.env.HARNERY_SYNC_PREFIX ?? "harnery";
|
|
34
36
|
if (envRemote)
|
|
35
37
|
return { remote: envRemote, prefix: envPrefix };
|
|
38
|
+
const jsonc = syncJsoncConfig();
|
|
39
|
+
if (jsonc)
|
|
40
|
+
return jsonc;
|
|
36
41
|
const p = configPath();
|
|
37
42
|
if (!existsSync(p))
|
|
38
43
|
return null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA0BnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA+FjF"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { workflowSubscriptionOnly } from "../core/config.js";
|
|
2
|
+
import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
|
|
3
|
+
import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
|
|
4
|
+
export function registerWorkflowCommand(program, emit) {
|
|
5
|
+
const registry = createBuiltinHarnessRegistry();
|
|
6
|
+
const harnesses = registry.ids();
|
|
7
|
+
const workflow = program
|
|
8
|
+
.command("workflow")
|
|
9
|
+
.description("Run bounded, schema-gated multi-subagent workflow scripts.");
|
|
10
|
+
workflow
|
|
11
|
+
.command("run <script>")
|
|
12
|
+
.description("Execute a workflow script (plain JS: `export default async ({agent, parallel, stage, log}) => …`). " +
|
|
13
|
+
"Subagents spawn as headless harness-CLI subprocesses, coordination-registered.")
|
|
14
|
+
.option("--max-agents <n>", "Total-agent ceiling for the run (default 50)")
|
|
15
|
+
.option("--concurrency <n>", "Concurrent-subagent cap (default 4)")
|
|
16
|
+
.option("--cwd <dir>", "Working directory children spawn in (default: coord root)")
|
|
17
|
+
.option("--harness <name>", `Default harness for agent() calls: ${harnesses.join(" | ")} (default claude-code); scripts can override per agent via opts.harness`)
|
|
18
|
+
.option("--resume-from <run-id>", "Reuse completed agent results from a prior run's journal; only changed/failed calls re-run")
|
|
19
|
+
.option("--subscription-only", "Guarantee subscription billing: scrub API-key vars from child envs; fail loud when a " +
|
|
20
|
+
"harness has no stored login (repo default via config.jsonc workflow.subscriptionOnly)")
|
|
21
|
+
.option("--allow-api-billing", "Permit an exported API key to override a stored subscription login (per-token billing); " +
|
|
22
|
+
"without this the engine refuses that silent-override state")
|
|
23
|
+
.option("--json", "Emit the full RunReport as JSON")
|
|
24
|
+
.action(async (script, opts) => {
|
|
25
|
+
const coordRoot = findCoordRoot();
|
|
26
|
+
if (!coordRoot) {
|
|
27
|
+
emit.error({
|
|
28
|
+
code: "no_coord_root",
|
|
29
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
30
|
+
});
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
if (opts.harness && !registry.get(opts.harness)) {
|
|
34
|
+
emit.error({
|
|
35
|
+
code: "bad_harness",
|
|
36
|
+
message: `unknown harness "${opts.harness}" (expected: ${harnesses.join(" | ")})`,
|
|
37
|
+
});
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
// Flag beats config; the two flags contradict each other by design.
|
|
41
|
+
const subscriptionOnly = opts.subscriptionOnly || workflowSubscriptionOnly(coordRoot);
|
|
42
|
+
if (subscriptionOnly && opts.allowApiBilling) {
|
|
43
|
+
emit.error({
|
|
44
|
+
code: "billing_flags_conflict",
|
|
45
|
+
message: "--allow-api-billing contradicts subscription-only mode " +
|
|
46
|
+
"(flag or config.jsonc workflow.subscriptionOnly); pick one",
|
|
47
|
+
});
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const { runWorkflow } = await import("../core/workflow/engine.js");
|
|
52
|
+
const report = await runWorkflow(script, {
|
|
53
|
+
coordRoot,
|
|
54
|
+
spawners: registry.spawners(),
|
|
55
|
+
defaultHarness: opts.harness,
|
|
56
|
+
resumeFrom: opts.resumeFrom,
|
|
57
|
+
subscriptionOnly,
|
|
58
|
+
allowApiBilling: opts.allowApiBilling,
|
|
59
|
+
maxAgents: opts.maxAgents ? Number.parseInt(opts.maxAgents, 10) : undefined,
|
|
60
|
+
concurrency: opts.concurrency ? Number.parseInt(opts.concurrency, 10) : undefined,
|
|
61
|
+
cwd: opts.cwd,
|
|
62
|
+
});
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
emit.config({ format: "json" });
|
|
65
|
+
emit.data(report);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const cachedPart = report.agentsCached > 0 ? ` (+${report.agentsCached} cached)` : "";
|
|
69
|
+
const billingPart = report.billing.length
|
|
70
|
+
? `billing: ${report.billing.map((b) => `${b.harness}=${b.mode}`).join(", ")}\n`
|
|
71
|
+
: "";
|
|
72
|
+
emit.text(`run ${report.runId} (${report.name}) finished: ${report.agentsSpawned} agent(s)${cachedPart}, ` +
|
|
73
|
+
`$${report.costUsd.toFixed(4)}, ${Math.round(report.durationMs / 1000)}s\n${billingPart}` +
|
|
74
|
+
`journal: ${report.journalPath}\n` +
|
|
75
|
+
`result: ${typeof report.result === "string" ? report.result : JSON.stringify(report.result, null, 2)}\n`);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
emit.error({ code: "workflow_failed", message: err.message });
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -18,6 +18,21 @@ export interface CanonicalEmitInput {
|
|
|
18
18
|
parentSessionId?: string;
|
|
19
19
|
parentTurnId?: string;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the coord root for a canonical emit.
|
|
23
|
+
*
|
|
24
|
+
* Order: explicit `HARNERY_COORD_ROOT_OVERRIDE` → git-superproject-aware
|
|
25
|
+
* `monorepoRoot()` (must actually carry `.harnery/`) → cwd walk (non-git
|
|
26
|
+
* hosts). The git-aware step is load-bearing: a shell cd'd into a nested
|
|
27
|
+
* directory that carries its own `.harnery/` (e.g. an embedded harnery
|
|
28
|
+
* checkout) made the old cwd-walk resolve the NESTED root, from which
|
|
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.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveEmitRoot(start?: string): string | null;
|
|
21
36
|
export declare function emitCanonical(input: CanonicalEmitInput): void;
|
|
22
37
|
/**
|
|
23
38
|
* Normalize the heartbeat's `platform` field (which uses snake_case
|
|
@@ -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;AAOH,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;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAW5E;AAID,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"}
|
|
@@ -11,8 +11,38 @@
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
import { monorepoRoot } from "./coord-client.js";
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the coord root for a canonical emit.
|
|
17
|
+
*
|
|
18
|
+
* Order: explicit `HARNERY_COORD_ROOT_OVERRIDE` → git-superproject-aware
|
|
19
|
+
* `monorepoRoot()` (must actually carry `.harnery/`) → cwd walk (non-git
|
|
20
|
+
* hosts). The git-aware step is load-bearing: a shell cd'd into a nested
|
|
21
|
+
* directory that carries its own `.harnery/` (e.g. an embedded harnery
|
|
22
|
+
* checkout) made the old cwd-walk resolve the NESTED root, from which
|
|
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.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveEmitRoot(start = process.cwd()) {
|
|
30
|
+
const override = process.env.HARNERY_COORD_ROOT_OVERRIDE;
|
|
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;
|
|
42
|
+
}
|
|
43
|
+
let cachedRoot;
|
|
14
44
|
export function emitCanonical(input) {
|
|
15
|
-
const root =
|
|
45
|
+
const root = resolveEmitRoot();
|
|
16
46
|
if (!root)
|
|
17
47
|
return;
|
|
18
48
|
const binary = resolve(root, "harnery", "bin", "agent-coord");
|
|
@@ -38,11 +68,19 @@ export function emitCanonical(input) {
|
|
|
38
68
|
args.push("--parent-session-id", input.parentSessionId);
|
|
39
69
|
if (input.parentTurnId)
|
|
40
70
|
args.push("--parent-turn-id", input.parentTurnId);
|
|
41
|
-
|
|
71
|
+
// Pin cwd + HARNERY_COORD_ROOT_OVERRIDE so the child writes to the SAME
|
|
72
|
+
// stream we resolved, regardless of where the caller's shell is cd'd.
|
|
73
|
+
const result = spawnSync(binary, args, {
|
|
42
74
|
encoding: "utf8",
|
|
43
75
|
stdio: "ignore",
|
|
44
76
|
timeout: 3000,
|
|
77
|
+
cwd: root,
|
|
78
|
+
env: { ...process.env, HARNERY_COORD_ROOT_OVERRIDE: root },
|
|
45
79
|
});
|
|
80
|
+
if (result.error || result.status !== 0) {
|
|
81
|
+
const why = result.error ? result.error.message : `exit ${result.status}`;
|
|
82
|
+
process.stderr.write(`emitCanonical: ${input.type} emit failed (${why})\n`); // lint-ok-emission: soft-fail diagnostic promised by the module doc; silent drops cost blocked turns
|
|
83
|
+
}
|
|
46
84
|
}
|
|
47
85
|
catch {
|
|
48
86
|
/* never break the caller */
|
|
@@ -59,7 +97,7 @@ export function normalizeHarness(platform) {
|
|
|
59
97
|
return "codex";
|
|
60
98
|
return "claude-code";
|
|
61
99
|
}
|
|
62
|
-
function findRepoRoot(start
|
|
100
|
+
function findRepoRoot(start) {
|
|
63
101
|
let dir = resolve(start);
|
|
64
102
|
while (true) {
|
|
65
103
|
if (existsSync(join(dir, ".harnery")))
|
package/dist/core/agents/cli.js
CHANGED
|
@@ -188,7 +188,9 @@ async function handleStateAction(root, action, rest) {
|
|
|
188
188
|
const task = args.join(" ");
|
|
189
189
|
const hb = writer.setTask(root, owner, task);
|
|
190
190
|
if (!hb) {
|
|
191
|
-
|
|
191
|
+
// Name the RESOLVED root: when a nested .harnery/ shadows the real
|
|
192
|
+
// coordination home, the full path is what makes that diagnosable.
|
|
193
|
+
process.stderr.write(`agent-coord set-task: no heartbeat at ${root}/.harnery/active/${owner}.json\n`);
|
|
192
194
|
return 1;
|
|
193
195
|
}
|
|
194
196
|
process.stdout.write(`${JSON.stringify({ instance_id: owner, task: hb.task ?? null, cleared: !task })}\n`);
|
|
@@ -427,11 +429,16 @@ async function handlePostCommit(root) {
|
|
|
427
429
|
}
|
|
428
430
|
const { groupUnclaim } = await import("./state/heartbeat-writer.js");
|
|
429
431
|
// Session-group-wide unclaim. `owner` is the parent's session_id which is
|
|
430
|
-
// also the group key (parent + subagents share session_id).
|
|
432
|
+
// also the group key (parent + subagents share session_id). Each actual
|
|
433
|
+
// removal also emits a durable claim.release event — the projector rebuilds
|
|
434
|
+
// files_touched from the permanent Edit/Write events, so a file-only prune
|
|
435
|
+
// would resurrect on the next replay.
|
|
431
436
|
if (req.owner && Array.isArray(req.prune)) {
|
|
432
437
|
for (const path of req.prune) {
|
|
433
438
|
try {
|
|
434
|
-
groupUnclaim(root, req.owner, path)
|
|
439
|
+
for (const hit of groupUnclaim(root, req.owner, path)) {
|
|
440
|
+
await emitClaimRelease(root, hit.instance_id, hit, path, "commit");
|
|
441
|
+
}
|
|
435
442
|
}
|
|
436
443
|
catch {
|
|
437
444
|
/* best-effort */
|
|
@@ -453,7 +460,9 @@ async function handlePostCheckout(root, _rest) {
|
|
|
453
460
|
if (req.owner && Array.isArray(req.removed)) {
|
|
454
461
|
for (const path of req.removed) {
|
|
455
462
|
try {
|
|
456
|
-
groupUnclaim(root, req.owner, path)
|
|
463
|
+
for (const hit of groupUnclaim(root, req.owner, path)) {
|
|
464
|
+
await emitClaimRelease(root, hit.instance_id, hit, path, "checkout");
|
|
465
|
+
}
|
|
457
466
|
}
|
|
458
467
|
catch {
|
|
459
468
|
/* best-effort */
|
|
@@ -657,7 +666,7 @@ async function handleResolveName(root, rest) {
|
|
|
657
666
|
process.stdout.write(`${JSON.stringify({ instance_id: owner, name: null, kind: null })}\n`);
|
|
658
667
|
return 0;
|
|
659
668
|
}
|
|
660
|
-
process.stdout.write(`${JSON.stringify({ instance_id: owner, name: resolved.name, kind: resolved.kind })}\n`);
|
|
669
|
+
process.stdout.write(`${JSON.stringify({ instance_id: owner, name: resolved.name, kind: resolved.kind, agent_id: resolved.agent_id ?? null })}\n`);
|
|
661
670
|
return 0;
|
|
662
671
|
}
|
|
663
672
|
async function handleEmitEvent(root, rest) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;
|
|
1
|
+
{"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAiCH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;iDAC6C;IAC7C,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAqBnE"}
|
|
@@ -26,6 +26,7 @@ import { createHash } from "node:crypto";
|
|
|
26
26
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
27
27
|
import { join } from "node:path";
|
|
28
28
|
import { coordEnv } from "../../../lib/env.js";
|
|
29
|
+
import { readRemoteMachines } from "../../presence/index.js";
|
|
29
30
|
import { formatPendingCouncils } from "./session-context.js";
|
|
30
31
|
/**
|
|
31
32
|
* Build the combined UserPromptSubmit additionalContext string. Returns "" when
|
|
@@ -132,7 +133,10 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
|
|
|
132
133
|
continue;
|
|
133
134
|
peers.push(hb);
|
|
134
135
|
}
|
|
135
|
-
|
|
136
|
+
// Cross-machine presence (ADR 0016): sessions on other machines, from the
|
|
137
|
+
// locally-fetched presence refs. Advisory (no claim blocking in v1).
|
|
138
|
+
const remote = readRemoteMachinesSafe(coordRoot);
|
|
139
|
+
if (peers.length === 0 && remote.length === 0)
|
|
136
140
|
return "";
|
|
137
141
|
// Build hash basis: sorted-by-instance_id projection of semantically-relevant fields.
|
|
138
142
|
const basis = peers
|
|
@@ -146,19 +150,56 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
|
|
|
146
150
|
platform: p.platform ?? null,
|
|
147
151
|
}))
|
|
148
152
|
.sort((a, b) => (a.instance_id ?? "").localeCompare(b.instance_id ?? ""));
|
|
149
|
-
|
|
153
|
+
// Remote basis: machine + per-agent identity/task/files (published_at is
|
|
154
|
+
// excluded — a keepalive re-publish alone shouldn't re-emit the table).
|
|
155
|
+
const remoteBasis = remote.map((m) => ({
|
|
156
|
+
machine: m.machine,
|
|
157
|
+
agents: m.agents.map((a) => ({
|
|
158
|
+
instance_id: a.instance_id,
|
|
159
|
+
name: a.name ?? null,
|
|
160
|
+
task: a.task ?? null,
|
|
161
|
+
files_touched: Array.from(a.files_touched ?? []).sort(),
|
|
162
|
+
})),
|
|
163
|
+
}));
|
|
164
|
+
const newHash = sha256Hex16(JSON.stringify({ local: basis, remote: remoteBasis }));
|
|
150
165
|
const hashFile = join(coordRoot, ".harnery", `.last-peer-hash.${selfInstanceId}`);
|
|
151
166
|
const oldHash = safeRead(hashFile);
|
|
152
167
|
if (oldHash && oldHash === newHash)
|
|
153
168
|
return "";
|
|
154
169
|
// Render peer table via the same formatter used at SessionStart.
|
|
155
|
-
const
|
|
170
|
+
const local = formatPeerTable(peers, mySessionId);
|
|
171
|
+
const remoteTable = formatRemoteTable(remote);
|
|
172
|
+
const table = [local, remoteTable].filter(Boolean).join("\n\n");
|
|
156
173
|
if (!table)
|
|
157
174
|
return "";
|
|
158
175
|
// Persist hash atomically (temp + rename, same convention as other coord writes).
|
|
159
176
|
writeHashFile(hashFile, newHash);
|
|
160
177
|
return table;
|
|
161
178
|
}
|
|
179
|
+
/** readRemoteMachines with a local failure guard (rendering must never throw). */
|
|
180
|
+
function readRemoteMachinesSafe(coordRoot) {
|
|
181
|
+
try {
|
|
182
|
+
return readRemoteMachines(coordRoot);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Render sessions on other machines (presence transport) as a subtable. */
|
|
189
|
+
function formatRemoteTable(remote) {
|
|
190
|
+
if (remote.length === 0)
|
|
191
|
+
return "";
|
|
192
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
193
|
+
const lines = [];
|
|
194
|
+
for (const m of remote) {
|
|
195
|
+
for (const a of m.agents.slice(0, 10)) {
|
|
196
|
+
lines.push(formatRow({ ...a, display_files: Array.from(a.files_touched ?? []).sort() }, nowSec).replace(/^ {2}- (agent-[^ ]+)/, ` - $1 @${m.machine}`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (lines.length === 0)
|
|
200
|
+
return "";
|
|
201
|
+
return `Sessions on other machines (advisory, via presence refs):\n${lines.join("\n")}`;
|
|
202
|
+
}
|
|
162
203
|
function computeCouncilPendingIfChanged(coordRoot, selfInstanceId, agentName) {
|
|
163
204
|
const councilsDir = join(coordRoot, ".harnery", "councils");
|
|
164
205
|
if (!existsSync(councilsDir))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AAqB1E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiE7D;AA+ID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AAwDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
|
|
@@ -11,6 +11,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { resolveBinName, resolveHooksSetupHint } from "../../config.js";
|
|
13
13
|
import { harneryVersion, loadHarnessWiring } from "../../hooks/harness/wiring.js";
|
|
14
|
+
import { readRemoteMachines } from "../../presence/index.js";
|
|
14
15
|
/**
|
|
15
16
|
* Build the combined SessionStart systemMessage. Returns the additionalContext
|
|
16
17
|
* string (or "" if there's nothing to say).
|
|
@@ -20,7 +21,9 @@ export function renderSessionContext(opts) {
|
|
|
20
21
|
const messages = [];
|
|
21
22
|
// 1. Self-name line + peer table (folded if peers present)
|
|
22
23
|
const peers = readActivePeers(coordRoot, instanceId);
|
|
23
|
-
const
|
|
24
|
+
const localTable = formatPeerTable(peers, sessionId);
|
|
25
|
+
// Cross-machine presence (ADR 0016): sessions on other machines (advisory).
|
|
26
|
+
const peerTable = [localTable, formatRemoteMachines(coordRoot)].filter(Boolean).join("\n\n");
|
|
24
27
|
if (agentName) {
|
|
25
28
|
const suffix = platformLabel ? ` (${platformLabel})` : "";
|
|
26
29
|
const selfLine = `You are agent-${agentName}${suffix}.`;
|
|
@@ -72,6 +75,28 @@ export function renderSessionContext(opts) {
|
|
|
72
75
|
}
|
|
73
76
|
return messages.join("\n\n");
|
|
74
77
|
}
|
|
78
|
+
/** One-line-per-agent view of sessions on other machines (presence refs). */
|
|
79
|
+
function formatRemoteMachines(coordRoot) {
|
|
80
|
+
try {
|
|
81
|
+
const remote = readRemoteMachines(coordRoot);
|
|
82
|
+
if (remote.length === 0)
|
|
83
|
+
return "";
|
|
84
|
+
const lines = [];
|
|
85
|
+
for (const m of remote) {
|
|
86
|
+
for (const a of m.agents.slice(0, 10)) {
|
|
87
|
+
const task = a.task ? ` "${a.task.slice(0, 60)}"` : "";
|
|
88
|
+
const files = a.files_touched?.length
|
|
89
|
+
? `holds: ${a.files_touched.slice(0, 3).join(", ")}${a.files_touched.length > 3 ? `, +${a.files_touched.length - 3} more` : ""}`
|
|
90
|
+
: "nothing held";
|
|
91
|
+
lines.push(` - agent-${a.name ?? a.instance_id.slice(0, 8)} @${m.machine}${task} (${files})`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return `Sessions on other machines (advisory, via presence refs):\n${lines.join("\n")}`;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
75
100
|
/** Read all peer heartbeats from .harnery/active/, excluding self. */
|
|
76
101
|
function readActivePeers(coordRoot, selfInstanceId) {
|
|
77
102
|
const out = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/claim-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"claim-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/claim-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAeF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,aAAa,CAoGjF"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { spawnSync } from "node:child_process";
|
|
17
17
|
import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
+
import { emit } from "../events/emit.js";
|
|
19
20
|
const FRESHNESS_SECS = 600;
|
|
20
21
|
/**
|
|
21
22
|
* Evaluate a file-claim. Returns deny when another fresh peer claims the
|
|
@@ -267,13 +268,37 @@ function pruneClaimFromPeer(coordRoot, instanceId, relPath) {
|
|
|
267
268
|
try {
|
|
268
269
|
const body = JSON.parse(readFileSync(path, "utf8"));
|
|
269
270
|
const files = body.files_touched ?? [];
|
|
270
|
-
|
|
271
|
+
// Normalize both sides: files_touched can hold absolute-under-coordRoot
|
|
272
|
+
// entries (legacy projections) as well as canonical relative ones — an
|
|
273
|
+
// exact-string filter silently no-ops on the mixed-form case and the
|
|
274
|
+
// stale claim never heals.
|
|
275
|
+
const norm = (p) => p.startsWith(`${coordRoot}/`) ? p.slice(coordRoot.length + 1) : p;
|
|
276
|
+
const target = norm(relPath);
|
|
277
|
+
const next = files.filter((p) => norm(p) !== target);
|
|
271
278
|
if (next.length === files.length)
|
|
272
279
|
return;
|
|
273
280
|
body.files_touched = next;
|
|
274
281
|
const tmp = `${path}.tmp.${process.pid}`;
|
|
275
282
|
writeFileSync(tmp, JSON.stringify(body, null, 2), "utf8");
|
|
276
283
|
renameSync(tmp, path);
|
|
284
|
+
// Durable subtraction: the projector rebuilds files_touched from the
|
|
285
|
+
// permanent Edit/Write events, so a file-only prune resurrects on the
|
|
286
|
+
// next replay. Soft-fail — the heartbeat write already landed.
|
|
287
|
+
try {
|
|
288
|
+
const platform = body.platform;
|
|
289
|
+
emit(coordRoot, {
|
|
290
|
+
event_type: "claim.release",
|
|
291
|
+
instance_id: instanceId,
|
|
292
|
+
session_id: body.session_id ?? instanceId,
|
|
293
|
+
harness: platform === "cursor" ? "cursor" : platform === "codex" ? "codex" : "claude-code",
|
|
294
|
+
source: "agent-coord",
|
|
295
|
+
data: { path: target, reason: "heal" },
|
|
296
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* soft-fail */
|
|
301
|
+
}
|
|
277
302
|
}
|
|
278
303
|
catch {
|
|
279
304
|
/* silent */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commit-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/commit-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;
|
|
1
|
+
{"version":3,"file":"commit-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/commit-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAgBH,UAAU,QAAQ;IAChB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;wEACoE;IACpE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,mBAAmB,CAsGhG"}
|