@staix/agent-hub 0.13.0 → 0.15.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/CHANGELOG.md +30 -0
- package/README.md +19 -7
- package/docs/agent-notes/README.md +4 -0
- package/docs/agent-notes/adapters.md +18 -4
- package/docs/agent-notes/budget.md +5 -4
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +19 -7
- package/docs/agent-notes/tasks.md +15 -7
- package/docs/agent-notes/tests.md +4 -4
- package/docs/dogfood.md +18 -10
- package/docs/events.md +21 -7
- package/docs/operations.md +131 -27
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +13 -3
- package/docs/sdlc.md +40 -2
- package/docs/security.md +45 -14
- package/docs/specs/2026-09-19-agent-hub-design.md +112 -10
- package/docs/verification/2026-10-22-codex-reviewer-isolation.md +14 -0
- package/docs/verification/2026-10-23-0.14.0.md +69 -0
- package/docs/verification/2026-10-23-0.15.0.md +80 -0
- package/docs/verification/2026-10-23-daemon-responsiveness.md +44 -0
- package/docs/verification/2026-10-23-headless-claude-permissions.md +47 -0
- package/docs/verification/2026-10-23-terminal-claude-execution-root.md +41 -0
- package/docs/verified.json +34 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1078 -327
- package/src/adapters/acp.ts +43 -7
- package/src/adapters/claude-channel.ts +26 -4
- package/src/adapters/claude-implementer.ts +243 -0
- package/src/adapters/claude-print-child.ts +90 -0
- package/src/adapters/claude-task-sessions.ts +80 -0
- package/src/adapters/codex-review-isolation.ts +18 -7
- package/src/adapters/compat.ts +4 -0
- package/src/adapters/conformance/enrollment.ts +2 -1
- package/src/adapters/conformance/fakes/acp-server.ts +3 -1
- package/src/adapters/definitions/builtin-metadata.ts +15 -3
- package/src/adapters/definitions/native-channel-state.ts +1 -1
- package/src/adapters/definitions/native-cli-entry-claude.ts +13 -8
- package/src/adapters/definitions/native-cli-entry-codex.ts +13 -0
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/kimi-wire-usage.ts +303 -0
- package/src/adapters/registry-types.ts +7 -3
- package/src/adapters/registry.ts +14 -2
- package/src/adapters/reviewer-print.ts +64 -60
- package/src/cli/console-config.ts +123 -0
- package/src/cli/console-state.ts +28 -1
- package/src/cli/console.ts +25 -1
- package/src/cli/facts-hook.ts +9 -5
- package/src/cli/help.ts +6 -4
- package/src/cli/launch.ts +10 -4
- package/src/cli/main.ts +52 -9
- package/src/cli/output.ts +75 -2
- package/src/cli/reset.ts +1 -1
- package/src/cli/settings-edit.ts +89 -0
- package/src/cli/status-lines.ts +1 -1
- package/src/cli/terminal-recovery.ts +1 -6
- package/src/hub/action-enforce.ts +82 -0
- package/src/hub/action-policy.ts +22 -10
- package/src/hub/board.ts +48 -3
- package/src/hub/budget.ts +5 -0
- package/src/hub/bus.ts +5 -5
- package/src/hub/claude-launch-proof.ts +33 -0
- package/src/hub/config-apply.ts +5 -0
- package/src/hub/config-edit-host.ts +173 -0
- package/src/hub/config-edit.ts +282 -0
- package/src/hub/config-tier.ts +89 -0
- package/src/hub/control-client.ts +24 -3
- package/src/hub/daemon.ts +450 -71
- package/src/hub/deliverable-files.ts +131 -0
- package/src/hub/deliverables.ts +125 -0
- package/src/hub/delivery-journal.ts +18 -2
- package/src/hub/events.ts +3 -1
- package/src/hub/hub-tools.ts +21 -4
- package/src/hub/instances.ts +19 -6
- package/src/hub/kimi-usage.ts +144 -0
- package/src/hub/manager.ts +3 -3
- package/src/hub/peers.ts +7 -0
- package/src/hub/permission-payload.ts +15 -0
- package/src/hub/routing.ts +118 -3
- package/src/hub/sdlc-metrics.ts +260 -0
- package/src/hub/sdlc.ts +5 -3
- package/src/hub/settings.ts +1 -1
- package/src/hub/spec-read.ts +31 -0
- package/src/hub/task-code-group.ts +52 -0
- package/src/hub/tasks.ts +374 -37
- package/src/hub/token-window.ts +33 -0
- package/src/hub/ui.ts +3 -0
- package/src/hub/usage-rollup.ts +74 -6
- package/src/hub/workflow.ts +16 -0
- package/src/ui/index.html +388 -3
|
@@ -44,34 +44,6 @@ var __esm = (fn, res, err) => () => {
|
|
|
44
44
|
return res;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
// src/adapters/definitions/native-cli-preview-common.ts
|
|
48
|
-
function publicArg(arg) {
|
|
49
|
-
const flags = new Set(["--settings", "--model", "--mcp-config", "--session-id", "--session-file", "--resume", "--continue", "--print", "--safe", "--new"]);
|
|
50
|
-
const key = arg.split("=", 1)[0];
|
|
51
|
-
return flags.has(key) ? arg.includes("=") ? key + "=" + REDACTED : key : REDACTED;
|
|
52
|
-
}
|
|
53
|
-
function nativeCommand(command, trusted) {
|
|
54
|
-
return command.map((value, i) => value === trusted[i] ? value : REDACTED);
|
|
55
|
-
}
|
|
56
|
-
function previewState(args, baseEnvNames) {
|
|
57
|
-
const unresolved = [];
|
|
58
|
-
const envNames = [...baseEnvNames];
|
|
59
|
-
const value = (flag) => {
|
|
60
|
-
const at = args.indexOf(flag);
|
|
61
|
-
if (at < 0)
|
|
62
|
-
return;
|
|
63
|
-
const next = args[at + 1];
|
|
64
|
-
if (!next || next.startsWith("--"))
|
|
65
|
-
throw new Error("missing option value");
|
|
66
|
-
return next;
|
|
67
|
-
};
|
|
68
|
-
return { unresolved, envNames, value };
|
|
69
|
-
}
|
|
70
|
-
function finishPreview(ctx, launch, envNames, unresolved, metadata, settings) {
|
|
71
|
-
return { tool: ctx.tool, ...metadata ?? {}, cmd: launch.cmd, args: launch.args, settings, envNames: [...new Set(envNames)].sort(), ...launch.warning ? { warning: launch.warning } : {}, unresolved, redaction: "Arbitrary user arguments, settings, commands and configured executable overrides are redacted. Environment values are never displayed.", needs: ["Native executable, accounts and runtime readiness are not checked."] };
|
|
72
|
-
}
|
|
73
|
-
var REDACTED = "[redacted]", UNRESOLVED = "[unresolved]";
|
|
74
|
-
|
|
75
47
|
// node_modules/@staix/agent-mem/src/memory/platform.ts
|
|
76
48
|
import { realpathSync } from "fs";
|
|
77
49
|
import { basename, dirname, join, resolve, sep } from "path";
|
|
@@ -233,6 +205,34 @@ var init_project = __esm(() => {
|
|
|
233
205
|
init_platform();
|
|
234
206
|
});
|
|
235
207
|
|
|
208
|
+
// src/adapters/definitions/native-cli-preview-common.ts
|
|
209
|
+
function publicArg(arg) {
|
|
210
|
+
const flags = new Set(["--settings", "--model", "--mcp-config", "--session-id", "--session-file", "--resume", "--continue", "--print", "--safe", "--new"]);
|
|
211
|
+
const key = arg.split("=", 1)[0];
|
|
212
|
+
return flags.has(key) ? arg.includes("=") ? key + "=" + REDACTED : key : REDACTED;
|
|
213
|
+
}
|
|
214
|
+
function nativeCommand(command, trusted) {
|
|
215
|
+
return command.map((value, i) => value === trusted[i] ? value : REDACTED);
|
|
216
|
+
}
|
|
217
|
+
function previewState(args, baseEnvNames) {
|
|
218
|
+
const unresolved = [];
|
|
219
|
+
const envNames = [...baseEnvNames];
|
|
220
|
+
const value = (flag) => {
|
|
221
|
+
const at = args.indexOf(flag);
|
|
222
|
+
if (at < 0)
|
|
223
|
+
return;
|
|
224
|
+
const next = args[at + 1];
|
|
225
|
+
if (!next || next.startsWith("--"))
|
|
226
|
+
throw new Error("missing option value");
|
|
227
|
+
return next;
|
|
228
|
+
};
|
|
229
|
+
return { unresolved, envNames, value };
|
|
230
|
+
}
|
|
231
|
+
function finishPreview(ctx, launch, envNames, unresolved, metadata, settings) {
|
|
232
|
+
return { tool: ctx.tool, ...metadata ?? {}, cmd: launch.cmd, args: launch.args, settings, envNames: [...new Set(envNames)].sort(), ...launch.warning ? { warning: launch.warning } : {}, unresolved, redaction: "Arbitrary user arguments, settings, commands and configured executable overrides are redacted. Environment values are never displayed.", needs: ["Native executable, accounts and runtime readiness are not checked."] };
|
|
233
|
+
}
|
|
234
|
+
var REDACTED = "[redacted]", UNRESOLVED = "[unresolved]";
|
|
235
|
+
|
|
236
236
|
// src/adapters/compat.ts
|
|
237
237
|
import { closeSync, constants, fstatSync, lstatSync as lstatSync2, mkdirSync, openSync, readSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
238
238
|
import { dirname as dirname3, join as join3 } from "path";
|
|
@@ -290,6 +290,14 @@ var plain = (text) => text.length <= 64 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
|
|
|
290
290
|
var init_compat = __esm(() => {
|
|
291
291
|
init_project();
|
|
292
292
|
COMPAT = {
|
|
293
|
+
"claude-implementer": {
|
|
294
|
+
label: "Claude headless implementer",
|
|
295
|
+
verified: ["2.1.309"],
|
|
296
|
+
gate: "contract",
|
|
297
|
+
probe: ["claude", "--version"],
|
|
298
|
+
parse: (out) => plain(out.trim().replace(/ \(Claude Code\)$/, "")),
|
|
299
|
+
trustNote: "The native prompt-tool allow, deny, MCP error/disconnect and hook-failure fallthrough have an isolated receipt. No dontAsk/bypass fallback exists; another version's permission behavior is unverified."
|
|
300
|
+
},
|
|
293
301
|
"codex-review": {
|
|
294
302
|
label: "Codex reviewer (codex-cli)",
|
|
295
303
|
verified: ["0.146.0", "0.163.0"],
|
|
@@ -1134,6 +1142,106 @@ var init_review_brief = __esm(() => {
|
|
|
1134
1142
|
LOCKFILES = new Set(["bun.lock", "bun.lockb", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "Cargo.lock", "poetry.lock", "uv.lock", "go.sum", "composer.lock", "Gemfile.lock"]);
|
|
1135
1143
|
});
|
|
1136
1144
|
|
|
1145
|
+
// src/adapters/claude-print-child.ts
|
|
1146
|
+
import { spawn as spawn2 } from "child_process";
|
|
1147
|
+
|
|
1148
|
+
class OwnedPrintChild {
|
|
1149
|
+
options;
|
|
1150
|
+
process;
|
|
1151
|
+
done;
|
|
1152
|
+
stopping;
|
|
1153
|
+
limited = false;
|
|
1154
|
+
constructor(options) {
|
|
1155
|
+
this.options = options;
|
|
1156
|
+
const [command, ...prefix] = options.command;
|
|
1157
|
+
if (!command)
|
|
1158
|
+
throw new Error("print child command is missing");
|
|
1159
|
+
const child = this.process = spawn2(command, [...prefix, ...options.args], {
|
|
1160
|
+
cwd: options.cwd,
|
|
1161
|
+
detached: true,
|
|
1162
|
+
env: peerChildEnv(options.peer, options.envMode === "replace" ? options.env : { ...process.env, ...options.env }),
|
|
1163
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
1164
|
+
});
|
|
1165
|
+
const frameLimit = options.maxFrameBytes ?? 256 * 1024, outputLimit = options.maxOutputBytes ?? 4 * 1024 * 1024;
|
|
1166
|
+
let buffer = "", outputBytes = 0;
|
|
1167
|
+
const current = () => !this.limited && options.current?.() !== false;
|
|
1168
|
+
const limit = () => {
|
|
1169
|
+
if (!this.limited) {
|
|
1170
|
+
this.limited = true;
|
|
1171
|
+
options.onLimit?.();
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
this.done = new Promise((resolve, reject) => {
|
|
1175
|
+
child.once("error", (error) => {
|
|
1176
|
+
options.onError?.(error);
|
|
1177
|
+
reject(error);
|
|
1178
|
+
});
|
|
1179
|
+
child.stdin.on("error", () => {});
|
|
1180
|
+
child.once("spawn", () => {
|
|
1181
|
+
trackGroup(child);
|
|
1182
|
+
if (!current()) {
|
|
1183
|
+
child.stdin.destroy();
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
options.onSpawn?.();
|
|
1187
|
+
if (current())
|
|
1188
|
+
child.stdin.end(options.input);
|
|
1189
|
+
else
|
|
1190
|
+
child.stdin.destroy();
|
|
1191
|
+
});
|
|
1192
|
+
child.stdout.on("data", (chunk) => {
|
|
1193
|
+
if (!current())
|
|
1194
|
+
return;
|
|
1195
|
+
outputBytes += chunk.length;
|
|
1196
|
+
if (outputBytes > outputLimit)
|
|
1197
|
+
return limit();
|
|
1198
|
+
buffer += chunk.toString("utf8");
|
|
1199
|
+
for (let index;(index = buffer.indexOf(`
|
|
1200
|
+
`)) >= 0; ) {
|
|
1201
|
+
const line = buffer.slice(0, index);
|
|
1202
|
+
buffer = buffer.slice(index + 1);
|
|
1203
|
+
if (Buffer.byteLength(line) > frameLimit)
|
|
1204
|
+
return limit();
|
|
1205
|
+
try {
|
|
1206
|
+
options.onLine?.(line);
|
|
1207
|
+
} catch {}
|
|
1208
|
+
if (!current())
|
|
1209
|
+
return;
|
|
1210
|
+
try {
|
|
1211
|
+
options.onFrame?.(JSON.parse(line));
|
|
1212
|
+
} catch {}
|
|
1213
|
+
if (!current())
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (Buffer.byteLength(buffer) > frameLimit)
|
|
1217
|
+
return limit();
|
|
1218
|
+
options.onActivity?.();
|
|
1219
|
+
});
|
|
1220
|
+
child.once("exit", (code) => {
|
|
1221
|
+
options.onLeaderExit?.();
|
|
1222
|
+
this.stop().then(() => resolve(code), (error) => reject(new PrintChildCleanupError(error.message)));
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
stop() {
|
|
1227
|
+
if (this.stopping)
|
|
1228
|
+
return this.stopping;
|
|
1229
|
+
const stopping = Promise.resolve().then(() => this.options.stop ? this.options.stop(this.process) : stopOwnedProcess(this.process, { group: true }));
|
|
1230
|
+
this.stopping = stopping;
|
|
1231
|
+
stopping.catch(() => {
|
|
1232
|
+
if (this.stopping === stopping)
|
|
1233
|
+
this.stopping = undefined;
|
|
1234
|
+
});
|
|
1235
|
+
return stopping;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
var PrintChildCleanupError;
|
|
1239
|
+
var init_claude_print_child = __esm(() => {
|
|
1240
|
+
init_child_process();
|
|
1241
|
+
PrintChildCleanupError = class PrintChildCleanupError extends Error {
|
|
1242
|
+
};
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1137
1245
|
// src/hub/envelope.ts
|
|
1138
1246
|
function renderDigest(envs, primed) {
|
|
1139
1247
|
const items = envs.map(frame).join(`
|
|
@@ -1222,33 +1330,36 @@ class BasePeer {
|
|
|
1222
1330
|
var DEFAULT_WATCHDOG_MS = 300000;
|
|
1223
1331
|
|
|
1224
1332
|
// src/adapters/reviewer-print.ts
|
|
1225
|
-
import { spawn as spawn2 } from "child_process";
|
|
1226
1333
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1227
|
-
var
|
|
1334
|
+
var ReviewerUnavailableError, reviewTarget = (env) => {
|
|
1228
1335
|
const task = reviewTask(env);
|
|
1229
1336
|
if (task === undefined || env.private)
|
|
1230
1337
|
return;
|
|
1231
1338
|
const revision = Number(env.refs?.reviewRevision);
|
|
1232
1339
|
return { task, ...typeof env.refs?.commit === "string" ? { head: env.refs.commit } : {}, ...Number.isSafeInteger(revision) && revision > 0 ? { flowRevision: revision } : {} };
|
|
1233
|
-
}, remainingFailures = (batch, start, cause) => batch.targets.slice(start).flatMap((target) => target ? [{ ...target, cause }] : []), reviewTask = (env) => {
|
|
1340
|
+
}, remainingFailures = (batch, start, cause, why) => batch.targets.slice(start).flatMap((target) => target ? [{ ...target, cause, ...why ? { causeClass: why.causeClass, ...why.causeDetail ? { causeDetail: why.causeDetail } : {} } : {} }] : []), reviewTask = (env) => {
|
|
1234
1341
|
if (env.from !== HUB || env.kind !== "review")
|
|
1235
1342
|
return;
|
|
1236
1343
|
const id = Number(env.refs?.task);
|
|
1237
1344
|
return Number.isSafeInteger(id) && id > 0 ? id : undefined;
|
|
1238
1345
|
}, ReviewerPrintPeer;
|
|
1239
1346
|
var init_reviewer_print = __esm(() => {
|
|
1240
|
-
|
|
1347
|
+
init_claude_print_child();
|
|
1241
1348
|
init_envelope();
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1349
|
+
ReviewerUnavailableError = class ReviewerUnavailableError extends Error {
|
|
1350
|
+
causeClass;
|
|
1351
|
+
detail;
|
|
1352
|
+
constructor(message, causeClass, detail) {
|
|
1353
|
+
super(message);
|
|
1354
|
+
this.causeClass = causeClass;
|
|
1355
|
+
this.detail = detail;
|
|
1356
|
+
}
|
|
1245
1357
|
};
|
|
1246
1358
|
ReviewerPrintPeer = class ReviewerPrintPeer extends BasePeer {
|
|
1247
1359
|
opts;
|
|
1248
1360
|
hubNative = true;
|
|
1249
1361
|
generation = 0;
|
|
1250
1362
|
child;
|
|
1251
|
-
childStop;
|
|
1252
1363
|
childCleanup;
|
|
1253
1364
|
childCurrent;
|
|
1254
1365
|
reviewMcpClose;
|
|
@@ -1398,13 +1509,15 @@ var init_reviewer_print = __esm(() => {
|
|
|
1398
1509
|
} catch (error) {
|
|
1399
1510
|
if (generation !== this.generation)
|
|
1400
1511
|
return;
|
|
1401
|
-
if (error instanceof
|
|
1512
|
+
if (error instanceof PrintChildCleanupError) {
|
|
1402
1513
|
this.opts.log?.(`[${this.id}] review of task #${task}: ${error.message}`);
|
|
1403
1514
|
this.settle("needs_review", `review process group still running: ${error.message}`);
|
|
1404
1515
|
return;
|
|
1405
1516
|
}
|
|
1406
1517
|
failure = { state: started ? "needs_review" : "failed_safe", reason: `reviewer could not start: ${error.message}` };
|
|
1407
|
-
|
|
1518
|
+
const why = error instanceof ReviewerUnavailableError ? { causeClass: error.causeClass, causeDetail: error.detail } : { causeClass: "unknown" };
|
|
1519
|
+
this.opts.log?.(`[${this.id}] review of task #${task} could not start [cause: ${why.causeClass}]`);
|
|
1520
|
+
reroute.push(...remainingFailures(batch, index, "start_failed", why));
|
|
1408
1521
|
batch.next = envs.length;
|
|
1409
1522
|
break;
|
|
1410
1523
|
}
|
|
@@ -1476,12 +1589,12 @@ var init_reviewer_print = __esm(() => {
|
|
|
1476
1589
|
...task === undefined ? {} : { task }
|
|
1477
1590
|
});
|
|
1478
1591
|
if ("unavailable" in launch)
|
|
1479
|
-
throw new
|
|
1592
|
+
throw new ReviewerUnavailableError(launch.unavailable, launch.causeClass ?? "unknown", launch.detail);
|
|
1480
1593
|
const proof = launch.isolation;
|
|
1481
1594
|
const policy = this.opts.backend.nativePolicy ? proof?.enforcement === "native-policy" && proof.freshSession === true && proof.hubToolsOnly === true : proof?.strictMcp === true && proof.ephemeral === true;
|
|
1482
1595
|
if (abort.signal.aborted || !proof || proof.backend !== this.opts.backend.id || proof.readOnly !== true || !policy || !proof.binary || !proof.version || !proof.profile || !proof.evidence.length || !launch.command.length) {
|
|
1483
1596
|
await launch.cleanup?.();
|
|
1484
|
-
throw new
|
|
1597
|
+
throw new ReviewerUnavailableError("reviewer isolation is unavailable or preparation was cancelled", "unknown");
|
|
1485
1598
|
}
|
|
1486
1599
|
return launch;
|
|
1487
1600
|
})();
|
|
@@ -1525,36 +1638,24 @@ ${facts}` : ""}`;
|
|
|
1525
1638
|
try {
|
|
1526
1639
|
launch = await this.prepare(randomUUID2(), input, task, mcp?.config);
|
|
1527
1640
|
if (generation !== this.generation || this.opts.admit?.(env) === false || launch.current?.() === false)
|
|
1528
|
-
throw new
|
|
1641
|
+
throw new ReviewerUnavailableError("reviewer isolation or admission changed before child spawn", launch.current?.() === false ? "binary" : "unknown");
|
|
1529
1642
|
this.receipt = launch.isolation;
|
|
1530
|
-
const [cmd, ...prefix] = launch.command;
|
|
1531
1643
|
let quota = false;
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
child.once("error", (error) => {
|
|
1545
|
-
if (this.child === child) {
|
|
1644
|
+
const child = new OwnedPrintChild({
|
|
1645
|
+
peer: this.id,
|
|
1646
|
+
command: launch.command,
|
|
1647
|
+
args: launch.args,
|
|
1648
|
+
cwd: launch.cwd ?? this.opts.cwd,
|
|
1649
|
+
env: launch.env,
|
|
1650
|
+
envMode: launch.envMode,
|
|
1651
|
+
input,
|
|
1652
|
+
current: () => generation === this.generation,
|
|
1653
|
+
stop: launch.stop,
|
|
1654
|
+
onError: () => {
|
|
1655
|
+
if (this.child === child)
|
|
1546
1656
|
this.child = undefined;
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
reject(error);
|
|
1550
|
-
});
|
|
1551
|
-
child.stdin.on("error", () => {});
|
|
1552
|
-
child.once("spawn", () => {
|
|
1553
|
-
trackGroup(child);
|
|
1554
|
-
if (generation !== this.generation) {
|
|
1555
|
-
child.stdin.destroy();
|
|
1556
|
-
return;
|
|
1557
|
-
}
|
|
1657
|
+
},
|
|
1658
|
+
onSpawn: () => {
|
|
1558
1659
|
this.runningBinding = { task, binding };
|
|
1559
1660
|
this.reviewing = task;
|
|
1560
1661
|
this.reviewSeq++;
|
|
@@ -1562,55 +1663,30 @@ ${facts}` : ""}`;
|
|
|
1562
1663
|
policyTimer = setInterval(() => {
|
|
1563
1664
|
this.reviewToken(task);
|
|
1564
1665
|
}, 100);
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
buffer += chunk.toString("utf8");
|
|
1577
|
-
for (let i;(i = buffer.indexOf(`
|
|
1578
|
-
`)) >= 0; ) {
|
|
1579
|
-
const line = buffer.slice(0, i);
|
|
1580
|
-
buffer = buffer.slice(i + 1);
|
|
1581
|
-
if (Buffer.byteLength(line) > MAX_REVIEW_FRAME_BYTES) {
|
|
1582
|
-
this.abortReview("review output limit exceeded");
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
|
-
try {
|
|
1586
|
-
const event = JSON.parse(line);
|
|
1587
|
-
const usage = this.opts.backend.usage?.(event);
|
|
1588
|
-
if (usage)
|
|
1589
|
-
this.opts.onUsage?.(usage, task);
|
|
1590
|
-
if (this.opts.backend.failure?.(event) === "quota")
|
|
1591
|
-
quota = true;
|
|
1592
|
-
} catch {}
|
|
1593
|
-
}
|
|
1594
|
-
if (Buffer.byteLength(buffer) > MAX_REVIEW_FRAME_BYTES) {
|
|
1595
|
-
this.abortReview("review output limit exceeded");
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
this.touch();
|
|
1599
|
-
});
|
|
1600
|
-
child.once("exit", (code) => {
|
|
1666
|
+
},
|
|
1667
|
+
onFrame: (event) => {
|
|
1668
|
+
const usage = this.opts.backend.usage?.(event);
|
|
1669
|
+
if (usage)
|
|
1670
|
+
this.opts.onUsage?.(usage, task);
|
|
1671
|
+
if (this.opts.backend.failure?.(event) === "quota")
|
|
1672
|
+
quota = true;
|
|
1673
|
+
},
|
|
1674
|
+
onActivity: () => this.touch(),
|
|
1675
|
+
onLimit: () => this.abortReview("review output limit exceeded"),
|
|
1676
|
+
onLeaderExit: () => {
|
|
1601
1677
|
if (this.child === child) {
|
|
1602
1678
|
this.reviewing = undefined;
|
|
1603
1679
|
this.runningBinding = undefined;
|
|
1604
1680
|
}
|
|
1605
|
-
|
|
1606
|
-
if (this.child === child) {
|
|
1607
|
-
this.child = undefined;
|
|
1608
|
-
this.childStop = undefined;
|
|
1609
|
-
}
|
|
1610
|
-
resolve({ code, quota });
|
|
1611
|
-
}, (error) => reject(new GroupCleanupError(error.message)));
|
|
1612
|
-
});
|
|
1681
|
+
}
|
|
1613
1682
|
});
|
|
1683
|
+
this.child = ownedChild = child;
|
|
1684
|
+
this.childCleanup = release;
|
|
1685
|
+
this.childCurrent = launch.watchCurrent ? launch.current : undefined;
|
|
1686
|
+
const code = await child.done;
|
|
1687
|
+
if (this.child === child)
|
|
1688
|
+
this.child = undefined;
|
|
1689
|
+
return { code, quota };
|
|
1614
1690
|
} finally {
|
|
1615
1691
|
closeMcp();
|
|
1616
1692
|
if (policyTimer)
|
|
@@ -1638,11 +1714,10 @@ ${facts}` : ""}`;
|
|
|
1638
1714
|
const child = this.child;
|
|
1639
1715
|
if (!child)
|
|
1640
1716
|
return;
|
|
1641
|
-
await
|
|
1717
|
+
await child.stop();
|
|
1642
1718
|
if (this.child === child) {
|
|
1643
1719
|
await this.childCleanup?.();
|
|
1644
1720
|
this.child = undefined;
|
|
1645
|
-
this.childStop = undefined;
|
|
1646
1721
|
this.childCleanup = undefined;
|
|
1647
1722
|
this.childCurrent = undefined;
|
|
1648
1723
|
}
|
|
@@ -1916,6 +1991,7 @@ function ownServer(text) {
|
|
|
1916
1991
|
}
|
|
1917
1992
|
async function prepare(ctx) {
|
|
1918
1993
|
let root;
|
|
1994
|
+
let leg = "binary", timedOut = false;
|
|
1919
1995
|
try {
|
|
1920
1996
|
const cwd = realPath(ctx.cwd), env = peerChildEnv(ctx.peer), bound = resolveCommand(ctx, cwd);
|
|
1921
1997
|
const initialBinaryMetadata = bound.files.map(metadata).join(";");
|
|
@@ -1931,6 +2007,8 @@ async function prepare(ctx) {
|
|
|
1931
2007
|
};
|
|
1932
2008
|
const run = (args, capture = true, maxBytes = CAP) => new Promise((resolveOutput, reject) => {
|
|
1933
2009
|
if (ctx.signal.aborted || performance.now() >= deadline) {
|
|
2010
|
+
if (!ctx.signal.aborted)
|
|
2011
|
+
timedOut = true;
|
|
1934
2012
|
reject(new Error("metadata deadline"));
|
|
1935
2013
|
return;
|
|
1936
2014
|
}
|
|
@@ -1941,7 +2019,11 @@ async function prepare(ctx) {
|
|
|
1941
2019
|
failed = true;
|
|
1942
2020
|
finish(null);
|
|
1943
2021
|
};
|
|
1944
|
-
const timer = setTimeout(
|
|
2022
|
+
const timer = setTimeout(() => {
|
|
2023
|
+
if (!ctx.signal.aborted)
|
|
2024
|
+
timedOut = true;
|
|
2025
|
+
stop();
|
|
2026
|
+
}, Math.max(1, Math.min(2000, deadline - performance.now())));
|
|
1945
2027
|
ctx.signal.addEventListener("abort", stop, { once: true });
|
|
1946
2028
|
const finish = async (code) => {
|
|
1947
2029
|
if (settled)
|
|
@@ -1985,15 +2067,19 @@ async function prepare(ctx) {
|
|
|
1985
2067
|
finish(code);
|
|
1986
2068
|
});
|
|
1987
2069
|
});
|
|
2070
|
+
leg = "version";
|
|
1988
2071
|
const verdict = assessVersion("codex-review", COMPAT["codex-review"].parse(await run(["--version"])));
|
|
1989
2072
|
if (!verdict.ok)
|
|
1990
2073
|
throw new VersionRefusal(verdict.reason);
|
|
1991
2074
|
const version = verdict.version;
|
|
2075
|
+
leg = "mcp_inventory";
|
|
1992
2076
|
if (Buffer.byteLength(ctx.mcpConfig) > CAP || ctx.privatePaths.length > 128)
|
|
1993
2077
|
throw new Error("policy bounds exceeded");
|
|
1994
2078
|
const hub = ownServer(ctx.mcpConfig);
|
|
1995
2079
|
const policy = ["-c", "notify=[]", "--disable", "plugins", "--disable", "hooks", "--disable", "apps", "-c", 'web_search="disabled"', "-c", `projects=${toml({ [cwd]: { trust_level: "untrusted" } })}`];
|
|
2080
|
+
leg = "plugins_hooks_apps";
|
|
1996
2081
|
proveDisabledFeatures(await run(["features", "list", ...policy]));
|
|
2082
|
+
leg = "mcp_inventory";
|
|
1997
2083
|
const before = inventory(await run(["mcp", "list", "--json", ...policy]));
|
|
1998
2084
|
if (before.some((row) => row.name === "agent-hub"))
|
|
1999
2085
|
throw new Error("ambient hub MCP collision");
|
|
@@ -2011,6 +2097,7 @@ async function prepare(ctx) {
|
|
|
2011
2097
|
const observedEnv = object2(observedTransport.env ?? {}), intendedEnv = object2(hub.env);
|
|
2012
2098
|
if (observedTransport.type !== "stdio" || observedTransport.command !== hub.command || JSON.stringify(observedTransport.args ?? []) !== JSON.stringify(hub.args) || Object.keys(observedEnv).length !== Object.keys(intendedEnv).length || Object.entries(intendedEnv).some(([key, value]) => observedEnv[key] !== value))
|
|
2013
2099
|
throw new Error("hub transport changed");
|
|
2100
|
+
leg = "sandbox_proof";
|
|
2014
2101
|
root = realPath(mkdtempSync(join5(tmpdir(), "ahub-codex-review-isolation-")));
|
|
2015
2102
|
const privateFile = join5(root, "private.env"), publicFile = join5(root, "public.txt"), writeFile = join5(root, "write-proof");
|
|
2016
2103
|
const coreName = join5(root, ".env"), coreStateDir = join5(root, ".agenthub", "state"), coreState = join5(coreStateDir, "private.txt");
|
|
@@ -2031,9 +2118,11 @@ async function prepare(ctx) {
|
|
|
2031
2118
|
writeFileSync3(globFile, "glob private fixture");
|
|
2032
2119
|
symlinkSync(globFile, alias);
|
|
2033
2120
|
const filesystem = { "/": "read", [privateFile]: "deny", [join5(globRoot, "**", "*.env")]: "deny" };
|
|
2121
|
+
leg = "credential_root";
|
|
2034
2122
|
for (const glob of codexCoreDenyGlobs())
|
|
2035
2123
|
filesystem[glob] = "deny";
|
|
2036
2124
|
denyCredentialRoots(filesystem, [...hostCredentialRoots(env, cwd), ...hostCredentialRoots({ HOME: fixtureHome }, cwd)], cwd);
|
|
2125
|
+
leg = "sandbox_proof";
|
|
2037
2126
|
for (const entry of ctx.privatePaths) {
|
|
2038
2127
|
if (typeof entry !== "string" || !entry || Buffer.byteLength(entry) > 4096 || /\p{Cc}/u.test(entry))
|
|
2039
2128
|
throw new Error("invalid private path policy");
|
|
@@ -2070,6 +2159,7 @@ AHUB_CODEX_HOST_KUBE_DENIED
|
|
|
2070
2159
|
AHUB_CODEX_HOST_DOCKER_DENIED
|
|
2071
2160
|
AHUB_CODEX_HOST_ALIAS_DENIED` || existsSync2(writeFile))
|
|
2072
2161
|
throw new Error("native sandbox isolation unverified");
|
|
2162
|
+
leg = "binary";
|
|
2073
2163
|
if (!current())
|
|
2074
2164
|
throw new Error("binary or config changed during proof");
|
|
2075
2165
|
const cleanupRoot = root;
|
|
@@ -2104,12 +2194,12 @@ AHUB_CODEX_HOST_ALIAS_DENIED` || existsSync2(writeFile))
|
|
|
2104
2194
|
} catch {}
|
|
2105
2195
|
}
|
|
2106
2196
|
if (error instanceof VersionRefusal)
|
|
2107
|
-
return { unavailable: error.message };
|
|
2197
|
+
return { unavailable: error.message, causeClass: "version" };
|
|
2108
2198
|
if (error instanceof CredentialRootError) {
|
|
2109
2199
|
const why = { dangling: "a dangling link", unreadable: "an unreadable path", unsupported: "unsupported syntax or overlapping the checkout" }[error.kind];
|
|
2110
|
-
return { unavailable: `Codex reviewer isolation receipt unavailable (credential root ${error.label} is ${why}; remove or repair it)` };
|
|
2200
|
+
return { unavailable: `Codex reviewer isolation receipt unavailable (credential root ${error.label} is ${why}; remove or repair it)`, causeClass: "credential_root", detail: `${error.label}: ${why}` };
|
|
2111
2201
|
}
|
|
2112
|
-
return { unavailable: "Codex reviewer isolation receipt unavailable (version, MCP inventory, sandbox, configuration or process proof)" };
|
|
2202
|
+
return { unavailable: "Codex reviewer isolation receipt unavailable (version, MCP inventory, sandbox, configuration or process proof)", causeClass: timedOut ? "timeout" : leg };
|
|
2113
2203
|
}
|
|
2114
2204
|
}
|
|
2115
2205
|
var TOOLS, VersionRefusal, CAP, MAX_SERVERS = 64, digest = (value) => createHash2("sha256").update(value).digest("hex"), inside = (file, root) => {
|
|
@@ -3405,7 +3495,7 @@ function jsonObject(text) {
|
|
|
3405
3495
|
return;
|
|
3406
3496
|
}
|
|
3407
3497
|
}
|
|
3408
|
-
var HANDSHAKE_MS = 30000, TOOL_INPUT_CAP = 64, TOOL_CEILING_MS, RpcTimeoutError, AcpPeer;
|
|
3498
|
+
var ACTION_BUDGET_OPTION = "agent-hub:action-budget", HANDSHAKE_MS = 30000, TOOL_INPUT_CAP = 64, TOOL_CEILING_MS, RpcTimeoutError, AcpPeer;
|
|
3409
3499
|
var init_acp = __esm(() => {
|
|
3410
3500
|
init_envelope();
|
|
3411
3501
|
init_kimi_native();
|
|
@@ -3636,6 +3726,9 @@ var init_acp = __esm(() => {
|
|
|
3636
3726
|
getPermissionMode() {
|
|
3637
3727
|
return this.permissionMode;
|
|
3638
3728
|
}
|
|
3729
|
+
get actionGated() {
|
|
3730
|
+
return this.opts.actionGate !== undefined;
|
|
3731
|
+
}
|
|
3639
3732
|
get pendingOwnedProcess() {
|
|
3640
3733
|
return this.opts.requireOwnedGroup === true && this.proc !== undefined;
|
|
3641
3734
|
}
|
|
@@ -4022,20 +4115,30 @@ ${renderDigest(envs, false)}` }] });
|
|
|
4022
4115
|
const announced = id === undefined ? undefined : this.toolTitles.get(id);
|
|
4023
4116
|
const canonical = canonicalMcpToolName(announced, this.opts.mcpServers?.map((s) => s.name));
|
|
4024
4117
|
const identity = this.nativePolicy !== undefined || this.opts.requireOwnedGroup ? undefined : [typeof call.title === "string" ? call.title : undefined, canonical].find((candidate) => candidate !== undefined && this.opts.autoApprove?.(candidate));
|
|
4025
|
-
|
|
4118
|
+
const rawInput = call.rawInput ?? (id === undefined ? undefined : this.toolInputs.get(id) ?? jsonObject(this.toolText.get(id)));
|
|
4119
|
+
let gate;
|
|
4120
|
+
try {
|
|
4121
|
+
gate = rawInput === undefined || !this.sessionId ? undefined : this.opts.actionGate?.({ names: [call.title, announced, canonical].filter((name) => typeof name === "string"), ...kind ? { kind } : {}, input: rawInput }, this.sessionId);
|
|
4122
|
+
} catch {
|
|
4123
|
+
gate = { title: "Action approval: the always-ask check failed; approve only if you know what runs" };
|
|
4124
|
+
}
|
|
4125
|
+
if (once && identity && !gate) {
|
|
4026
4126
|
this.opts.log?.(`permission auto-approved for ${this.id}: ${identity}`);
|
|
4027
4127
|
this.emitActivity("ask", `${String(msg.id)}: ${activityText(announced ?? call.title)} (automatic)`);
|
|
4028
4128
|
this.emitActivity("answered", `${String(msg.id)}: allow_once (automatic)`);
|
|
4029
4129
|
return this.send({ jsonrpc: "2.0", id: msg.id, result: { outcome: { outcome: "selected", optionId: once.optionId } } });
|
|
4030
4130
|
}
|
|
4031
|
-
const raw =
|
|
4131
|
+
const raw = rawInput;
|
|
4032
4132
|
const full = raw === undefined ? "" : typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
4033
4133
|
const cut = full.length > 600;
|
|
4034
4134
|
const input = raw === undefined ? "" : `: ${cut ? `${full.slice(0, 600)} [cut, ${full.length} chars]` : full}`;
|
|
4035
4135
|
const named = typeof call.title === "string" && jsonObject(call.title) === undefined ? call.title : announced;
|
|
4036
|
-
const title = raw === undefined ? `${named ?? "tool call"} (payload not reported by the agent)` : `${named ?? "tool call"}${input}`;
|
|
4136
|
+
const title = gate ? gate.title : raw === undefined ? `${named ?? "tool call"} (payload not reported by the agent)` : `${named ?? "tool call"}${input}`;
|
|
4037
4137
|
this.emitActivity("ask", `${String(msg.id)}: ${activityText(title)}; answer in ahub console`);
|
|
4038
|
-
|
|
4138
|
+
let options = (msg.params?.options ?? []).filter((o) => o.kind !== "allow_always" || !gate && !this.nativePolicy?.lockedReadOnly && raw !== undefined && !cut);
|
|
4139
|
+
const budgetOption = gate?.budget && once ? { optionId: ACTION_BUDGET_OPTION, name: `Allow ${gate.budget.label} for this session`, kind: "allow_once" } : undefined;
|
|
4140
|
+
if (budgetOption)
|
|
4141
|
+
options = [...options, budgetOption];
|
|
4039
4142
|
const tool = typeof named === "string" && /^[\w.:@-]{1,80}$/.test(named) ? named : undefined;
|
|
4040
4143
|
const turn = this.turn;
|
|
4041
4144
|
const alive = setInterval(() => this.state === "busy" && turn === this.turn && this.touch(), Math.max(10, Math.min(30000, Math.floor(this.watchdogMs / 3))));
|
|
@@ -4044,20 +4147,31 @@ ${renderDigest(envs, false)}` }] });
|
|
|
4044
4147
|
let picked;
|
|
4045
4148
|
try {
|
|
4046
4149
|
const cancelled = new Promise((resolve) => controller.signal.addEventListener("abort", () => resolve(undefined), { once: true }));
|
|
4047
|
-
picked = await Promise.race([this.opts.onPermission?.({ peer: this.id, title, options, ...tool ? { tool } : {}, ...kind ? { kind } : {} }, controller.signal).catch(() => {
|
|
4150
|
+
picked = await Promise.race([this.opts.onPermission?.({ peer: this.id, title, options, ...tool ? { tool } : {}, ...kind ? { kind } : {}, ...gate ? { classed: true } : {} }, controller.signal).catch(() => {
|
|
4048
4151
|
return;
|
|
4049
4152
|
}), cancelled]);
|
|
4050
4153
|
} finally {
|
|
4051
4154
|
clearInterval(alive);
|
|
4052
4155
|
this.permissionWaits.delete(controller);
|
|
4053
4156
|
}
|
|
4054
|
-
|
|
4157
|
+
let valid = !controller.signal.aborted && this.state !== "offline" && turn === this.turn && !this.stopping && options.some((o) => o.optionId === picked);
|
|
4158
|
+
let answer = picked;
|
|
4159
|
+
if (valid && budgetOption && picked === budgetOption.optionId) {
|
|
4160
|
+
let granted = false;
|
|
4161
|
+
try {
|
|
4162
|
+
granted = gate.budget.approve();
|
|
4163
|
+
} catch {}
|
|
4164
|
+
if (granted && once)
|
|
4165
|
+
answer = once.optionId;
|
|
4166
|
+
else
|
|
4167
|
+
valid = false;
|
|
4168
|
+
}
|
|
4055
4169
|
if (turn === this.turn)
|
|
4056
4170
|
this.emitActivity("answered", `${String(msg.id)}: ${valid ? options.find((o) => o.optionId === picked)?.kind : "cancelled"}`);
|
|
4057
4171
|
this.send({
|
|
4058
4172
|
jsonrpc: "2.0",
|
|
4059
4173
|
id: msg.id,
|
|
4060
|
-
result: { outcome: valid ? { outcome: "selected", optionId:
|
|
4174
|
+
result: { outcome: valid ? { outcome: "selected", optionId: answer } : { outcome: "cancelled" } }
|
|
4061
4175
|
});
|
|
4062
4176
|
}
|
|
4063
4177
|
request(method, params, timeoutMs) {
|
|
@@ -4494,13 +4608,26 @@ var init_process_signature = __esm(() => {
|
|
|
4494
4608
|
});
|
|
4495
4609
|
|
|
4496
4610
|
// src/hub/recovery-store.ts
|
|
4611
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
4612
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4613
|
+
function atomicPrivateJSON(file, value) {
|
|
4614
|
+
const temporary = `${file}.${randomUUID3()}.tmp`;
|
|
4615
|
+
try {
|
|
4616
|
+
writeFileSync5(temporary, `${JSON.stringify(value, null, 2)}
|
|
4617
|
+
`, { mode: 384, flag: "wx" });
|
|
4618
|
+
chmodSync(temporary, 384);
|
|
4619
|
+
renameSync3(temporary, file);
|
|
4620
|
+
} finally {
|
|
4621
|
+
rmSync4(temporary, { force: true });
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4497
4624
|
var init_recovery_store = __esm(() => {
|
|
4498
4625
|
init_project();
|
|
4499
4626
|
init_process_signature();
|
|
4500
4627
|
});
|
|
4501
4628
|
|
|
4502
4629
|
// src/pi/session.ts
|
|
4503
|
-
import { readFileSync as
|
|
4630
|
+
import { readFileSync as readFileSync6, openSync as openSync4, closeSync as closeSync4, fsyncSync, mkdirSync as mkdirSync6 } from "fs";
|
|
4504
4631
|
import { join as join9, relative as relative2, resolve as resolve7, dirname as dirname6 } from "path";
|
|
4505
4632
|
function sessionReadFailure(error) {
|
|
4506
4633
|
if (error.code === "ENOENT")
|
|
@@ -4520,7 +4647,7 @@ function validatePiSession(stateDir, cwd, sessionFile, sessionId) {
|
|
|
4520
4647
|
throw new PiSessionUnresumable("outside_managed", "Pi session file is outside the managed project session directory");
|
|
4521
4648
|
let first;
|
|
4522
4649
|
try {
|
|
4523
|
-
first =
|
|
4650
|
+
first = readFileSync6(file, "utf8").split(`
|
|
4524
4651
|
`, 1)[0];
|
|
4525
4652
|
} catch (error) {
|
|
4526
4653
|
return sessionReadFailure(error);
|
|
@@ -4828,7 +4955,7 @@ var init_deny = __esm(() => {
|
|
|
4828
4955
|
|
|
4829
4956
|
// src/local/sandbox.ts
|
|
4830
4957
|
import { spawn as spawn8, spawnSync as spawnSync4 } from "child_process";
|
|
4831
|
-
import { existsSync as
|
|
4958
|
+
import { existsSync as existsSync6, mkdtempSync as mkdtempSync4, readdirSync, rmSync as rmSync5 } from "fs";
|
|
4832
4959
|
import { homedir as homedir4, tmpdir as tmpdir4 } from "os";
|
|
4833
4960
|
import { dirname as dirname9, join as join11, resolve as resolve9 } from "path";
|
|
4834
4961
|
function developerDir() {
|
|
@@ -4955,11 +5082,11 @@ function sandboxedExec(argv, opts) {
|
|
|
4955
5082
|
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
4956
5083
|
const dropOwn = () => {
|
|
4957
5084
|
try {
|
|
4958
|
-
|
|
5085
|
+
rmSync5(own, { recursive: true, force: true });
|
|
4959
5086
|
} catch {
|
|
4960
5087
|
try {
|
|
4961
5088
|
spawnSync4("chflags", ["-R", "nouchg", own]);
|
|
4962
|
-
|
|
5089
|
+
rmSync5(own, { recursive: true, force: true });
|
|
4963
5090
|
} catch {}
|
|
4964
5091
|
}
|
|
4965
5092
|
};
|
|
@@ -4988,7 +5115,7 @@ function proxyEnv(network) {
|
|
|
4988
5115
|
const url = `http://127.0.0.1:${network.proxyPort}`;
|
|
4989
5116
|
return { HTTPS_PROXY: url, https_proxy: url, HTTP_PROXY: url, http_proxy: url, ALL_PROXY: url, all_proxy: url, NO_PROXY: "", no_proxy: "" };
|
|
4990
5117
|
}
|
|
4991
|
-
var SANDBOX_EXEC = "/usr/bin/sandbox-exec", OUTPUT_CAP = 20000, sandboxAvailable = () => process.platform === "darwin" &&
|
|
5118
|
+
var SANDBOX_EXEC = "/usr/bin/sandbox-exec", OUTPUT_CAP = 20000, sandboxAvailable = () => process.platform === "darwin" && existsSync6(SANDBOX_EXEC), q, HOME_READABLE, CA_BUNDLES, SYSTEM_READABLE, MACH_SERVICES, NETWORK_MACH_SERVICES, PROXY_MACH_SERVICES;
|
|
4992
5119
|
var init_sandbox = __esm(() => {
|
|
4993
5120
|
init_project();
|
|
4994
5121
|
init_git();
|
|
@@ -5003,7 +5130,7 @@ var init_sandbox = __esm(() => {
|
|
|
5003
5130
|
});
|
|
5004
5131
|
|
|
5005
5132
|
// src/local/tools.ts
|
|
5006
|
-
import { lstatSync as lstatSync8, mkdirSync as
|
|
5133
|
+
import { lstatSync as lstatSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
5007
5134
|
import { dirname as dirname10, join as join12, relative as relative4, resolve as resolve10 } from "path";
|
|
5008
5135
|
function guardPath(ctx, path, mode) {
|
|
5009
5136
|
const root = realPath(ctx.cwd);
|
|
@@ -5075,7 +5202,7 @@ async function runTool(name, rawArgs, ctx) {
|
|
|
5075
5202
|
const file = guardPath(ctx, String(a.path), "read");
|
|
5076
5203
|
if (statSync3(file).size > 5000000)
|
|
5077
5204
|
return "error: file is larger than 5 MB; use bash (head, grep, sed -n) to look at parts of it";
|
|
5078
|
-
const lines =
|
|
5205
|
+
const lines = readFileSync7(file, "utf8").split(`
|
|
5079
5206
|
`);
|
|
5080
5207
|
const from = Math.max(1, Number(a.offset) || 1);
|
|
5081
5208
|
const out = lines.slice(from - 1, from - 1 + (Number(a.limit) || 2000)).map((l, i) => `${from + i} ${l}`).join(`
|
|
@@ -5096,14 +5223,14 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path)), "er
|
|
|
5096
5223
|
file = guardPath(ctx, String(a.path), "write");
|
|
5097
5224
|
if (file !== approvedTarget)
|
|
5098
5225
|
return "error: path target changed during approval; inspect before retrying";
|
|
5099
|
-
|
|
5100
|
-
|
|
5226
|
+
mkdirSync7(dirname10(file), { recursive: true });
|
|
5227
|
+
writeFileSync6(file, content);
|
|
5101
5228
|
return `wrote ${a.path}`;
|
|
5102
5229
|
}
|
|
5103
5230
|
case "edit": {
|
|
5104
5231
|
let file = guardPath(ctx, String(a.path), "write");
|
|
5105
5232
|
const approvedTarget = file;
|
|
5106
|
-
let text =
|
|
5233
|
+
let text = readFileSync7(file, "utf8");
|
|
5107
5234
|
let count = text.split(String(a.old)).length - 1;
|
|
5108
5235
|
if (!a.old || count !== 1)
|
|
5109
5236
|
return `error: \`old\` must match exactly once, it matched ${count} times`;
|
|
@@ -5117,11 +5244,11 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path)), "er
|
|
|
5117
5244
|
file = guardPath(ctx, String(a.path), "write");
|
|
5118
5245
|
if (file !== approvedTarget)
|
|
5119
5246
|
return "error: path target changed during approval; inspect before retrying";
|
|
5120
|
-
text =
|
|
5247
|
+
text = readFileSync7(file, "utf8");
|
|
5121
5248
|
count = text.split(String(a.old)).length - 1;
|
|
5122
5249
|
if (count !== 1)
|
|
5123
5250
|
return `error: file changed during approval; \`old\` must match exactly once, it matched ${count} times`;
|
|
5124
|
-
|
|
5251
|
+
writeFileSync6(file, text.replace(String(a.old), () => String(a.new ?? "")));
|
|
5125
5252
|
return `edited ${a.path}`;
|
|
5126
5253
|
}
|
|
5127
5254
|
case "bash": {
|
|
@@ -5224,8 +5351,8 @@ function piToolStepCeiling(value) {
|
|
|
5224
5351
|
var PI_CEILING_KIND = "tool-step-ceiling", PI_CEILING_UNIT = "tool-step";
|
|
5225
5352
|
|
|
5226
5353
|
// src/adapters/pi.ts
|
|
5227
|
-
import { randomUUID as
|
|
5228
|
-
import { existsSync as
|
|
5354
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
5355
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync8 } from "fs";
|
|
5229
5356
|
import { join as join13, resolve as resolve11 } from "path";
|
|
5230
5357
|
import { spawn as spawn9 } from "child_process";
|
|
5231
5358
|
function ownerStillAlive(pid, signature) {
|
|
@@ -5280,7 +5407,7 @@ var init_pi = __esm(() => {
|
|
|
5280
5407
|
idleBashReservations = new Map;
|
|
5281
5408
|
budgetGeneration = 0;
|
|
5282
5409
|
modelStep = 0;
|
|
5283
|
-
observationScope =
|
|
5410
|
+
observationScope = randomUUID4();
|
|
5284
5411
|
emptyResumeVerified = false;
|
|
5285
5412
|
verifiedEmptyResume;
|
|
5286
5413
|
activityObserved = false;
|
|
@@ -5298,7 +5425,7 @@ var init_pi = __esm(() => {
|
|
|
5298
5425
|
approvalAbortSent = false;
|
|
5299
5426
|
approvalStopReason = "";
|
|
5300
5427
|
requestedStopReason;
|
|
5301
|
-
owner =
|
|
5428
|
+
owner = randomUUID4();
|
|
5302
5429
|
_tuiLaunch;
|
|
5303
5430
|
requestedModel = "";
|
|
5304
5431
|
starting;
|
|
@@ -5344,7 +5471,7 @@ var init_pi = __esm(() => {
|
|
|
5344
5471
|
get recoveryReady() {
|
|
5345
5472
|
if (this.state === "busy" || this.starting || this.activeTools)
|
|
5346
5473
|
return false;
|
|
5347
|
-
if (this.sessionFile && !
|
|
5474
|
+
if (this.sessionFile && !existsSync7(this.sessionFile) && (this.activityObserved || this.persistedOnce))
|
|
5348
5475
|
return false;
|
|
5349
5476
|
if (this.opts.mode === "headless")
|
|
5350
5477
|
return this.state === "idle" || !this.proc || this.proc.exitCode !== null || this.proc.signalCode !== null;
|
|
@@ -5364,7 +5491,7 @@ var init_pi = __esm(() => {
|
|
|
5364
5491
|
}
|
|
5365
5492
|
checkpointUsesSessionGeneration = false;
|
|
5366
5493
|
recoveryMetadata() {
|
|
5367
|
-
const exists = !!this.sessionFile &&
|
|
5494
|
+
const exists = !!this.sessionFile && existsSync7(this.sessionFile);
|
|
5368
5495
|
if (exists) {
|
|
5369
5496
|
this.persistedOnce = true;
|
|
5370
5497
|
this.verifiedEmptyResume = undefined;
|
|
@@ -5377,14 +5504,14 @@ var init_pi = __esm(() => {
|
|
|
5377
5504
|
if (this.state === "offline") {
|
|
5378
5505
|
if (this.canReuseVerifiedEmptyResume())
|
|
5379
5506
|
return this.recoveryMetadata();
|
|
5380
|
-
if (this.recoveryReady && this.sessionFile &&
|
|
5507
|
+
if (this.recoveryReady && this.sessionFile && existsSync7(this.sessionFile))
|
|
5381
5508
|
return this.recoveryMetadata();
|
|
5382
5509
|
throw new Error("Pi source session is offline and has no verified persisted resume state; inspect preserved history, then a person may run ahub pi --replace-session --session-file <managed-file>");
|
|
5383
5510
|
}
|
|
5384
5511
|
this.emptyResumeVerified = false;
|
|
5385
5512
|
if (this.state !== "idle" || this.stopping || this.activeTools)
|
|
5386
5513
|
throw new Error("Pi must settle before session capture");
|
|
5387
|
-
if (this.sessionFile &&
|
|
5514
|
+
if (this.sessionFile && existsSync7(this.sessionFile))
|
|
5388
5515
|
return this.recoveryMetadata();
|
|
5389
5516
|
const snapshot = this.opts.mode === "headless" ? (await this.waitRpc("get_state", 15000)).data : await this.sendTui({ type: "get_session_state" });
|
|
5390
5517
|
if (this.state !== "idle" || this.activeTools || snapshot?.sessionId !== this.sessionId || snapshot?.sessionFile !== this.sessionFile)
|
|
@@ -5399,7 +5526,7 @@ var init_pi = __esm(() => {
|
|
|
5399
5526
|
return captured;
|
|
5400
5527
|
}
|
|
5401
5528
|
canReuseVerifiedEmptyResume() {
|
|
5402
|
-
return !!this.verifiedEmptyResume && this.recoveryReady && !this.activityObserved && !this.persistedOnce && this.sessionId === this.verifiedEmptyResume.sessionId && (!this.sessionFile || !
|
|
5529
|
+
return !!this.verifiedEmptyResume && this.recoveryReady && !this.activityObserved && !this.persistedOnce && this.sessionId === this.verifiedEmptyResume.sessionId && (!this.sessionFile || !existsSync7(this.sessionFile));
|
|
5403
5530
|
}
|
|
5404
5531
|
noteActivity() {
|
|
5405
5532
|
this.activityObserved = true;
|
|
@@ -5555,7 +5682,7 @@ var init_pi = __esm(() => {
|
|
|
5555
5682
|
for (const [key, value] of this.idleBashReservations)
|
|
5556
5683
|
if (value.expiresAt < Date.now())
|
|
5557
5684
|
this.idleBashReservations.delete(key);
|
|
5558
|
-
reservation =
|
|
5685
|
+
reservation = randomUUID4();
|
|
5559
5686
|
this.idleBashReservations.set(reservation, { generation: body.generation, expiresAt: Date.now() + 30000, ...remaining === undefined ? {} : { deadlineAt: Date.now() + remaining } });
|
|
5560
5687
|
}
|
|
5561
5688
|
clearTimeout(this.executionBudgetTimer);
|
|
@@ -5592,9 +5719,9 @@ var init_pi = __esm(() => {
|
|
|
5592
5719
|
this.approvalAnswerEpoch = undefined;
|
|
5593
5720
|
this.approvalStopReason = "";
|
|
5594
5721
|
this.approvalAbortSent = false;
|
|
5595
|
-
|
|
5722
|
+
mkdirSync8(this.opts.stateDir, { recursive: true });
|
|
5596
5723
|
const sessions = join13(this.opts.stateDir, "pi-sessions");
|
|
5597
|
-
|
|
5724
|
+
mkdirSync8(sessions, { recursive: true, mode: 448 });
|
|
5598
5725
|
if (this.opts.sessionFile) {
|
|
5599
5726
|
try {
|
|
5600
5727
|
validatePiSession(this.opts.stateDir, this.opts.cwd, this.opts.sessionFile, this.opts.sessionId);
|
|
@@ -5604,7 +5731,7 @@ var init_pi = __esm(() => {
|
|
|
5604
5731
|
throw error;
|
|
5605
5732
|
}
|
|
5606
5733
|
}
|
|
5607
|
-
const token =
|
|
5734
|
+
const token = randomUUID4();
|
|
5608
5735
|
this.tuiExit = new Promise((resolvePromise) => {
|
|
5609
5736
|
this.resolveTuiExit = resolvePromise;
|
|
5610
5737
|
});
|
|
@@ -5693,7 +5820,7 @@ var init_pi = __esm(() => {
|
|
|
5693
5820
|
this.ownerToken = String(this._tuiLaunch?.env.AGENTHUB_PI_OWNER_TOKEN ?? "");
|
|
5694
5821
|
this.ownerClaimed = true;
|
|
5695
5822
|
}
|
|
5696
|
-
this.persistedOnce =
|
|
5823
|
+
this.persistedOnce = existsSync7(this.sessionFile);
|
|
5697
5824
|
if (this.opts.sessionId && this.sessionId !== this.opts.sessionId || this.opts.sessionFile && this.sessionFile !== this.opts.sessionFile)
|
|
5698
5825
|
throw new Error("Pi startup session identity mismatch");
|
|
5699
5826
|
this.started = true;
|
|
@@ -7473,7 +7600,7 @@ var init_signals = __esm(() => {
|
|
|
7473
7600
|
});
|
|
7474
7601
|
|
|
7475
7602
|
// src/models/route/labels.ts
|
|
7476
|
-
import { createHash as createHash4, randomUUID as
|
|
7603
|
+
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
7477
7604
|
|
|
7478
7605
|
class RouteLabelTracker {
|
|
7479
7606
|
sink;
|
|
@@ -7497,7 +7624,7 @@ class RouteLabelTracker {
|
|
|
7497
7624
|
addDecision(turn, dimensions, priorMessages = []) {
|
|
7498
7625
|
if (!turn || turn.closed || this.turns.get(turn.context.turn) !== turn)
|
|
7499
7626
|
return;
|
|
7500
|
-
const id =
|
|
7627
|
+
const id = randomUUID5();
|
|
7501
7628
|
turn.decisions.set(id, { id, dimensions: { ...dimensions }, baselineFingerprints: batchFingerprints(priorMessages) });
|
|
7502
7629
|
this.decisionOwners.set(id, turn);
|
|
7503
7630
|
return id;
|
|
@@ -7968,6 +8095,19 @@ var init_review_findings = __esm(() => {
|
|
|
7968
8095
|
MAX_BYTES2 = 64 * 1024;
|
|
7969
8096
|
});
|
|
7970
8097
|
|
|
8098
|
+
// src/hub/sdlc-metrics.ts
|
|
8099
|
+
var HOUR = 3600000, SD_FLOOR;
|
|
8100
|
+
var init_sdlc_metrics = __esm(() => {
|
|
8101
|
+
SD_FLOOR = {
|
|
8102
|
+
acceptance_to_plan_ms: HOUR / 2,
|
|
8103
|
+
acceptance_to_first_review_ms: HOUR / 2,
|
|
8104
|
+
first_pass_approval: 0.5,
|
|
8105
|
+
rework_rounds: 0.5,
|
|
8106
|
+
first_pass_check: 0.5,
|
|
8107
|
+
stale_head_re_reviews: 0.5
|
|
8108
|
+
};
|
|
8109
|
+
});
|
|
8110
|
+
|
|
7971
8111
|
// src/hub/workflow.ts
|
|
7972
8112
|
var init_workflow = __esm(() => {
|
|
7973
8113
|
init_sdlc();
|
|
@@ -7984,6 +8124,7 @@ var init_sdlc = __esm(() => {
|
|
|
7984
8124
|
init_test_integrity();
|
|
7985
8125
|
init_plan_conformance();
|
|
7986
8126
|
init_review_findings();
|
|
8127
|
+
init_sdlc_metrics();
|
|
7987
8128
|
init_workflow();
|
|
7988
8129
|
init_child_process();
|
|
7989
8130
|
init_host_executable();
|
|
@@ -8175,6 +8316,19 @@ var init_task_code_symbols = __esm(() => {
|
|
|
8175
8316
|
|
|
8176
8317
|
// src/hub/planning.ts
|
|
8177
8318
|
var init_planning = () => {};
|
|
8319
|
+
// src/hub/deliverables.ts
|
|
8320
|
+
var DELIVERABLE_LIMITS;
|
|
8321
|
+
var init_deliverables = __esm(() => {
|
|
8322
|
+
DELIVERABLE_LIMITS = Object.freeze({
|
|
8323
|
+
count: 16,
|
|
8324
|
+
fileBytes: 256 * 1024 * 1024,
|
|
8325
|
+
totalBytes: 512 * 1024 * 1024,
|
|
8326
|
+
pathChars: 512,
|
|
8327
|
+
formatChars: 32,
|
|
8328
|
+
versionChars: 64
|
|
8329
|
+
});
|
|
8330
|
+
});
|
|
8331
|
+
|
|
8178
8332
|
// src/hub/board.ts
|
|
8179
8333
|
var CLASSES, PLAN_KEYS, ALL_PLAN_KEYS, OUTCOMES_KEPT_MS;
|
|
8180
8334
|
var init_board = __esm(() => {
|
|
@@ -8183,6 +8337,7 @@ var init_board = __esm(() => {
|
|
|
8183
8337
|
init_planning();
|
|
8184
8338
|
init_envelope();
|
|
8185
8339
|
init_review_findings();
|
|
8340
|
+
init_deliverables();
|
|
8186
8341
|
CLASSES = ["plan", "implement", "bulk_edit", "test", "review", "summarize", "triage"];
|
|
8187
8342
|
PLAN_KEYS = ["paths", "symbols", "signatures", "insertion_points"];
|
|
8188
8343
|
ALL_PLAN_KEYS = [...PLAN_KEYS, "sections", "sources", "claims", "proof", "risks"];
|
|
@@ -8877,7 +9032,7 @@ function profileTaskTools(profile = "dev", stages, criteria = false) {
|
|
|
8877
9032
|
return { ...tool, description, inputSchema: { ...tool.inputSchema, properties } };
|
|
8878
9033
|
});
|
|
8879
9034
|
}
|
|
8880
|
-
var ACTIONABLE_MESSAGE_INSTRUCTION = "Do not use [FYI] for PR ready, done, submitted, ruling needed, blocker, or changed contract or path messages: each requires action from the recipient. Use [FYI] only for progress or information that needs no action. hub_task_done and hub_review already notify the assigned recipients; no duplicate hub_send chat is needed.", str2, id, refs, list = (description) => ({ type: "array", items: str2, description }), reviewFindings, costPlanProperties, evidencePlanProperties, plan, acceptPlan, tool = (name, description, properties, required = []) => ({
|
|
9035
|
+
var IMPLEMENTER_HUB_TOOL_NAMES, IMPLEMENTER_PERMISSION_TOOL, ACTIONABLE_MESSAGE_INSTRUCTION = "Do not use [FYI] for PR ready, done, submitted, ruling needed, blocker, or changed contract or path messages: each requires action from the recipient. Use [FYI] only for progress or information that needs no action. hub_task_done and hub_review already notify the assigned recipients; no duplicate hub_send chat is needed.", str2, id, refs, list = (description) => ({ type: "array", items: str2, description }), deliverablesSchema, reviewFindings, costPlanProperties, evidencePlanProperties, plan, acceptPlan, tool = (name, description, properties, required = []) => ({
|
|
8881
9036
|
name,
|
|
8882
9037
|
description,
|
|
8883
9038
|
inputSchema: { type: "object", properties, required, additionalProperties: false }
|
|
@@ -8889,6 +9044,12 @@ var init_hub_tools = __esm(() => {
|
|
|
8889
9044
|
init_roles();
|
|
8890
9045
|
init_registry();
|
|
8891
9046
|
init_envelope();
|
|
9047
|
+
IMPLEMENTER_HUB_TOOL_NAMES = Object.freeze(["hub_task_accept", "hub_task_decline", "hub_task_done", "hub_checkpoint", "hub_send", "hub_task_show", "hub_task_propose", "hub_remember", "hub_inbox", "hub_permission_request"]);
|
|
9048
|
+
IMPLEMENTER_PERMISSION_TOOL = {
|
|
9049
|
+
name: "hub_permission_request",
|
|
9050
|
+
description: "Native permission prompt for this current owned child. The person allows once or denies; failure is deny.",
|
|
9051
|
+
inputSchema: { type: "object", properties: { tool_name: { type: "string", maxLength: 128 }, input: { type: "object" }, tool_use_id: { type: "string", maxLength: 256 } }, required: ["tool_name", "input", "tool_use_id"], additionalProperties: false }
|
|
9052
|
+
};
|
|
8892
9053
|
str2 = { type: "string" };
|
|
8893
9054
|
id = { type: "integer", description: "task id" };
|
|
8894
9055
|
refs = {
|
|
@@ -8897,6 +9058,13 @@ var init_hub_tools = __esm(() => {
|
|
|
8897
9058
|
properties: { repo: { type: "string", description: "owner/name repository slug or checkout path; set for work in another repository" }, branch: str2, commit: str2, paths: { type: "array", items: str2 } },
|
|
8898
9059
|
additionalProperties: false
|
|
8899
9060
|
};
|
|
9061
|
+
deliverablesSchema = {
|
|
9062
|
+
type: "array",
|
|
9063
|
+
minItems: 1,
|
|
9064
|
+
maxItems: 16,
|
|
9065
|
+
description: 'files you delivered, as paths inside the project: the hub reads and hashes each file itself when you call this, a claimed hash is refused, and approval binds those hashes so a later edit reads as "deliverable changed since review". Works in a plain folder without git or a PR.',
|
|
9066
|
+
items: { type: "object", properties: { path: { type: "string", description: "project-relative path, forward slashes" }, format: { type: "string", description: "lowercase label such as md, pdf, docx, csv" }, version: { type: "string", description: "optional label such as v2" } }, required: ["path", "format"], additionalProperties: false }
|
|
9067
|
+
};
|
|
8900
9068
|
reviewFindings = {
|
|
8901
9069
|
type: "array",
|
|
8902
9070
|
maxItems: 64,
|
|
@@ -8952,10 +9120,10 @@ var init_hub_tools = __esm(() => {
|
|
|
8952
9120
|
tool("hub_task_accept", "Take a task that was assigned to you, with your plan for it. Set refs.repo for work in another repository; acceptance may correct only that repository metadata.", { id, plan: acceptPlan, refs: { ...refs, properties: { repo: refs.properties.repo } } }, ["id"]),
|
|
8953
9121
|
tool("hub_task_cancel", "Cancel unaccepted proposed/backlog work you proposed as its configured planner; accepted work is person-only. Keeps history and does not stop a native turn or delete files.", { id, reason: str2, superseded_by: str2, revision: { type: "integer", minimum: 0 } }, ["id", "reason"]),
|
|
8954
9122
|
tool("hub_task_decline", "Pass on a task assigned to you; the hub offers it to the next peer. A plain decline is a lasting refusal: you are never offered this task again. Pass capacity: true when you only have no room right now: you are skipped for this move only, and a later reroute or planner pull may offer it to you again.", { id, reason: str2, capacity: { type: "boolean" } }, ["id"]),
|
|
8955
|
-
tool("hub_task_done", "Mark your task finished. It goes to its reviewer with your summary and refs; no duplicate hub_send chat is needed. When the project configures a check for its class, the hub runs it first and the result comes as a task message: a failed check keeps the task with you.", { id, summary: { type: "string", description: "what changed, why, and the check you ran with its result" }, refs }, ["id", "summary"]),
|
|
8956
|
-
tool("hub_sdlc_record", "Record artifact ids or claimed local checks on your task. This grants no authority or external-check evidence. Only its owner/reviewer or the person may record; only its reviewer/person may name a reviewed head or phase checklist. Phase evidence refs are content hashes, not external verification.", { id, receipt: { type: "object", properties: { stage: { type: "string", enum: ["spec", "plan", "implement", "review", "verify", "merge", "release"] }, repo: str2, issue: id, pr: id, localHead: str2, localResult: { type: "string", enum: ["passed", "failed", "unknown"] }, reviewedHead: str2, phase: { type: "object", properties: { id: str2, head: str2, tasks: { type: "array", maxItems: 64, items: { type: "object", properties: { id, after: { type: "array", maxItems: 16, items: id } }, required: ["id", "after"], additionalProperties: false } }, criteria: { type: "array", maxItems: 32, items: { type: "object", properties: { id: str2, required: { type: "boolean" }, result: { type: "string", enum: ["verified", "failed", "unverified"] }, refs: { type: "array", maxItems: 8, items: str2 } }, required: ["id", "required", "result", "refs"], additionalProperties: false } } }, required: ["id", "tasks", "criteria"], additionalProperties: false } }, additionalProperties: false } }, ["id", "receipt"]),
|
|
9123
|
+
tool("hub_task_done", "Mark your task finished. It goes to its reviewer with your summary and refs; no duplicate hub_send chat is needed. When the project configures a check for its class, the hub runs it first and the result comes as a task message: a failed check keeps the task with you.", { id, summary: { type: "string", description: "what changed, why, and the check you ran with its result" }, refs, deliverables: deliverablesSchema }, ["id", "summary"]),
|
|
9124
|
+
tool("hub_sdlc_record", "Record artifact ids or claimed local checks on your task. This grants no authority or external-check evidence. Only its owner/reviewer or the person may record; only its reviewer/person may name a reviewed head or phase checklist. Phase evidence refs are content hashes, not external verification.", { id, receipt: { type: "object", properties: { deliverables: deliverablesSchema, stage: { type: "string", enum: ["spec", "plan", "implement", "review", "verify", "merge", "release"] }, repo: str2, issue: id, pr: id, localHead: str2, localResult: { type: "string", enum: ["passed", "failed", "unknown"] }, reviewedHead: str2, phase: { type: "object", properties: { id: str2, head: str2, tasks: { type: "array", maxItems: 64, items: { type: "object", properties: { id, after: { type: "array", maxItems: 16, items: id } }, required: ["id", "after"], additionalProperties: false } }, criteria: { type: "array", maxItems: 32, items: { type: "object", properties: { id: str2, required: { type: "boolean" }, result: { type: "string", enum: ["verified", "failed", "unverified"] }, refs: { type: "array", maxItems: 8, items: str2 } }, required: ["id", "required", "result", "refs"], additionalProperties: false } } }, required: ["id", "tasks", "criteria"], additionalProperties: false } }, additionalProperties: false } }, ["id", "receipt"]),
|
|
8957
9125
|
tool("hub_task_list", "The task board. PII tasks show as [pii].", { state: { type: "string", enum: ["proposed", "in_progress", "in_review", "approved", "changes_requested", "cancelled"] }, ready: { type: "boolean", description: "only proposed tasks with nothing left to wait for" } }),
|
|
8958
|
-
tool("hub_review", "Give your verdict on a task you were asked to review: map the changed signatures and call sites to the task's plan or detail, read the check result (hub_task_show has it, with the done summary), and list what is unmet. The verdict notifies the task owner; no duplicate hub_send chat is needed. Consecutive changes_requested verdicts (three by default, `tasks.escalate_after`) move the task to another peer.", { id, head: { type: "string", description: "full SHA actually reviewed; enforce binds approval to the current PR head" }, findings: reviewFindings, verdict: { type: "string", enum: ["approved", "changes_requested"] }, note: str2, unmet: { type: "array", items: str2, description:
|
|
9126
|
+
tool("hub_review", "Give your verdict on a task you were asked to review: map the changed signatures and call sites to the task's plan or detail, read the check result (hub_task_show has it, with the done summary), and list what is unmet. The verdict notifies the task owner; no duplicate hub_send chat is needed. Consecutive changes_requested verdicts (three by default, `tasks.escalate_after`) move the task to another peer.", { id, head: { type: "string", description: "full SHA actually reviewed; enforce binds approval to the current PR head" }, findings: reviewFindings, verdict: { type: "string", enum: ["approved", "changes_requested"] }, note: str2, unmet: { type: "array", items: str2, description: 'each requirement of the plan or detail that the change does not meet; when the review request lists acceptance criteria, start an item with its id ("AC1: what is missing") so the verdict names it' }, coverage: { type: "object", description: "the review brief's reviewable paths you read (reviewed) and any you did not (skipped, each with a reason); in enforce an approval must account for every one", properties: { reviewed: { type: "array", items: str2 }, skipped: { type: "array", items: { type: "object", properties: { path: str2, reason: str2 }, required: ["path", "reason"] } } } } }, ["id", "verdict"]),
|
|
8959
9127
|
tool("hub_checkpoint", "Answer a checkpoint request from the hub (quota or context window pressure): what you were doing, what is half done, what whoever continues must know. Write the same to .agenthub/checkpoint.md first if you can.", { summary: str2, request_id: { type: "string", description: "The request id supplied by a context checkpoint request; required for context checkpoints" } }, ["summary"]),
|
|
8960
9128
|
tool("hub_memory_search", "Search daemon-scoped visible memory, fails first. Query <=200 UTF-8 bytes; default 8, max 20 lines; whole output <=4 KB. Incomplete excerpts are marked. No project argument.", { query: { type: "string", maxLength: 200, description: "At most 200 UTF-8 bytes, enforced by the daemon" }, limit: { type: "integer", minimum: 1, maximum: 20 } }, ["query"]),
|
|
8961
9129
|
tool("hub_memory_show", "Read up to 8 visible memory ids or unique prefixes (6..64 UTF-8 bytes each). Original ids redirect; unavailable ids share one refusal. Whole output <=8 KB.", { ids: { type: "array", minItems: 1, maxItems: 8, items: { type: "string", minLength: 6, maxLength: 64, description: "6 to 64 UTF-8 bytes" } } }, ["ids"]),
|
|
@@ -8966,7 +9134,8 @@ var init_hub_tools = __esm(() => {
|
|
|
8966
9134
|
PLANNER_REVIEW_TOOLS = [
|
|
8967
9135
|
tool("hub_planner_groom", "Read a bounded planner grooming digest of dependencies, repository-scoped overlaps, WIP and blockers. No board change; unavailable evidence stays unknown.", {}),
|
|
8968
9136
|
tool("hub_plan_review", "Record a planner review against the task planning revision and host-read issue edit time. Configured planner or person only; never accepts caller issue timestamps.", { id, revision: { type: "integer", minimum: 0 }, reason: { ...str2, maxLength: 1000 }, ...costPlanProperties }, ["id", "revision", "reason"]),
|
|
8969
|
-
tool("hub_sdlc_costs", "Read bounded review and CI cost metrics for linked merged PRs. Planner/person only; metadata only, no board writes.
|
|
9137
|
+
tool("hub_sdlc_costs", "Read bounded review and CI cost metrics for linked merged PRs. Planner/person only; metadata only, no board writes.", { repo: str2 }),
|
|
9138
|
+
tool("hub_sdlc_metrics", "Read SDLC metrics from board history: time from acceptance to plan, first review and merge, first-pass approval and rework rounds, first-pass CI and same-head reruns, stale-head re-reviews, stalls, and the control band tiers when sdlc.toml sets [bands]. Planner/person only; read-only, no model and no GitHub issue.", { since: str2 }),
|
|
8970
9139
|
tool("hub_backlog_return_untouched", "Return accepted but untouched work to the backlog with its history and planning retained. Refuses execution/branch/commit evidence; planner or person only.", { id, revision: { type: "integer", minimum: 0 }, reason: { ...str2, maxLength: 1000 } }, ["id", "revision", "reason"])
|
|
8971
9140
|
];
|
|
8972
9141
|
TASK_TOOLS.push(...PLANNER_REVIEW_TOOLS);
|
|
@@ -9003,7 +9172,7 @@ function safeModelLabel(value) {
|
|
|
9003
9172
|
var init_tool_refusal = () => {};
|
|
9004
9173
|
|
|
9005
9174
|
// src/hub/spec-read.ts
|
|
9006
|
-
var BODY_LIMIT, RESULT_LIMIT, STDOUT_LIMIT;
|
|
9175
|
+
var BODY_LIMIT, RESULT_LIMIT, STDOUT_LIMIT, NOT_A_REPOSITORY;
|
|
9007
9176
|
var init_spec_read = __esm(() => {
|
|
9008
9177
|
init_tool_refusal();
|
|
9009
9178
|
init_host_executable();
|
|
@@ -9011,6 +9180,7 @@ var init_spec_read = __esm(() => {
|
|
|
9011
9180
|
BODY_LIMIT = 64 * 1024;
|
|
9012
9181
|
RESULT_LIMIT = 80 * 1024;
|
|
9013
9182
|
STDOUT_LIMIT = 512 * 1024;
|
|
9183
|
+
NOT_A_REPOSITORY = new Set(["pr", "pull", "issue", "issues", "task", "tasks", "bug", "fix", "phase", "step", "item", "ticket", "gh", "id", "no", "nr", "p", "s", "t", "u"]);
|
|
9014
9184
|
});
|
|
9015
9185
|
|
|
9016
9186
|
// src/models/route/config.ts
|
|
@@ -9036,7 +9206,7 @@ var init_routing = __esm(() => {
|
|
|
9036
9206
|
});
|
|
9037
9207
|
|
|
9038
9208
|
// src/adapters/local-worker.ts
|
|
9039
|
-
import { randomUUID as
|
|
9209
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
9040
9210
|
function safeParse(json) {
|
|
9041
9211
|
try {
|
|
9042
9212
|
return JSON.parse(json) ?? {};
|
|
@@ -9069,7 +9239,7 @@ var init_local_worker = __esm(() => {
|
|
|
9069
9239
|
opts;
|
|
9070
9240
|
hubNative = true;
|
|
9071
9241
|
history = [];
|
|
9072
|
-
sessionId = `agent-hub-local-${
|
|
9242
|
+
sessionId = `agent-hub-local-${randomUUID6()}`;
|
|
9073
9243
|
routes;
|
|
9074
9244
|
warnedLegacyRoutes = new Set;
|
|
9075
9245
|
routeEnvs = [];
|
|
@@ -9484,7 +9654,7 @@ var init_local_worker = __esm(() => {
|
|
|
9484
9654
|
recordUsage(res, requestedModel, task) {
|
|
9485
9655
|
try {
|
|
9486
9656
|
this.opts.onUsage?.({
|
|
9487
|
-
id:
|
|
9657
|
+
id: randomUUID6(),
|
|
9488
9658
|
at: new Date().toISOString(),
|
|
9489
9659
|
...res.usage ? { usage: res.usage } : {},
|
|
9490
9660
|
requestedModel: safeModelLabel(requestedModel) ?? "unknown",
|
|
@@ -9514,6 +9684,530 @@ var init_local_worker = __esm(() => {
|
|
|
9514
9684
|
};
|
|
9515
9685
|
});
|
|
9516
9686
|
|
|
9687
|
+
// src/adapters/definitions/native-channel-state.ts
|
|
9688
|
+
function claudeStateFile(base, peer) {
|
|
9689
|
+
if (!CLAUDE_PEER.test(peer) || !PEER2.test(peer))
|
|
9690
|
+
throw new Error(`${peer} is not a Claude peer; Claude state files belong to Claude peers only`);
|
|
9691
|
+
return peer.includes("-") ? `${base}.${peer}.json` : `${base}.json`;
|
|
9692
|
+
}
|
|
9693
|
+
var PEER2, CLAUDE_PEER;
|
|
9694
|
+
var init_native_channel_state = __esm(() => {
|
|
9695
|
+
PEER2 = /^[a-z][a-z0-9-]{0,31}$/;
|
|
9696
|
+
CLAUDE_PEER = /^claude(?:-[a-z0-9-]+)?$/;
|
|
9697
|
+
});
|
|
9698
|
+
|
|
9699
|
+
// src/hub/claude-state.ts
|
|
9700
|
+
var init_claude_state = __esm(() => {
|
|
9701
|
+
init_native_channel_state();
|
|
9702
|
+
});
|
|
9703
|
+
|
|
9704
|
+
// src/adapters/definitions/native-daemon-kimi.ts
|
|
9705
|
+
function kimiAcpCommand(command, model) {
|
|
9706
|
+
const [cmd, ...args] = command;
|
|
9707
|
+
return [cmd, ...model ? ["--model", model, ...args] : args];
|
|
9708
|
+
}
|
|
9709
|
+
|
|
9710
|
+
// src/adapters/definitions/native-cli-retained.ts
|
|
9711
|
+
function retainedLaunchWarning(status, peer, definition) {
|
|
9712
|
+
const keys = definition.cli.launchConfigKeys;
|
|
9713
|
+
const retained = status?.retainedConfig;
|
|
9714
|
+
if (!keys?.length || !Array.isArray(retained))
|
|
9715
|
+
return;
|
|
9716
|
+
const pending = retained.flatMap((row) => {
|
|
9717
|
+
if (!row || typeof row !== "object")
|
|
9718
|
+
return [];
|
|
9719
|
+
const key = row.key;
|
|
9720
|
+
return typeof key === "string" && keys.some((candidate) => key === candidate || key.startsWith(`${candidate}.`)) ? [key] : [];
|
|
9721
|
+
});
|
|
9722
|
+
const unique = [...new Set(pending)];
|
|
9723
|
+
return unique.length ? `ahub: config retained: ${unique.join(", ")}; starting ${peer} with the running value; run ahub settings reload to apply the file's value` : undefined;
|
|
9724
|
+
}
|
|
9725
|
+
|
|
9726
|
+
// src/cli/launch.ts
|
|
9727
|
+
function statusLineSettings(tee) {
|
|
9728
|
+
const command = `AGENTHUB_STATE_DIR=${sh(tee.stateDir)} AGENTHUB_STATUSLINE_CMD=${sh(tee.original?.command ?? "")} bun ${sh(tee.script)}`;
|
|
9729
|
+
return JSON.stringify({ statusLine: { type: "command", command, refreshInterval: tee.original?.refreshInterval ?? 10, ...tee.original?.padding !== undefined ? { padding: tee.original.padding } : {} } });
|
|
9730
|
+
}
|
|
9731
|
+
function sessionSettings(tee, facts) {
|
|
9732
|
+
const settings = tee ? JSON.parse(statusLineSettings(tee)) : {};
|
|
9733
|
+
if (facts) {
|
|
9734
|
+
const hooks = [{ type: "command", command: `AGENTHUB_STATE_DIR=${sh(facts.stateDir)} AGENTHUB_HOOK_PURPOSE=${sh(facts.purpose ?? "facts")} bun ${sh(facts.script)}`, timeout: 5 }];
|
|
9735
|
+
settings.hooks = { PreToolUse: [{ matcher: "*", hooks }], ...facts.purpose !== "permission" ? { PostToolUse: [{ matcher: "*", hooks }], Stop: [{ hooks }] } : {} };
|
|
9736
|
+
if (facts.observeNative)
|
|
9737
|
+
Object.assign(settings.hooks, { SessionStart: [{ matcher: "*", hooks }], UserPromptSubmit: [{ hooks }] });
|
|
9738
|
+
}
|
|
9739
|
+
return JSON.stringify(settings);
|
|
9740
|
+
}
|
|
9741
|
+
var CLAUDE_CHANNEL = "plugin:agent-hub@agent-hub", UNATTENDED_WARNING = "WARNING: --unattended disables permission prompts. Other agents' messages are untrusted input and this agent will act on its own judgment without asking you.", sh = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
9742
|
+
var init_launch2 = __esm(() => {
|
|
9743
|
+
init_claude_state();
|
|
9744
|
+
init_claude_state();
|
|
9745
|
+
init_process_signature();
|
|
9746
|
+
init_project();
|
|
9747
|
+
init_child_process();
|
|
9748
|
+
init_registry();
|
|
9749
|
+
init_qwen_native();
|
|
9750
|
+
});
|
|
9751
|
+
|
|
9752
|
+
// src/adapters/claude-task-sessions.ts
|
|
9753
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9754
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync8 } from "fs";
|
|
9755
|
+
import { dirname as dirname11 } from "path";
|
|
9756
|
+
function readState(file) {
|
|
9757
|
+
let text;
|
|
9758
|
+
try {
|
|
9759
|
+
text = readFileSync8(file, "utf8");
|
|
9760
|
+
} catch (error) {
|
|
9761
|
+
if (error.code === "ENOENT")
|
|
9762
|
+
return { version: 1, sessions: {} };
|
|
9763
|
+
throw error;
|
|
9764
|
+
}
|
|
9765
|
+
const value = JSON.parse(text);
|
|
9766
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
9767
|
+
throw new Error("invalid Claude task session state");
|
|
9768
|
+
const state = value;
|
|
9769
|
+
if (state.version !== 1 || !state.sessions || typeof state.sessions !== "object" || Array.isArray(state.sessions) || Object.keys(state).some((key) => key !== "version" && key !== "sessions"))
|
|
9770
|
+
throw new Error("invalid Claude task session state");
|
|
9771
|
+
const sessions = {};
|
|
9772
|
+
for (const [key, raw] of Object.entries(state.sessions)) {
|
|
9773
|
+
if (taskKey(Number(key)) !== key || !raw || typeof raw !== "object" || Array.isArray(raw))
|
|
9774
|
+
throw new Error("invalid Claude task session entry");
|
|
9775
|
+
const entry = raw;
|
|
9776
|
+
if (typeof entry.sessionId !== "string" || !UUID.test(entry.sessionId) || typeof entry.started !== "boolean" || Object.keys(entry).some((key) => key !== "sessionId" && key !== "started"))
|
|
9777
|
+
throw new Error("invalid Claude task session entry");
|
|
9778
|
+
sessions[key] = { sessionId: entry.sessionId, started: entry.started };
|
|
9779
|
+
}
|
|
9780
|
+
return { version: 1, sessions };
|
|
9781
|
+
}
|
|
9782
|
+
|
|
9783
|
+
class ClaudeTaskSessions {
|
|
9784
|
+
file;
|
|
9785
|
+
state;
|
|
9786
|
+
constructor(file) {
|
|
9787
|
+
this.file = file;
|
|
9788
|
+
this.state = readState(file);
|
|
9789
|
+
}
|
|
9790
|
+
write(sessions) {
|
|
9791
|
+
const next = { version: 1, sessions };
|
|
9792
|
+
mkdirSync9(dirname11(this.file), { recursive: true, mode: 448 });
|
|
9793
|
+
atomicPrivateJSON(this.file, next);
|
|
9794
|
+
this.state = next;
|
|
9795
|
+
}
|
|
9796
|
+
session(task) {
|
|
9797
|
+
if (task === undefined)
|
|
9798
|
+
return { sessionId: randomUUID7(), resume: false, persistent: false };
|
|
9799
|
+
const key = taskKey(task);
|
|
9800
|
+
let entry = this.state.sessions[key];
|
|
9801
|
+
if (!entry) {
|
|
9802
|
+
entry = { sessionId: randomUUID7(), started: false };
|
|
9803
|
+
this.write({ ...this.state.sessions, [key]: entry });
|
|
9804
|
+
}
|
|
9805
|
+
return { sessionId: entry.sessionId, resume: entry.started, persistent: true };
|
|
9806
|
+
}
|
|
9807
|
+
markStarted(task, expectedSessionId) {
|
|
9808
|
+
const key = taskKey(task), entry = this.state.sessions[key];
|
|
9809
|
+
if (!entry || entry.sessionId !== expectedSessionId)
|
|
9810
|
+
return false;
|
|
9811
|
+
if (!entry.started)
|
|
9812
|
+
this.write({ ...this.state.sessions, [key]: { ...entry, started: true } });
|
|
9813
|
+
return true;
|
|
9814
|
+
}
|
|
9815
|
+
forgetMissing(task, expectedSessionId) {
|
|
9816
|
+
const key = taskKey(task), entry = this.state.sessions[key];
|
|
9817
|
+
if (!entry || entry.sessionId !== expectedSessionId)
|
|
9818
|
+
return false;
|
|
9819
|
+
const sessions = { ...this.state.sessions };
|
|
9820
|
+
delete sessions[key];
|
|
9821
|
+
this.write(sessions);
|
|
9822
|
+
return true;
|
|
9823
|
+
}
|
|
9824
|
+
}
|
|
9825
|
+
var UUID, taskKey = (task) => {
|
|
9826
|
+
if (!Number.isSafeInteger(task) || task <= 0)
|
|
9827
|
+
throw new Error("invalid Claude task session task id");
|
|
9828
|
+
return String(task);
|
|
9829
|
+
};
|
|
9830
|
+
var init_claude_task_sessions = __esm(() => {
|
|
9831
|
+
init_recovery_store();
|
|
9832
|
+
UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
9833
|
+
});
|
|
9834
|
+
|
|
9835
|
+
// src/adapters/claude-implementer.ts
|
|
9836
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
9837
|
+
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
9838
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
9839
|
+
import { join as join16 } from "path";
|
|
9840
|
+
function claudeImplementerArgs(turn, mcp, settings, prompt) {
|
|
9841
|
+
return [
|
|
9842
|
+
"-p",
|
|
9843
|
+
"--output-format",
|
|
9844
|
+
"stream-json",
|
|
9845
|
+
"--verbose",
|
|
9846
|
+
turn.resume ? "--resume" : "--session-id",
|
|
9847
|
+
turn.sessionId,
|
|
9848
|
+
...!turn.persistent ? ["--no-session-persistence"] : [],
|
|
9849
|
+
"--strict-mcp-config",
|
|
9850
|
+
"--mcp-config",
|
|
9851
|
+
mcp,
|
|
9852
|
+
"--setting-sources",
|
|
9853
|
+
"",
|
|
9854
|
+
"--settings",
|
|
9855
|
+
settings,
|
|
9856
|
+
"--permission-mode",
|
|
9857
|
+
"default",
|
|
9858
|
+
"--permission-prompts",
|
|
9859
|
+
"host",
|
|
9860
|
+
"--permission-prompt-tool",
|
|
9861
|
+
"mcp__agent-hub__hub_permission_request",
|
|
9862
|
+
"--allowedTools",
|
|
9863
|
+
...IMPLEMENTER_HUB_TOOL_NAMES.map((name) => `mcp__agent-hub__${name}`),
|
|
9864
|
+
"--append-system-prompt",
|
|
9865
|
+
prompt
|
|
9866
|
+
];
|
|
9867
|
+
}
|
|
9868
|
+
async function proveClaudeImplementer(peer, command) {
|
|
9869
|
+
const binary = Bun.which(command[0], { PATH: process.env.PATH }) ?? command[0];
|
|
9870
|
+
const fingerprint = () => {
|
|
9871
|
+
const file = realPath(binary), stat = lstatSync10(file);
|
|
9872
|
+
return [file, stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
|
|
9873
|
+
};
|
|
9874
|
+
const binding = fingerprint(), home = mkdtempSync5(join16(tmpdir5(), "ahub-claude-proof-"));
|
|
9875
|
+
let version = "", timedOut = false;
|
|
9876
|
+
const child = new OwnedPrintChild({
|
|
9877
|
+
peer,
|
|
9878
|
+
command,
|
|
9879
|
+
args: ["--version"],
|
|
9880
|
+
cwd: home,
|
|
9881
|
+
input: "",
|
|
9882
|
+
envMode: "replace",
|
|
9883
|
+
env: { PATH: process.env.PATH, HOME: home, CLAUDE_CONFIG_DIR: join16(home, ".claude") },
|
|
9884
|
+
maxFrameBytes: 4096,
|
|
9885
|
+
maxOutputBytes: 4096,
|
|
9886
|
+
onLine: (line) => {
|
|
9887
|
+
version += line;
|
|
9888
|
+
},
|
|
9889
|
+
onLimit: () => {
|
|
9890
|
+
timedOut = true;
|
|
9891
|
+
child.stop().catch(() => {});
|
|
9892
|
+
}
|
|
9893
|
+
});
|
|
9894
|
+
const timer = setTimeout(() => {
|
|
9895
|
+
timedOut = true;
|
|
9896
|
+
child.stop().catch(() => {});
|
|
9897
|
+
}, 3000);
|
|
9898
|
+
try {
|
|
9899
|
+
const code = await child.done;
|
|
9900
|
+
if (code !== 0 || timedOut)
|
|
9901
|
+
throw new Error("Claude implementer permission proof unavailable");
|
|
9902
|
+
const observed = COMPAT["claude-implementer"].parse(version), verdict = assessVersion("claude-implementer", observed);
|
|
9903
|
+
if (!verdict.ok)
|
|
9904
|
+
throw new Error(verdict.reason);
|
|
9905
|
+
if (fingerprint() !== binding)
|
|
9906
|
+
throw new Error("Claude implementer binary changed during proof");
|
|
9907
|
+
return () => {
|
|
9908
|
+
try {
|
|
9909
|
+
return fingerprint() === binding;
|
|
9910
|
+
} catch {
|
|
9911
|
+
return false;
|
|
9912
|
+
}
|
|
9913
|
+
};
|
|
9914
|
+
} finally {
|
|
9915
|
+
clearTimeout(timer);
|
|
9916
|
+
await child.stop();
|
|
9917
|
+
rmSync6(home, { recursive: true, force: true });
|
|
9918
|
+
}
|
|
9919
|
+
}
|
|
9920
|
+
var ClaudeImplementerPeer;
|
|
9921
|
+
var init_claude_implementer = __esm(() => {
|
|
9922
|
+
init_envelope();
|
|
9923
|
+
init_project();
|
|
9924
|
+
init_hub_tools();
|
|
9925
|
+
init_claude_state();
|
|
9926
|
+
init_launch2();
|
|
9927
|
+
init_compat();
|
|
9928
|
+
init_claude_print_child();
|
|
9929
|
+
init_claude_task_sessions();
|
|
9930
|
+
ClaudeImplementerPeer = class ClaudeImplementerPeer extends BasePeer {
|
|
9931
|
+
options;
|
|
9932
|
+
hubNative = true;
|
|
9933
|
+
generation = 0;
|
|
9934
|
+
owned;
|
|
9935
|
+
running;
|
|
9936
|
+
active;
|
|
9937
|
+
childAbort;
|
|
9938
|
+
abort;
|
|
9939
|
+
cleanup;
|
|
9940
|
+
binding;
|
|
9941
|
+
command;
|
|
9942
|
+
deliveryId;
|
|
9943
|
+
inputReleased = false;
|
|
9944
|
+
sessions;
|
|
9945
|
+
stopping;
|
|
9946
|
+
stoppingTarget;
|
|
9947
|
+
mode;
|
|
9948
|
+
constructor(id, options) {
|
|
9949
|
+
super(id, options.watchdogMs);
|
|
9950
|
+
this.options = options;
|
|
9951
|
+
if (options.mode !== undefined && options.mode !== "ask" && options.mode !== "ask-when-needed")
|
|
9952
|
+
throw new Error("Claude implementer refuses unsupported permission mode");
|
|
9953
|
+
this.mode = options.mode ?? "ask";
|
|
9954
|
+
this.sessions = new ClaudeTaskSessions(join16(options.stateDir, claudeStateFile("claude-task-sessions", id)));
|
|
9955
|
+
}
|
|
9956
|
+
get permissionModeState() {
|
|
9957
|
+
return this.mode;
|
|
9958
|
+
}
|
|
9959
|
+
async setPermissionMode(mode) {
|
|
9960
|
+
if (mode !== "ask" && mode !== "ask-when-needed")
|
|
9961
|
+
throw new Error("Claude implementer refuses never-ask");
|
|
9962
|
+
this.mode = mode;
|
|
9963
|
+
}
|
|
9964
|
+
get sessionGeneration() {
|
|
9965
|
+
return this.active?.generation;
|
|
9966
|
+
}
|
|
9967
|
+
ownedTools(launchId) {
|
|
9968
|
+
return typeof launchId === "string" && this.active?.launchId === launchId && !this.abort?.signal.aborted ? this.active : undefined;
|
|
9969
|
+
}
|
|
9970
|
+
keepApprovalAlive() {
|
|
9971
|
+
if (this.state === "busy" && this.active)
|
|
9972
|
+
this.touch();
|
|
9973
|
+
}
|
|
9974
|
+
recoveryMetadata() {
|
|
9975
|
+
return { launch: { kind: "claude-implementer", peer: this.id }, ...this.active ? { sessionId: this.active.sessionId } : {} };
|
|
9976
|
+
}
|
|
9977
|
+
async start() {
|
|
9978
|
+
if (this.owned || this.running || this.stopping)
|
|
9979
|
+
throw new Error("Claude implementer cleanup is not complete");
|
|
9980
|
+
if (this.state !== "offline")
|
|
9981
|
+
return;
|
|
9982
|
+
const generation = ++this.generation;
|
|
9983
|
+
const selected = this.options.command ?? ["claude"];
|
|
9984
|
+
const command = this.options.prove ? selected : [realPath(Bun.which(selected[0], { PATH: process.env.PATH }) ?? selected[0]), ...selected.slice(1)];
|
|
9985
|
+
const binding = await (this.options.prove?.() ?? proveClaudeImplementer(this.id, command));
|
|
9986
|
+
if (generation !== this.generation)
|
|
9987
|
+
return;
|
|
9988
|
+
this.command = command;
|
|
9989
|
+
this.binding = binding;
|
|
9990
|
+
this.setState("idle");
|
|
9991
|
+
}
|
|
9992
|
+
async stop(reason = "Claude implementer stopped before settlement") {
|
|
9993
|
+
return this.cancel(reason, "offline");
|
|
9994
|
+
}
|
|
9995
|
+
onWatchdog() {
|
|
9996
|
+
this.cancel("Claude implementer watchdog timeout", "idle").catch(() => {});
|
|
9997
|
+
}
|
|
9998
|
+
settle(state, reason) {
|
|
9999
|
+
const id = this.deliveryId;
|
|
10000
|
+
this.deliveryId = undefined;
|
|
10001
|
+
if (id)
|
|
10002
|
+
this.delivery({ id, state, ...reason ? { reason } : {} });
|
|
10003
|
+
}
|
|
10004
|
+
cancel(reason, next) {
|
|
10005
|
+
if (this.stopping) {
|
|
10006
|
+
if (next === "offline")
|
|
10007
|
+
this.stoppingTarget = "offline";
|
|
10008
|
+
return this.stopping;
|
|
10009
|
+
}
|
|
10010
|
+
this.stoppingTarget = next;
|
|
10011
|
+
++this.generation;
|
|
10012
|
+
this.active = undefined;
|
|
10013
|
+
this.childAbort?.abort();
|
|
10014
|
+
this.childAbort = undefined;
|
|
10015
|
+
this.abort?.abort();
|
|
10016
|
+
this.clearWatchdog();
|
|
10017
|
+
this.settle(this.inputReleased ? "needs_review" : "failed_safe", reason);
|
|
10018
|
+
if (this.owned || this.running)
|
|
10019
|
+
this.setState("busy");
|
|
10020
|
+
const work = Promise.resolve().then(async () => {
|
|
10021
|
+
if (this.owned)
|
|
10022
|
+
await this.owned.stop();
|
|
10023
|
+
const running = this.running;
|
|
10024
|
+
await running?.catch(() => {});
|
|
10025
|
+
if (this.running === running)
|
|
10026
|
+
this.running = undefined;
|
|
10027
|
+
this.cleanup?.();
|
|
10028
|
+
this.cleanup = undefined;
|
|
10029
|
+
this.owned = undefined;
|
|
10030
|
+
const target = this.stoppingTarget ?? next;
|
|
10031
|
+
if (this.stopping === work) {
|
|
10032
|
+
this.stopping = undefined;
|
|
10033
|
+
this.stoppingTarget = undefined;
|
|
10034
|
+
}
|
|
10035
|
+
this.setState(target);
|
|
10036
|
+
});
|
|
10037
|
+
this.stopping = work;
|
|
10038
|
+
work.finally(() => {
|
|
10039
|
+
if (this.stopping === work) {
|
|
10040
|
+
this.stopping = undefined;
|
|
10041
|
+
this.stoppingTarget = undefined;
|
|
10042
|
+
}
|
|
10043
|
+
}).catch(() => {});
|
|
10044
|
+
return work;
|
|
10045
|
+
}
|
|
10046
|
+
async deliver(envs, deliveryId) {
|
|
10047
|
+
if (this.state !== "idle" || this.owned || this.running || this.stopping)
|
|
10048
|
+
throw new Error("Claude implementer is not idle");
|
|
10049
|
+
if (envs.some((env) => env.private))
|
|
10050
|
+
throw new Error("Claude implementer refuses private delivery");
|
|
10051
|
+
const groups = [];
|
|
10052
|
+
for (const env of envs) {
|
|
10053
|
+
if (!this.options.admit(env))
|
|
10054
|
+
continue;
|
|
10055
|
+
const raw = env.refs?.task, task = raw === undefined ? this.options.chatTask() : Number(raw);
|
|
10056
|
+
if (task !== undefined && (!Number.isSafeInteger(task) || task <= 0))
|
|
10057
|
+
continue;
|
|
10058
|
+
let group = groups.find((item) => item.task === task);
|
|
10059
|
+
if (!group) {
|
|
10060
|
+
group = { task, envs: [] };
|
|
10061
|
+
groups.push(group);
|
|
10062
|
+
}
|
|
10063
|
+
group.envs.push(env);
|
|
10064
|
+
}
|
|
10065
|
+
const generation = this.generation;
|
|
10066
|
+
this.deliveryId = deliveryId;
|
|
10067
|
+
this.inputReleased = false;
|
|
10068
|
+
this.abort = new AbortController;
|
|
10069
|
+
this.setState("busy");
|
|
10070
|
+
if (deliveryId)
|
|
10071
|
+
this.delivery({ id: deliveryId, state: "accepted" });
|
|
10072
|
+
const running = Promise.resolve().then(() => this.run(groups, generation, running));
|
|
10073
|
+
this.running = running;
|
|
10074
|
+
running.finally(() => {
|
|
10075
|
+
if (this.running === running)
|
|
10076
|
+
this.running = undefined;
|
|
10077
|
+
}).catch(() => {});
|
|
10078
|
+
}
|
|
10079
|
+
async run(groups, generation, running) {
|
|
10080
|
+
const current = () => generation === this.generation && !this.abort?.signal.aborted;
|
|
10081
|
+
try {
|
|
10082
|
+
for (let group of groups) {
|
|
10083
|
+
if (!current() || !this.binding?.())
|
|
10084
|
+
throw new Error("Claude implementer admission changed");
|
|
10085
|
+
group = { ...group, envs: group.envs.filter((env) => !env.private && this.options.admit(env)) };
|
|
10086
|
+
if (!group.envs.length)
|
|
10087
|
+
continue;
|
|
10088
|
+
let turn = this.sessions.session(group.task);
|
|
10089
|
+
if (turn.resume && group.task !== undefined) {
|
|
10090
|
+
let persisted = "unknown";
|
|
10091
|
+
try {
|
|
10092
|
+
persisted = this.options.transcriptPersisted?.(group.task, turn.sessionId) ?? "unknown";
|
|
10093
|
+
} catch {}
|
|
10094
|
+
if (persisted === false) {
|
|
10095
|
+
this.sessions.forgetMissing(group.task, turn.sessionId);
|
|
10096
|
+
turn = this.sessions.session(group.task);
|
|
10097
|
+
this.options.notify?.(`${this.id}: task #${group.task} session fresh; recorded transcript is missing`);
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
10100
|
+
const launchId = randomUUID8(), directory = mkdtempSync5(join16(this.options.stateDir, "claude-child-")), settings = join16(directory, "settings.json");
|
|
10101
|
+
this.cleanup = () => rmSync6(directory, { recursive: true, force: true });
|
|
10102
|
+
writeFileSync7(settings, sessionSettings(undefined, { script: this.options.factsHook, stateDir: this.options.stateDir, purpose: "permission" }), { mode: 384, flag: "wx" });
|
|
10103
|
+
const env = {
|
|
10104
|
+
AGENTHUB_MODE: "tools",
|
|
10105
|
+
AGENTHUB_PEER_ID: this.id,
|
|
10106
|
+
AGENTHUB_STATE_DIR: this.options.stateDir,
|
|
10107
|
+
AGENTHUB_PROJECT_DIR: this.options.projectRoot,
|
|
10108
|
+
AGENTHUB_INSTANCE_ID: this.options.instanceId,
|
|
10109
|
+
AGENTHUB_LAUNCH_ID: launchId,
|
|
10110
|
+
AGENTHUB_HOOK_PURPOSE: "permission",
|
|
10111
|
+
AGENTHUB_PERMISSION_TOOL: "1"
|
|
10112
|
+
};
|
|
10113
|
+
const mcp = JSON.stringify({ mcpServers: { "agent-hub": { command: process.execPath, args: ["run", this.options.server], env } } });
|
|
10114
|
+
let result = false, failed = false;
|
|
10115
|
+
const child = new OwnedPrintChild({
|
|
10116
|
+
peer: this.id,
|
|
10117
|
+
command: this.command,
|
|
10118
|
+
args: claudeImplementerArgs(turn, mcp, settings, this.options.systemPrompt),
|
|
10119
|
+
cwd: this.options.cwd,
|
|
10120
|
+
input: group.envs.map(frame).join(`
|
|
10121
|
+
|
|
10122
|
+
`),
|
|
10123
|
+
env,
|
|
10124
|
+
current,
|
|
10125
|
+
stop: this.options.stopChild,
|
|
10126
|
+
onSpawn: () => {
|
|
10127
|
+
try {
|
|
10128
|
+
if (!this.binding?.() || group.envs.some((env) => env.private || !this.options.admit(env)))
|
|
10129
|
+
throw new Error("Claude implementer admission changed before child start");
|
|
10130
|
+
} catch {
|
|
10131
|
+
this.cancel("Claude implementer admission changed before child start", "idle").catch(() => {});
|
|
10132
|
+
return;
|
|
10133
|
+
}
|
|
10134
|
+
this.inputReleased = true;
|
|
10135
|
+
this.childAbort = new AbortController;
|
|
10136
|
+
this.active = { permissionHook: true, launchId, generation: `${generation}:${launchId}`, sessionId: turn.sessionId, signal: this.childAbort.signal, ...group.task !== undefined ? { task: group.task } : {} };
|
|
10137
|
+
},
|
|
10138
|
+
onLeaderExit: () => {
|
|
10139
|
+
this.active = undefined;
|
|
10140
|
+
this.childAbort?.abort();
|
|
10141
|
+
this.childAbort = undefined;
|
|
10142
|
+
},
|
|
10143
|
+
onActivity: () => this.touch(),
|
|
10144
|
+
onLimit: () => {
|
|
10145
|
+
this.cancel("Claude implementer output limit exceeded", "idle").catch(() => {});
|
|
10146
|
+
},
|
|
10147
|
+
onFrame: (event) => {
|
|
10148
|
+
const row = event;
|
|
10149
|
+
if (row?.session_id === turn.sessionId && group.task !== undefined) {
|
|
10150
|
+
try {
|
|
10151
|
+
if (!this.sessions.markStarted(group.task, turn.sessionId))
|
|
10152
|
+
throw new Error("session changed");
|
|
10153
|
+
} catch {
|
|
10154
|
+
this.cancel("Claude implementer session publication failed", "idle").catch(() => {});
|
|
10155
|
+
return;
|
|
10156
|
+
}
|
|
10157
|
+
}
|
|
10158
|
+
if (row?.type !== "result" || result)
|
|
10159
|
+
return;
|
|
10160
|
+
result = true;
|
|
10161
|
+
failed = row.is_error === true || row.subtype !== "success";
|
|
10162
|
+
if (row.usage)
|
|
10163
|
+
this.options.onUsage?.(row.usage, group.task);
|
|
10164
|
+
}
|
|
10165
|
+
});
|
|
10166
|
+
this.owned = child;
|
|
10167
|
+
let code;
|
|
10168
|
+
try {
|
|
10169
|
+
code = await child.done;
|
|
10170
|
+
} catch (error) {
|
|
10171
|
+
if (child.process.pid === undefined)
|
|
10172
|
+
this.owned = undefined;
|
|
10173
|
+
throw error;
|
|
10174
|
+
}
|
|
10175
|
+
this.active = undefined;
|
|
10176
|
+
this.owned = undefined;
|
|
10177
|
+
this.cleanup?.();
|
|
10178
|
+
this.cleanup = undefined;
|
|
10179
|
+
if (!current())
|
|
10180
|
+
return;
|
|
10181
|
+
if (code !== 0 || !result || failed)
|
|
10182
|
+
throw new Error("Claude implementer turn failed");
|
|
10183
|
+
}
|
|
10184
|
+
if (current()) {
|
|
10185
|
+
if (this.running === running)
|
|
10186
|
+
this.running = undefined;
|
|
10187
|
+
this.settle(this.inputReleased ? "completed" : "failed_safe", this.inputReleased ? undefined : "Claude implementer has no admissible envelopes");
|
|
10188
|
+
this.setState("idle");
|
|
10189
|
+
}
|
|
10190
|
+
} catch (error) {
|
|
10191
|
+
this.active = undefined;
|
|
10192
|
+
this.childAbort?.abort();
|
|
10193
|
+
this.childAbort = undefined;
|
|
10194
|
+
if (current()) {
|
|
10195
|
+
this.settle(this.inputReleased ? "needs_review" : "failed_safe", error instanceof PrintChildCleanupError ? "Claude implementer cleanup incomplete" : "Claude implementer turn failed");
|
|
10196
|
+
if (this.owned)
|
|
10197
|
+
this.setState("busy");
|
|
10198
|
+
else {
|
|
10199
|
+
this.cleanup?.();
|
|
10200
|
+
this.cleanup = undefined;
|
|
10201
|
+
if (this.running === running)
|
|
10202
|
+
this.running = undefined;
|
|
10203
|
+
this.setState("idle");
|
|
10204
|
+
}
|
|
10205
|
+
}
|
|
10206
|
+
}
|
|
10207
|
+
}
|
|
10208
|
+
};
|
|
10209
|
+
});
|
|
10210
|
+
|
|
9517
10211
|
// src/adapters/definitions/native-factories.ts
|
|
9518
10212
|
function createCodex(ctx) {
|
|
9519
10213
|
if (!("codex" in ctx.daemon))
|
|
@@ -9577,6 +10271,14 @@ function createReviewer(ctx) {
|
|
|
9577
10271
|
init_reviewer_print();
|
|
9578
10272
|
return new ReviewerPrintPeer(ctx.instance, ctx.daemon.reviewerPrint);
|
|
9579
10273
|
}
|
|
10274
|
+
function createClaudeImplementer(ctx) {
|
|
10275
|
+
if (!("claudeImplementer" in ctx.daemon))
|
|
10276
|
+
throw new Error("Claude implementer requires its per-task transport options");
|
|
10277
|
+
if (ctx.permission !== "ask" && ctx.permission !== "ask-when-needed")
|
|
10278
|
+
throw new Error("Claude implementer requires ask or ask-when-needed");
|
|
10279
|
+
init_claude_implementer();
|
|
10280
|
+
return new ClaudeImplementerPeer(ctx.instance, ctx.daemon.claudeImplementer);
|
|
10281
|
+
}
|
|
9580
10282
|
var DAEMON_PORTS;
|
|
9581
10283
|
var init_native_factories = __esm(() => {
|
|
9582
10284
|
DAEMON_PORTS = {
|
|
@@ -9586,6 +10288,7 @@ var init_native_factories = __esm(() => {
|
|
|
9586
10288
|
"pi-rpc": (options) => ({ pi: options }),
|
|
9587
10289
|
"local-worker": (options) => ({ local: options }),
|
|
9588
10290
|
"reviewer-print": (options) => ({ reviewerPrint: options }),
|
|
10291
|
+
"claude-implementer": (options) => ({ claudeImplementer: options }),
|
|
9589
10292
|
"claude-print": (options) => ({ claudePrint: options })
|
|
9590
10293
|
};
|
|
9591
10294
|
});
|
|
@@ -9652,12 +10355,12 @@ var init_reviewer_variants = __esm(() => {
|
|
|
9652
10355
|
});
|
|
9653
10356
|
|
|
9654
10357
|
// src/adapters/agy/hooks.ts
|
|
9655
|
-
import { closeSync as closeSync6, constants as constants5, fstatSync as fstatSync5, lstatSync as
|
|
10358
|
+
import { closeSync as closeSync6, constants as constants5, fstatSync as fstatSync5, lstatSync as lstatSync11, openSync as openSync6, readSync as readSync4 } from "fs";
|
|
9656
10359
|
import { homedir as homedir5 } from "os";
|
|
9657
|
-
import { isAbsolute as isAbsolute6, join as
|
|
10360
|
+
import { isAbsolute as isAbsolute6, join as join17, relative as relative5 } from "path";
|
|
9658
10361
|
import { createHash as createHash6 } from "crypto";
|
|
9659
10362
|
function decodeAgyHook(event, value) {
|
|
9660
|
-
if (!EVENTS2.includes(event) || !record3(value) || typeof value.conversationId !== "string" || !
|
|
10363
|
+
if (!EVENTS2.includes(event) || !record3(value) || typeof value.conversationId !== "string" || !UUID2.test(value.conversationId) || typeof value.transcriptPath !== "string" || !isAbsolute6(value.transcriptPath) || value.transcriptPath.length > 4096 || !Array.isArray(value.workspacePaths) || !value.workspacePaths.length || value.workspacePaths.length > 32 || !value.workspacePaths.every((path) => typeof path === "string" && path.length <= 4096 && isAbsolute6(path)))
|
|
9661
10364
|
return;
|
|
9662
10365
|
if (event === "Stop" && typeof value.fullyIdle !== "boolean")
|
|
9663
10366
|
return;
|
|
@@ -9689,18 +10392,18 @@ function encodeAgyHook(event, text) {
|
|
|
9689
10392
|
}
|
|
9690
10393
|
function readAgyTranscript(session, transcript) {
|
|
9691
10394
|
const empty = { factIds: [] };
|
|
9692
|
-
if (!
|
|
10395
|
+
if (!UUID2.test(session))
|
|
9693
10396
|
return empty;
|
|
9694
10397
|
const home = process.env.HOME ?? homedir5();
|
|
9695
|
-
const candidates = ["antigravity-cli", "antigravity", "antigravity-ide"].map((surface) =>
|
|
10398
|
+
const candidates = ["antigravity-cli", "antigravity", "antigravity-ide"].map((surface) => join17(home, ".gemini", surface, "brain", session, ".system_generated/logs/transcript_full.jsonl"));
|
|
9696
10399
|
if (!candidates.includes(transcript))
|
|
9697
10400
|
return empty;
|
|
9698
10401
|
let fd;
|
|
9699
10402
|
try {
|
|
9700
10403
|
let cursor = home;
|
|
9701
10404
|
for (const name of relative5(home, transcript).split("/")) {
|
|
9702
|
-
cursor =
|
|
9703
|
-
if (
|
|
10405
|
+
cursor = join17(cursor, name);
|
|
10406
|
+
if (lstatSync11(cursor).isSymbolicLink())
|
|
9704
10407
|
return empty;
|
|
9705
10408
|
}
|
|
9706
10409
|
fd = openSync6(transcript, constants5.O_RDONLY | constants5.O_NOFOLLOW);
|
|
@@ -9749,10 +10452,10 @@ function readAgyTranscript(session, transcript) {
|
|
|
9749
10452
|
closeSync6(fd);
|
|
9750
10453
|
}
|
|
9751
10454
|
}
|
|
9752
|
-
var EVENTS2,
|
|
10455
|
+
var EVENTS2, UUID2, record3 = (value) => !!value && typeof value === "object" && !Array.isArray(value), AGY_FACTS_HOOK;
|
|
9753
10456
|
var init_hooks = __esm(() => {
|
|
9754
10457
|
EVENTS2 = ["SessionStart", "PreInvocation", "PostInvocation", "PreToolUse", "PostToolUse", "Stop"];
|
|
9755
|
-
|
|
10458
|
+
UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9756
10459
|
AGY_FACTS_HOOK = { decode: decodeAgyHook, encode: encodeAgyHook, read: readAgyTranscript };
|
|
9757
10460
|
});
|
|
9758
10461
|
|
|
@@ -9823,49 +10526,19 @@ var init_definition = __esm(() => {
|
|
|
9823
10526
|
function builtinCliEntries(definitions) {
|
|
9824
10527
|
return Object.freeze(definitions.flatMap((definition) => definition.cli.entry ? [Object.freeze({ ...definition.cli.entry, command: definition.cli.command })] : []));
|
|
9825
10528
|
}
|
|
9826
|
-
// src/adapters/definitions/native-cli-retained.ts
|
|
9827
|
-
function retainedLaunchWarning(status, peer, definition) {
|
|
9828
|
-
const keys = definition.cli.launchConfigKeys;
|
|
9829
|
-
const retained = status?.retainedConfig;
|
|
9830
|
-
if (!keys?.length || !Array.isArray(retained))
|
|
9831
|
-
return;
|
|
9832
|
-
const pending = retained.flatMap((row) => {
|
|
9833
|
-
if (!row || typeof row !== "object")
|
|
9834
|
-
return [];
|
|
9835
|
-
const key = row.key;
|
|
9836
|
-
return typeof key === "string" && keys.some((candidate) => key === candidate || key.startsWith(`${candidate}.`)) ? [key] : [];
|
|
9837
|
-
});
|
|
9838
|
-
const unique = [...new Set(pending)];
|
|
9839
|
-
return unique.length ? `ahub: config retained: ${unique.join(", ")}; starting ${peer} with the running value; run ahub settings reload to apply the file's value` : undefined;
|
|
9840
|
-
}
|
|
9841
|
-
|
|
9842
|
-
// src/adapters/definitions/native-channel-state.ts
|
|
9843
|
-
function claudeStateFile(base, peer) {
|
|
9844
|
-
if (!CLAUDE_PEER.test(peer) || !PEER2.test(peer))
|
|
9845
|
-
throw new Error(`${peer} is not a Claude peer; Claude state files belong to Claude peers only`);
|
|
9846
|
-
return peer.includes("-") ? `${base}.${peer}.json` : `${base}.json`;
|
|
9847
|
-
}
|
|
9848
|
-
var PEER2, CLAUDE_PEER;
|
|
9849
|
-
var init_native_channel_state = __esm(() => {
|
|
9850
|
-
PEER2 = /^[a-z][a-z0-9-]{0,31}$/;
|
|
9851
|
-
CLAUDE_PEER = /^claude(?:-[a-z0-9-]+)?$/;
|
|
9852
|
-
});
|
|
9853
|
-
|
|
9854
|
-
// src/hub/claude-state.ts
|
|
9855
|
-
var init_claude_state = __esm(() => {
|
|
9856
|
-
init_native_channel_state();
|
|
9857
|
-
});
|
|
9858
|
-
|
|
9859
10529
|
// src/adapters/definitions/native-cli-entry-claude.ts
|
|
9860
|
-
import { readFileSync as
|
|
9861
|
-
import { join as
|
|
9862
|
-
import { randomUUID as
|
|
10530
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
10531
|
+
import { join as join18 } from "path";
|
|
10532
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
9863
10533
|
async function runClaudeEntry(ctx) {
|
|
9864
10534
|
const { args, cwd, stateDir, cliDir, unattendedEnv, projectConfig, fail } = ctx;
|
|
9865
10535
|
const peer = ctx.instance ?? "claude";
|
|
9866
10536
|
if (ctx.hubStarted) {
|
|
10537
|
+
const declaration = ctx.instance ? projectConfig().instances?.[ctx.instance] : undefined;
|
|
10538
|
+
const writer = declaration?.roles?.includes("implementer") && declaration.start_mode === "headless";
|
|
10539
|
+
const description = writer ? "a headless implementer the hub runs per task" : "a headless reviewer the hub runs per review";
|
|
9867
10540
|
if (args.includes("--print-command") || args.includes("--dry-run"))
|
|
9868
|
-
return console.log(JSON.stringify({ peer, start: "hub", note:
|
|
10541
|
+
return console.log(JSON.stringify({ peer, start: "hub", note: description }, null, 2));
|
|
9869
10542
|
const hub = await ctx.connect();
|
|
9870
10543
|
try {
|
|
9871
10544
|
const snapshot = (await hub.request({ t: "status" })).status;
|
|
@@ -9878,7 +10551,7 @@ async function runClaudeEntry(ctx) {
|
|
|
9878
10551
|
} finally {
|
|
9879
10552
|
hub.close();
|
|
9880
10553
|
}
|
|
9881
|
-
return console.log(`${peer} started:
|
|
10554
|
+
return console.log(`${peer} started: ${description} (stop it with ahub stop ${peer})`);
|
|
9882
10555
|
}
|
|
9883
10556
|
const { launcherPreview, readControl, prepareTerminalLaunch, registerTerminalLaunch, claudeObservationHooks, buildLaunch, runClaudeLaunch, processSignature, recordClaudeLaunch, cleanupStaleClaudeSettings, nativeLaunchEnv, childEnv, cleanupClaudeSettings } = ctx.helpers;
|
|
9884
10557
|
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
@@ -9888,12 +10561,14 @@ async function runClaudeEntry(ctx) {
|
|
|
9888
10561
|
fail("cannot preview launch: invalid arguments or configuration (details withheld to protect credentials)");
|
|
9889
10562
|
}
|
|
9890
10563
|
}
|
|
10564
|
+
const executionCwd = ctx.instance ? ctx.helpers.resolveExecutionCwd(peer) : cwd;
|
|
10565
|
+
ctx.executionCwd = executionCwd;
|
|
9891
10566
|
const control = readControl(stateDir);
|
|
9892
10567
|
if (control?.instanceId) {
|
|
9893
10568
|
process.env.AGENTHUB_INSTANCE_ID = control.instanceId;
|
|
9894
|
-
const terminal = await prepareTerminalLaunch(peer, cwd, stateDir, control.instanceId, ctx.instance ? { executionCwd
|
|
10569
|
+
const terminal = await prepareTerminalLaunch(peer, cwd, stateDir, control.instanceId, ctx.instance ? { executionCwd } : undefined);
|
|
9895
10570
|
if (!terminal)
|
|
9896
|
-
process.env.AGENTHUB_LAUNCH_ID =
|
|
10571
|
+
process.env.AGENTHUB_LAUNCH_ID = randomUUID9();
|
|
9897
10572
|
else {
|
|
9898
10573
|
const hub = await ctx.connect();
|
|
9899
10574
|
try {
|
|
@@ -9904,22 +10579,22 @@ async function runClaudeEntry(ctx) {
|
|
|
9904
10579
|
}
|
|
9905
10580
|
}
|
|
9906
10581
|
let original;
|
|
9907
|
-
for (const file of [
|
|
10582
|
+
for (const file of [join18(executionCwd, ".claude", "settings.local.json"), join18(executionCwd, ".claude", "settings.json"), join18(process.env.HOME ?? "", ".claude", "settings.json")]) {
|
|
9908
10583
|
try {
|
|
9909
|
-
original ??= JSON.parse(
|
|
10584
|
+
original ??= JSON.parse(readFileSync9(file, "utf8")).statusLine;
|
|
9910
10585
|
} catch {}
|
|
9911
10586
|
}
|
|
9912
|
-
const facts = claudeObservationHooks(projectConfig(), { script:
|
|
10587
|
+
const facts = claudeObservationHooks(projectConfig(), { script: join18(cliDir, "facts-hook.ts"), stateDir });
|
|
9913
10588
|
let previousLaunch;
|
|
9914
10589
|
try {
|
|
9915
|
-
previousLaunch = JSON.parse(
|
|
10590
|
+
previousLaunch = JSON.parse(readFileSync9(join18(stateDir, claudeStateFile("claude-launch", peer)), "utf8"));
|
|
9916
10591
|
} catch {}
|
|
9917
|
-
const launch = buildLaunch("claude", args, { unattended: unattendedEnv, statusLine: { script:
|
|
10592
|
+
const launch = buildLaunch("claude", args, { unattended: unattendedEnv, statusLine: { script: join18(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original } : {} }, ...facts ? { facts } : {} });
|
|
9918
10593
|
let result;
|
|
9919
10594
|
try {
|
|
9920
10595
|
let launchRecord;
|
|
9921
10596
|
if (control?.instanceId) {
|
|
9922
|
-
launchRecord = { ...ctx.instance ? { peer } : {}, instanceId: control.instanceId, launchId: process.env.AGENTHUB_LAUNCH_ID, permissionHook: launch.permissionHook === true, hookPurpose: launch.hookPurpose, settingsFile: launch.settingsFile, launcherPid: process.pid, launcherSignature: processSignature(process.pid), unattended: launch.unattended === true };
|
|
10597
|
+
launchRecord = { ...ctx.instance ? { peer, executionCwd } : {}, instanceId: control.instanceId, launchId: process.env.AGENTHUB_LAUNCH_ID, permissionHook: launch.permissionHook === true, hookPurpose: launch.hookPurpose, settingsFile: launch.settingsFile, launcherPid: process.pid, launcherSignature: processSignature(process.pid), unattended: launch.unattended === true };
|
|
9923
10598
|
recordClaudeLaunch(stateDir, launchRecord);
|
|
9924
10599
|
cleanupStaleClaudeSettings(stateDir, previousLaunch);
|
|
9925
10600
|
}
|
|
@@ -9928,7 +10603,7 @@ async function runClaudeEntry(ctx) {
|
|
|
9928
10603
|
const env = nativeLaunchEnv("claude", childEnv());
|
|
9929
10604
|
if (ctx.instance)
|
|
9930
10605
|
env.AGENTHUB_PEER_ID = peer;
|
|
9931
|
-
result = await runClaudeLaunch(launch, { cwd, stateDir, env: { ...env, AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd }, ...launchRecord ? { record: launchRecord } : {} });
|
|
10606
|
+
result = await runClaudeLaunch(launch, { cwd: executionCwd, stateDir, env: { ...env, AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd }, ...launchRecord ? { record: launchRecord } : {} });
|
|
9932
10607
|
} finally {
|
|
9933
10608
|
cleanupClaudeSettings(stateDir, launch.settingsFile);
|
|
9934
10609
|
}
|
|
@@ -9945,6 +10620,23 @@ async function runCodexEntry(ctx) {
|
|
|
9945
10620
|
const peer = ctx.instance ?? "codex";
|
|
9946
10621
|
const { args, cwd, stateDir, unattendedEnv, projectConfig, connect, fail, exec } = ctx;
|
|
9947
10622
|
const { launcherPreview, buildLaunch, readControl, prepareTerminalLaunch, registerTerminalLaunch } = ctx.helpers;
|
|
10623
|
+
if (ctx.hubStarted) {
|
|
10624
|
+
if (args.includes("--print-command") || args.includes("--dry-run"))
|
|
10625
|
+
return console.log(JSON.stringify({ peer, start: "hub", note: "a headless reviewer the hub runs per review" }, null, 2));
|
|
10626
|
+
const hub = await connect();
|
|
10627
|
+
try {
|
|
10628
|
+
const snapshot = (await hub.request({ t: "status" })).status;
|
|
10629
|
+
const warning = retainedLaunchWarning(snapshot, peer, ctx.definition);
|
|
10630
|
+
if (warning)
|
|
10631
|
+
console.error(warning);
|
|
10632
|
+
const res = await hub.request({ t: "start", peer, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
10633
|
+
if (res.ok !== true)
|
|
10634
|
+
fail(String(res.error ?? `${peer} was not started`));
|
|
10635
|
+
} finally {
|
|
10636
|
+
hub.close();
|
|
10637
|
+
}
|
|
10638
|
+
return console.log(`${peer} started: a headless reviewer the hub runs per review (stop it with ahub stop ${peer})`);
|
|
10639
|
+
}
|
|
9948
10640
|
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
9949
10641
|
try {
|
|
9950
10642
|
return console.log(JSON.stringify(launcherPreview("codex", args, cwd, stateDir, unattendedEnv), null, 2));
|
|
@@ -10381,8 +11073,8 @@ var WAITING = "jcode's TUI opens after its first hub turn; send it work, or Ctrl
|
|
|
10381
11073
|
var init_native_cli_entry_jcode = () => {};
|
|
10382
11074
|
|
|
10383
11075
|
// src/adapters/definitions/native-cli-preview-claude.ts
|
|
10384
|
-
import { readFileSync as
|
|
10385
|
-
import { join as
|
|
11076
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
11077
|
+
import { join as join19 } from "path";
|
|
10386
11078
|
function previewClaude(ctx) {
|
|
10387
11079
|
const { args, cwd, stateDir, cliDir, unattended, config } = ctx;
|
|
10388
11080
|
const { buildLaunch, claudeObservationHooks } = ctx.helpers;
|
|
@@ -10390,13 +11082,13 @@ function previewClaude(ctx) {
|
|
|
10390
11082
|
let envNames = state.envNames;
|
|
10391
11083
|
let launch;
|
|
10392
11084
|
let original;
|
|
10393
|
-
for (const file of [
|
|
11085
|
+
for (const file of [join19(cwd, ".claude/settings.local.json"), join19(cwd, ".claude/settings.json"), join19(process.env.HOME ?? "", ".claude/settings.json")]) {
|
|
10394
11086
|
try {
|
|
10395
|
-
original ??= JSON.parse(
|
|
11087
|
+
original ??= JSON.parse(readFileSync10(file, "utf8")).statusLine;
|
|
10396
11088
|
} catch {}
|
|
10397
11089
|
}
|
|
10398
|
-
const facts = claudeObservationHooks(config, { script:
|
|
10399
|
-
launch = buildLaunch("claude", args, { preview: true, unattended, statusLine: { script:
|
|
11090
|
+
const facts = claudeObservationHooks(config, { script: join19(cliDir, "facts-hook.ts"), stateDir });
|
|
11091
|
+
launch = buildLaunch("claude", args, { preview: true, unattended, statusLine: { script: join19(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original: { command: original.command ? REDACTED : "", ...typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}, ...typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {} } } : {} }, ...facts ? { facts } : {} });
|
|
10400
11092
|
const safeArgs = args.map((arg, i) => {
|
|
10401
11093
|
if (args[i - 1] === "--settings")
|
|
10402
11094
|
return "{}";
|
|
@@ -10407,7 +11099,7 @@ function previewClaude(ctx) {
|
|
|
10407
11099
|
return publicArg(arg);
|
|
10408
11100
|
});
|
|
10409
11101
|
const publicLaunch = buildLaunch("claude", safeArgs, { preview: true, unattended, statusLine: {
|
|
10410
|
-
script:
|
|
11102
|
+
script: join19(cliDir, "statusline-tee.ts"),
|
|
10411
11103
|
stateDir,
|
|
10412
11104
|
...original ? { original: { command: original.command ? REDACTED : "", ...typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}, ...typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {} } } : {}
|
|
10413
11105
|
}, facts });
|
|
@@ -10440,7 +11132,7 @@ function previewCodex(ctx) {
|
|
|
10440
11132
|
var init_native_cli_preview_codex = () => {};
|
|
10441
11133
|
|
|
10442
11134
|
// src/adapters/definitions/native-cli-preview-kimi.ts
|
|
10443
|
-
import { join as
|
|
11135
|
+
import { join as join20 } from "path";
|
|
10444
11136
|
function previewKimi(ctx) {
|
|
10445
11137
|
const { args, cwd, stateDir, cliDir, config } = ctx;
|
|
10446
11138
|
const { childEnv, startModeOf, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, kimiMcpPlan } = ctx.helpers;
|
|
@@ -10463,14 +11155,14 @@ function previewKimi(ctx) {
|
|
|
10463
11155
|
launch = { ...native, cmd: native.cmd === "kimi" ? "kimi" : REDACTED, args: native.args.map((arg) => arg === "--model" || arg === REDACTED ? arg : REDACTED) };
|
|
10464
11156
|
envNames = Object.keys(nativeLaunchEnv("kimi", childEnv())).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR"]);
|
|
10465
11157
|
}
|
|
10466
|
-
const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir,
|
|
11158
|
+
const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir, join20(cliDir, "../../plugins/agent-hub/server.js"), mode);
|
|
10467
11159
|
kimiMetadata = { mode, mcp: { file, action, changed, tracked } };
|
|
10468
11160
|
return finishPreview(ctx, launch, envNames, unresolved, kimiMetadata);
|
|
10469
11161
|
}
|
|
10470
11162
|
var init_native_cli_preview_kimi = () => {};
|
|
10471
11163
|
|
|
10472
11164
|
// src/adapters/definitions/native-cli-preview-pi.ts
|
|
10473
|
-
import { join as
|
|
11165
|
+
import { join as join21, resolve as resolve15 } from "path";
|
|
10474
11166
|
function previewPi(ctx) {
|
|
10475
11167
|
const { args, stateDir, cliDir, config } = ctx;
|
|
10476
11168
|
const { relayModelIds, buildPiLaunch, startModeOf } = ctx.helpers;
|
|
@@ -10505,7 +11197,7 @@ function previewPi(ctx) {
|
|
|
10505
11197
|
tools: [],
|
|
10506
11198
|
preamble: REDACTED,
|
|
10507
11199
|
maxSteps: config.pi.max_steps
|
|
10508
|
-
}, {}, resolve15(
|
|
11200
|
+
}, {}, resolve15(join21(cliDir, "../pi/extension.ts")), UNRESOLVED, UNRESOLVED);
|
|
10509
11201
|
launch = pi;
|
|
10510
11202
|
envNames = Object.keys(pi.env).concat(["PATH", "HOME", "USER", "SHELL", "TMPDIR", "TERM", "TERM_PROGRAM", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "CODEX_HOME"].filter((key) => process.env[key] !== undefined));
|
|
10511
11203
|
if (mode === "tui")
|
|
@@ -10543,12 +11235,6 @@ function previewJcode(ctx) {
|
|
|
10543
11235
|
}
|
|
10544
11236
|
var init_native_cli_preview_jcode = () => {};
|
|
10545
11237
|
|
|
10546
|
-
// src/adapters/definitions/native-daemon-kimi.ts
|
|
10547
|
-
function kimiAcpCommand(command, model) {
|
|
10548
|
-
const [cmd, ...args] = command;
|
|
10549
|
-
return [cmd, ...model ? ["--model", model, ...args] : args];
|
|
10550
|
-
}
|
|
10551
|
-
|
|
10552
11238
|
// src/adapters/definitions/native-daemon-policy.ts
|
|
10553
11239
|
var claudeRecoveryLaunch = (instance) => ({ kind: "claude-channel", peer: instance }), kimiRecoveryLaunch = (instance) => ({ kind: "kimi-tui", peer: instance, mode: "tui" }), claudeRetainedTerminalNotice = (instance) => `Stop ${instance}'s existing work in its retained terminal; removal detaches the hub connection and does not stop that terminal's work.`, piRetainedTerminalNotice = (instance, form) => form === "tui" ? claudeRetainedTerminalNotice(instance) : undefined, codexRetainedTerminalNotice = (_instance, _form, resumeCommand) => `Check codex's existing terminal and stop any remaining work. Its app-server and proxy stay available, but Codex 0.146.0 exits its TUI on disconnect and does not reconnect automatically; after ahub roster add codex, resume the previous session with ${resumeCommand}.`, codexRosterReturnNotice = (resumeCommand) => `Codex 0.146.0 needs a manual return: resume the previous native session with ${resumeCommand}.`, jcodeStartPreflight = (request) => request.form !== undefined && request.form !== "headless" && request.form !== "tui" ? "invalid Jcode start mode" : undefined, claudeObservedVersion = (context) => context.observedVersion, codexObservedVersion = (context) => context.peer?.version ?? context.observedVersion;
|
|
10554
11240
|
|
|
@@ -10596,8 +11282,8 @@ var init_native_config_policy = __esm(() => {
|
|
|
10596
11282
|
});
|
|
10597
11283
|
|
|
10598
11284
|
// src/adapters/definitions/native-policy-aux.ts
|
|
10599
|
-
import { closeSync as closeSync7, constants as constants6, fstatSync as fstatSync6, openSync as openSync7, readdirSync as readdirSync3, readFileSync as
|
|
10600
|
-
import { isAbsolute as isAbsolute7, join as
|
|
11285
|
+
import { closeSync as closeSync7, constants as constants6, fstatSync as fstatSync6, openSync as openSync7, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
|
|
11286
|
+
import { isAbsolute as isAbsolute7, join as join22, normalize } from "path";
|
|
10601
11287
|
function claudeCrashPlan(_meta) {
|
|
10602
11288
|
return { how: "claude: the Claude Code plugin reconnects by itself while that session is still open" };
|
|
10603
11289
|
}
|
|
@@ -10642,13 +11328,13 @@ function localCrashPlan(meta) {
|
|
|
10642
11328
|
}
|
|
10643
11329
|
function claudePlugins(root, cwd) {
|
|
10644
11330
|
const enabled = new Map;
|
|
10645
|
-
for (const file of [
|
|
11331
|
+
for (const file of [join22(root, "settings.json"), ...cwd ? [join22(cwd, ".claude", "settings.json"), join22(cwd, ".claude", "settings.local.json")] : []]) {
|
|
10646
11332
|
const map = json(file)?.enabledPlugins;
|
|
10647
11333
|
if (map && typeof map === "object")
|
|
10648
11334
|
for (const [key, on] of Object.entries(map))
|
|
10649
11335
|
enabled.set(key, on === true);
|
|
10650
11336
|
}
|
|
10651
|
-
const installed = json(
|
|
11337
|
+
const installed = json(join22(root, "plugins", "installed_plugins.json"))?.plugins ?? {};
|
|
10652
11338
|
const out = [];
|
|
10653
11339
|
for (const [key, on] of enabled) {
|
|
10654
11340
|
if (!on)
|
|
@@ -10662,7 +11348,7 @@ function claudePlugins(root, cwd) {
|
|
|
10662
11348
|
return out;
|
|
10663
11349
|
}
|
|
10664
11350
|
function codexPlugins(root) {
|
|
10665
|
-
const config = text(
|
|
11351
|
+
const config = text(join22(root, "config.toml"));
|
|
10666
11352
|
if (config === undefined)
|
|
10667
11353
|
return [];
|
|
10668
11354
|
const found = new Map, disabled = new Set;
|
|
@@ -10681,7 +11367,7 @@ function codexPlugins(root) {
|
|
|
10681
11367
|
continue;
|
|
10682
11368
|
let versions = [];
|
|
10683
11369
|
try {
|
|
10684
|
-
versions = readdirSync3(
|
|
11370
|
+
versions = readdirSync3(join22(root, "plugins", "cache", market, plugin));
|
|
10685
11371
|
} catch {}
|
|
10686
11372
|
out.push({ name, version: versions.length === 1 ? version2(versions[0]) : null });
|
|
10687
11373
|
}
|
|
@@ -10705,7 +11391,7 @@ var str3 = (value) => typeof value === "string" && value ? value : undefined, PL
|
|
|
10705
11391
|
const st = fstatSync6(fd);
|
|
10706
11392
|
if (!st.isFile() || st.size > 1024 * 1024)
|
|
10707
11393
|
return;
|
|
10708
|
-
return
|
|
11394
|
+
return readFileSync11(fd, "utf8");
|
|
10709
11395
|
} catch {
|
|
10710
11396
|
return;
|
|
10711
11397
|
} finally {
|
|
@@ -10719,7 +11405,7 @@ var str3 = (value) => typeof value === "string" && value ? value : undefined, PL
|
|
|
10719
11405
|
} catch {
|
|
10720
11406
|
return;
|
|
10721
11407
|
}
|
|
10722
|
-
}, version2 = (v) => typeof v === "string" && /^[0-9A-Za-z.+_-]{1,40}$/.test(v) ? v : null, claudePluginInventory = (ctx) => claudePlugins(ctx.env.CLAUDE_CONFIG_DIR?.trim() ||
|
|
11408
|
+
}, version2 = (v) => typeof v === "string" && /^[0-9A-Za-z.+_-]{1,40}$/.test(v) ? v : null, claudePluginInventory = (ctx) => claudePlugins(ctx.env.CLAUDE_CONFIG_DIR?.trim() || join22(ctx.home, ".claude"), ctx.cwd), codexPluginInventory = (ctx) => codexPlugins(ctx.env.CODEX_HOME?.trim() || join22(ctx.home, ".codex")), codexToolSemantic = (name, _command) => name.toLowerCase() === "filechange" ? "edit" : undefined;
|
|
10723
11409
|
var init_native_policy_aux = __esm(() => {
|
|
10724
11410
|
PLUGINS = new Map([["claude-mem", "claude-mem"], ["ponytail", "ponytail"], ["open-code-review", "open-code-review"]]);
|
|
10725
11411
|
CLAUDE_ONLY = new Map([["codex@openai-codex", "codex-plugin"]]);
|
|
@@ -10739,32 +11425,6 @@ function piRecoveryContinuity(peer, binding) {
|
|
|
10739
11425
|
} };
|
|
10740
11426
|
}
|
|
10741
11427
|
|
|
10742
|
-
// src/cli/launch.ts
|
|
10743
|
-
function statusLineSettings(tee) {
|
|
10744
|
-
const command = `AGENTHUB_STATE_DIR=${sh(tee.stateDir)} AGENTHUB_STATUSLINE_CMD=${sh(tee.original?.command ?? "")} bun ${sh(tee.script)}`;
|
|
10745
|
-
return JSON.stringify({ statusLine: { type: "command", command, refreshInterval: tee.original?.refreshInterval ?? 10, ...tee.original?.padding !== undefined ? { padding: tee.original.padding } : {} } });
|
|
10746
|
-
}
|
|
10747
|
-
function sessionSettings(tee, facts) {
|
|
10748
|
-
const settings = tee ? JSON.parse(statusLineSettings(tee)) : {};
|
|
10749
|
-
if (facts) {
|
|
10750
|
-
const hooks = [{ type: "command", command: `AGENTHUB_STATE_DIR=${sh(facts.stateDir)} AGENTHUB_HOOK_PURPOSE=${sh(facts.purpose ?? "facts")} bun ${sh(facts.script)}`, timeout: 5 }];
|
|
10751
|
-
settings.hooks = { PreToolUse: [{ matcher: "*", hooks }], ...facts.purpose !== "permission" ? { PostToolUse: [{ matcher: "*", hooks }], Stop: [{ hooks }] } : {} };
|
|
10752
|
-
if (facts.observeNative)
|
|
10753
|
-
Object.assign(settings.hooks, { SessionStart: [{ matcher: "*", hooks }], UserPromptSubmit: [{ hooks }] });
|
|
10754
|
-
}
|
|
10755
|
-
return JSON.stringify(settings);
|
|
10756
|
-
}
|
|
10757
|
-
var CLAUDE_CHANNEL = "plugin:agent-hub@agent-hub", UNATTENDED_WARNING = "WARNING: --unattended disables permission prompts. Other agents' messages are untrusted input and this agent will act on its own judgment without asking you.", sh = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10758
|
-
var init_launch2 = __esm(() => {
|
|
10759
|
-
init_claude_state();
|
|
10760
|
-
init_claude_state();
|
|
10761
|
-
init_process_signature();
|
|
10762
|
-
init_project();
|
|
10763
|
-
init_child_process();
|
|
10764
|
-
init_registry();
|
|
10765
|
-
init_qwen_native();
|
|
10766
|
-
});
|
|
10767
|
-
|
|
10768
11428
|
// src/cli/shell-quote.ts
|
|
10769
11429
|
function shellQuote3(value) {
|
|
10770
11430
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -10788,9 +11448,9 @@ var init_native_cli_launch_common = __esm(() => {
|
|
|
10788
11448
|
});
|
|
10789
11449
|
|
|
10790
11450
|
// src/adapters/definitions/native-cli-claude-launch.ts
|
|
10791
|
-
import { mkdirSync as
|
|
10792
|
-
import { randomUUID as
|
|
10793
|
-
import { resolve as resolve16, join as
|
|
11451
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, renameSync as renameSync4, unlinkSync as unlinkSync3, readFileSync as readFileSync12 } from "fs";
|
|
11452
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
11453
|
+
import { resolve as resolve16, join as join23 } from "path";
|
|
10794
11454
|
function claudeChannel(userArgs) {
|
|
10795
11455
|
const bundle = resolve16(import.meta.dir, "../../../plugins/agent-hub/server.js");
|
|
10796
11456
|
for (let i = 0;i < userArgs.length; i++) {
|
|
@@ -10799,7 +11459,7 @@ function claudeChannel(userArgs) {
|
|
|
10799
11459
|
if (!path)
|
|
10800
11460
|
continue;
|
|
10801
11461
|
try {
|
|
10802
|
-
const server = JSON.parse(
|
|
11462
|
+
const server = JSON.parse(readFileSync12(resolve16(path), "utf8")).mcpServers?.["agent-hub"];
|
|
10803
11463
|
if (server?.command === "bun" && Array.isArray(server.args) && server.args.some((value) => typeof value === "string" && resolve16(value) === bundle))
|
|
10804
11464
|
return "server:agent-hub";
|
|
10805
11465
|
} catch {}
|
|
@@ -10823,7 +11483,7 @@ function claudeNativeLaunch(ctx) {
|
|
|
10823
11483
|
throw new Error("--settings requires a JSON object or readable file so the hub permission hook can be installed");
|
|
10824
11484
|
let caller;
|
|
10825
11485
|
try {
|
|
10826
|
-
caller = JSON.parse(value.trim().startsWith("{") ? value :
|
|
11486
|
+
caller = JSON.parse(value.trim().startsWith("{") ? value : readFileSync12(resolve16(value), "utf8"));
|
|
10827
11487
|
if (!caller || typeof caller !== "object" || Array.isArray(caller))
|
|
10828
11488
|
throw new Error("not an object");
|
|
10829
11489
|
} catch {
|
|
@@ -10852,11 +11512,11 @@ function claudeNativeLaunch(ctx) {
|
|
|
10852
11512
|
const stateDir = ctx.facts?.stateDir ?? ctx.statusLine?.stateDir;
|
|
10853
11513
|
if (!stateDir)
|
|
10854
11514
|
throw new Error("Claude session settings require the hub state directory");
|
|
10855
|
-
const file =
|
|
11515
|
+
const file = join23(stateDir, `claude-settings-${randomUUID10()}.json`);
|
|
10856
11516
|
try {
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
11517
|
+
mkdirSync10(stateDir, { recursive: true, mode: 448 });
|
|
11518
|
+
writeFileSync8(`${file}.tmp`, settingsArgs[1], { mode: 384, flag: "wx" });
|
|
11519
|
+
renameSync4(`${file}.tmp`, file);
|
|
10860
11520
|
} catch {
|
|
10861
11521
|
try {
|
|
10862
11522
|
unlinkSync3(`${file}.tmp`);
|
|
@@ -10909,11 +11569,11 @@ var init_native_cli_codex_launch = __esm(() => {
|
|
|
10909
11569
|
});
|
|
10910
11570
|
|
|
10911
11571
|
// src/hub/events.ts
|
|
10912
|
-
import { appendFileSync, closeSync as closeSync8, existsSync as
|
|
11572
|
+
import { appendFileSync, closeSync as closeSync8, existsSync as existsSync8, fstatSync as fstatSync7, openSync as openSync8, readFileSync as readFileSync13, readSync as readSync5 } from "fs";
|
|
10913
11573
|
function readEvents(file, since = 0) {
|
|
10914
|
-
if (!
|
|
11574
|
+
if (!existsSync8(file))
|
|
10915
11575
|
return [];
|
|
10916
|
-
return
|
|
11576
|
+
return readFileSync13(file, "utf8").split(`
|
|
10917
11577
|
`).flatMap((line) => {
|
|
10918
11578
|
if (!line.trim())
|
|
10919
11579
|
return [];
|
|
@@ -10931,13 +11591,13 @@ var init_events = () => {};
|
|
|
10931
11591
|
// src/cli/recovery-session-file.ts
|
|
10932
11592
|
import { statSync as statSync4 } from "fs";
|
|
10933
11593
|
import { homedir as homedir6 } from "os";
|
|
10934
|
-
import { join as
|
|
11594
|
+
import { join as join24 } from "path";
|
|
10935
11595
|
function checkedSessionPath(binding, kind) {
|
|
10936
11596
|
if (kind === "rollout")
|
|
10937
|
-
return
|
|
10938
|
-
const config = binding.launch.env.CLAUDE_CONFIG_DIR ??
|
|
11597
|
+
return join24(binding.launch.env.CODEX_HOME ?? join24(homedir6(), ".codex"), "sessions");
|
|
11598
|
+
const config = binding.launch.env.CLAUDE_CONFIG_DIR ?? join24(homedir6(), ".claude");
|
|
10939
11599
|
const slug = binding.projectRoot.replace(/[^a-zA-Z0-9]/g, "-");
|
|
10940
|
-
return
|
|
11600
|
+
return join24(config, "projects", slug, `${binding.sessionId}.jsonl`);
|
|
10941
11601
|
}
|
|
10942
11602
|
function recordedFileState(file) {
|
|
10943
11603
|
try {
|
|
@@ -10950,7 +11610,7 @@ var init_recovery_session_file = () => {};
|
|
|
10950
11610
|
|
|
10951
11611
|
// src/adapters/definitions/native-recovery.ts
|
|
10952
11612
|
import { readdirSync as readdirSync4 } from "fs";
|
|
10953
|
-
import { basename as basename5, join as
|
|
11613
|
+
import { basename as basename5, join as join25 } from "path";
|
|
10954
11614
|
var rolloutState = (binding) => {
|
|
10955
11615
|
try {
|
|
10956
11616
|
return readdirSync4(checkedSessionPath(binding, "rollout"), { recursive: true }).some((name) => String(name).endsWith(".jsonl") && basename5(String(name)).includes(binding.sessionId));
|
|
@@ -10974,7 +11634,7 @@ var init_native_recovery = __esm(() => {
|
|
|
10974
11634
|
if (phase !== "plan")
|
|
10975
11635
|
return false;
|
|
10976
11636
|
let current, started = false, turned = false;
|
|
10977
|
-
for (const e of readEvents(
|
|
11637
|
+
for (const e of readEvents(join25(stateDir, "events.jsonl"))) {
|
|
10978
11638
|
if (e.type === "native_thread" && e.peer === binding.peer) {
|
|
10979
11639
|
current = e.thread;
|
|
10980
11640
|
if (e.thread === binding.sessionId && e.fresh)
|
|
@@ -11103,7 +11763,7 @@ function sessionId(field) {
|
|
|
11103
11763
|
return typeof value === "string" && value ? value : undefined;
|
|
11104
11764
|
};
|
|
11105
11765
|
}
|
|
11106
|
-
var switches, implementerRoles, dualForms, headlessForms, hubGrants, CLAUDE_REVIEWER_VARIANT, NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, terminal = (form) => form === "tui", headless = () => false, palette = (fg) => ({ palette: { fg, bg: "transparent" } }), BUILTIN_DEFINITIONS_WITHOUT_QWEN;
|
|
11766
|
+
var switches, implementerRoles, dualForms, headlessForms, hubGrants, CLAUDE_REVIEWER_VARIANT, CLAUDE_IMPLEMENTER_VARIANT, NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, terminal = (form) => form === "tui", headless = () => false, palette = (fg) => ({ palette: { fg, bg: "transparent" } }), BUILTIN_DEFINITIONS_WITHOUT_QWEN;
|
|
11107
11767
|
var init_builtin_metadata = __esm(() => {
|
|
11108
11768
|
init_native_cli_entry_claude();
|
|
11109
11769
|
init_native_cli_entry_codex();
|
|
@@ -11156,6 +11816,25 @@ var init_builtin_metadata = __esm(() => {
|
|
|
11156
11816
|
cli: { command: "claude", startWords: () => ["claude"] },
|
|
11157
11817
|
display: CLAUDE_DISPLAY
|
|
11158
11818
|
};
|
|
11819
|
+
CLAUDE_IMPLEMENTER_VARIANT = {
|
|
11820
|
+
api: 1,
|
|
11821
|
+
kind: "claude",
|
|
11822
|
+
displayName: "Claude Code implementer",
|
|
11823
|
+
family: "claude",
|
|
11824
|
+
transport: "hub-native",
|
|
11825
|
+
roles: Object.freeze(["implementer"]),
|
|
11826
|
+
defaultOnRoster: true,
|
|
11827
|
+
trust: { onPrem: false, hubSandboxed: false },
|
|
11828
|
+
start: { create: createClaudeImplementer, daemonPort: "claude-implementer", forms: headlessForms, defaultForm: "headless", conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
11829
|
+
identity: { mcp: "env", currentChild: true },
|
|
11830
|
+
instances: { mode: "headless", roles: ["implementer", "verifier"] },
|
|
11831
|
+
permission: { grants: { ask: "every write, edit and shell command asks", "ask-when-needed": "file tools inside the owned worktree may run automatically; shell and protected paths ask" } },
|
|
11832
|
+
session: { idField: "sessionId", currentId: sessionId("sessionId"), freshOnRestore: false, launchKind: "claude-implementer", crashPlan: () => ({ resume: {}, how: "Claude implementer resumes its recorded per-task sessions" }) },
|
|
11833
|
+
turnEnd: "adapter",
|
|
11834
|
+
budget: { account: "claude" },
|
|
11835
|
+
cli: { command: "claude", startWords: () => ["claude"] },
|
|
11836
|
+
display: CLAUDE_DISPLAY
|
|
11837
|
+
};
|
|
11159
11838
|
NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN = Object.freeze([
|
|
11160
11839
|
{
|
|
11161
11840
|
projection: { roleRank: 0, helpRank: 0, plannerEligible: true },
|
|
@@ -11179,7 +11858,7 @@ var init_builtin_metadata = __esm(() => {
|
|
|
11179
11858
|
ownedFlags: Object.freeze(["--dangerously-load-development-channels", "--dangerously-skip-permissions"])
|
|
11180
11859
|
},
|
|
11181
11860
|
identity: { mcp: "channel-plugin" },
|
|
11182
|
-
instances: { mode: "external", roles: ["
|
|
11861
|
+
instances: { mode: "external", roles: ["planner", "implementer", "verifier", "reviewer"] },
|
|
11183
11862
|
attach: { externalHello: true, recoveryLaunch: claudeRecoveryLaunch },
|
|
11184
11863
|
permission: { grants: {
|
|
11185
11864
|
ask: "Claude's own permission rules decide",
|
|
@@ -11192,7 +11871,8 @@ var init_builtin_metadata = __esm(() => {
|
|
|
11192
11871
|
cli: { command: "claude", entry: { lifecycleAdmission: true, execute: runClaudeEntry }, preview: previewClaude, usage: [["ahub claude [--print-command] [--unattended] [args...]", "launch Claude Code with the hub channel"]], retainedTerminalNotice: claudeRetainedTerminalNotice, stopNotice: () => "this peer belongs to a native terminal; end it in its terminal", startWords: () => ["claude"] },
|
|
11193
11872
|
display: CLAUDE_DISPLAY,
|
|
11194
11873
|
hooks: { recallPlatform: "claude", recallSource: "claude", recallInjected: true, pluginInventory: claudePluginInventory },
|
|
11195
|
-
reviewerVariant: CLAUDE_REVIEWER_VARIANT
|
|
11874
|
+
reviewerVariant: CLAUDE_REVIEWER_VARIANT,
|
|
11875
|
+
headlessVariant: CLAUDE_IMPLEMENTER_VARIANT
|
|
11196
11876
|
},
|
|
11197
11877
|
{
|
|
11198
11878
|
reviewerVariant: CODEX_REVIEWER_VARIANT,
|
|
@@ -11525,7 +12205,7 @@ async function runQwenEntry(ctx) {
|
|
|
11525
12205
|
var init_native_cli_entry_qwen = () => {};
|
|
11526
12206
|
|
|
11527
12207
|
// src/adapters/definitions/native-cli-preview-qwen.ts
|
|
11528
|
-
import { join as
|
|
12208
|
+
import { join as join26 } from "path";
|
|
11529
12209
|
function previewQwen(ctx) {
|
|
11530
12210
|
const { tool, args, cwd, stateDir, cliDir, config } = ctx;
|
|
11531
12211
|
const state = previewState(args, ctx.baseEnvNames), { unresolved, value } = state;
|
|
@@ -11548,7 +12228,7 @@ function previewQwen(ctx) {
|
|
|
11548
12228
|
form: "headless",
|
|
11549
12229
|
projectRoot: cwd,
|
|
11550
12230
|
stateDir,
|
|
11551
|
-
entrypoint:
|
|
12231
|
+
entrypoint: join26(cliDir, "main.js"),
|
|
11552
12232
|
userArgs: [],
|
|
11553
12233
|
unattended: false,
|
|
11554
12234
|
env: {},
|
|
@@ -11886,7 +12566,12 @@ function resolvedDefinition(id, kind, instances, catalog) {
|
|
|
11886
12566
|
const reviewerOnly = Array.isArray(roles) && !types2.isProxy(roles) && roles.length === 1 && Object.getOwnPropertyDescriptor(roles, "0")?.value === "reviewer";
|
|
11887
12567
|
const variant = definition?.reviewerVariant;
|
|
11888
12568
|
const fresh = variant?.reviewer?.declaration === "automatic" || ownField(declaration, "review_mode") === "fresh";
|
|
11889
|
-
|
|
12569
|
+
const headless = kind !== id && ownField(declaration, "start_mode") === "headless";
|
|
12570
|
+
if (variant && headless && reviewerOnly && fresh)
|
|
12571
|
+
return variant;
|
|
12572
|
+
const roleValues = Array.isArray(roles) && !types2.isProxy(roles) && roles.length <= 4 ? Array.from({ length: roles.length }, (_, index) => Object.getOwnPropertyDescriptor(roles, String(index))?.value) : [];
|
|
12573
|
+
const writerRoles = roleValues.includes("implementer") && roleValues.every((role) => role === "implementer" || role === "verifier");
|
|
12574
|
+
return headless && writerRoles && definition?.headlessVariant ? definition.headlessVariant : definition;
|
|
11890
12575
|
}
|
|
11891
12576
|
function createProjectAdapterCatalog(rawAdapters, externalDefinitions = []) {
|
|
11892
12577
|
const configured = configuredAcpDefinitions(rawAdapters, [...definitions.keys()]);
|
|
@@ -11989,6 +12674,8 @@ function assertDefinitionCoherence(definition) {
|
|
|
11989
12674
|
}
|
|
11990
12675
|
if (definition.reviewerVariant && !definition.reviewerVariant.reviewer)
|
|
11991
12676
|
fail("reviewerVariant.reviewer", "reviewer variant lacks reviewer policy");
|
|
12677
|
+
if (definition.headlessVariant && (definition.headlessVariant.transport !== "hub-native" || !definition.headlessVariant.start.create || definition.headlessVariant.instances?.mode !== "headless"))
|
|
12678
|
+
fail("headlessVariant", "implementer variant requires its headless hub-native factory");
|
|
11992
12679
|
if (definition.reviewer?.unavailable && (definition.start.create || definition.reviewer.backend))
|
|
11993
12680
|
fail("reviewer", "unavailable reviewer must remain metadata-only");
|
|
11994
12681
|
}
|
|
@@ -12005,7 +12692,10 @@ function assertNativeCapabilityScope(definition) {
|
|
|
12005
12692
|
["routing", definition.routing !== undefined],
|
|
12006
12693
|
["projection", definition.projection !== undefined],
|
|
12007
12694
|
["instances.nativeHome", declared(definition.instances, "nativeHome")],
|
|
12695
|
+
["identity.currentChild", declared(definition.identity, "currentChild")],
|
|
12008
12696
|
["reviewerVariant", definition.reviewerVariant !== undefined],
|
|
12697
|
+
["headlessVariant", definition.headlessVariant !== undefined],
|
|
12698
|
+
["start.daemonPort claude-implementer", definition.start.daemonPort === "claude-implementer"],
|
|
12009
12699
|
["reviewer", definition.reviewer !== undefined],
|
|
12010
12700
|
["hooks.factsHook", declared(definition.hooks, "factsHook")],
|
|
12011
12701
|
["cli.entry", declared(definition.cli, "entry")],
|
|
@@ -12028,9 +12718,12 @@ var init_registry = __esm(() => {
|
|
|
12028
12718
|
init_native_cli_doctor();
|
|
12029
12719
|
init_native_display();
|
|
12030
12720
|
fixedBuiltinDefinitions = new Set([...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, QWEN_DEFINITION, AGY_DEFINITION, AGY_REVIEWER_VARIANT]);
|
|
12031
|
-
for (const definition of [...fixedBuiltinDefinitions])
|
|
12721
|
+
for (const definition of [...fixedBuiltinDefinitions]) {
|
|
12032
12722
|
if (definition.reviewerVariant)
|
|
12033
12723
|
fixedBuiltinDefinitions.add(definition.reviewerVariant);
|
|
12724
|
+
if (definition.headlessVariant)
|
|
12725
|
+
fixedBuiltinDefinitions.add(definition.headlessVariant);
|
|
12726
|
+
}
|
|
12034
12727
|
ADAPTERS = immutable([
|
|
12035
12728
|
...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter((definition) => definition.kind !== "jcode"),
|
|
12036
12729
|
QWEN_DEFINITION,
|
|
@@ -12047,6 +12740,10 @@ var init_registry = __esm(() => {
|
|
|
12047
12740
|
assertDefinitionCoherence(definition.reviewerVariant);
|
|
12048
12741
|
assertNativeCapabilityScope(definition.reviewerVariant);
|
|
12049
12742
|
}
|
|
12743
|
+
if (definition.headlessVariant) {
|
|
12744
|
+
assertDefinitionCoherence(definition.headlessVariant);
|
|
12745
|
+
assertNativeCapabilityScope(definition.headlessVariant);
|
|
12746
|
+
}
|
|
12050
12747
|
if (definition.api !== ADAPTER_API || definitions.has(definition.kind))
|
|
12051
12748
|
throw new Error("invalid builtin adapter registry");
|
|
12052
12749
|
definitions.set(definition.kind, definition);
|
|
@@ -19235,10 +19932,13 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
19235
19932
|
exports.default = formatsPlugin;
|
|
19236
19933
|
});
|
|
19237
19934
|
|
|
19935
|
+
// src/adapters/claude-channel.ts
|
|
19936
|
+
init_project();
|
|
19937
|
+
|
|
19238
19938
|
// src/hub/instance-config.ts
|
|
19239
19939
|
init_registry();
|
|
19240
|
-
import { readFileSync as
|
|
19241
|
-
import { join as
|
|
19940
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
19941
|
+
import { join as join29 } from "path";
|
|
19242
19942
|
|
|
19243
19943
|
// src/adapters/external-metadata.ts
|
|
19244
19944
|
import { types as types3 } from "util";
|
|
@@ -19315,8 +20015,8 @@ init_registry();
|
|
|
19315
20015
|
// src/hub/config-trust.ts
|
|
19316
20016
|
init_child_process();
|
|
19317
20017
|
init_git();
|
|
19318
|
-
import { lstatSync as
|
|
19319
|
-
import { dirname as
|
|
20018
|
+
import { lstatSync as lstatSync12, statSync as statSync5 } from "fs";
|
|
20019
|
+
import { dirname as dirname12, join as join27 } from "path";
|
|
19320
20020
|
var MACHINE_LOCAL = [
|
|
19321
20021
|
"kimi_cmd",
|
|
19322
20022
|
"permission_modes",
|
|
@@ -19348,7 +20048,7 @@ function configTracked(cwd, name) {
|
|
|
19348
20048
|
return;
|
|
19349
20049
|
let opened;
|
|
19350
20050
|
try {
|
|
19351
|
-
opened = statSync5(
|
|
20051
|
+
opened = statSync5(join27(cwd, ".agenthub", name));
|
|
19352
20052
|
} catch {}
|
|
19353
20053
|
return r.stdout.split("\x00").filter(Boolean).some((row) => {
|
|
19354
20054
|
const path = row.slice(row.indexOf("\t") + 1);
|
|
@@ -19357,7 +20057,7 @@ function configTracked(cwd, name) {
|
|
|
19357
20057
|
if (!opened)
|
|
19358
20058
|
return path === `.agenthub/${name}`;
|
|
19359
20059
|
try {
|
|
19360
|
-
const s = statSync5(
|
|
20060
|
+
const s = statSync5(join27(cwd, path));
|
|
19361
20061
|
return !!opened && s.dev === opened.dev && s.ino === opened.ino;
|
|
19362
20062
|
} catch {
|
|
19363
20063
|
return false;
|
|
@@ -19413,8 +20113,8 @@ function readAdapterModuleDeclarations(cwd, layers) {
|
|
|
19413
20113
|
|
|
19414
20114
|
// src/hub/control-client.ts
|
|
19415
20115
|
init_project();
|
|
19416
|
-
import { readFileSync as
|
|
19417
|
-
import { join as
|
|
20116
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
20117
|
+
import { join as join28, resolve as resolve17 } from "path";
|
|
19418
20118
|
function stateDirFor(cwd) {
|
|
19419
20119
|
return projectContext(cwd).stateDir;
|
|
19420
20120
|
}
|
|
@@ -19422,8 +20122,8 @@ var PROTOCOL = 16;
|
|
|
19422
20122
|
var RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, 15, PROTOCOL];
|
|
19423
20123
|
function readControl(stateDir) {
|
|
19424
20124
|
try {
|
|
19425
|
-
const status = JSON.parse(
|
|
19426
|
-
const token =
|
|
20125
|
+
const status = JSON.parse(readFileSync14(join28(stateDir, "status.json"), "utf8"));
|
|
20126
|
+
const token = readFileSync14(join28(stateDir, "control-token"), "utf8").trim();
|
|
19427
20127
|
if (!Number.isInteger(status.controlPort) || status.controlPort < 1 || status.controlPort > 65535 || !token)
|
|
19428
20128
|
return;
|
|
19429
20129
|
const startModes = status.startModes && typeof status.startModes === "object" && !Array.isArray(status.startModes) ? Object.fromEntries(Object.entries(status.startModes).filter((entry) => entry[1] === "tui" || entry[1] === "headless")) : undefined;
|
|
@@ -19497,13 +20197,13 @@ class ControlClient {
|
|
|
19497
20197
|
return new Promise((resolve, reject) => {
|
|
19498
20198
|
const ws = new WebSocket(control.url);
|
|
19499
20199
|
const client = new ControlClient(ws);
|
|
19500
|
-
const timer = setTimeout(() => refuse(new Error(`hub connection timed out at ${control.url}`)), timeoutMs);
|
|
20200
|
+
const timer = setTimeout(() => refuse(Object.assign(new Error(`hub connection timed out at ${control.url}`), { code: "AHUB_CONNECT_TIMEOUT" })), timeoutMs);
|
|
19501
20201
|
const refuse = (error) => {
|
|
19502
20202
|
clearTimeout(timer);
|
|
19503
20203
|
reject(error);
|
|
19504
20204
|
ws.close();
|
|
19505
20205
|
};
|
|
19506
|
-
ws.onerror = () => refuse(new Error(`cannot reach hub at ${control.url}`));
|
|
20206
|
+
ws.onerror = () => refuse(Object.assign(new Error(`cannot reach hub at ${control.url}`), { code: "AHUB_CONNECT_UNREACHABLE" }));
|
|
19507
20207
|
ws.onclose = (ev) => {
|
|
19508
20208
|
clearTimeout(timer);
|
|
19509
20209
|
reject(Object.assign(new Error(`hub closed the connection: ${ev.reason || "stale token?"}`), { code: ev.code }));
|
|
@@ -19574,10 +20274,11 @@ init_git();
|
|
|
19574
20274
|
|
|
19575
20275
|
// src/hub/tasks.ts
|
|
19576
20276
|
init_spec_read();
|
|
20277
|
+
init_sdlc_metrics();
|
|
19577
20278
|
// package.json
|
|
19578
20279
|
var package_default = {
|
|
19579
20280
|
name: "@staix/agent-hub",
|
|
19580
|
-
version: "0.
|
|
20281
|
+
version: "0.15.0",
|
|
19581
20282
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
19582
20283
|
license: "MIT",
|
|
19583
20284
|
type: "module",
|
|
@@ -19784,6 +20485,18 @@ init_path_policy();
|
|
|
19784
20485
|
// src/hub/tasks.ts
|
|
19785
20486
|
init_profiles();
|
|
19786
20487
|
init_workflow();
|
|
20488
|
+
init_deliverables();
|
|
20489
|
+
|
|
20490
|
+
// src/hub/deliverable-files.ts
|
|
20491
|
+
import { constants as constants7, lstatSync as lstatSync13 } from "fs";
|
|
20492
|
+
init_tools();
|
|
20493
|
+
init_permission_mode();
|
|
20494
|
+
init_project();
|
|
20495
|
+
init_deliverables();
|
|
20496
|
+
var CHUNK = 1024 * 1024;
|
|
20497
|
+
var NOFOLLOW = constants7.O_NOFOLLOW ?? 0;
|
|
20498
|
+
|
|
20499
|
+
// src/hub/tasks.ts
|
|
19787
20500
|
init_board();
|
|
19788
20501
|
init_envelope();
|
|
19789
20502
|
init_routing();
|
|
@@ -19825,7 +20538,7 @@ import { types as types4 } from "util";
|
|
|
19825
20538
|
init_project();
|
|
19826
20539
|
init_registry();
|
|
19827
20540
|
init_permission_mode();
|
|
19828
|
-
import { dirname as
|
|
20541
|
+
import { dirname as dirname13, isAbsolute as isAbsolute8, sep as sep3 } from "path";
|
|
19829
20542
|
var JCODE_FIELDS = ["jcode_home", "provider", "model", "provider_profile"];
|
|
19830
20543
|
var MAX_JCODE_INSTANCES = 8;
|
|
19831
20544
|
var NATIVE_WORD = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,127}$/;
|
|
@@ -19921,23 +20634,36 @@ function validateInstances(raw, catalog) {
|
|
|
19921
20634
|
if (freshReviewer && entry.permission_mode)
|
|
19922
20635
|
return fail4(`${path}.permission_mode`, "a fresh reviewer has a fixed read-only policy");
|
|
19923
20636
|
const external = definition.instances?.mode === "external";
|
|
19924
|
-
const
|
|
20637
|
+
const headlessWriter = entry.start_mode?.value === "headless" && !!definition.headlessVariant && declaredRoles.includes("implementer") && declaredRoles.every((role) => role === "implementer" || role === "verifier");
|
|
20638
|
+
if (headlessWriter && !entry.worktree)
|
|
20639
|
+
return fail4(`${path}.worktree`, "a headless implementer needs its own worktree");
|
|
20640
|
+
if (headlessWriter && entry.permission_mode && entry.permission_mode.value !== "ask" && entry.permission_mode.value !== "ask-when-needed")
|
|
20641
|
+
return fail4(`${path}.permission_mode`, "a headless implementer permits only ask or ask-when-needed");
|
|
20642
|
+
const runtime = freshReviewer ? definition.reviewerVariant ?? definition : headlessWriter ? definition.headlessVariant : definition;
|
|
19925
20643
|
const unavailableReviewer = freshReviewer && !!(definition.reviewerVariant ?? definition).reviewer?.unavailable;
|
|
19926
20644
|
if (!unavailableReviewer && runtime.instances?.mode !== "external" && !supportsHeadlessInstances(runtime))
|
|
19927
20645
|
return fail4(`${path}.kind`, definition.reviewerVariant ? 'unsupported; this kind only declares headless reviewer instances with roles exactly ["reviewer"]' : "unsupported; explicit external instance policy or a headless instance policy with a headless factory is required");
|
|
20646
|
+
if (declaredRoles.includes("planner") && definition.projection?.plannerEligible !== true)
|
|
20647
|
+
return fail4(`${path}.roles`, "this kind cannot plan");
|
|
19928
20648
|
if (external) {
|
|
19929
20649
|
const declared = entry.roles ? roles(entry.roles.value, `${path}.roles`) : [];
|
|
19930
|
-
if (entry.start_mode && definition.reviewerVariant) {
|
|
20650
|
+
if (headlessWriter) {} else if (entry.start_mode && definition.reviewerVariant) {
|
|
20651
|
+
const reviewerRoles = declared.length === 1 && declared[0] === "reviewer";
|
|
20652
|
+
if (!reviewerRoles)
|
|
20653
|
+
return fail4(`${path}.roles`, 'a headless channel-kind instance is a reviewer: roles must be exactly ["reviewer"]');
|
|
19931
20654
|
if (entry.worktree)
|
|
19932
20655
|
return fail4(path, "a headless reviewer reads the project checkout: worktree is not supported");
|
|
19933
|
-
if (declared.length !== 1 || declared[0] !== "reviewer")
|
|
19934
|
-
return fail4(`${path}.roles`, 'a headless channel-kind instance is a reviewer: roles must be exactly ["reviewer"]');
|
|
19935
20656
|
} else {
|
|
19936
|
-
if (entry.start_mode
|
|
19937
|
-
return fail4(path
|
|
20657
|
+
if (entry.start_mode)
|
|
20658
|
+
return fail4(`${path}.start_mode`, `a channel instance starts only from the person's terminal (ahub <kind> --as <id>); a headless reviewer declares roles ["reviewer"] and start_mode "headless"`);
|
|
19938
20659
|
const allowed = definition.instances?.roles ?? definition.roles;
|
|
19939
20660
|
if (!declared.length || declared.some((role) => !allowed.includes(role)))
|
|
19940
|
-
return fail4(`${path}.roles`,
|
|
20661
|
+
return fail4(`${path}.roles`, `a channel instance must declare its roles within: ${allowed.join(", ")}`);
|
|
20662
|
+
const writes = declared.includes("implementer") || declared.includes("verifier");
|
|
20663
|
+
if (writes && !entry.worktree)
|
|
20664
|
+
return fail4(`${path}.worktree`, "an implementer or verifier channel instance needs its own worktree");
|
|
20665
|
+
if (!writes && entry.worktree)
|
|
20666
|
+
return fail4(`${path}.worktree`, "a planner or reviewer channel instance reads the project checkout: worktree is not supported");
|
|
19941
20667
|
}
|
|
19942
20668
|
} else {
|
|
19943
20669
|
const allowed = runtime.instances?.roles;
|
|
@@ -19990,7 +20716,8 @@ function effectiveInstanceRoles(id, declarations, policyRoles, catalog) {
|
|
|
19990
20716
|
throw new Error(`${id}: no declared native instance`);
|
|
19991
20717
|
const declaration = declarations && Object.hasOwn(declarations, id) ? declarations[id] : undefined;
|
|
19992
20718
|
const ownRoles = (key) => Object.hasOwn(policyRoles, key) ? policyRoles[key] : undefined;
|
|
19993
|
-
|
|
20719
|
+
const roles = declaration?.roles ?? ownRoles(id) ?? ownRoles(definition.kind) ?? definition.roles;
|
|
20720
|
+
return roles.filter((role) => role !== "planner" || definition.projection?.plannerEligible === true);
|
|
19994
20721
|
}
|
|
19995
20722
|
|
|
19996
20723
|
// src/hub/instance-config.ts
|
|
@@ -19998,7 +20725,7 @@ var INSTANCE_CONFIG_FILES = ["config.json", "config.local.json"];
|
|
|
19998
20725
|
function readInstanceConfigLayers(cwd) {
|
|
19999
20726
|
return INSTANCE_CONFIG_FILES.flatMap((name) => {
|
|
20000
20727
|
try {
|
|
20001
|
-
return [{ name, raw: JSON.parse(
|
|
20728
|
+
return [{ name, raw: JSON.parse(readFileSync15(join29(cwd, ".agenthub", name), "utf8")) }];
|
|
20002
20729
|
} catch (error) {
|
|
20003
20730
|
if (error.code === "ENOENT")
|
|
20004
20731
|
return [];
|
|
@@ -28351,8 +29078,8 @@ class StdioServerTransport {
|
|
|
28351
29078
|
}
|
|
28352
29079
|
|
|
28353
29080
|
// src/adapters/claude-channel.ts
|
|
28354
|
-
import { readFileSync as
|
|
28355
|
-
import { join as
|
|
29081
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
29082
|
+
import { join as join30 } from "path";
|
|
28356
29083
|
init_hub_tools();
|
|
28357
29084
|
init_envelope();
|
|
28358
29085
|
|
|
@@ -28385,6 +29112,7 @@ ${action}`;
|
|
|
28385
29112
|
var stateDir = process.env.AGENTHUB_STATE_DIR ?? stateDirFor(process.cwd());
|
|
28386
29113
|
var projectRoot2 = process.env.AGENTHUB_PROJECT_DIR ?? process.cwd();
|
|
28387
29114
|
var toolsMode = process.env.AGENTHUB_MODE === "tools";
|
|
29115
|
+
var permissionTool = toolsMode && process.env.AGENTHUB_PERMISSION_TOOL === "1";
|
|
28388
29116
|
var unidentified = toolsMode && !process.env.AGENTHUB_PEER_ID;
|
|
28389
29117
|
var peerId = process.env.AGENTHUB_PEER_ID ?? (toolsMode ? "" : "claude");
|
|
28390
29118
|
var UNIDENTIFIED = "agent-hub tools refused: this process was not started by the hub (no AGENTHUB_PEER_ID), so it has no peer identity. Start the agent with ahub jcode (or the hub's own start) to use hub tools.";
|
|
@@ -28393,7 +29121,7 @@ var channel = !toolsMode && process.env.AGENTHUB_CHANNEL === "1";
|
|
|
28393
29121
|
function roles2() {
|
|
28394
29122
|
const read = (name) => {
|
|
28395
29123
|
try {
|
|
28396
|
-
return JSON.parse(
|
|
29124
|
+
return JSON.parse(readFileSync16(join30(projectRoot2, ".agenthub", name), "utf8")).roles ?? {};
|
|
28397
29125
|
} catch {
|
|
28398
29126
|
return {};
|
|
28399
29127
|
}
|
|
@@ -28412,7 +29140,7 @@ function sessionProfile() {
|
|
|
28412
29140
|
for (const name of ["config.json", "config.local.json"]) {
|
|
28413
29141
|
let data;
|
|
28414
29142
|
try {
|
|
28415
|
-
data = JSON.parse(
|
|
29143
|
+
data = JSON.parse(readFileSync16(join30(projectRoot2, ".agenthub", name), "utf8"));
|
|
28416
29144
|
} catch (error) {
|
|
28417
29145
|
if (error.code === "ENOENT")
|
|
28418
29146
|
continue;
|
|
@@ -28439,7 +29167,7 @@ function sessionRoles() {
|
|
|
28439
29167
|
const read = (name) => {
|
|
28440
29168
|
let raw;
|
|
28441
29169
|
try {
|
|
28442
|
-
raw = JSON.parse(
|
|
29170
|
+
raw = JSON.parse(readFileSync16(join30(projectRoot2, ".agenthub", name), "utf8")).catalog_roles;
|
|
28443
29171
|
} catch {
|
|
28444
29172
|
return {};
|
|
28445
29173
|
}
|
|
@@ -28469,7 +29197,7 @@ var HELD = `another session is attached to the hub as "${peerId}"; this one is s
|
|
|
28469
29197
|
var INBOX_CAP = 200;
|
|
28470
29198
|
function peerHeld() {
|
|
28471
29199
|
try {
|
|
28472
|
-
const status = JSON.parse(
|
|
29200
|
+
const status = JSON.parse(readFileSync16(join30(stateDir, "status.json"), "utf8"));
|
|
28473
29201
|
const peer = status.peers?.[peerId];
|
|
28474
29202
|
if (channel && peer?.toolsOnly && !peer.claiming)
|
|
28475
29203
|
return false;
|
|
@@ -28605,11 +29333,18 @@ async function connectLoop() {
|
|
|
28605
29333
|
let code;
|
|
28606
29334
|
let closeReason;
|
|
28607
29335
|
try {
|
|
29336
|
+
let executionCwd;
|
|
29337
|
+
if (!toolsMode) {
|
|
29338
|
+
try {
|
|
29339
|
+
executionCwd = realPath(process.cwd());
|
|
29340
|
+
} catch {}
|
|
29341
|
+
}
|
|
28608
29342
|
const client = await ControlClient.connect(stateDir, {
|
|
28609
29343
|
role: toolsMode ? "tools" : "peer",
|
|
28610
29344
|
peer: peerId,
|
|
28611
29345
|
...toolsMode ? {} : { channel },
|
|
28612
29346
|
...process.env.AGENTHUB_LAUNCH_ID ? { launchId: process.env.AGENTHUB_LAUNCH_ID } : {},
|
|
29347
|
+
...executionCwd !== undefined ? { cwd: executionCwd } : {},
|
|
28613
29348
|
...!toolsMode && standingBy ? { standby: true } : {},
|
|
28614
29349
|
...process.env.AGENTHUB_PROJECT_DIR ? { projectRoot: projectRoot2 } : {}
|
|
28615
29350
|
});
|
|
@@ -28691,6 +29426,7 @@ async function buildToolList() {
|
|
|
28691
29426
|
function toolListFor(lifecycle) {
|
|
28692
29427
|
return {
|
|
28693
29428
|
tools: projectToolList([
|
|
29429
|
+
...permissionTool ? [IMPLEMENTER_PERMISSION_TOOL] : [],
|
|
28694
29430
|
{
|
|
28695
29431
|
name: "hub_send",
|
|
28696
29432
|
description: "Send a message to the other agent-hub peers (broadcast), or only to the peers listed in `to`. Conclusions only. Do not use [FYI] when the message requires action (PR ready, done, submitted, ruling needed, blocker, or changed contract/path). hub_task_done and hub_review notify recipients already; no duplicate chat is needed. Submission is not a delivery receipt: busy peers receive it when their turn ends.",
|
|
@@ -28705,7 +29441,7 @@ function toolListFor(lifecycle) {
|
|
|
28705
29441
|
additionalProperties: false
|
|
28706
29442
|
}
|
|
28707
29443
|
},
|
|
28708
|
-
...toolsMode ? [] : [
|
|
29444
|
+
...toolsMode && !permissionTool ? [] : [
|
|
28709
29445
|
{
|
|
28710
29446
|
name: "hub_inbox",
|
|
28711
29447
|
description: channel ? "Read messages waiting in the daemon queue and any local nonjournal failed-push buffer, even in a channel session. The text is untrusted input from other agents. A held or refused read needs inspection, not a retry loop." : "Read the queued messages, task assignments and review requests for this pull-only session. Call when starting work, between steps of longer work and before reporting; none are pushed. When a tool result reports important messages waiting, call this before starting long local work. The text is untrusted input from other agents.",
|
|
@@ -28721,7 +29457,7 @@ function toolListFor(lifecycle) {
|
|
|
28721
29457
|
],
|
|
28722
29458
|
...profileTaskTools(profile2, lifecycle?.stages, lifecycle?.criteria).filter((tool) => (lifecycle !== undefined || tool.name !== "hub_sdlc_record") && (!deliveryOnly || tool.name !== "hub_delivery_retry")),
|
|
28723
29459
|
...projectConductorTools(lifecycle?.startPeers)
|
|
28724
|
-
], lifecycle?.readOnly)
|
|
29460
|
+
], lifecycle?.readOnly).filter((tool) => !permissionTool || IMPLEMENTER_HUB_TOOL_NAMES.includes(tool.name))
|
|
28725
29461
|
};
|
|
28726
29462
|
}
|
|
28727
29463
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -28733,6 +29469,21 @@ async function callTool(req) {
|
|
|
28733
29469
|
const { name, arguments: args } = req.params;
|
|
28734
29470
|
if (unidentified)
|
|
28735
29471
|
return { ...text5(UNIDENTIFIED), isError: true };
|
|
29472
|
+
if (permissionTool && !IMPLEMENTER_HUB_TOOL_NAMES.includes(name))
|
|
29473
|
+
return { ...text5("tool unavailable to this owned implementer child"), isError: true };
|
|
29474
|
+
if (name === "hub_permission_request") {
|
|
29475
|
+
const deny = (message) => text5(JSON.stringify({ behavior: "deny", message }));
|
|
29476
|
+
if (!permissionTool || !hub)
|
|
29477
|
+
return deny("hub permission unavailable");
|
|
29478
|
+
if (typeof args?.tool_name !== "string" || !/^[A-Za-z][A-Za-z0-9_.:/-]{0,127}$/.test(args.tool_name) || !args.input || typeof args.input !== "object" || Array.isArray(args.input) || typeof args.tool_use_id !== "string" || !args.tool_use_id.length || args.tool_use_id.length > 256)
|
|
29479
|
+
return deny("invalid native permission request");
|
|
29480
|
+
try {
|
|
29481
|
+
const res = await hub.request({ t: "permission_request", tool: args?.tool_name, input: args?.input }, 3605000);
|
|
29482
|
+
return res.ok === true && res.behavior === "allow" ? text5(JSON.stringify({ behavior: "allow", updatedInput: args?.input })) : deny(typeof res.message === "string" ? res.message : "permission denied");
|
|
29483
|
+
} catch {
|
|
29484
|
+
return deny("hub permission unavailable");
|
|
29485
|
+
}
|
|
29486
|
+
}
|
|
28736
29487
|
if (name === "hub_delivery_done" && channel) {
|
|
28737
29488
|
if (!hub)
|
|
28738
29489
|
return text5(offline());
|
|
@@ -28740,7 +29491,7 @@ async function callTool(req) {
|
|
|
28740
29491
|
const result = await hub.request({ t: "delivery_complete", deliveryId: a.delivery_id, generation: a.delivery_generation });
|
|
28741
29492
|
return text5(result.ok ? "delivery completed" : `not completed: ${result.error}`);
|
|
28742
29493
|
}
|
|
28743
|
-
if (name === "hub_inbox" && !toolsMode) {
|
|
29494
|
+
if (name === "hub_inbox" && (!toolsMode || permissionTool)) {
|
|
28744
29495
|
let remote = [], notice = "", polling = "", counted = false;
|
|
28745
29496
|
if (hub) {
|
|
28746
29497
|
try {
|