pluriply 0.1.0 → 0.3.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 +28 -10
- package/bin/pluriply.js +122 -34
- package/package.json +1 -1
- package/src/hub/index.js +10 -7
- package/src/setup/clients.js +400 -32
- package/src/setup/hook-stop.js +157 -0
- package/src/setup/hooks.js +233 -0
- package/src/setup/run-setup.js +309 -41
- package/src/setup/toml-lite.js +87 -0
- package/src/shared/config.js +102 -0
- package/src/shared/identity.js +13 -2
- package/src/shared/lock.js +20 -0
- package/src/shared/mcp-register.js +25 -67
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@ Pluriply is a trademark of TQSoft.
|
|
|
8
8
|
|
|
9
9
|
## Supported tools
|
|
10
10
|
|
|
11
|
-
| Tool
|
|
12
|
-
|
|
|
13
|
-
| Claude Code (CLI)
|
|
14
|
-
| Codex (CLI)
|
|
15
|
-
| Antigravity CLI (`agy`)
|
|
16
|
-
| Claude Desktop
|
|
17
|
-
| Antigravity IDE
|
|
11
|
+
| Tool | Role |
|
|
12
|
+
| ----------------------- | --------------------------------- |
|
|
13
|
+
| Claude Code (CLI) | interactive peer, headless worker |
|
|
14
|
+
| Codex (CLI) | interactive peer, headless worker |
|
|
15
|
+
| Antigravity CLI (`agy`) | interactive peer, headless worker |
|
|
16
|
+
| Claude Desktop | interactive peer |
|
|
17
|
+
| Antigravity IDE | interactive peer |
|
|
18
18
|
|
|
19
19
|
Requires Node.js 20 or newer. macOS is tested; Linux should work; Windows is untested.
|
|
20
20
|
|
|
@@ -30,6 +30,10 @@ Pluriply MCP connector with each of them (idempotent — run it again any time).
|
|
|
30
30
|
- `npx pluriply setup --dry-run` — show what would change without touching anything.
|
|
31
31
|
- `npx pluriply setup --workers` — also let the hub run Claude Code / Codex / Antigravity headlessly for `send_task` and `ask_agent`.
|
|
32
32
|
- `npx pluriply setup --only claude-code,codex` — limit to specific tools.
|
|
33
|
+
- `npx pluriply setup --remove` — unregister Pluriply from every tool, disable headless workers and stop the hub. Your channels and task history under `~/.pluriply` stay; add `--purge` to delete them too.
|
|
34
|
+
- Codex and Antigravity get a 600 s MCP tool timeout written into their config at registration (their default is 60 s, too short for `ask_agent`/`request_review` waits). If you registered with an earlier version, run `setup --remove` then `setup` again to pick it up.
|
|
35
|
+
- `setup` also installs a Stop hook for Claude Code, Codex and Antigravity CLI so a live session notices new tasks and finished results at the end of its turn (see _Warm reception_). `--no-hooks` skips it; `setup --remove` takes it out again.
|
|
36
|
+
- Re-run `setup` after upgrading or cleaning the npx cache — the hook command embeds the installed path.
|
|
33
37
|
|
|
34
38
|
Restart your AI tools afterwards so they pick up the new MCP server.
|
|
35
39
|
|
|
@@ -53,6 +57,17 @@ npx pluriply worker enable codex # allow headless Codex workers
|
|
|
53
57
|
npx pluriply worker list
|
|
54
58
|
```
|
|
55
59
|
|
|
60
|
+
## Warm reception
|
|
61
|
+
|
|
62
|
+
With the Stop hook installed, a Claude Code, Codex or Antigravity CLI session
|
|
63
|
+
that is finishing a turn asks the hub whether anything arrived for it: tasks
|
|
64
|
+
sent to it, or results of tasks it delegated. If so, the session is asked to
|
|
65
|
+
handle them before it stops — no polling, no "check your tasks" from you.
|
|
66
|
+
Each item is announced once; the session reads details with `list_tasks` and
|
|
67
|
+
`get_task_result`. An idle session (waiting for your input) notices them at
|
|
68
|
+
the end of its next turn. Codex asks you to trust the new hook the first time
|
|
69
|
+
it runs. Antigravity's hook lives in `~/.gemini/config/hooks.json`.
|
|
70
|
+
|
|
56
71
|
## Where your data lives
|
|
57
72
|
|
|
58
73
|
Everything stays on your machine under `~/.pluriply/` (channels, task history,
|
|
@@ -70,7 +85,10 @@ source is not in this repository. We keep the hub proprietary because it is
|
|
|
70
85
|
the part of Pluriply we intend to build a business on; the parts that run
|
|
71
86
|
inside your tools stay open so you can audit them.
|
|
72
87
|
|
|
73
|
-
## Issues
|
|
88
|
+
## Issues and contributions
|
|
89
|
+
|
|
90
|
+
Bug reports and feature requests: https://github.com/pluriply/pluriply/issues — the templates ask for the details we need.
|
|
91
|
+
|
|
92
|
+
Pull requests are welcome for the connector, setup and shared code in this repository. `main` accepts changes only through pull requests, and the `test` workflow (ubuntu, windows, macos × Node 20, 22) must pass. The hub itself ships as a bundle under LICENSE-HUB.md and is developed separately.
|
|
74
93
|
|
|
75
|
-
|
|
76
|
-
Licensing inquiries: support@pluriply.com
|
|
94
|
+
Accepted pull requests are applied to the upstream (private) repository and land here with the next sync, so a merged PR may be rewritten by a later sync commit.
|
package/bin/pluriply.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import {
|
|
6
6
|
Hub,
|
|
7
7
|
stopHub,
|
|
8
8
|
spawnHub,
|
|
9
9
|
readLock,
|
|
10
10
|
loadConfig,
|
|
11
|
-
|
|
11
|
+
setWorkerEnabled,
|
|
12
12
|
TEMPLATE_AGENTS,
|
|
13
13
|
} from "../src/hub/index.js";
|
|
14
14
|
import { pluriplyHome } from "../src/shared/paths.js";
|
|
15
|
-
import { pingHub } from "../src/shared/probe.js";
|
|
15
|
+
import { pingHub, pidAlive } from "../src/shared/probe.js";
|
|
16
16
|
import { connectIfLive } from "../src/connector/hub-client.js";
|
|
17
17
|
import { isValidAgentName } from "../src/shared/identity.js";
|
|
18
18
|
import { registerMcpServer } from "../src/shared/mcp-register.js";
|
|
@@ -22,12 +22,31 @@ const BIN_PATH = fileURLToPath(import.meta.url);
|
|
|
22
22
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
23
23
|
const sub = rest[0];
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* `--agent x` 와 `--agent=x` 둘 다 파싱한다. 값이 없거나(마지막 토큰) 뒤 토큰이 `--`로 시작하면
|
|
27
|
+
* (다음 플래그를 값으로 삼켜버린 것) 조용히 undefined 를 돌려주지 않고 즉시 사용법 오류로
|
|
28
|
+
* 종료한다 — 그렇지 않으면 `setup --remove --purge --only`(값 없이 끝남)나
|
|
29
|
+
* `setup --remove --purge --only=codex`(`=` 형을 못 읽어 undefined)처럼 스코프를 좁히는 플래그가
|
|
30
|
+
* 조용히 무시되어 의도보다 넓은 범위(--purge 전체 삭제)가 exit 0 으로 실행된다.
|
|
31
|
+
*/
|
|
26
32
|
function flag(name) {
|
|
27
|
-
const i = rest.
|
|
28
|
-
|
|
33
|
+
const i = rest.findIndex(
|
|
34
|
+
(t) => t === `--${name}` || t.startsWith(`--${name}=`),
|
|
35
|
+
);
|
|
36
|
+
if (i === -1) return undefined;
|
|
37
|
+
const inline = rest[i].startsWith(`--${name}=`);
|
|
38
|
+
const v = inline ? rest[i].slice(name.length + 3) : rest[i + 1];
|
|
39
|
+
if (v === undefined || v === "" || (!inline && v.startsWith("--"))) {
|
|
40
|
+
console.error(`missing value for --${name}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
return v;
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
/** setup 이 아는 플래그. 오타 하나가 파괴적인 명령의 범위를 넓히지 못하게 한다. */
|
|
47
|
+
const SETUP_BOOL_FLAGS = ["workers", "dry-run", "remove", "purge", "no-hooks"];
|
|
48
|
+
const SETUP_VALUE_FLAGS = ["only"];
|
|
49
|
+
|
|
31
50
|
if (cmd === "hub" && sub === "start") {
|
|
32
51
|
try {
|
|
33
52
|
const hub = new Hub();
|
|
@@ -105,27 +124,7 @@ if (cmd === "hub" && sub === "start") {
|
|
|
105
124
|
console.error(`no worker template for "${agent}"`);
|
|
106
125
|
process.exit(1);
|
|
107
126
|
}
|
|
108
|
-
|
|
109
|
-
const workers = { ...cfg.workers };
|
|
110
|
-
if (sub === "enable")
|
|
111
|
-
workers[agent] = { ...(workers[agent] ?? {}), enabled: true };
|
|
112
|
-
else delete workers[agent];
|
|
113
|
-
// 원본 config.json 문서를 그대로 보존한 채 workers만 갱신한다: loadConfig가
|
|
114
|
-
// 돌려주는 cfg는 allowedRoots·limits를 기본값으로 채워 넣은 파생값이라, 그걸
|
|
115
|
-
// 그대로 다시 쓰면 사용자가 직접 넣은 allowedRoots(Task 1의 cwd 경계 설정)나
|
|
116
|
-
// 손대지 않은 다른 키가 사라진다. 파일을 다시 읽어 병합한다(없거나 손상돼도 {}).
|
|
117
|
-
const file = join(home, "config.json");
|
|
118
|
-
let rawDoc = {};
|
|
119
|
-
if (existsSync(file)) {
|
|
120
|
-
try {
|
|
121
|
-
rawDoc = JSON.parse(readFileSync(file, "utf8"));
|
|
122
|
-
} catch {
|
|
123
|
-
rawDoc = {};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (!rawDoc || typeof rawDoc !== "object" || Array.isArray(rawDoc))
|
|
127
|
-
rawDoc = {};
|
|
128
|
-
saveConfig(home, { ...rawDoc, workers });
|
|
127
|
+
setWorkerEnabled(home, [agent], sub === "enable");
|
|
129
128
|
if (sub === "enable") registerMcpServer(agent, { binPath: BIN_PATH });
|
|
130
129
|
console.log(`worker ${agent} ${sub}d`);
|
|
131
130
|
} else {
|
|
@@ -137,14 +136,53 @@ if (cmd === "hub" && sub === "start") {
|
|
|
137
136
|
} else if (cmd === "setup") {
|
|
138
137
|
const { runSetup, formatSetup } = await import("../src/setup/run-setup.js");
|
|
139
138
|
const { makeEnv } = await import("../src/setup/clients.js");
|
|
139
|
+
const usage = (msg) => {
|
|
140
|
+
console.error(`setup: ${msg}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
};
|
|
143
|
+
// `--only a,b` 처럼 값 플래그 바로 뒤에 오는 토큰만 대시 없는 인자로 허용한다.
|
|
144
|
+
const valueSlots = new Set();
|
|
145
|
+
rest.forEach((tok, i) => {
|
|
146
|
+
if (!tok.startsWith("--") || tok.includes("=")) return;
|
|
147
|
+
if (SETUP_VALUE_FLAGS.includes(tok.slice(2))) valueSlots.add(i + 1);
|
|
148
|
+
});
|
|
149
|
+
// 모르는 플래그는 아무것도 실행하기 전에 거부한다: `--pruge` 같은 오타가 조용히 무시되면
|
|
150
|
+
// `--remove --pruge` 가 "그냥 제거"로 통과하고, 반대로 좁히려던 플래그의 오타는 범위를 넓힌다.
|
|
151
|
+
// 대시 없는 토큰도 마찬가지다 — `pluriply setup remove` 는 지금까지 조용히 "등록"을 실행했다.
|
|
152
|
+
for (const [i, tok] of rest.entries()) {
|
|
153
|
+
if (!tok.startsWith("--")) {
|
|
154
|
+
if (!valueSlots.has(i)) usage(`unexpected argument "${tok}"`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const nm = tok.slice(2).split("=")[0];
|
|
158
|
+
if (SETUP_BOOL_FLAGS.includes(nm)) {
|
|
159
|
+
// `--dry-run=false` 처럼 값을 붙이면 지금까지는 토큰 자체가 안 맞아 조용히 무시됐다.
|
|
160
|
+
if (tok.includes("=")) usage(`--${nm} takes no value`);
|
|
161
|
+
} else if (!SETUP_VALUE_FLAGS.includes(nm)) usage(`unknown flag --${nm}`);
|
|
162
|
+
}
|
|
140
163
|
const onlyArg = flag("only");
|
|
141
164
|
const workers = rest.includes("--workers");
|
|
142
165
|
const dryRun = rest.includes("--dry-run");
|
|
166
|
+
const remove = rest.includes("--remove");
|
|
167
|
+
const purge = rest.includes("--purge");
|
|
168
|
+
const hooks = !rest.includes("--no-hooks");
|
|
169
|
+
if (remove && workers) usage("--remove cannot be combined with --workers");
|
|
170
|
+
if (purge && !remove) usage("--purge requires --remove");
|
|
171
|
+
if (purge && onlyArg) usage("--purge cannot be combined with --only");
|
|
172
|
+
if (remove && !hooks) usage("--no-hooks has no effect with --remove");
|
|
143
173
|
try {
|
|
144
174
|
const r = await runSetup({
|
|
145
|
-
only: onlyArg
|
|
175
|
+
only: onlyArg
|
|
176
|
+
? onlyArg
|
|
177
|
+
.split(",")
|
|
178
|
+
.map((x) => x.trim())
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
: undefined,
|
|
146
181
|
workers,
|
|
147
182
|
dryRun,
|
|
183
|
+
remove,
|
|
184
|
+
purge,
|
|
185
|
+
hooks,
|
|
148
186
|
env: makeEnv({ binPath: BIN_PATH }),
|
|
149
187
|
home: pluriplyHome(),
|
|
150
188
|
});
|
|
@@ -156,19 +194,69 @@ if (cmd === "hub" && sub === "start") {
|
|
|
156
194
|
process.exit(1);
|
|
157
195
|
}
|
|
158
196
|
} else if (cmd === "status") {
|
|
159
|
-
const
|
|
197
|
+
const home = pluriplyHome();
|
|
198
|
+
const lock = readLock(home);
|
|
160
199
|
if (!lock) {
|
|
161
200
|
console.log("not running");
|
|
162
201
|
} else {
|
|
163
202
|
const info = await pingHub(lock.port);
|
|
164
|
-
if (
|
|
165
|
-
console.log(`stale lockfile (pid ${lock.pid} not responding)`);
|
|
166
|
-
} else {
|
|
203
|
+
if (info) {
|
|
167
204
|
console.log(
|
|
168
205
|
`running (port ${lock.port}, pid ${info.pid ?? lock.pid}, version ${info.version ?? "unknown"}, protocol ${info.protocol ?? 1})`,
|
|
169
206
|
);
|
|
207
|
+
} else if (!pidAlive(lock.pid)) {
|
|
208
|
+
// Windows 에서는 SIGTERM 이 정리 핸들러 없이 즉시 종료라 허브가 락을 못 지운다.
|
|
209
|
+
// pid 가 죽었으면 stopHub 와 같은 판정으로 락을 지우고 not running 으로 본다.
|
|
210
|
+
// 단, pingHub 가 기다리는 동안 hub start 가 새 락을 썼을 수 있으니 다시 읽어
|
|
211
|
+
// pid 가 그대로일 때만 지운다(남의 새 락을 지우지 않기 위해).
|
|
212
|
+
const current = readLock(home);
|
|
213
|
+
if (current?.pid === lock.pid) {
|
|
214
|
+
rmSync(join(home, "hub.json"), { force: true });
|
|
215
|
+
}
|
|
216
|
+
console.log("not running");
|
|
217
|
+
} else {
|
|
218
|
+
console.log(`stale lockfile (pid ${lock.pid} not responding)`);
|
|
170
219
|
}
|
|
171
220
|
}
|
|
221
|
+
} else if (cmd === "hook" && sub === "stop") {
|
|
222
|
+
// Stop 훅(스펙 §3). 어떤 경우에도 JSON 한 줄 + exit 0. 늦게 열린 소켓이 프로세스를 잡아두지
|
|
223
|
+
// 않도록 출력 뒤 바로 종료한다.
|
|
224
|
+
const { runStopHook } = await import("../src/setup/hook-stop.js");
|
|
225
|
+
// 기존 flag()는 값이 없거나 모양이 이상하면(마지막 토큰, `--agent=`, 다음 플래그를
|
|
226
|
+
// 값으로 삼키려는 모양) exit(1)을 부르는데, 훅은 무슨 입력이 와도 항상 exit 0이어야
|
|
227
|
+
// 한다 — flag()를 쓰지 않고 여기서 직접 파싱해, 못 읽으면 그냥 undefined로 둔다
|
|
228
|
+
// (runStopHook이 알 수 없는/undefined agent를 {}로 처리한다).
|
|
229
|
+
let agent;
|
|
230
|
+
const ai = rest.findIndex((t) => t === "--agent" || t.startsWith("--agent="));
|
|
231
|
+
if (ai !== -1) {
|
|
232
|
+
if (rest[ai].startsWith("--agent=")) {
|
|
233
|
+
const v = rest[ai].slice("--agent=".length);
|
|
234
|
+
agent = v === "" ? undefined : v;
|
|
235
|
+
} else {
|
|
236
|
+
const v = rest[ai + 1];
|
|
237
|
+
agent = v === undefined || v.startsWith("--") ? undefined : v;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
let input = "";
|
|
241
|
+
if (!process.stdin.isTTY) {
|
|
242
|
+
try {
|
|
243
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
244
|
+
} catch {
|
|
245
|
+
input = "";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let out = {};
|
|
249
|
+
try {
|
|
250
|
+
out = await runStopHook({ agent, input, home: pluriplyHome() });
|
|
251
|
+
} catch {
|
|
252
|
+
out = {};
|
|
253
|
+
}
|
|
254
|
+
// 출력 뒤 바로 종료한다: 늦게 열린 소켓이 프로세스를 잡아두지 않게 한다. write()의
|
|
255
|
+
// 콜백을 기다려 스트림이 비동기 파이프인 플랫폼(Windows)에서 출력이 잘리지 않게 하고,
|
|
256
|
+
// 그 콜백이 오지 않는 경우를 대비해 폴백 타이머도 둔다(타이머 자체가 프로세스를
|
|
257
|
+
// 붙잡지 않도록 unref).
|
|
258
|
+
process.stdout.write(JSON.stringify(out) + "\n", () => process.exit(0));
|
|
259
|
+
setTimeout(() => process.exit(0), 500).unref?.();
|
|
172
260
|
} else if (cmd === "connector") {
|
|
173
261
|
const agent = flag("agent");
|
|
174
262
|
if (!agent) {
|
|
@@ -183,7 +271,7 @@ if (cmd === "hub" && sub === "start") {
|
|
|
183
271
|
await startConnector({ agent });
|
|
184
272
|
} else {
|
|
185
273
|
console.error(
|
|
186
|
-
"usage: pluriply <setup [--workers] [--dry-run] [--only a,b]|hub start|hub stop|hub restart|connector --agent <name>|status|worker enable|disable <codex|claude-code|antigravity>|worker list>",
|
|
274
|
+
"usage: pluriply <setup [--workers] [--dry-run] [--only a,b] [--no-hooks]|setup --remove [--purge] [--dry-run] [--only a,b]|hub start|hub stop|hub restart|hook stop --agent <claude-code|codex|antigravity>|connector --agent <name>|status|worker enable|disable <codex|claude-code|antigravity>|worker list>",
|
|
187
275
|
);
|
|
188
276
|
process.exit(1);
|
|
189
277
|
}
|
package/package.json
CHANGED
package/src/hub/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Copyright (c) 2026 TQSoft. All rights reserved.
|
|
2
2
|
// Licensed under LICENSE-HUB.md — not open source.
|
|
3
|
-
import{WebSocketServer as $e}from"ws";import{mkdirSync as xe,writeFileSync as be,rmSync as Et,readFileSync as Ae}from"node:fs";import{join as vt,isAbsolute as Re}from"node:path";import{mkdirSync as Ot,readFileSync as at,writeFileSync as ct,renameSync as lt,existsSync as ut,readdirSync as F,rmSync as ht}from"node:fs";import{join as _}from"node:path";import{pluriplyHome as Lt}from"../shared/paths.js";var J=/^plp-[a-z0-9]{4}-[a-z0-9]{4}$/,P=class{constructor(e=Lt()){this.root=e,this.dir=_(e,"channels"),Ot(this.dir,{recursive:!0}),this.#t()}#t(){for(let e of F(this.dir))e.endsWith(".tmp")&&ht(_(this.dir,e),{force:!0});for(let e of F(this.root))e.startsWith("agents.json.")&&e.endsWith(".tmp")&&ht(_(this.root,e),{force:!0})}loadChannel(e){if(!J.test(e))throw new Error(`invalid channel code: ${e}`);let t=_(this.dir,`${e}.json`);return ut(t)?JSON.parse(at(t,"utf8")):null}saveChannel(e,t){if(!J.test(e))throw new Error(`invalid channel code: ${e}`);let n=_(this.dir,`${e}.json`),r=`${n}.${process.pid}.tmp`;ct(r,JSON.stringify(t,null,2)),lt(r,n)}listChannels(){return F(this.dir).filter(e=>e.endsWith(".json")).map(e=>e.slice(0,-5)).filter(e=>J.test(e))}loadAgents(){let e=_(this.root,"agents.json");if(!ut(e))return{};try{let t=JSON.parse(at(e,"utf8"));return t&&typeof t=="object"&&!Array.isArray(t)?t:{}}catch{return{}}}saveAgents(e){let t=_(this.root,"agents.json"),n=`${t}.${process.pid}.tmp`;ct(n,JSON.stringify(e,null,2)),lt(n,t)}};import{channelCode as Dt}from"../shared/ids.js";var O=class{constructor(e){this.store=e}touch(e,t,n=new Date){try{let r=this.store.loadAgents();r[e]={lastChannel:t,lastSeenAt:n.toISOString()},this.store.saveAgents(r)}catch{}}resume(e,t=new Date){let n=this.store.loadAgents()[e];if(!n)return null;let r=t.getTime()-Date.parse(n.lastSeenAt);if(!(r>=0&&r<=432e5))return null;try{if(!this.store.loadChannel(n.lastChannel))return null}catch{return null}return n.lastChannel}};var B=class extends Error{constructor(e){super(`channel not found: ${e}`),this.code=e}},L=class{constructor(e){this.store=e}create(){let e={channel:{code:Dt(),createdAt:new Date().toISOString(),peers:[]},tasks:[],context:[]};return this.store.saveChannel(e.channel.code,e),e}get(e){let t=this.store.loadChannel(e);if(!t)throw new B(e);return t}save(e,t){this.store.saveChannel(e,t)}join(e,{instanceId:t,tool:n,worker:r=!1},{online:s=new Set,now:i=new Date}={}){let a=this.get(e);this.#t(a,s,i);let c=i.toISOString(),l=a.channel.peers.find(u=>u.instanceId===t);return l?l.lastSeenAt=c:a.channel.peers.push({instanceId:t,tool:n,worker:!!r,joinedAt:c,lastSeenAt:c}),this.save(e,a),{channel:a.channel,peers:a.channel.peers}}peers(e,{online:t=new Set,now:n=new Date}={}){let r=this.get(e);return this.#t(r,t,n)&&this.save(e,r),r.channel.peers}#t(e,t,n){let r=e.channel.peers.length;return e.channel.peers=e.channel.peers.filter(s=>{if(!s.instanceId)return!1;if(t.has(s.instanceId))return!0;let i=n.getTime()-Date.parse(s.lastSeenAt);return i>=0&&i<=432e5}),e.channel.peers.length!==r}};import{statSync as Mt,realpathSync as C}from"node:fs";import{sep as Nt,join as Wt,isAbsolute as qt}from"node:path";import{taskId as Kt}from"../shared/ids.js";import{parseTarget as dt,toolOf as wt,isInstanceId as Ut}from"../shared/identity.js";var Y=new Set(["completed","failed","cancelled"]),j=class extends Error{constructor(e){super(`task not found: ${e}`)}},x=class extends Error{constructor(e,t){super(`invalid task transition: ${e} -> ${t}`)}},D=class extends Error{constructor(e,t,n){super(`task ${e} is addressed to "${t}", not "${n}"`)}},G=class extends Error{constructor(e,t,n){super(`only the sender "${t}" can cancel task ${e}, not "${n}"`)}},E=class extends Error{constructor(e){super(e)}},z=class extends Error{constructor(e){super(`delegation depth limit (${e}) exceeded`)}},Ht=["task","review"],ft=["approve","request_changes","comment"],mt=["critical","important","minor"],y=class extends Error{constructor(e){super(e)}},m=class extends Error{constructor(e){super(e)}},V=class extends Error{constructor(e){super(`use submit_review for review tasks (task ${e})`)}},Q=class extends Error{constructor(e){super(`task ${e} is not a review`)}},X=class extends Error{constructor(e){super(`cwd is not an existing directory: ${e}`)}},Z=class extends Error{constructor(e){super(`cwd outside allowed roots: ${e}`)}},Ft=["auto","spawn","interactive"];function gt(o,e){return o===e||o.startsWith(e+Nt)}function Jt(o,e){if(!o||typeof o!="object"||Array.isArray(o))throw new y("review must be an object");let t={};if(o.gitRange!==void 0){if(typeof o.gitRange!="string"||o.gitRange.length===0||o.gitRange.length>200||/[\r\n]/.test(o.gitRange))throw new y("gitRange must be a single line of at most 200 characters");if(o.gitRange.startsWith("-"))throw new y("gitRange must not start with '-'");t.gitRange=o.gitRange}if(o.paths!==void 0){if(!Array.isArray(o.paths)||o.paths.some(n=>typeof n!="string"||n.length===0))throw new y("paths must be an array of strings");if(o.paths.length>0){if(e===void 0)throw new y("paths need a cwd to resolve against");let n=e;try{n=C(e)}catch{}t.paths=o.paths.map(r=>{let s=qt(r)?r:Wt(e,r),i;try{i=C(s)}catch{throw new y(`path does not exist: ${r}`)}if(!gt(i,n))throw new y(`path outside cwd: ${r}`);return i})}}if(o.focus!==void 0){if(typeof o.focus!="string"||o.focus.length>500)throw new y("focus must be a string of at most 500 characters");o.focus.length>0&&(t.focus=o.focus)}return t}function Bt(o){return`Review ${o.gitRange?`git range ${o.gitRange}`:o.paths?.length?`files ${o.paths.join(", ")}`:"the uncommitted changes (git diff HEAD)"}${o.focus?`, focusing on ${o.focus}`:""}.`}function Yt(o){if(!o||typeof o!="object"||Array.isArray(o))throw new m("review result must be an object");if(!ft.includes(o.verdict))throw new m(`verdict must be one of ${ft.join(", ")}`);if(typeof o.summary!="string"||o.summary.trim().length===0)throw new m("summary is required");let e=o.findings??[];if(!Array.isArray(e))throw new m("findings must be an array");if(e.length>200)throw new m("findings must have at most 200 items");let t=e.map((n,r)=>{if(!n||typeof n!="object"||Array.isArray(n))throw new m(`findings[${r}] must be an object`);if(!mt.includes(n.severity))throw new m(`findings[${r}].severity must be one of ${mt.join(", ")}`);if(typeof n.message!="string"||n.message.length===0)throw new m(`findings[${r}].message is required`);let s={severity:n.severity,message:n.message};if(n.file!==void 0){if(typeof n.file!="string")throw new m(`findings[${r}].file must be a string`);s.file=n.file}if(n.line!==void 0){if(!Number.isInteger(n.line)||n.line<1)throw new m(`findings[${r}].line must be a positive integer`);s.line=n.line}if(n.suggestion!==void 0){if(typeof n.suggestion!="string")throw new m(`findings[${r}].suggestion must be a string`);s.suggestion=n.suggestion}return s});return{verdict:o.verdict,findings:t,summary:o.summary}}function pt(o,e){return o.toInstance?e===o.toInstance:wt(e)===(o.toTool??o.to)}function Gt(o,e){return o.from===e?!0:!o.from.includes("#")&&wt(e)===o.from}var M=class{constructor(e){this.registry=e,this.waiters=new Map}create(e,{from:t,to:n,request:r,attachments:s=[],depth:i=0,cwd:a,origin:c,allowedRoots:l=[],mode:u="auto",maxDepth:h=2,online:p=new Set,kind:f="task",review:b}){if(typeof n!="string"||n.length===0)throw new E("target agent name is required");if(n.includes("#")&&!Ut(n))throw new E(`no peer "${n}" on this channel`);let d=dt(n);if(d.instance!==null&&d.instance===t)throw new E("cannot delegate a task to yourself");if((!Number.isInteger(i)||i<0)&&(i=0),i>h)throw new z(h);if(!Ft.includes(u))throw new Error(`invalid mode: ${u}`);if(a!==void 0){let w=!1;try{w=Mt(a).isDirectory()}catch{w=!1}if(!w)throw new X(a);let $=C(a),U=[];if(c!==void 0)try{U.push(C(c))}catch{}for(let H of l)try{U.push(C(H))}catch{}if(!U.some(H=>gt($,H)))throw new Z(a);a=$}if(!Ht.includes(f))throw new Error(`invalid kind: ${f}`);let A;if(f==="review")A=Jt(b??{},a??c),(typeof r!="string"||r.length===0)&&(r=Bt(A));else if(b!==void 0)throw new y('review is only valid for kind "review"');let v=this.registry.get(e),g=v.channel.peers,k=g.filter(w=>w.tool===d.tool),I;if(d.instance===null){if(I=k.length>0,!I){let w=g.find($=>$.tool?.toLowerCase()===d.tool.toLowerCase());if(w)throw new E(`no peer named "${n}" on this channel; did you mean "${w.tool}"?`)}}else if(I=k.some(w=>w.instanceId===d.instance),!I&&k.length>0){let w=k.find($=>p.has($.instanceId))??k[0];throw new E(`no peer "${n}" on this channel; did you mean "${w.instanceId}"?`)}let S=new Date().toISOString(),R={taskId:Kt(),from:t,to:n,toTool:d.tool,request:r,attachments:s,depth:i,mode:u,kind:f,status:"submitted",result:null,createdAt:S,updatedAt:S};d.instance!==null&&(R.toInstance=d.instance),a!==void 0&&(R.cwd=a),f==="review"&&(R.review=A),v.tasks.push(R),this.registry.save(e,v);let T={task:R,targetJoined:I};return d.instance!==null&&(T.targetOnline=p.has(d.instance),I&&!T.targetOnline&&(T.warning=`"${n}" is not online; the task will wait until it reconnects.`)),I||(T.warning=`"${n}" has not joined this channel yet; the task will wait until it joins.`),T}list(e,{to:t,from:n,status:r,kind:s}={}){let i=this.registry.get(e).tasks;if(t){let a=dt(t);i=i.filter(c=>a.instance===null?(c.toTool??c.to)===a.tool:c.toInstance===a.instance||!c.toInstance&&(c.toTool??c.to)===a.tool)}return n&&(i=i.filter(a=>a.from===n)),r&&(i=i.filter(a=>a.status===r)),s&&(i=i.filter(a=>(a.kind??"task")===s)),i}get(e,t){let n=this.registry.get(e).tasks.find(r=>r.taskId===t);if(!n)throw new j(t);return n}#t(e,t,n){let r=this.registry.get(e),s=r.tasks.find(a=>a.taskId===t);if(!s)throw new j(t);let i=s.status;return n(s),s.updatedAt=new Date().toISOString(),this.registry.save(e,r),s.status!==i&&this.#n(e,t,s),s}#n(e,t,n){let r=this.waiters.get(`${e}/${t}`);if(r){this.waiters.delete(`${e}/${t}`);for(let s of r)s(n)}}waitFor(e,t,n,{signal:r}={}){let s=this.get(e,t);if(Y.has(s.status))return Promise.resolve(s);if(r?.aborted)return Promise.resolve(null);let i=`${e}/${t}`;return new Promise(a=>{let c,l=this.waiters.get(i)??new Set;this.waiters.set(i,l);let u=p=>{clearTimeout(c),r?.removeEventListener("abort",h),l.delete(u),l.size===0&&this.waiters.get(i)===l&&this.waiters.delete(i),a(p)},h=()=>u(null);l.add(u),r?.addEventListener("abort",h,{once:!0}),c=setTimeout(()=>{let p=s;try{p=this.get(e,t)}catch{}u(p)},n)})}claim(e,t,n){return this.#t(e,t,r=>{if(!pt(r,n))throw new D(t,r.to,n);if(r.status!=="submitted")throw new x(r.status,"working");r.status="working"})}complete(e,t,{from:n,result:r,status:s="completed",worker:i=!1,review:a}){if(s!=="completed"&&s!=="failed")throw new x("?",s);return this.#t(e,t,c=>{if(!pt(c,n))throw new D(t,c.to,n);if(Y.has(c.status))throw new x(c.status,s);let l=(c.kind??"task")==="review";if(!l&&a!==void 0)throw new Q(t);if(l&&s==="completed"){if(a===void 0)throw new V(t);r=Yt(a)}c.status=s,c.result=r,c.completedBy=n,i?c.completedByWorker=!0:delete c.completedByWorker})}cancel(e,t,{agent:n,reason:r}){return this.#t(e,t,s=>{if(!Gt(s,n))throw new G(t,s.from,n);if(s.status!=="submitted"&&s.status!=="working")throw new x(s.status,"cancelled");s.status="cancelled",s.result=r??null,s.cancelledBy=n})}setWorker(e,t,n){return this.#t(e,t,r=>{r.worker={...r.worker??{},...n}})}failIfOpen(e,t,{result:n,by:r}){return this.#t(e,t,s=>{Y.has(s.status)||(s.status="failed",s.result=n,s.completedBy=r)})}};import{entryId as zt}from"../shared/ids.js";var N=class{constructor(e){this.registry=e}add(e,{from:t,summary:n,artifacts:r=[]}){let s=this.registry.get(e),i={entryId:zt(),from:t,summary:n,artifacts:r,at:new Date().toISOString()};return s.context.push(i),this.registry.save(e,s),i}list(e,{limit:t}={}){let n=this.registry.get(e).context;return Number.isInteger(t)&&t>0?n.slice(-t):n}};import{spawn as de}from"node:child_process";import{mkdirSync as _t,openSync as fe,closeSync as me,readFileSync as pe,appendFileSync as we}from"node:fs";import{join as q}from"node:path";import{readFileSync as Vt,writeFileSync as Qt,renameSync as Xt,existsSync as Zt}from"node:fs";import{join as yt,isAbsolute as te}from"node:path";var W=Object.freeze({maxDepth:2,timeoutMs:1200*1e3,maxConcurrentPerAgent:1,maxQueuedPerAgent:10});function tt(o){let e=yt(o,"config.json"),t={};if(Zt(e))try{t=JSON.parse(Vt(e,"utf8"))}catch{t={}}(!t||typeof t!="object"||Array.isArray(t))&&(t={});let n=t.workers&&typeof t.workers=="object"&&!Array.isArray(t.workers)?t.workers:{},r={...W};for(let i of Object.keys(W)){let a=t.limits?.[i];Number.isInteger(a)&&a>0&&(r[i]=a)}let s=Array.isArray(t.allowedRoots)?t.allowedRoots.filter(i=>typeof i=="string"&&te(i)):[];return{workers:n,limits:r,allowedRoots:s}}function ee(o,e){let t=yt(o,"config.json"),n=`${t}.${process.pid}.tmp`;Qt(n,JSON.stringify(e,null,2)),Xt(n,t)}function kt(o,e){return o.workers[e]?.enabled===!0}import{join as ne}from"node:path";import{fileURLToPath as re}from"node:url";import{agyCommand as se}from"../shared/agy.js";var oe=re(new URL("../../bin/pluriply.js",import.meta.url)),et=["codex","claude-code","antigravity"],ie=["acceptEdits","bypassPermissions"],ae=["mcp__pluriply__join_channel","mcp__pluriply__channel_status","mcp__pluriply__list_peers","mcp__pluriply__list_tasks","mcp__pluriply__get_task_result","mcp__pluriply__submit_review","mcp__pluriply__submit_result","mcp__pluriply__share_update","mcp__pluriply__get_channel_context"];function nt(){if(process.env.PLURIPLY_ALLOW_TEMPLATE_OVERRIDE!=="1")return null;let o=process.env.PLURIPLY_WORKER_TEMPLATE_OVERRIDE;if(!o)return null;try{return JSON.parse(o)}catch(e){throw new Error(`PLURIPLY_WORKER_TEMPLATE_OVERRIDE is not valid JSON: ${e.message}`)}}function ce(o,e){let t=nt()?.[o];if(!t)return;let n=r=>r.replace(/\{(taskId|channelCode|home|cwd|prompt|readOnly)\}/g,(s,i)=>String(e[i]));return{command:t.command,args:t.args.map(n)}}function It(o,{home:e,cwd:t,prompt:n,logDir:r,taskId:s,channelCode:i,permissionMode:a="acceptEdits",timeoutMs:c=W.timeoutMs,readOnly:l=!1}){let u=ce(o,{taskId:s,channelCode:i,home:e,cwd:t,prompt:n,readOnly:l});if(u)return u;switch(o){case"codex":return{command:"codex",args:["exec","-C",t,"--skip-git-repo-check","-s",l?"read-only":"workspace-write","-c",'approval_policy="never"',"-o",ne(r,`${s}.last.md`),n]};case"claude-code":{if(!ie.includes(a))throw new Error(`invalid permissionMode "${a}" for claude-code worker`);let h=JSON.stringify({mcpServers:{pluriply:{command:process.execPath,args:[oe,"connector","--agent","claude-code"],env:{PLURIPLY_HOME:e}}}});return{command:"claude",args:["-p",...l?["--allowedTools",...ae,"--permission-mode","default","--add-dir",r]:["--permission-mode",a],"--mcp-config",h,"--strict-mcp-config","--output-format","json",n]}}case"antigravity":return{command:se(),args:["-p",n,...l?["--mode","plan"]:[],"--dangerously-skip-permissions","--output-format","text","--print-timeout",`${Math.ceil(c/1e3)}s`]};default:return null}}import{pidAlive as ge}from"../shared/probe.js";import{execFileSync as le}from"node:child_process";import{writeFileSync as ue}from"node:fs";var he=20*1024*1024;function St({cwd:o,review:e,outFile:t}){let n=["-c","core.fsmonitor=false","diff","--no-color","--no-ext-diff","--no-textconv"],r;e.gitRange?(n.push(e.gitRange,"--"),e.paths?.length&&n.push(...e.paths),r=`git diff ${e.gitRange}`):e.paths?.length?(n.push("HEAD","--",...e.paths),r=`git diff HEAD -- ${e.paths.join(" ")}`):(n.push("HEAD"),r="git diff HEAD");try{let s=le("git",n,{cwd:o,encoding:"utf8",maxBuffer:he,timeout:2e4,stdio:["ignore","pipe","pipe"]});return ue(t,s),{file:t,bytes:Buffer.byteLength(s),target:r}}catch(s){return{error:(s.stderr?String(s.stderr).trim().split(`
|
|
4
|
-
`).
|
|
3
|
+
import{WebSocketServer as He}from"ws";import{mkdirSync as Fe,writeFileSync as Ue,rmSync as xt,readFileSync as Be}from"node:fs";import{join as At,isAbsolute as Ot}from"node:path";import{mkdirSync as qt,readFileSync as ct,writeFileSync as lt,renameSync as ut,existsSync as ht,readdirSync as U,rmSync as dt}from"node:fs";import{join as T}from"node:path";import{pluriplyHome as Kt}from"../shared/paths.js";var B=/^plp-[a-z0-9]{4}-[a-z0-9]{4}$/,D=class{constructor(e=Kt()){this.root=e,this.dir=T(e,"channels"),qt(this.dir,{recursive:!0}),this.#t()}#t(){for(let e of U(this.dir))e.endsWith(".tmp")&&dt(T(this.dir,e),{force:!0});for(let e of U(this.root))e.startsWith("agents.json.")&&e.endsWith(".tmp")&&dt(T(this.root,e),{force:!0})}loadChannel(e){if(!B.test(e))throw new Error(`invalid channel code: ${e}`);let t=T(this.dir,`${e}.json`);return ht(t)?JSON.parse(ct(t,"utf8")):null}saveChannel(e,t){if(!B.test(e))throw new Error(`invalid channel code: ${e}`);let n=T(this.dir,`${e}.json`),r=`${n}.${process.pid}.tmp`;lt(r,JSON.stringify(t,null,2)),ut(r,n)}listChannels(){return U(this.dir).filter(e=>e.endsWith(".json")).map(e=>e.slice(0,-5)).filter(e=>B.test(e))}loadAgents(){let e=T(this.root,"agents.json");if(!ht(e))return{};try{let t=JSON.parse(ct(e,"utf8"));return t&&typeof t=="object"&&!Array.isArray(t)?t:{}}catch{return{}}}saveAgents(e){let t=T(this.root,"agents.json"),n=`${t}.${process.pid}.tmp`;lt(n,JSON.stringify(e,null,2)),ut(n,t)}};import{channelCode as Ft}from"../shared/ids.js";var M=class{constructor(e){this.store=e}touch(e,t,n=new Date){try{let r=this.store.loadAgents();r[e]={lastChannel:t,lastSeenAt:n.toISOString()},this.store.saveAgents(r)}catch{}}resume(e,t=new Date){let n=this.store.loadAgents()[e];if(!n)return null;let r=t.getTime()-Date.parse(n.lastSeenAt);if(!(r>=0&&r<=432e5))return null;try{if(!this.store.loadChannel(n.lastChannel))return null}catch{return null}return n.lastChannel}};var Y=class extends Error{constructor(e){super(`channel not found: ${e}`),this.code=e}},N=class{constructor(e){this.store=e}create(){let e={channel:{code:Ft(),createdAt:new Date().toISOString(),peers:[]},tasks:[],context:[]};return this.store.saveChannel(e.channel.code,e),e}get(e){let t=this.store.loadChannel(e);if(!t)throw new Y(e);return t}save(e,t){this.store.saveChannel(e,t)}join(e,{instanceId:t,tool:n,worker:r=!1},{online:s=new Set,now:o=new Date}={}){let c=this.get(e);this.#t(c,s,o);let l=o.toISOString(),u=c.channel.peers.find(h=>h.instanceId===t);return u?u.lastSeenAt=l:c.channel.peers.push({instanceId:t,tool:n,worker:!!r,joinedAt:l,lastSeenAt:l}),this.save(e,c),{channel:c.channel,peers:c.channel.peers}}peers(e,{online:t=new Set,now:n=new Date}={}){let r=this.get(e);return this.#t(r,t,n)&&this.save(e,r),r.channel.peers}#t(e,t,n){let r=e.channel.peers.length;return e.channel.peers=e.channel.peers.filter(s=>{if(!s.instanceId)return!1;if(t.has(s.instanceId))return!0;let o=n.getTime()-Date.parse(s.lastSeenAt);return o>=0&&o<=432e5}),e.channel.peers.length!==r}};import{statSync as Ut,realpathSync as P}from"node:fs";import{sep as Bt,join as Yt,isAbsolute as Jt}from"node:path";import{taskId as zt}from"../shared/ids.js";import{parseTarget as ft,toolOf as gt,isInstanceId as Vt}from"../shared/identity.js";var J=new Set(["completed","failed","cancelled"]),C=class extends Error{constructor(e){super(`task not found: ${e}`)}},A=class extends Error{constructor(e,t){super(`invalid task transition: ${e} -> ${t}`)}},j=class extends Error{constructor(e,t,n){super(`task ${e} is addressed to "${t}", not "${n}"`)}},z=class extends Error{constructor(e,t,n){super(`only the sender "${t}" can cancel task ${e}, not "${n}"`)}},b=class extends Error{constructor(e){super(e)}},V=class extends Error{constructor(e){super(`delegation depth limit (${e}) exceeded`)}},Xt=["task","review"],mt=["approve","request_changes","comment"],pt=["critical","important","minor"],$=class extends Error{constructor(e){super(e)}},_=class extends Error{constructor(e){super(e)}},X=class extends Error{constructor(e){super(`use submit_review for review tasks (task ${e})`)}},Q=class extends Error{constructor(e){super(`task ${e} is not a review`)}},Z=class extends Error{constructor(e){super(`cwd is not an existing directory: ${e}`)}},tt=class extends Error{constructor(e){super(`cwd outside allowed roots: ${e}`)}},Qt=["auto","spawn","interactive"];function yt(i,e){return i===e||i.startsWith(e+Bt)}function Zt(i,e){if(!i||typeof i!="object"||Array.isArray(i))throw new $("review must be an object");let t={};if(i.gitRange!==void 0){if(typeof i.gitRange!="string"||i.gitRange.length===0||i.gitRange.length>200||/[\r\n]/.test(i.gitRange))throw new $("gitRange must be a single line of at most 200 characters");if(i.gitRange.startsWith("-"))throw new $("gitRange must not start with '-'");t.gitRange=i.gitRange}if(i.paths!==void 0){if(!Array.isArray(i.paths)||i.paths.some(n=>typeof n!="string"||n.length===0))throw new $("paths must be an array of strings");if(i.paths.length>0){if(e===void 0)throw new $("paths need a cwd to resolve against");let n=e;try{n=P(e)}catch{}t.paths=i.paths.map(r=>{let s=Jt(r)?r:Yt(e,r),o;try{o=P(s)}catch{throw new $(`path does not exist: ${r}`)}if(!yt(o,n))throw new $(`path outside cwd: ${r}`);return o})}}if(i.focus!==void 0){if(typeof i.focus!="string"||i.focus.length>500)throw new $("focus must be a string of at most 500 characters");i.focus.length>0&&(t.focus=i.focus)}return t}function te(i){return`Review ${i.gitRange?`git range ${i.gitRange}`:i.paths?.length?`files ${i.paths.join(", ")}`:"the uncommitted changes (git diff HEAD)"}${i.focus?`, focusing on ${i.focus}`:""}.`}function ee(i){if(!i||typeof i!="object"||Array.isArray(i))throw new _("review result must be an object");if(!mt.includes(i.verdict))throw new _(`verdict must be one of ${mt.join(", ")}`);if(typeof i.summary!="string"||i.summary.trim().length===0)throw new _("summary is required");let e=i.findings??[];if(!Array.isArray(e))throw new _("findings must be an array");if(e.length>200)throw new _("findings must have at most 200 items");let t=e.map((n,r)=>{if(!n||typeof n!="object"||Array.isArray(n))throw new _(`findings[${r}] must be an object`);if(!pt.includes(n.severity))throw new _(`findings[${r}].severity must be one of ${pt.join(", ")}`);if(typeof n.message!="string"||n.message.length===0)throw new _(`findings[${r}].message is required`);let s={severity:n.severity,message:n.message};if(n.file!==void 0){if(typeof n.file!="string")throw new _(`findings[${r}].file must be a string`);s.file=n.file}if(n.line!==void 0){if(!Number.isInteger(n.line)||n.line<1)throw new _(`findings[${r}].line must be a positive integer`);s.line=n.line}if(n.suggestion!==void 0){if(typeof n.suggestion!="string")throw new _(`findings[${r}].suggestion must be a string`);s.suggestion=n.suggestion}return s});return{verdict:i.verdict,findings:t,summary:i.summary}}function wt(i,e){return i.toInstance?e===i.toInstance:gt(e)===(i.toTool??i.to)}function ne(i,e){return i.from===e?!0:!i.from.includes("#")&>(e)===i.from}var G=class{constructor(e){this.registry=e,this.waiters=new Map}create(e,{from:t,to:n,request:r,attachments:s=[],depth:o=0,cwd:c,origin:l,allowedRoots:u=[],mode:h="auto",maxDepth:d=2,online:p=new Set,kind:w="task",review:y,fromCwdKey:k}){if(typeof n!="string"||n.length===0)throw new b("target agent name is required");if(n.includes("#")&&!Vt(n))throw new b(`no peer "${n}" on this channel`);let g=ft(n);if(g.instance!==null&&g.instance===t)throw new b("cannot delegate a task to yourself");if((!Number.isInteger(o)||o<0)&&(o=0),o>d)throw new V(d);if(!Qt.includes(h))throw new Error(`invalid mode: ${h}`);if(c!==void 0){let S=!1;try{S=Ut(c).isDirectory()}catch{S=!1}if(!S)throw new Z(c);let x=P(c),H=[];if(l!==void 0)try{H.push(P(l))}catch{}for(let F of u)try{H.push(P(F))}catch{}if(!H.some(F=>yt(x,F)))throw new tt(c);c=x}if(!Xt.includes(w))throw new Error(`invalid kind: ${w}`);let v;if(w==="review")v=Zt(y??{},c??l),(typeof r!="string"||r.length===0)&&(r=te(v));else if(y!==void 0)throw new $('review is only valid for kind "review"');let f=this.registry.get(e),a=f.channel.peers,m=a.filter(S=>S.tool===g.tool),I;if(g.instance===null){if(I=m.length>0,!I){let S=a.find(x=>x.tool?.toLowerCase()===g.tool.toLowerCase());if(S)throw new b(`no peer named "${n}" on this channel; did you mean "${S.tool}"?`)}}else if(I=m.some(S=>S.instanceId===g.instance),!I&&m.length>0){let S=m.find(x=>p.has(x.instanceId))??m[0];throw new b(`no peer "${n}" on this channel; did you mean "${S.instanceId}"?`)}let at=new Date().toISOString(),R={taskId:zt(),from:t,to:n,toTool:g.tool,request:r,attachments:s,depth:o,mode:h,kind:w,status:"submitted",result:null,createdAt:at,updatedAt:at};g.instance!==null&&(R.toInstance=g.instance),c!==void 0&&(R.cwd=c),typeof k=="string"&&k.length>0&&(R.fromCwdKey=k),w==="review"&&(R.review=v),f.tasks.push(R),this.registry.save(e,f);let O={task:R,targetJoined:I};return g.instance!==null&&(O.targetOnline=p.has(g.instance),I&&!O.targetOnline&&(O.warning=`"${n}" is not online; the task will wait until it reconnects.`)),I||(O.warning=`"${n}" has not joined this channel yet; the task will wait until it joins.`),O}list(e,{to:t,from:n,status:r,kind:s}={}){let o=this.registry.get(e).tasks;if(t){let c=ft(t);o=o.filter(l=>c.instance===null?(l.toTool??l.to)===c.tool:l.toInstance===c.instance||!l.toInstance&&(l.toTool??l.to)===c.tool)}return n&&(o=o.filter(c=>c.from===n)),r&&(o=o.filter(c=>c.status===r)),s&&(o=o.filter(c=>(c.kind??"task")===s)),o}get(e,t){let n=this.registry.get(e).tasks.find(r=>r.taskId===t);if(!n)throw new C(t);return n}#t(e,t,n){let r=this.registry.get(e),s=r.tasks.find(c=>c.taskId===t);if(!s)throw new C(t);let o=s.status;return n(s),s.updatedAt=new Date().toISOString(),this.registry.save(e,r),s.status!==o&&this.#r(e,t,s),s}#r(e,t,n){let r=this.waiters.get(`${e}/${t}`);if(r){this.waiters.delete(`${e}/${t}`);for(let s of r)s(n)}}waitFor(e,t,n,{signal:r}={}){let s=this.get(e,t);if(J.has(s.status))return Promise.resolve(s);if(r?.aborted)return Promise.resolve(null);let o=`${e}/${t}`;return new Promise(c=>{let l,u=this.waiters.get(o)??new Set;this.waiters.set(o,u);let h=p=>{clearTimeout(l),r?.removeEventListener("abort",d),u.delete(h),u.size===0&&this.waiters.get(o)===u&&this.waiters.delete(o),c(p)},d=()=>h(null);u.add(h),r?.addEventListener("abort",d,{once:!0}),l=setTimeout(()=>{let p=s;try{p=this.get(e,t)}catch{}h(p)},n)})}claim(e,t,n){return this.#t(e,t,r=>{if(!wt(r,n))throw new j(t,r.to,n);if(r.status!=="submitted")throw new A(r.status,"working");r.status="working"})}complete(e,t,{from:n,result:r,status:s="completed",worker:o=!1,review:c}){if(s!=="completed"&&s!=="failed")throw new A("?",s);return this.#t(e,t,l=>{if(!wt(l,n))throw new j(t,l.to,n);if(J.has(l.status))throw new A(l.status,s);let u=(l.kind??"task")==="review";if(!u&&c!==void 0)throw new Q(t);if(u&&s==="completed"){if(c===void 0)throw new X(t);r=ee(c)}l.status=s,l.result=r,l.completedBy=n,o?l.completedByWorker=!0:delete l.completedByWorker})}cancel(e,t,{agent:n,reason:r}){return this.#t(e,t,s=>{if(!ne(s,n))throw new z(t,s.from,n);if(s.status!=="submitted"&&s.status!=="working")throw new A(s.status,"cancelled");s.status="cancelled",s.result=r??null,s.cancelledBy=n})}markHookDelivered(e,t,n,r=new Date){let s=this.registry.get(e),o=s.tasks.find(c=>c.taskId===t);if(!o)throw new C(t);return o.hookDelivered={...o.hookDelivered??{},[n]:r.toISOString()},this.registry.save(e,s),o}setWorker(e,t,n){return this.#t(e,t,r=>{r.worker={...r.worker??{},...n}})}failIfOpen(e,t,{result:n,by:r}){return this.#t(e,t,s=>{J.has(s.status)||(s.status="failed",s.result=n,s.completedBy=r)})}};import{entryId as re}from"../shared/ids.js";var W=class{constructor(e){this.registry=e}add(e,{from:t,summary:n,artifacts:r=[]}){let s=this.registry.get(e),o={entryId:re(),from:t,summary:n,artifacts:r,at:new Date().toISOString()};return s.context.push(o),this.registry.save(e,s),o}list(e,{limit:t}={}){let n=this.registry.get(e).context;return Number.isInteger(t)&&t>0?n.slice(-t):n}};import{spawn as Te}from"node:child_process";import{mkdirSync as Rt,openSync as be,closeSync as Re,readFileSync as xe,appendFileSync as nt,readdirSync as Ae,rmSync as Oe}from"node:fs";import{mkdir as Pe,rm as rt}from"node:fs/promises";import{join as E}from"node:path";import{workerEnabled as Ce,TEMPLATE_AGENTS as Le}from"../shared/config.js";import{join as kt}from"node:path";import{fileURLToPath as se}from"node:url";import{agyCommand as ie}from"../shared/agy.js";import{DEFAULT_LIMITS as oe}from"../shared/config.js";var ae=se(new URL("../../bin/pluriply.js",import.meta.url)),ce=["acceptEdits","bypassPermissions"],le=["mcp__pluriply__join_channel","mcp__pluriply__channel_status","mcp__pluriply__list_peers","mcp__pluriply__list_tasks","mcp__pluriply__get_task_result","mcp__pluriply__submit_review","mcp__pluriply__submit_result","mcp__pluriply__share_update","mcp__pluriply__get_channel_context"];function et(){if(process.env.PLURIPLY_ALLOW_TEMPLATE_OVERRIDE!=="1")return null;let i=process.env.PLURIPLY_WORKER_TEMPLATE_OVERRIDE;if(!i)return null;try{return JSON.parse(i)}catch(e){throw new Error(`PLURIPLY_WORKER_TEMPLATE_OVERRIDE is not valid JSON: ${e.message}`)}}function ue(i,e){let t=et()?.[i];if(!t)return;let n=r=>r.replace(/\{(taskId|channelCode|home|cwd|prompt|readOnly)\}/g,(s,o)=>String(e[o]));return{command:t.command,args:t.args.map(n)}}function It(i,{home:e,cwd:t,prompt:n,logDir:r,taskId:s,channelCode:o,taskDir:c=kt(r,s),permissionMode:l="acceptEdits",timeoutMs:u=oe.timeoutMs,readOnly:h=!1}){let d=ue(i,{taskId:s,channelCode:o,home:e,cwd:t,prompt:n,readOnly:h});if(d)return d;switch(i){case"codex":return{command:"codex",args:["exec","-C",t,"--skip-git-repo-check","-s",h?"read-only":"workspace-write","-c",'approval_policy="never"',"-o",kt(r,`${s}.last.md`),n]};case"claude-code":{if(!ce.includes(l))throw new Error(`invalid permissionMode "${l}" for claude-code worker`);let p=JSON.stringify({mcpServers:{pluriply:{command:process.execPath,args:[ae,"connector","--agent","claude-code"],env:{PLURIPLY_HOME:e}}}});return{command:"claude",args:["-p",...h?["--allowedTools",...le,"--permission-mode","default","--add-dir",c]:["--permission-mode",l],"--mcp-config",p,"--strict-mcp-config","--output-format","json",n]}}case"antigravity":return{command:ie(),args:["-p",n,...h?["--mode","plan"]:[],"--dangerously-skip-permissions","--output-format","text","--print-timeout",`${Math.ceil(u/1e3)}s`]};default:return null}}import{pidAlive as De}from"../shared/probe.js";import{writeFile as ye}from"node:fs/promises";import{execFile as he}from"node:child_process";import{promisify as de}from"node:util";var fe=de(he),me=[/^filter\..+\.(clean|smudge|process|required)$/,/^diff\..+\.(command|textconv)$/,/^merge\..+\.driver$/,/^core\.(hookspath|fsmonitor|sshcommand|pager|editor|askpass|gitproxy)$/,/^credential\.(.+\.)?helper$/,/^alias\..+$/,/^sequence\.editor$/,/^gpg\.(.+\.)?program$/],pe=["GIT_DIR","GIT_WORK_TREE","GIT_INDEX_FILE","GIT_CONFIG_PARAMETERS","GIT_CONFIG_COUNT","GIT_EXTERNAL_DIFF","GIT_SSH","GIT_SSH_COMMAND","GIT_ASKPASS","GIT_EDITOR","GIT_PAGER"];function we(){let i={...process.env};for(let e of pe)delete i[e];return i.GIT_CONFIG_GLOBAL=process.platform==="win32"?"NUL":"/dev/null",i.GIT_CONFIG_NOSYSTEM="1",i.GIT_ATTR_NOSYSTEM="1",i.GIT_TERMINAL_PROMPT="0",i}function ge(i){if(!i)return"";let e=String(i).trim().split(`
|
|
4
|
+
`).map(t=>t.trim()).filter(Boolean);return e.length===0?"":e.find(t=>/^(fatal|error):/i.test(t))??e.at(-1)}async function L(i,{cwd:e,env:t,maxBuffer:n=4*1024*1024,timeout:r=2e4}){try{let{stdout:s}=await fe("git",i,{cwd:e,env:t,encoding:"utf8",maxBuffer:n,timeout:r,windowsHide:!0});return s}catch(s){throw new Error(ge(s.stderr)||s.message)}}async function _t(i){let e=we(),t=o=>L(["config","--list",o,"--includes","-z"],{cwd:i,env:e,timeout:1e4}),n=[await t("--local")];try{n.push(await t("--worktree"))}catch(o){if(!/cannot be used with multiple working trees|unable to read config file/i.test(o.message))throw o}let r=["-c","core.fsmonitor=false"],s=new Set(["core.fsmonitor"]);for(let o of n)for(let c of o.split("\0")){if(!c)continue;let l=c.split(`
|
|
5
|
+
`,1)[0];if(s.has(l))continue;let u=l.toLowerCase();me.some(h=>h.test(u))&&(s.add(l),r.push("-c",`${l}=`))}return{args:r,env:e}}var ke=20*1024*1024;async function St({cwd:i,review:e,outFile:t,git:n}){if(e.gitRange?.startsWith("-"))throw new Error('gitRange must not start with "-"');let r=[...n.args,"diff","--no-color","--no-ext-diff","--no-textconv"],s;e.gitRange?(r.push(e.gitRange,"--"),e.paths?.length&&r.push(...e.paths),s=`git diff ${e.gitRange}`):e.paths?.length?(r.push("HEAD","--",...e.paths),s=`git diff HEAD -- ${e.paths.join(" ")}`):(r.push("HEAD"),s="git diff HEAD");let o;try{o=await L(r,{cwd:i,env:n.env,maxBuffer:ke,timeout:2e4})}catch(c){throw new Error(`${s} failed: ${c.message}`)}return await ye(t,o),{file:t,bytes:Buffer.byteLength(o),target:s}}import{copyFile as Ie,lstat as _e,mkdir as Et,rm as vt}from"node:fs/promises";import{dirname as Se,isAbsolute as Ee,join as $t}from"node:path";var ve=512*1024*1024,$e=16,Tt=Object.freeze({recursive:!0,force:!0,maxRetries:5,retryDelay:100});async function bt({repoDir:i,destDir:e,git:t,maxBytes:n=ve}){let s=(await L([...t.args,"ls-files","-z","-co","--exclude-standard"],{cwd:i,env:t.env,maxBuffer:67108864})).split("\0").filter(Boolean);await vt(e,Tt),await Et(e,{recursive:!0});let o=0,c=0,l=0,u=0,h=async()=>{for(;u<s.length;){let w=s[u++];if(Ee(w)||w.split("/").includes(".."))continue;let y=$t(i,w),k;try{k=await _e(y)}catch{continue}if(k.isSymbolicLink()){l++;continue}if(!k.isFile())continue;if(c+=k.size,c>n)throw new Error(`snapshot exceeds ${Math.floor(n/1024/1024)}MB`);let g=$t(e,w);await Et(Se(g),{recursive:!0}),await Ie(y,g),o++}},p=(await Promise.allSettled(Array.from({length:Math.min($e,s.length)},h))).find(w=>w.status==="rejected");if(p)throw await vt(e,Tt),p.reason;return{files:o,bytes:c,skippedSymlinks:l}}var q=Object.freeze({recursive:!0,force:!0,maxRetries:5,retryDelay:100}),Me=i=>`run \`pluriply worker enable ${i}\` to let the hub process this automatically`,Ne=i=>`no worker template is configured for "${i}"; enable it via config or set PLURIPLY_WORKER_TEMPLATE_OVERRIDE`,je=new Set(["completed","failed","cancelled"]);function Ge(i){return Le.includes(i)?!0:!!et()?.[i]}function We(i){try{return xe(i,"utf8").trimEnd().split(`
|
|
5
6
|
`).slice(-20).join(`
|
|
6
|
-
`)}catch{return""}}function
|
|
7
|
-
`)}function
|
|
8
|
-
`)}var K=class{constructor({home:e,tasks:t}){this.home=e,this.tasks=t,this.running=new Map,this.queue=[],this.stopping=!1}dispatch(e,t,{interactive:n,config:r}){if(t.toInstance)return{kind:"pinned",hint:"pinned tasks are never handed to a worker"};let s=t.toTool??t.to;if(t.mode==="interactive")return{kind:"interactive"};if(t.mode==="auto"&&n)return{kind:"interactive"};if(this.stopping)return{kind:"none",hint:"hub is stopping"};if(!
|
|
9
|
-
`)}catch{}}
|
|
7
|
+
`)}catch{return""}}function qe({agent:i,channelCode:e,taskId:t,cwd:n,from:r}){return[`\uB2F9\uC2E0\uC740 Pluriply \uCC44\uB110 ${e}\uC5D0\uC11C \uC704\uC784\uBC1B\uC740 \uC791\uC5C5\uC744 \uCC98\uB9AC\uD558\uB294 ${i} \uC6CC\uCEE4\uC785\uB2C8\uB2E4.`,`\uC694\uCCAD\uC790\uB294 ${r} \uC785\uB2C8\uB2E4. join_channel \uC751\uB2F5\uC758 me \uAC00 \uB2F9\uC2E0\uC758 \uC778\uC2A4\uD134\uC2A4 ID\uC785\uB2C8\uB2E4.`,`1. pluriply MCP \uB3C4\uAD6C join_channel \uB85C \uCC44\uB110 ${e} \uC5D0 \uCC38\uC5EC\uD558\uC138\uC694.`,`2. get_task_result \uB85C \uD0DC\uC2A4\uD06C ${t} \uB97C \uC77D\uC73C\uC138\uC694. \uC694\uCCAD \uBCF8\uBB38\uACFC \uCCA8\uBD80 \uACBD\uB85C\uAC00 \uC788\uC2B5\uB2C8\uB2E4.`,`3. \uC791\uC5C5\uC744 \uC218\uD589\uD558\uC138\uC694. \uC791\uC5C5 \uD3F4\uB354\uB294 ${n} \uC785\uB2C8\uB2E4. \uADF8 \uBC16\uC758 \uD30C\uC77C\uC740 \uC218\uC815\uD558\uC9C0 \uB9C8\uC138\uC694.`,"4. \uC911\uAC04\uC5D0 share_update \uB85C \uC9C4\uD589 \uC0C1\uD669\uC744 \uD55C \uBC88 \uC774\uC0C1 \uB0A8\uAE30\uC138\uC694.","5. \uB05D\uB098\uBA74 submit_result \uB85C \uACB0\uACFC\uB97C \uC81C\uCD9C\uD558\uC138\uC694. \uD560 \uC218 \uC5C6\uC73C\uBA74 failed: true \uB85C \uC774\uC720\uB97C \uC81C\uCD9C\uD558\uC138\uC694.","6. \uB2E4\uB978 \uB3C4\uAD6C\uC5D0 \uC704\uC784\uC774 \uAF2D \uD544\uC694\uD560 \uB54C\uB9CC send_task \uB97C \uC4F0\uC138\uC694. \uC704\uC784 \uAE4A\uC774 \uC81C\uD55C\uC774 \uC788\uC2B5\uB2C8\uB2E4."].join(`
|
|
8
|
+
`)}function Ke({agent:i,channelCode:e,taskId:t,cwd:n,origin:r,from:s,diff:o}){return[`\uB2F9\uC2E0\uC740 Pluriply \uCC44\uB110 ${e}\uC5D0\uC11C \uCF54\uB4DC \uB9AC\uBDF0\uB97C \uC704\uC784\uBC1B\uC740 ${i} \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.`,`\uC694\uCCAD\uC790\uB294 ${s} \uC785\uB2C8\uB2E4. join_channel \uC751\uB2F5\uC758 me \uAC00 \uB2F9\uC2E0\uC758 \uC778\uC2A4\uD134\uC2A4 ID\uC785\uB2C8\uB2E4.`,`1. pluriply MCP \uB3C4\uAD6C join_channel \uB85C \uCC44\uB110 ${e} \uC5D0 \uCC38\uC5EC\uD558\uC138\uC694.`,`2. get_task_result \uB85C \uD0DC\uC2A4\uD06C ${t} \uB97C \uC77D\uC73C\uC138\uC694. review.gitRange / review.paths \uAC00 \uB300\uC0C1, review.focus \uAC00 \uAD00\uC810, request \uC5D0 \uCD94\uAC00 \uC124\uBA85\uC774 \uC788\uC2B5\uB2C8\uB2E4.`,`3. \uC791\uC5C5 \uD3F4\uB354 ${n} \uB294 \uC6D0\uBCF8 \uC800\uC7A5\uC18C ${r} \uC758 \uC2A4\uB0C5\uC0F7 \uBCF5\uC0AC\uBCF8\uC774\uBA70 git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uD5C8\uBE0C\uAC00 \uB9CC\uB4E0 unified diff \uD30C\uC77C ${o.file} (${o.target}) \uC744 \uC77D\uACE0, \uD544\uC694\uD558\uBA74 \uC791\uC5C5 \uD3F4\uB354\uC758 \uD30C\uC77C\uC744 \uD568\uAED8 \uC77D\uC5B4 \uAC80\uD1A0\uD558\uC138\uC694. git \uC744 \uC2E4\uD589\uD558\uC9C0 \uB9D0\uACE0, \uD30C\uC77C\uC744 \uC218\uC815\uD558\uAC70\uB098 \uC0C1\uD0DC\uB97C \uBC14\uAFB8\uB294 \uBA85\uB839\uC744 \uC2E4\uD589\uD558\uC9C0 \uB9C8\uC138\uC694.`,"4. \uBC1C\uACAC\uC744 \uC2EC\uAC01\uB3C4(critical/important/minor)\uC640 file/line \uC73C\uB85C \uC815\uB9AC\uD574 submit_review \uB85C \uC81C\uCD9C\uD558\uC138\uC694. file \uC740 \uC800\uC7A5\uC18C \uB8E8\uD2B8 \uAE30\uC900 \uC0C1\uB300\uACBD\uB85C\uB85C \uC801\uC73C\uC138\uC694. critical \uC774\uB098 important \uAC00 \uD558\uB098\uB77C\uB3C4 \uC788\uC73C\uBA74 verdict \uB294 request_changes, \uC5C6\uC73C\uBA74 approve, \uD310\uB2E8\uC744 \uC720\uBCF4\uD558\uBA74 comment \uC785\uB2C8\uB2E4.","5. \uAC80\uD1A0\uAC00 \uBD88\uAC00\uB2A5\uD558\uBA74(diff \uAC00 \uBE44\uC5B4 \uC788\uAC70\uB098 \uAC80\uD1A0 \uBC94\uC704\uB97C \uD310\uB2E8\uD560 \uC218 \uC5C6\uC73C\uBA74) submit_result \uC5D0 failed: true \uB85C \uC0AC\uC720\uB97C \uC81C\uCD9C\uD558\uC138\uC694.","6. \uB2E4\uB978 \uB3C4\uAD6C\uC5D0 \uC704\uC784\uD558\uC9C0 \uB9C8\uC138\uC694."].join(`
|
|
9
|
+
`)}var K=class{constructor({home:e,tasks:t}){this.home=e,this.tasks=t,this.running=new Map,this.swept=!1,this.queue=[],this.stopping=!1}dispatch(e,t,{interactive:n,config:r}){if(t.toInstance)return{kind:"pinned",hint:"pinned tasks are never handed to a worker"};let s=t.toTool??t.to;if(t.mode==="interactive")return{kind:"interactive"};if(t.mode==="auto"&&n)return{kind:"interactive"};if(this.stopping)return{kind:"none",hint:"hub is stopping"};if(!Ce(r,s))return{kind:"none",hint:Me(s)};try{if(!Ge(s))return{kind:"none",hint:Ne(s)};if(this.#t(s)>=r.limits.maxConcurrentPerAgent)return this.#r(s)>=r.limits.maxQueuedPerAgent?{kind:"none",hint:`worker queue full (${r.limits.maxQueuedPerAgent}) for ${s}`}:(this.queue.push({code:e,task:t,config:r}),{kind:"queued"});this.#i(e,t,r)}catch(o){return{kind:"none",hint:`worker spawn failed: ${o.message}`}}return{kind:"spawned"}}runningCount(e){let t=0;for(let n of this.running.values())for(let r of n)(e===void 0||r.code===e)&&t++;return t}onCancelled(e,t){for(let r of this.running.values())for(let s of r)s.code===e&&s.taskId===t&&(s.child?s.child.kill("SIGTERM"):s.cancelled=!0);let n=this.queue.findIndex(r=>r.code===e&&r.task.taskId===t);n!==-1&&this.queue.splice(n,1)}reconcile(e){for(let t of this.tasks.list(e)){let n=t.worker;!n||n.endedAt||!Number.isInteger(n.pid)||this.#c(e,t.taskId)||De(n.pid)||(this.tasks.setWorker(e,t.taskId,{endedAt:new Date().toISOString()}),this.tasks.failIfOpen(e,t.taskId,{result:"hub restarted while worker was running",by:`${n.agent} worker`}))}this.swept||(this.swept=!0,this.#e())}async stopAll(){this.stopping=!0;let e=[];for(let[r,s]of this.running)for(let o of s){if(!o.child){o.cancelled=!0,s.delete(o);try{this.tasks.failIfOpen(o.code,o.taskId,{result:"hub stopped while the review was being prepared",by:`${r} worker`})}catch{}rt(E(this.home,"workers",o.taskId,"tree"),q).catch(()=>{});continue}o.child.kill("SIGTERM"),e.push(o.child)}for(let r of this.queue)try{this.tasks.failIfOpen(r.code,r.task.taskId,{result:"hub stopped while the task was queued",by:`${r.task.toTool??r.task.to} worker`})}catch{}this.queue.length=0;let t=r=>r.exitCode===null&&r.signalCode===null,n=Date.now()+2e3;for(;e.some(t)&&Date.now()<n;)await new Promise(r=>setTimeout(r,100));for(let r of e)if(t(r))try{r.kill("SIGKILL")}catch{}}#t(e){return this.running.get(e)?.size??0}#r(e){let t=0;for(let n of this.queue)(n.task.toTool??n.task.to)===e&&t++;return t}#c(e,t){for(let n of this.running.values())for(let r of n)if(r.code===e&&r.taskId===t)return!0;return!1}#s(e){for(let t of this.running.values())for(let n of t)if(n.taskId===e)return!0;return!1}#e(){let e=E(this.home,"workers"),t;try{t=Ae(e,{withFileTypes:!0})}catch{return}for(let n of t)if(!(!n.isDirectory()||this.#s(n.name)))try{Oe(E(e,n.name,"tree"),q)}catch{}}#i(e,t,n){let r=t.toTool??t.to,s={code:e,taskId:t.taskId,child:null,cancelled:!1};this.running.has(r)||this.running.set(r,new Set),this.running.get(r).add(s),this.#l(e,t,n,s).catch(o=>{this.#n(s,r,t,`worker spawn failed: ${o.message}`)})}#n(e,t,n,r){this.running.get(t)?.delete(e);let s=E(this.home,"workers",`${n.taskId}.log`);try{nt(s,`${r}
|
|
10
|
+
`)}catch{}try{this.tasks.failIfOpen(e.code,n.taskId,{result:r,by:`${t} worker`})}catch{}rt(E(this.home,"workers",n.taskId,"tree"),q).catch(()=>{}),this.#a(t)}async#l(e,t,n,r){let s=t.toTool??t.to,o=E(this.home,"workers");Rt(o,{recursive:!0});let c=E(o,`${t.taskId}.log`),l=(t.kind??"task")==="review",u=E(o,t.taskId),h={agent:s,channelCode:e,taskId:t.taskId,from:t.from},d,p,w="";try{if(l){if(!t.cwd)throw new Error("review task has no cwd");await Pe(u,{recursive:!0});let f=await _t(t.cwd);d=E(u,"tree");let a=await bt({repoDir:t.cwd,destDir:d,git:f}),m=await St({cwd:t.cwd,review:t.review??{},outFile:E(u,"review.diff"),git:f});w=`snapshot: ${a.files} files, ${a.bytes} bytes, ${a.skippedSymlinks} symlinks skipped
|
|
11
|
+
`,p=Ke({...h,cwd:d,origin:t.cwd,diff:m}),await this.#o()}else d=t.cwd??E(this.home,"workspaces",t.taskId),Rt(d,{recursive:!0}),p=qe({...h,cwd:d})}catch(f){this.#n(r,s,t,`${l?"review preparation":"worker spawn"} failed: ${f.message}`);return}if(r.cancelled||this.stopping){this.#n(r,s,t,this.stopping?"hub stopped while the worker was being prepared":"worker cancelled while it was being prepared");return}let y;try{let f=It(s,{home:this.home,cwd:d,prompt:p,logDir:o,taskDir:u,taskId:t.taskId,channelCode:e,permissionMode:n.workers[s]?.permissionMode,timeoutMs:n.limits.timeoutMs,readOnly:l});w&&nt(c,w);let a=be(c,"a");try{y=Te(f.command,f.args,{cwd:d,shell:!1,stdio:["ignore",a,a],env:{...process.env,PLURIPLY_HOME:this.home,PLURIPLY_WORKER_TASK:t.taskId,PLURIPLY_WORKER_AGENT:s,PLURIPLY_DEPTH:String(t.depth??0)}})}finally{Re(a)}}catch(f){this.#n(r,s,t,`worker spawn failed: ${f.message}`);return}r.child=y;try{this.tasks.setWorker(e,t.taskId,{agent:s,pid:y.pid,startedAt:new Date().toISOString(),log:c})}catch(f){try{y.kill("SIGKILL")}catch{}this.#n(r,s,t,`worker spawn failed: ${f.message}`);return}let k=!1,g=setTimeout(()=>{k=!0,y.kill("SIGTERM"),setTimeout(()=>y.kill("SIGKILL"),5e3).unref()},n.limits.timeoutMs),v=(f,a)=>{clearTimeout(g),this.running.get(s)?.delete(r);let m={endedAt:new Date().toISOString(),exitCode:f};k&&(m.timedOut=!0);try{this.tasks.setWorker(e,t.taskId,m);let I;if(a){I=`worker failed to start: ${a.message}`;try{nt(c,`${I}
|
|
12
|
+
`)}catch{}}else k?I=`worker timed out after ${n.limits.timeoutMs/1e3}s`:I=`worker exited without submitting a result (exit ${f})
|
|
10
13
|
--- last log lines ---
|
|
11
|
-
${_e(a)}`;this.tasks.failIfOpen(e,t.taskId,{result:S,by:`${r} worker`})}catch{}this.#e(r)};f.on("exit",(g,k)=>v(g??(k?-1:0))),f.on("error",g=>v(-1,g))}#e(e){if(!this.stopping)for(let t=0;t<this.queue.length;t++){let n=this.queue[t];if((n.task.toTool??n.task.to)!==e)continue;let r;try{r=this.tasks.get(n.code,n.task.taskId).status}catch{this.queue.splice(t,1),t--;continue}if(Ie.has(r)){this.queue.splice(t,1),t--;continue}if(this.#t(e)>=n.config.limits.maxConcurrentPerAgent)return;this.queue.splice(t,1);try{this.#r(n.code,n.task,n.config)}catch(s){try{this.tasks.failIfOpen(n.code,n.task.taskId,{result:`worker spawn failed: ${s.message}`,by:`${n.task.toTool??n.task.to} worker`})}catch{}}return}}};import{pluriplyHome as Te}from"../shared/paths.js";import{shortId as Ce}from"../shared/ids.js";import{isValidAgentName as Pe,isInstanceId as Oe,makeInstanceId as Le,toolOf as je,cwdKey as De}from"../shared/identity.js";import{PACKAGE_VERSION as $t,PROTOCOL_VERSION as xt}from"../shared/version.js";import{pingHub as Me,pidAlive as bt,homeId as At}from"../shared/probe.js";function rt(o){try{let e=JSON.parse(Ae(o,"utf8"));return Number.isInteger(e?.pid)&&Number.isInteger(e?.port)?e:null}catch{return null}}var st=class{constructor({home:e=Te(),port:t=0,verifyDelayMs:n=100}={}){this.home=e,this.requestedPort=t,this.verifyDelayMs=n;let r=new P(e);this.channels=new L(r),this.tasks=new M(this.channels),this.context=new N(this.channels),this.agents=new O(r),this.workers=new K({home:e,tasks:this.tasks}),this.wss=null,this.redundant=!1,this.redundantPort=null,this.connections=new Map,this.issued=new Set}get port(){return this.redundant?this.redundantPort:this.wss?.address()?.port}async start(){xe(this.home,{recursive:!0}),await new Promise((t,n)=>{this.wss=new $e({host:"127.0.0.1",port:this.requestedPort,verifyClient:(r,s)=>{"origin"in r.req.headers?s(!1,403,"Forbidden"):s(!0)}}),this.wss.on("listening",t),this.wss.on("error",n)}),this.wss.on("connection",t=>{this.connections.set(t,{instanceId:null,tool:null,worker:!1,cwdKey:null,channels:new Set,abort:new AbortController}),t.on("message",n=>this.#s(t,n)),t.on("close",()=>{this.connections.get(t)?.abort.abort(),this.connections.delete(t)})});let e=vt(this.home,"hub.json");for(let t=0;t<2;t++){if(this.#n(e)){await new Promise(s=>setTimeout(s,this.verifyDelayMs));let r=rt(e);return r&&r.pid!==process.pid?(await new Promise(s=>this.wss.close(s)),this.wss=null,this.redundant=!0,this.redundantPort=r.port,{port:r.port,redundant:!0}):{port:this.port}}let n=rt(e);if(n&&await this.#t(n))return await new Promise(r=>this.wss.close(r)),this.wss=null,this.redundant=!0,this.redundantPort=n.port,{port:n.port,redundant:!0};Et(e,{force:!0})}throw await new Promise(t=>this.wss.close(t)),this.wss=null,new Error("could not acquire hub lock")}async#t(e){if(!bt(e.pid))return!1;let t=Date.now()+2e3;for(;;){let n=await Me(e.port,300);if(n)return!n.home||n.home===At(this.home);if(Date.now()>=t||!bt(e.pid))return!1;await new Promise(r=>setTimeout(r,200))}}#n(e){let t=JSON.stringify({pid:process.pid,port:this.wss.address().port,version:$t,protocol:xt,startedAt:new Date().toISOString()},null,2);try{return be(e,t,{flag:"wx"}),!0}catch(n){if(n.code==="EEXIST")return!1;throw n}}async stop(){if(this.redundant||!this.wss)return;await this.workers.stopAll();let e=vt(this.home,"hub.json");rt(e)?.pid===process.pid&&Et(e,{force:!0});for(let t of this.connections.keys())t.terminate();await new Promise(t=>this.wss.close(t)),this.wss=null}async#s(e,t){let n;try{n=JSON.parse(t.toString())}catch{return}try{let r=await this.#l(n.type,n.payload??{},e);this.#r(e,{id:n.id,ok:!0,payload:r})}catch(r){this.#r(e,{id:n.id,ok:!1,error:{message:r.message}})}}#r(e,t){e.readyState===e.OPEN&&e.send(JSON.stringify(t))}#e(e){let t=this.connections.get(e);if(!t||!t.instanceId)throw new Error("say hello first");return t}onlineInstances(e){let t=new Set;for(let n of this.connections.values())n.instanceId&&n.channels.has(e)&&t.add(n.instanceId);return t}isInteractive(e,t){for(let n of this.connections.values())if(n.tool===t&&!n.worker&&n.channels.has(e))return!0;return!1}#a(e){let t=[],n=[];for(let r of this.connections.values())!r.instanceId||!r.channels.has(e)||(r.worker?n:t).push(r.instanceId);return{interactive:t,workers:n}}#o(e,t){let n=this.onlineInstances(e);return t.map(r=>({...r,online:n.has(r.instanceId)}))}#c(e){for(;;){let t=Le(e,Ce(4));if(!(this.issued.has(t)||[...this.connections.values()].some(r=>r.instanceId===t)))return this.issued.add(t),t}}#i(e,t,n){let r=this.onlineInstances(n);r.add(t.instanceId);let{peers:s}=this.channels.join(n,{instanceId:t.instanceId,tool:t.tool,worker:t.worker},{online:r});return this.agents.touch(t.cwdKey,n),t.channels.add(n),this.#o(n,s)}#l(e,t,n){switch(e){case"ping":return{pong:!0,version:$t,protocol:xt,pid:process.pid,home:At(this.home)};case"channel.create":return{channelCode:this.channels.create().channel.code};case"agent.hello":{if(!Pe(t.tool))throw new Error(`invalid agent name: ${t.tool}`);if(typeof t.cwd!="string"||t.cwd.length===0||!Re(t.cwd))throw new Error("cwd is required");let r=this.connections.get(n);if(r.instanceId)return{instanceId:r.instanceId};let s=t.instanceId;if(s!==void 0){if(!Oe(s)||je(s)!==t.tool)throw new Error(`invalid instanceId: ${s}`)}else s=this.#c(t.tool);return r.instanceId=s,r.tool=t.tool,r.worker=!!t.worker,r.cwdKey=De(t.tool,t.cwd),{instanceId:s}}case"channel.join":{let r=this.#e(n);this.channels.get(t.channelCode);let s=this.#i(n,r,t.channelCode);return{channelCode:t.channelCode,peers:s}}case"channel.peers":return{peers:this.#o(t.channelCode,this.channels.peers(t.channelCode,{online:this.onlineInstances(t.channelCode)}))};case"channel.presence":return this.channels.get(t.channelCode),this.#a(t.channelCode);case"agent.resume":{let r=this.#e(n);if(r.channels.size>0)return{channelCode:null,alreadyJoined:!0};let s=this.agents.resume(r.cwdKey);if(!s)return{channelCode:null};let i=this.#i(n,r,s);return{channelCode:s,peers:i}}case"task.create":{let r=this.#e(n),s=tt(this.home),{task:i,targetJoined:a,targetOnline:c,warning:l}=this.tasks.create(t.channelCode,{...t,from:r.instanceId,depth:t.depth??0,mode:t.mode??"auto",maxDepth:s.limits.maxDepth,allowedRoots:s.allowedRoots,online:this.onlineInstances(t.channelCode)});this.agents.touch(r.cwdKey,t.channelCode);let u=this.workers.dispatch(t.channelCode,i,{interactive:this.isInteractive(t.channelCode,i.toTool),config:s}),h={taskId:i.taskId,targetJoined:a,dispatch:u.kind};return c!==void 0&&(h.targetOnline=c),l&&(h.warning=l),u.hint&&(h.hint=u.hint),h}case"task.list":return this.workers.reconcile(t.channelCode),{tasks:this.tasks.list(t.channelCode,{to:t.to,from:t.from,status:t.status,kind:t.kind})};case"task.get":return this.workers.reconcile(t.channelCode),{task:this.tasks.get(t.channelCode,t.taskId)};case"task.wait":{this.workers.reconcile(t.channelCode);let r=Math.min(Math.max(Number(t.timeoutMs)||0,1),15e3),s=this.connections.get(n)?.abort.signal;return this.tasks.waitFor(t.channelCode,t.taskId,r,{signal:s}).then(i=>{if(i===null)throw new Error("connection closed while waiting");return{task:i}})}case"task.claim":{let r=this.#e(n),s=this.tasks.claim(t.channelCode,t.taskId,r.instanceId);return this.agents.touch(r.cwdKey,t.channelCode),{task:s}}case"task.complete":{let r=this.#e(n),s=this.tasks.complete(t.channelCode,t.taskId,{from:r.instanceId,result:t.result,status:t.status,worker:r.worker,review:t.review});if(this.agents.touch(r.cwdKey,t.channelCode),(s.kind??"task")==="review"&&s.status==="completed")try{this.context.add(t.channelCode,{from:r.instanceId,summary:`[review] ${s.result.verdict} by ${r.instanceId}: ${s.result.summary}`,artifacts:[]})}catch{}return{task:s}}case"task.cancel":{let r=this.#e(n),s=this.tasks.cancel(t.channelCode,t.taskId,{agent:r.instanceId,reason:t.reason});return this.agents.touch(r.cwdKey,t.channelCode),this.workers.onCancelled(t.channelCode,t.taskId),{task:s}}case"worker.status":return t.channelCode!==void 0&&this.channels.get(t.channelCode),{running:this.workers.runningCount(t.channelCode)};case"context.add":{let r=this.#e(n),s=this.context.add(t.channelCode,{...t,from:r.instanceId});return this.agents.touch(r.cwdKey,t.channelCode),{entryId:s.entryId}}case"context.list":return{entries:this.context.list(t.channelCode,{limit:t.limit})};default:throw new Error(`unknown message type: ${e}`)}}};import{spawn as Ne}from"node:child_process";import{readFileSync as We,existsSync as Rt,rmSync as ot}from"node:fs";import{join as Tt}from"node:path";import{fileURLToPath as qe}from"node:url";import{pingHub as Ct,homeId as Ke,pidAlive as Ue}from"../shared/probe.js";var He=qe(new URL("../../bin/pluriply.js",import.meta.url));function it(o){let e=Tt(o,"hub.json");if(!Rt(e))return null;try{let t=JSON.parse(We(e,"utf8"));return Number.isInteger(t?.pid)&&Number.isInteger(t?.port)?t:null}catch{return null}}async function Pt(o){let e=it(o);if(!e)return null;let t=await Ct(e.port);return!t||t.home&&t.home!==Ke(o)?null:{...t,port:e.port,lockPid:e.pid}}async function Fe({home:o,timeoutMs:e=5e3}){Ne(process.execPath,[He,"hub","start"],{detached:!0,stdio:"ignore",env:{...process.env,PLURIPLY_HOME:o}}).unref();let n=Date.now()+e;for(;Date.now()<n;){let r=await Pt(o);if(r)return r;await new Promise(s=>setTimeout(s,100))}throw new Error(`failed to start pluriply hub within ${e/1e3}s`)}async function Je({home:o,timeoutMs:e=5e3}){let t=it(o),n=Tt(o,"hub.json");if(!t)return"not-running";let r=await Ct(t.port,1e3),s=r&&Number.isInteger(r.pid)&&Number.isInteger(t.pid)?r.pid!==t.pid:!1;if(!r||s)return ot(n,{force:!0}),"not-running";try{process.kill(t.pid,"SIGTERM")}catch{return ot(n,{force:!0}),"stopped"}let i=Date.now()+e;for(;Date.now()<i;){if(!Rt(n))return"stopped";if(!Ue(t.pid))return ot(n,{force:!0}),"stopped";await new Promise(a=>setTimeout(a,100))}return"timeout"}export{st as Hub,et as TEMPLATE_AGENTS,Pt as liveHub,tt as loadConfig,it as readLock,ee as saveConfig,Fe as spawnHub,Je as stopHub};
|
|
14
|
+
${We(c)}`;this.tasks.failIfOpen(e,t.taskId,{result:I,by:`${s} worker`})}catch{}l&&rt(E(u,"tree"),q).catch(()=>{}),this.#a(s)};y.on("exit",(f,a)=>v(f??(a?-1:0))),y.on("error",f=>v(-1,f))}async#o(){if(process.env.PLURIPLY_ALLOW_TEMPLATE_OVERRIDE!=="1")return;let e=Number(process.env.PLURIPLY_TEST_PREPARE_DELAY_MS);e>0&&await new Promise(t=>setTimeout(t,e))}#a(e){if(!this.stopping)for(let t=0;t<this.queue.length;t++){let n=this.queue[t];if((n.task.toTool??n.task.to)!==e)continue;let r;try{r=this.tasks.get(n.code,n.task.taskId).status}catch{this.queue.splice(t,1),t--;continue}if(je.has(r)){this.queue.splice(t,1),t--;continue}if(this.#t(e)>=n.config.limits.maxConcurrentPerAgent)return;this.queue.splice(t,1);try{this.#i(n.code,n.task,n.config)}catch(s){try{this.tasks.failIfOpen(n.code,n.task.taskId,{result:`worker spawn failed: ${s.message}`,by:`${n.task.toTool??n.task.to} worker`})}catch{}}return}}};import{loadConfig as Ye}from"../shared/config.js";import{pluriplyHome as Je}from"../shared/paths.js";import{shortId as ze}from"../shared/ids.js";import{isValidAgentName as Pt,isInstanceId as Ve,makeInstanceId as Xe,toolOf as Qe,cwdKey as Ct}from"../shared/identity.js";import{PACKAGE_VERSION as Lt,PROTOCOL_VERSION as Dt}from"../shared/version.js";import{pingHub as Ze,pidAlive as Mt,homeId as Nt}from"../shared/probe.js";function st(i){try{let e=JSON.parse(Be(i,"utf8"));return Number.isInteger(e?.pid)&&Number.isInteger(e?.port)?e:null}catch{return null}}var it=class{constructor({home:e=Je(),port:t=0,verifyDelayMs:n=100}={}){this.home=e,this.requestedPort=t,this.verifyDelayMs=n;let r=new D(e);this.channels=new N(r),this.tasks=new G(this.channels),this.context=new W(this.channels),this.agents=new M(r),this.workers=new K({home:e,tasks:this.tasks}),this.wss=null,this.redundant=!1,this.redundantPort=null,this.connections=new Map,this.issued=new Set}get port(){return this.redundant?this.redundantPort:this.wss?.address()?.port}async start(){Fe(this.home,{recursive:!0}),await new Promise((t,n)=>{this.wss=new He({host:"127.0.0.1",port:this.requestedPort,verifyClient:(r,s)=>{"origin"in r.req.headers?s(!1,403,"Forbidden"):s(!0)}}),this.wss.on("listening",t),this.wss.on("error",n)}),this.wss.on("connection",t=>{this.connections.set(t,{instanceId:null,tool:null,worker:!1,cwdKey:null,channels:new Set,abort:new AbortController}),t.on("message",n=>this.#c(t,n)),t.on("close",()=>{this.connections.get(t)?.abort.abort(),this.connections.delete(t)})});let e=At(this.home,"hub.json");for(let t=0;t<2;t++){if(this.#r(e)){await new Promise(s=>setTimeout(s,this.verifyDelayMs));let r=st(e);return r&&r.pid!==process.pid?(await new Promise(s=>this.wss.close(s)),this.wss=null,this.redundant=!0,this.redundantPort=r.port,{port:r.port,redundant:!0}):{port:this.port}}let n=st(e);if(n&&await this.#t(n))return await new Promise(r=>this.wss.close(r)),this.wss=null,this.redundant=!0,this.redundantPort=n.port,{port:n.port,redundant:!0};xt(e,{force:!0})}throw await new Promise(t=>this.wss.close(t)),this.wss=null,new Error("could not acquire hub lock")}async#t(e){if(!Mt(e.pid))return!1;let t=Date.now()+2e3;for(;;){let n=await Ze(e.port,300);if(n)return!n.home||n.home===Nt(this.home);if(Date.now()>=t||!Mt(e.pid))return!1;await new Promise(r=>setTimeout(r,200))}}#r(e){let t=JSON.stringify({pid:process.pid,port:this.wss.address().port,version:Lt,protocol:Dt,startedAt:new Date().toISOString()},null,2);try{return Ue(e,t,{flag:"wx"}),!0}catch(n){if(n.code==="EEXIST")return!1;throw n}}async stop(){if(this.redundant||!this.wss)return;await this.workers.stopAll();let e=At(this.home,"hub.json");st(e)?.pid===process.pid&&xt(e,{force:!0});for(let t of this.connections.keys())t.terminate();await new Promise(t=>this.wss.close(t)),this.wss=null}async#c(e,t){let n;try{n=JSON.parse(t.toString())}catch{return}try{let r=await this.#a(n.type,n.payload??{},e);this.#s(e,{id:n.id,ok:!0,payload:r})}catch(r){this.#s(e,{id:n.id,ok:!1,error:{message:r.message}})}}#s(e,t){e.readyState===e.OPEN&&e.send(JSON.stringify(t))}#e(e){let t=this.connections.get(e);if(!t||!t.instanceId)throw new Error("say hello first");return t}onlineInstances(e){let t=new Set;for(let n of this.connections.values())n.instanceId&&n.channels.has(e)&&t.add(n.instanceId);return t}isInteractive(e,t){for(let n of this.connections.values())if(n.tool===t&&!n.worker&&n.channels.has(e))return!0;return!1}#i(e){let t=[],n=[];for(let r of this.connections.values())!r.instanceId||!r.channels.has(e)||(r.worker?n:t).push(r.instanceId);return{interactive:t,workers:n}}#n(e,t){let n=this.onlineInstances(e);return t.map(r=>({...r,online:n.has(r.instanceId)}))}#l(e){for(;;){let t=Xe(e,ze(4));if(!(this.issued.has(t)||[...this.connections.values()].some(r=>r.instanceId===t)))return this.issued.add(t),t}}#o(e,t,n){let r=this.onlineInstances(n);r.add(t.instanceId);let{peers:s}=this.channels.join(n,{instanceId:t.instanceId,tool:t.tool,worker:t.worker},{online:r});return this.agents.touch(t.cwdKey,n),t.channels.add(n),this.#n(n,s)}#a(e,t,n){switch(e){case"ping":return{pong:!0,version:Lt,protocol:Dt,pid:process.pid,home:Nt(this.home)};case"hook.poll":{let r=t.tool,s={channelCode:null,tool:r,incoming:[],results:[],more:0};if(!Pt(r)||typeof t.cwd!="string"||!Ot(t.cwd))return s;let o=Ct(r,t.cwd),c=[...this.connections.values()].filter(a=>a.cwdKey===o);if(c.length>0&&c.every(a=>a.worker))return s;let l=c.filter(a=>!a.worker),u=l.length>0?[...new Set(l.flatMap(a=>[...a.channels]))]:[this.agents.resume(o)].filter(Boolean);if(u.length===0)return s;for(let a of u)this.workers.reconcile(a);let h=new Set(l.map(a=>a.instanceId)),d=a=>!a.hookDelivered?.[o],p=u.flatMap(a=>this.tasks.list(a).map(m=>({t:m,code:a}))),w=p.filter(({t:a})=>a.status==="submitted"&&d(a)&&(a.toInstance?h.has(a.toInstance):(a.toTool??a.to)===r)),y=p.filter(({t:a})=>a.fromCwdKey===o&&(a.status==="completed"||a.status==="failed")&&d(a)),k=a=>{let m=String(a.request??"").replace(/\s+/g," ").trim();return m.length>80?`${m.slice(0,80)}\u2026`:m},g=[...w.map(({t:a,code:m})=>({t:a,code:m,at:a.createdAt,entry:{taskId:a.taskId,kind:a.kind??"task",from:a.from,summary:k(a)},side:"incoming"})),...y.map(({t:a,code:m})=>({t:a,code:m,at:a.updatedAt,entry:{taskId:a.taskId,status:a.status,to:a.completedBy??a.to,summary:k(a)},side:"results"}))].sort((a,m)=>a.at<m.at?-1:a.at>m.at?1:0),v=g.slice(0,10),f=new Date;for(let a of v)this.tasks.markHookDelivered(a.code,a.t.taskId,o,f);return{channelCode:u[0],tool:r,incoming:v.filter(a=>a.side==="incoming").map(a=>a.entry),results:v.filter(a=>a.side==="results").map(a=>a.entry),more:g.length-v.length}}case"channel.create":return{channelCode:this.channels.create().channel.code};case"agent.hello":{if(!Pt(t.tool))throw new Error(`invalid agent name: ${t.tool}`);if(typeof t.cwd!="string"||t.cwd.length===0||!Ot(t.cwd))throw new Error("cwd is required");let r=this.connections.get(n);if(r.instanceId)return{instanceId:r.instanceId};let s=t.instanceId;if(s!==void 0){if(!Ve(s)||Qe(s)!==t.tool)throw new Error(`invalid instanceId: ${s}`)}else s=this.#l(t.tool);return r.instanceId=s,r.tool=t.tool,r.worker=!!t.worker,r.cwdKey=Ct(t.tool,t.cwd),{instanceId:s}}case"channel.join":{let r=this.#e(n);this.channels.get(t.channelCode);let s=this.#o(n,r,t.channelCode);return{channelCode:t.channelCode,peers:s}}case"channel.peers":return{peers:this.#n(t.channelCode,this.channels.peers(t.channelCode,{online:this.onlineInstances(t.channelCode)}))};case"channel.presence":return this.channels.get(t.channelCode),this.#i(t.channelCode);case"agent.resume":{let r=this.#e(n);if(r.channels.size>0)return{channelCode:null,alreadyJoined:!0};let s=this.agents.resume(r.cwdKey);if(!s)return{channelCode:null};let o=this.#o(n,r,s);return{channelCode:s,peers:o}}case"task.create":{let r=this.#e(n),s=Ye(this.home),{task:o,targetJoined:c,targetOnline:l,warning:u}=this.tasks.create(t.channelCode,{...t,from:r.instanceId,fromCwdKey:r.cwdKey,depth:t.depth??0,mode:t.mode??"auto",maxDepth:s.limits.maxDepth,allowedRoots:s.allowedRoots,online:this.onlineInstances(t.channelCode)});this.agents.touch(r.cwdKey,t.channelCode);let h=this.workers.dispatch(t.channelCode,o,{interactive:this.isInteractive(t.channelCode,o.toTool),config:s}),d={taskId:o.taskId,targetJoined:c,dispatch:h.kind};return l!==void 0&&(d.targetOnline=l),u&&(d.warning=u),h.hint&&(d.hint=h.hint),d}case"task.list":return this.workers.reconcile(t.channelCode),{tasks:this.tasks.list(t.channelCode,{to:t.to,from:t.from,status:t.status,kind:t.kind})};case"task.get":return this.workers.reconcile(t.channelCode),{task:this.tasks.get(t.channelCode,t.taskId)};case"task.wait":{this.workers.reconcile(t.channelCode);let r=Math.min(Math.max(Number(t.timeoutMs)||0,1),15e3),s=this.connections.get(n)?.abort.signal;return this.tasks.waitFor(t.channelCode,t.taskId,r,{signal:s}).then(o=>{if(o===null)throw new Error("connection closed while waiting");return{task:o}})}case"task.claim":{let r=this.#e(n),s=this.tasks.claim(t.channelCode,t.taskId,r.instanceId);return this.agents.touch(r.cwdKey,t.channelCode),{task:s}}case"task.complete":{let r=this.#e(n),s=this.tasks.complete(t.channelCode,t.taskId,{from:r.instanceId,result:t.result,status:t.status,worker:r.worker,review:t.review});if(this.agents.touch(r.cwdKey,t.channelCode),(s.kind??"task")==="review"&&s.status==="completed")try{this.context.add(t.channelCode,{from:r.instanceId,summary:`[review] ${s.result.verdict} by ${r.instanceId}: ${s.result.summary}`,artifacts:[]})}catch{}return{task:s}}case"task.cancel":{let r=this.#e(n),s=this.tasks.cancel(t.channelCode,t.taskId,{agent:r.instanceId,reason:t.reason});return this.agents.touch(r.cwdKey,t.channelCode),this.workers.onCancelled(t.channelCode,t.taskId),{task:s}}case"worker.status":return t.channelCode!==void 0&&this.channels.get(t.channelCode),{running:this.workers.runningCount(t.channelCode)};case"context.add":{let r=this.#e(n),s=this.context.add(t.channelCode,{...t,from:r.instanceId});return this.agents.touch(r.cwdKey,t.channelCode),{entryId:s.entryId}}case"context.list":return{entries:this.context.list(t.channelCode,{limit:t.limit})};default:throw new Error(`unknown message type: ${e}`)}}};import{spawn as tn}from"node:child_process";import{existsSync as en,rmSync as ot}from"node:fs";import{join as nn}from"node:path";import{fileURLToPath as rn}from"node:url";import{pingHub as jt,homeId as sn,pidAlive as on}from"../shared/probe.js";import{readLock as Gt}from"../shared/lock.js";var an=rn(new URL("../../bin/pluriply.js",import.meta.url));async function Wt(i){let e=Gt(i);if(!e)return null;let t=await jt(e.port);return!t||t.home&&t.home!==sn(i)?null:{...t,port:e.port,lockPid:e.pid}}async function cn({home:i,timeoutMs:e=5e3}){tn(process.execPath,[an,"hub","start"],{detached:!0,stdio:"ignore",env:{...process.env,PLURIPLY_HOME:i}}).unref();let n=Date.now()+e;for(;Date.now()<n;){let r=await Wt(i);if(r)return r;await new Promise(s=>setTimeout(s,100))}throw new Error(`failed to start pluriply hub within ${e/1e3}s`)}async function ln({home:i,timeoutMs:e=5e3}){let t=Gt(i),n=nn(i,"hub.json");if(!t)return"not-running";let r=await jt(t.port,1e3),s=r&&Number.isInteger(r.pid)&&Number.isInteger(t.pid)?r.pid!==t.pid:!1;if(!r||s)return ot(n,{force:!0}),"not-running";try{process.kill(t.pid,"SIGTERM")}catch{return ot(n,{force:!0}),"stopped"}let o=Date.now()+e;for(;Date.now()<o;){if(!en(n))return"stopped";if(!on(t.pid))return ot(n,{force:!0}),"stopped";await new Promise(c=>setTimeout(c,100))}return"timeout"}import{readLock as vr}from"../shared/lock.js";import{loadConfig as Tr,saveConfig as br,setWorkerEnabled as Rr,TEMPLATE_AGENTS as xr}from"../shared/config.js";export{it as Hub,xr as TEMPLATE_AGENTS,Wt as liveHub,Tr as loadConfig,vr as readLock,br as saveConfig,Rr as setWorkerEnabled,cn as spawnHub,ln as stopHub};
|