pluriply 0.1.0 → 0.2.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 +15 -10
- package/bin/pluriply.js +80 -34
- package/package.json +1 -1
- package/src/hub/index.js +10 -7
- package/src/setup/clients.js +398 -32
- package/src/setup/run-setup.js +258 -41
- package/src/setup/toml-lite.js +87 -0
- package/src/shared/config.js +102 -0
- 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,8 @@ 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.
|
|
33
35
|
|
|
34
36
|
Restart your AI tools afterwards so they pick up the new MCP server.
|
|
35
37
|
|
|
@@ -70,7 +72,10 @@ source is not in this repository. We keep the hub proprietary because it is
|
|
|
70
72
|
the part of Pluriply we intend to build a business on; the parts that run
|
|
71
73
|
inside your tools stay open so you can audit them.
|
|
72
74
|
|
|
73
|
-
## Issues
|
|
75
|
+
## Issues and contributions
|
|
74
76
|
|
|
75
|
-
Bug reports and
|
|
76
|
-
|
|
77
|
+
Bug reports and feature requests: https://github.com/pluriply/pluriply/issues — the templates ask for the details we need.
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
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"];
|
|
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,50 @@ 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
|
+
if (remove && workers) usage("--remove cannot be combined with --workers");
|
|
169
|
+
if (purge && !remove) usage("--purge requires --remove");
|
|
170
|
+
if (purge && onlyArg) usage("--purge cannot be combined with --only");
|
|
143
171
|
try {
|
|
144
172
|
const r = await runSetup({
|
|
145
|
-
only: onlyArg
|
|
173
|
+
only: onlyArg
|
|
174
|
+
? onlyArg
|
|
175
|
+
.split(",")
|
|
176
|
+
.map((x) => x.trim())
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
: undefined,
|
|
146
179
|
workers,
|
|
147
180
|
dryRun,
|
|
181
|
+
remove,
|
|
182
|
+
purge,
|
|
148
183
|
env: makeEnv({ binPath: BIN_PATH }),
|
|
149
184
|
home: pluriplyHome(),
|
|
150
185
|
});
|
|
@@ -156,17 +191,28 @@ if (cmd === "hub" && sub === "start") {
|
|
|
156
191
|
process.exit(1);
|
|
157
192
|
}
|
|
158
193
|
} else if (cmd === "status") {
|
|
159
|
-
const
|
|
194
|
+
const home = pluriplyHome();
|
|
195
|
+
const lock = readLock(home);
|
|
160
196
|
if (!lock) {
|
|
161
197
|
console.log("not running");
|
|
162
198
|
} else {
|
|
163
199
|
const info = await pingHub(lock.port);
|
|
164
|
-
if (
|
|
165
|
-
console.log(`stale lockfile (pid ${lock.pid} not responding)`);
|
|
166
|
-
} else {
|
|
200
|
+
if (info) {
|
|
167
201
|
console.log(
|
|
168
202
|
`running (port ${lock.port}, pid ${info.pid ?? lock.pid}, version ${info.version ?? "unknown"}, protocol ${info.protocol ?? 1})`,
|
|
169
203
|
);
|
|
204
|
+
} else if (!pidAlive(lock.pid)) {
|
|
205
|
+
// Windows 에서는 SIGTERM 이 정리 핸들러 없이 즉시 종료라 허브가 락을 못 지운다.
|
|
206
|
+
// pid 가 죽었으면 stopHub 와 같은 판정으로 락을 지우고 not running 으로 본다.
|
|
207
|
+
// 단, pingHub 가 기다리는 동안 hub start 가 새 락을 썼을 수 있으니 다시 읽어
|
|
208
|
+
// pid 가 그대로일 때만 지운다(남의 새 락을 지우지 않기 위해).
|
|
209
|
+
const current = readLock(home);
|
|
210
|
+
if (current?.pid === lock.pid) {
|
|
211
|
+
rmSync(join(home, "hub.json"), { force: true });
|
|
212
|
+
}
|
|
213
|
+
console.log("not running");
|
|
214
|
+
} else {
|
|
215
|
+
console.log(`stale lockfile (pid ${lock.pid} not responding)`);
|
|
170
216
|
}
|
|
171
217
|
}
|
|
172
218
|
} else if (cmd === "connector") {
|
|
@@ -183,7 +229,7 @@ if (cmd === "hub" && sub === "start") {
|
|
|
183
229
|
await startConnector({ agent });
|
|
184
230
|
} else {
|
|
185
231
|
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>",
|
|
232
|
+
"usage: pluriply <setup [--workers] [--dry-run] [--only a,b]|setup --remove [--purge] [--dry-run] [--only a,b]|hub start|hub stop|hub restart|connector --agent <name>|status|worker enable|disable <codex|claude-code|antigravity>|worker list>",
|
|
187
233
|
);
|
|
188
234
|
process.exit(1);
|
|
189
235
|
}
|
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 Ge}from"ws";import{mkdirSync as We,writeFileSync as qe,rmSync as xt,readFileSync as Fe}from"node:fs";import{join as Rt,isAbsolute as Ue}from"node:path";import{mkdirSync as Nt,readFileSync as at,writeFileSync as ct,renameSync as lt,existsSync as ut,readdirSync as K,rmSync as ht}from"node:fs";import{join as $}from"node:path";import{pluriplyHome as jt}from"../shared/paths.js";var B=/^plp-[a-z0-9]{4}-[a-z0-9]{4}$/,L=class{constructor(e=jt()){this.root=e,this.dir=$(e,"channels"),Nt(this.dir,{recursive:!0}),this.#t()}#t(){for(let e of K(this.dir))e.endsWith(".tmp")&&ht($(this.dir,e),{force:!0});for(let e of K(this.root))e.startsWith("agents.json.")&&e.endsWith(".tmp")&&ht($(this.root,e),{force:!0})}loadChannel(e){if(!B.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(!B.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 K(this.dir).filter(e=>e.endsWith(".json")).map(e=>e.slice(0,-5)).filter(e=>B.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 Wt}from"../shared/ids.js";var D=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}},M=class{constructor(e){this.store=e}create(){let e={channel:{code:Wt(),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 a=this.get(e);this.#t(a,s,o);let c=o.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 o=n.getTime()-Date.parse(s.lastSeenAt);return o>=0&&o<=432e5}),e.channel.peers.length!==r}};import{statSync as qt,realpathSync as P}from"node:fs";import{sep as Ft,join as Ut,isAbsolute as Ht}from"node:path";import{taskId as Kt}from"../shared/ids.js";import{parseTarget as dt,toolOf as wt,isInstanceId as Bt}from"../shared/identity.js";var J=new Set(["completed","failed","cancelled"]),N=class extends Error{constructor(e){super(`task not found: ${e}`)}},R=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}"`)}},T=class extends Error{constructor(e){super(e)}},V=class extends Error{constructor(e){super(`delegation depth limit (${e}) exceeded`)}},Yt=["task","review"],ft=["approve","request_changes","comment"],mt=["critical","important","minor"],S=class extends Error{constructor(e){super(e)}},y=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}`)}},Jt=["auto","spawn","interactive"];function gt(i,e){return i===e||i.startsWith(e+Ft)}function zt(i,e){if(!i||typeof i!="object"||Array.isArray(i))throw new S("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 S("gitRange must be a single line of at most 200 characters");if(i.gitRange.startsWith("-"))throw new S("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 S("paths must be an array of strings");if(i.paths.length>0){if(e===void 0)throw new S("paths need a cwd to resolve against");let n=e;try{n=P(e)}catch{}t.paths=i.paths.map(r=>{let s=Ht(r)?r:Ut(e,r),o;try{o=P(s)}catch{throw new S(`path does not exist: ${r}`)}if(!gt(o,n))throw new S(`path outside cwd: ${r}`);return o})}}if(i.focus!==void 0){if(typeof i.focus!="string"||i.focus.length>500)throw new S("focus must be a string of at most 500 characters");i.focus.length>0&&(t.focus=i.focus)}return t}function Vt(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 Xt(i){if(!i||typeof i!="object"||Array.isArray(i))throw new y("review result must be an object");if(!ft.includes(i.verdict))throw new y(`verdict must be one of ${ft.join(", ")}`);if(typeof i.summary!="string"||i.summary.trim().length===0)throw new y("summary is required");let e=i.findings??[];if(!Array.isArray(e))throw new y("findings must be an array");if(e.length>200)throw new y("findings must have at most 200 items");let t=e.map((n,r)=>{if(!n||typeof n!="object"||Array.isArray(n))throw new y(`findings[${r}] must be an object`);if(!mt.includes(n.severity))throw new y(`findings[${r}].severity must be one of ${mt.join(", ")}`);if(typeof n.message!="string"||n.message.length===0)throw new y(`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 y(`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 y(`findings[${r}].line must be a positive integer`);s.line=n.line}if(n.suggestion!==void 0){if(typeof n.suggestion!="string")throw new y(`findings[${r}].suggestion must be a string`);s.suggestion=n.suggestion}return s});return{verdict:i.verdict,findings:t,summary:i.summary}}function pt(i,e){return i.toInstance?e===i.toInstance:wt(e)===(i.toTool??i.to)}function Qt(i,e){return i.from===e?!0:!i.from.includes("#")&&wt(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:a,origin:c,allowedRoots:l=[],mode:u="auto",maxDepth:h=2,online:w=new Set,kind:p="task",review:g}){if(typeof n!="string"||n.length===0)throw new T("target agent name is required");if(n.includes("#")&&!Bt(n))throw new T(`no peer "${n}" on this channel`);let f=dt(n);if(f.instance!==null&&f.instance===t)throw new T("cannot delegate a task to yourself");if((!Number.isInteger(o)||o<0)&&(o=0),o>h)throw new V(h);if(!Jt.includes(u))throw new Error(`invalid mode: ${u}`);if(a!==void 0){let k=!1;try{k=qt(a).isDirectory()}catch{k=!1}if(!k)throw new Z(a);let x=P(a),U=[];if(c!==void 0)try{U.push(P(c))}catch{}for(let H of l)try{U.push(P(H))}catch{}if(!U.some(H=>gt(x,H)))throw new tt(a);a=x}if(!Yt.includes(p))throw new Error(`invalid kind: ${p}`);let E;if(p==="review")E=zt(g??{},a??c),(typeof r!="string"||r.length===0)&&(r=Vt(E));else if(g!==void 0)throw new S('review is only valid for kind "review"');let b=this.registry.get(e),d=b.channel.peers,m=d.filter(k=>k.tool===f.tool),I;if(f.instance===null){if(I=m.length>0,!I){let k=d.find(x=>x.tool?.toLowerCase()===f.tool.toLowerCase());if(k)throw new T(`no peer named "${n}" on this channel; did you mean "${k.tool}"?`)}}else if(I=m.some(k=>k.instanceId===f.instance),!I&&m.length>0){let k=m.find(x=>w.has(x.instanceId))??m[0];throw new T(`no peer "${n}" on this channel; did you mean "${k.instanceId}"?`)}let v=new Date().toISOString(),A={taskId:Kt(),from:t,to:n,toTool:f.tool,request:r,attachments:s,depth:o,mode:u,kind:p,status:"submitted",result:null,createdAt:v,updatedAt:v};f.instance!==null&&(A.toInstance=f.instance),a!==void 0&&(A.cwd=a),p==="review"&&(A.review=E),b.tasks.push(A),this.registry.save(e,b);let O={task:A,targetJoined:I};return f.instance!==null&&(O.targetOnline=w.has(f.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 a=dt(t);o=o.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&&(o=o.filter(a=>a.from===n)),r&&(o=o.filter(a=>a.status===r)),s&&(o=o.filter(a=>(a.kind??"task")===s)),o}get(e,t){let n=this.registry.get(e).tasks.find(r=>r.taskId===t);if(!n)throw new N(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 N(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(a=>{let c,l=this.waiters.get(o)??new Set;this.waiters.set(o,l);let u=w=>{clearTimeout(c),r?.removeEventListener("abort",h),l.delete(u),l.size===0&&this.waiters.get(o)===l&&this.waiters.delete(o),a(w)},h=()=>u(null);l.add(u),r?.addEventListener("abort",h,{once:!0}),c=setTimeout(()=>{let w=s;try{w=this.get(e,t)}catch{}u(w)},n)})}claim(e,t,n){return this.#t(e,t,r=>{if(!pt(r,n))throw new j(t,r.to,n);if(r.status!=="submitted")throw new R(r.status,"working");r.status="working"})}complete(e,t,{from:n,result:r,status:s="completed",worker:o=!1,review:a}){if(s!=="completed"&&s!=="failed")throw new R("?",s);return this.#t(e,t,c=>{if(!pt(c,n))throw new j(t,c.to,n);if(J.has(c.status))throw new R(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 X(t);r=Xt(a)}c.status=s,c.result=r,c.completedBy=n,o?c.completedByWorker=!0:delete c.completedByWorker})}cancel(e,t,{agent:n,reason:r}){return this.#t(e,t,s=>{if(!Qt(s,n))throw new z(t,s.from,n);if(s.status!=="submitted"&&s.status!=="working")throw new R(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=>{J.has(s.status)||(s.status="failed",s.result=n,s.completedBy=r)})}};import{entryId as Zt}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:Zt(),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 Se}from"node:child_process";import{mkdirSync as bt,openSync as Ee,closeSync as ve,readFileSync as $e,appendFileSync as nt,readdirSync as Te,rmSync as be}from"node:fs";import{mkdir as xe,rm as rt}from"node:fs/promises";import{join as _}from"node:path";import{workerEnabled as Re,TEMPLATE_AGENTS as Ae}from"../shared/config.js";import{join as yt}from"node:path";import{fileURLToPath as te}from"node:url";import{agyCommand as ee}from"../shared/agy.js";import{DEFAULT_LIMITS as ne}from"../shared/config.js";var re=te(new URL("../../bin/pluriply.js",import.meta.url)),se=["acceptEdits","bypassPermissions"],ie=["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 oe(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:a=yt(r,s),permissionMode:c="acceptEdits",timeoutMs:l=ne.timeoutMs,readOnly:u=!1}){let h=oe(i,{taskId:s,channelCode:o,home:e,cwd:t,prompt:n,readOnly:u});if(h)return h;switch(i){case"codex":return{command:"codex",args:["exec","-C",t,"--skip-git-repo-check","-s",u?"read-only":"workspace-write","-c",'approval_policy="never"',"-o",yt(r,`${s}.last.md`),n]};case"claude-code":{if(!se.includes(c))throw new Error(`invalid permissionMode "${c}" for claude-code worker`);let w=JSON.stringify({mcpServers:{pluriply:{command:process.execPath,args:[re,"connector","--agent","claude-code"],env:{PLURIPLY_HOME:e}}}});return{command:"claude",args:["-p",...u?["--allowedTools",...ie,"--permission-mode","default","--add-dir",a]:["--permission-mode",c],"--mcp-config",w,"--strict-mcp-config","--output-format","json",n]}}case"antigravity":return{command:ee(),args:["-p",n,...u?["--mode","plan"]:[],"--dangerously-skip-permissions","--output-format","text","--print-timeout",`${Math.ceil(l/1e3)}s`]};default:return null}}import{pidAlive as Oe}from"../shared/probe.js";import{writeFile as me}from"node:fs/promises";import{execFile as ae}from"node:child_process";import{promisify as ce}from"node:util";var le=ce(ae),ue=[/^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$/],he=["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 de(){let i={...process.env};for(let e of he)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 fe(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 C(i,{cwd:e,env:t,maxBuffer:n=4*1024*1024,timeout:r=2e4}){try{let{stdout:s}=await le("git",i,{cwd:e,env:t,encoding:"utf8",maxBuffer:n,timeout:r,windowsHide:!0});return s}catch(s){throw new Error(fe(s.stderr)||s.message)}}async function kt(i){let e=de(),t=o=>C(["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 a of o.split("\0")){if(!a)continue;let c=a.split(`
|
|
5
|
+
`,1)[0];if(s.has(c))continue;let l=c.toLowerCase();ue.some(u=>u.test(l))&&(s.add(c),r.push("-c",`${c}=`))}return{args:r,env:e}}var pe=20*1024*1024;async function _t({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 C(r,{cwd:i,env:n.env,maxBuffer:pe,timeout:2e4})}catch(a){throw new Error(`${s} failed: ${a.message}`)}return await me(t,o),{file:t,bytes:Buffer.byteLength(o),target:s}}import{copyFile as we,lstat as ge,mkdir as St,rm as Et}from"node:fs/promises";import{dirname as ye,isAbsolute as Ie,join as vt}from"node:path";var ke=512*1024*1024,_e=16,$t=Object.freeze({recursive:!0,force:!0,maxRetries:5,retryDelay:100});async function Tt({repoDir:i,destDir:e,git:t,maxBytes:n=ke}){let s=(await C([...t.args,"ls-files","-z","-co","--exclude-standard"],{cwd:i,env:t.env,maxBuffer:67108864})).split("\0").filter(Boolean);await Et(e,$t),await St(e,{recursive:!0});let o=0,a=0,c=0,l=0,u=async()=>{for(;l<s.length;){let p=s[l++];if(Ie(p)||p.split("/").includes(".."))continue;let g=vt(i,p),f;try{f=await ge(g)}catch{continue}if(f.isSymbolicLink()){c++;continue}if(!f.isFile())continue;if(a+=f.size,a>n)throw new Error(`snapshot exceeds ${Math.floor(n/1024/1024)}MB`);let E=vt(e,p);await St(ye(E),{recursive:!0}),await we(g,E),o++}},w=(await Promise.allSettled(Array.from({length:Math.min(_e,s.length)},u))).find(p=>p.status==="rejected");if(w)throw await Et(e,$t),w.reason;return{files:o,bytes:a,skippedSymlinks:c}}var q=Object.freeze({recursive:!0,force:!0,maxRetries:5,retryDelay:100}),Pe=i=>`run \`pluriply worker enable ${i}\` to let the hub process this automatically`,Ce=i=>`no worker template is configured for "${i}"; enable it via config or set PLURIPLY_WORKER_TEMPLATE_OVERRIDE`,Le=new Set(["completed","failed","cancelled"]);function De(i){return Ae.includes(i)?!0:!!et()?.[i]}function Me(i){try{return $e(i,"utf8").trimEnd().split(`
|
|
5
6
|
`).slice(-20).join(`
|
|
6
|
-
`)}catch{return""}}function
|
|
7
|
-
`)}function
|
|
8
|
-
`)}var
|
|
9
|
-
`)}catch{}}
|
|
7
|
+
`)}catch{return""}}function Ne({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 je({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 F=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(!Re(r,s))return{kind:"none",hint:Pe(s)};try{if(!De(s))return{kind:"none",hint:Ce(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)||Oe(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(_(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=_(this.home,"workers"),t;try{t=Te(e,{withFileTypes:!0})}catch{return}for(let n of t)if(!(!n.isDirectory()||this.#s(n.name)))try{be(_(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=_(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(_(this.home,"workers",n.taskId,"tree"),q).catch(()=>{}),this.#a(t)}async#l(e,t,n,r){let s=t.toTool??t.to,o=_(this.home,"workers");bt(o,{recursive:!0});let a=_(o,`${t.taskId}.log`),c=(t.kind??"task")==="review",l=_(o,t.taskId),u={agent:s,channelCode:e,taskId:t.taskId,from:t.from},h,w,p="";try{if(c){if(!t.cwd)throw new Error("review task has no cwd");await xe(l,{recursive:!0});let d=await kt(t.cwd);h=_(l,"tree");let m=await Tt({repoDir:t.cwd,destDir:h,git:d}),I=await _t({cwd:t.cwd,review:t.review??{},outFile:_(l,"review.diff"),git:d});p=`snapshot: ${m.files} files, ${m.bytes} bytes, ${m.skippedSymlinks} symlinks skipped
|
|
11
|
+
`,w=je({...u,cwd:h,origin:t.cwd,diff:I}),await this.#o()}else h=t.cwd??_(this.home,"workspaces",t.taskId),bt(h,{recursive:!0}),w=Ne({...u,cwd:h})}catch(d){this.#n(r,s,t,`${c?"review preparation":"worker spawn"} failed: ${d.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 g;try{let d=It(s,{home:this.home,cwd:h,prompt:w,logDir:o,taskDir:l,taskId:t.taskId,channelCode:e,permissionMode:n.workers[s]?.permissionMode,timeoutMs:n.limits.timeoutMs,readOnly:c});p&&nt(a,p);let m=Ee(a,"a");try{g=Se(d.command,d.args,{cwd:h,shell:!1,stdio:["ignore",m,m],env:{...process.env,PLURIPLY_HOME:this.home,PLURIPLY_WORKER_TASK:t.taskId,PLURIPLY_WORKER_AGENT:s,PLURIPLY_DEPTH:String(t.depth??0)}})}finally{ve(m)}}catch(d){this.#n(r,s,t,`worker spawn failed: ${d.message}`);return}r.child=g;try{this.tasks.setWorker(e,t.taskId,{agent:s,pid:g.pid,startedAt:new Date().toISOString(),log:a})}catch(d){try{g.kill("SIGKILL")}catch{}this.#n(r,s,t,`worker spawn failed: ${d.message}`);return}let f=!1,E=setTimeout(()=>{f=!0,g.kill("SIGTERM"),setTimeout(()=>g.kill("SIGKILL"),5e3).unref()},n.limits.timeoutMs),b=(d,m)=>{clearTimeout(E),this.running.get(s)?.delete(r);let I={endedAt:new Date().toISOString(),exitCode:d};f&&(I.timedOut=!0);try{this.tasks.setWorker(e,t.taskId,I);let v;if(m){v=`worker failed to start: ${m.message}`;try{nt(a,`${v}
|
|
12
|
+
`)}catch{}}else f?v=`worker timed out after ${n.limits.timeoutMs/1e3}s`:v=`worker exited without submitting a result (exit ${d})
|
|
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
|
+
${Me(a)}`;this.tasks.failIfOpen(e,t.taskId,{result:v,by:`${s} worker`})}catch{}c&&rt(_(l,"tree"),q).catch(()=>{}),this.#a(s)};g.on("exit",(d,m)=>b(d??(m?-1:0))),g.on("error",d=>b(-1,d))}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(Le.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 He}from"../shared/config.js";import{pluriplyHome as Ke}from"../shared/paths.js";import{shortId as Be}from"../shared/ids.js";import{isValidAgentName as Ye,isInstanceId as Je,makeInstanceId as ze,toolOf as Ve,cwdKey as Xe}from"../shared/identity.js";import{PACKAGE_VERSION as At,PROTOCOL_VERSION as Ot}from"../shared/version.js";import{pingHub as Qe,pidAlive as Pt,homeId as Ct}from"../shared/probe.js";function st(i){try{let e=JSON.parse(Fe(i,"utf8"));return Number.isInteger(e?.pid)&&Number.isInteger(e?.port)?e:null}catch{return null}}var it=class{constructor({home:e=Ke(),port:t=0,verifyDelayMs:n=100}={}){this.home=e,this.requestedPort=t,this.verifyDelayMs=n;let r=new L(e);this.channels=new M(r),this.tasks=new G(this.channels),this.context=new W(this.channels),this.agents=new D(r),this.workers=new F({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(){We(this.home,{recursive:!0}),await new Promise((t,n)=>{this.wss=new Ge({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=Rt(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(!Pt(e.pid))return!1;let t=Date.now()+2e3;for(;;){let n=await Qe(e.port,300);if(n)return!n.home||n.home===Ct(this.home);if(Date.now()>=t||!Pt(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:At,protocol:Ot,startedAt:new Date().toISOString()},null,2);try{return qe(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=Rt(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=ze(e,Be(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:At,protocol:Ot,pid:process.pid,home:Ct(this.home)};case"channel.create":return{channelCode:this.channels.create().channel.code};case"agent.hello":{if(!Ye(t.tool))throw new Error(`invalid agent name: ${t.tool}`);if(typeof t.cwd!="string"||t.cwd.length===0||!Ue(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(!Je(s)||Ve(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=Xe(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=He(this.home),{task:o,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,o,{interactive:this.isInteractive(t.channelCode,o.toTool),config:s}),h={taskId:o.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(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 Ze}from"node:child_process";import{existsSync as tn,rmSync as ot}from"node:fs";import{join as en}from"node:path";import{fileURLToPath as nn}from"node:url";import{pingHub as Lt,homeId as rn,pidAlive as sn}from"../shared/probe.js";import{readLock as Dt}from"../shared/lock.js";var on=nn(new URL("../../bin/pluriply.js",import.meta.url));async function Mt(i){let e=Dt(i);if(!e)return null;let t=await Lt(e.port);return!t||t.home&&t.home!==rn(i)?null:{...t,port:e.port,lockPid:e.pid}}async function an({home:i,timeoutMs:e=5e3}){Ze(process.execPath,[on,"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 Mt(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 cn({home:i,timeoutMs:e=5e3}){let t=Dt(i),n=en(i,"hub.json");if(!t)return"not-running";let r=await Lt(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(!tn(n))return"stopped";if(!sn(t.pid))return ot(n,{force:!0}),"stopped";await new Promise(a=>setTimeout(a,100))}return"timeout"}import{readLock as Er}from"../shared/lock.js";import{loadConfig as $r,saveConfig as Tr,setWorkerEnabled as br,TEMPLATE_AGENTS as xr}from"../shared/config.js";export{it as Hub,xr as TEMPLATE_AGENTS,Mt as liveHub,$r as loadConfig,Er as readLock,Tr as saveConfig,br as setWorkerEnabled,an as spawnHub,cn as stopHub};
|