@staix/agent-hub 0.12.35 → 0.12.36
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 +14 -0
- package/README.md +8 -6
- package/docs/agent-notes/adapters.md +4 -1
- package/docs/agent-notes/budget.md +1 -1
- package/docs/agent-notes/bus.md +3 -0
- package/docs/agent-notes/daemon.md +6 -2
- package/docs/agent-notes/tasks.md +11 -1
- package/docs/agent-notes/tests.md +5 -1
- package/docs/operations.md +40 -16
- package/docs/quickstart.md +6 -8
- package/docs/sdlc.md +46 -5
- package/docs/security.md +5 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +76 -1
- package/docs/verification/2026-10-19-0.12.36.md +63 -0
- package/docs/verified.json +18 -14
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +385 -29
- package/src/adapters/claude-print.ts +230 -0
- package/src/adapters/definitions/builtin-metadata.ts +17 -1
- package/src/adapters/definitions/native-cli-entry-claude.ts +17 -3
- package/src/adapters/definitions/native-cli-entry-jcode.ts +3 -2
- package/src/adapters/definitions/native-cli-entry-kimi.ts +3 -2
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-factories.ts +7 -0
- package/src/adapters/registry-types.ts +8 -2
- package/src/adapters/registry.ts +13 -1
- package/src/cli/help.ts +5 -0
- package/src/cli/identity.ts +2 -1
- package/src/cli/launch.ts +19 -6
- package/src/cli/main.ts +38 -4
- package/src/cli/reset.ts +4 -2
- package/src/cli/statusline-tee.ts +5 -2
- package/src/cli/terminal-recovery.ts +7 -2
- package/src/cli/upgrade-runtime.ts +18 -15
- package/src/hub/board.ts +29 -7
- package/src/hub/bus.ts +11 -5
- package/src/hub/claude-state.ts +37 -0
- package/src/hub/cost-governance.ts +76 -0
- package/src/hub/daemon.ts +181 -73
- package/src/hub/hub-tools.ts +20 -1
- package/src/hub/instances.ts +17 -3
- package/src/hub/review-brief.ts +175 -0
- package/src/hub/routing.ts +17 -2
- package/src/hub/sdlc.ts +205 -4
- package/src/hub/status-roster.ts +10 -0
- package/src/hub/task-sweep.ts +143 -0
- package/src/hub/tasks.ts +544 -26
- package/src/hub/usage.ts +57 -20
|
@@ -44,12 +44,35 @@ var __esm = (fn, res, err) => () => {
|
|
|
44
44
|
return res;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
// src/hub/claude-state.ts
|
|
48
|
+
function claudeStateFile(base, peer) {
|
|
49
|
+
return peer.includes("-") && PEER.test(peer) ? `${base}.${peer}.json` : `${base}.json`;
|
|
50
|
+
}
|
|
51
|
+
var PEER;
|
|
52
|
+
var init_claude_state = __esm(() => {
|
|
53
|
+
PEER = /^[a-z][a-z0-9-]{0,31}$/;
|
|
54
|
+
});
|
|
55
|
+
|
|
47
56
|
// src/adapters/definitions/native-cli-entry-claude.ts
|
|
48
57
|
import { readFileSync } from "fs";
|
|
49
58
|
import { join } from "path";
|
|
50
59
|
import { randomUUID } from "crypto";
|
|
51
60
|
async function runClaudeEntry(ctx) {
|
|
52
61
|
const { args, cwd, stateDir, cliDir, unattendedEnv, projectConfig, fail } = ctx;
|
|
62
|
+
const peer = ctx.instance ?? "claude";
|
|
63
|
+
if (ctx.hubStarted) {
|
|
64
|
+
if (args.includes("--print-command") || args.includes("--dry-run"))
|
|
65
|
+
return console.log(JSON.stringify({ peer, start: "hub", note: "a headless reviewer the hub runs per review" }, null, 2));
|
|
66
|
+
const hub = await ctx.connect();
|
|
67
|
+
try {
|
|
68
|
+
const res = await hub.request({ t: "start", peer, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
69
|
+
if (res.ok !== true)
|
|
70
|
+
fail(String(res.error ?? `${peer} was not started`));
|
|
71
|
+
} finally {
|
|
72
|
+
hub.close();
|
|
73
|
+
}
|
|
74
|
+
return console.log(`${peer} started: a headless reviewer the hub runs per review (stop it with ahub stop ${peer})`);
|
|
75
|
+
}
|
|
53
76
|
const { launcherPreview, readControl, recordTerminalLaunch, claudeObservationHooks, buildLaunch, runClaudeLaunch, processSignature, recordClaudeLaunch, cleanupStaleClaudeSettings, nativeLaunchEnv, childEnv, cleanupClaudeSettings } = ctx.helpers;
|
|
54
77
|
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
55
78
|
try {
|
|
@@ -61,7 +84,7 @@ async function runClaudeEntry(ctx) {
|
|
|
61
84
|
const control = readControl(stateDir);
|
|
62
85
|
if (control?.instanceId) {
|
|
63
86
|
process.env.AGENTHUB_INSTANCE_ID = control.instanceId;
|
|
64
|
-
const terminal = await recordTerminalLaunch(
|
|
87
|
+
const terminal = await recordTerminalLaunch(peer, cwd, stateDir, control.instanceId, ctx.instance ? { executionCwd: cwd } : undefined);
|
|
65
88
|
if (!terminal)
|
|
66
89
|
process.env.AGENTHUB_LAUNCH_ID = randomUUID();
|
|
67
90
|
}
|
|
@@ -74,20 +97,22 @@ async function runClaudeEntry(ctx) {
|
|
|
74
97
|
const facts = claudeObservationHooks(projectConfig(), { script: join(cliDir, "facts-hook.ts"), stateDir });
|
|
75
98
|
let previousLaunch;
|
|
76
99
|
try {
|
|
77
|
-
previousLaunch = JSON.parse(readFileSync(join(stateDir, "claude-launch
|
|
100
|
+
previousLaunch = JSON.parse(readFileSync(join(stateDir, claudeStateFile("claude-launch", peer)), "utf8"));
|
|
78
101
|
} catch {}
|
|
79
102
|
const launch = buildLaunch("claude", args, { unattended: unattendedEnv, statusLine: { script: join(cliDir, "statusline-tee.ts"), stateDir, ...original ? { original } : {} }, ...facts ? { facts } : {} });
|
|
80
103
|
let result;
|
|
81
104
|
try {
|
|
82
105
|
let launchRecord;
|
|
83
106
|
if (control?.instanceId) {
|
|
84
|
-
launchRecord = { 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 };
|
|
107
|
+
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 };
|
|
85
108
|
recordClaudeLaunch(stateDir, launchRecord);
|
|
86
109
|
cleanupStaleClaudeSettings(stateDir, previousLaunch);
|
|
87
110
|
}
|
|
88
111
|
if (launch.warning)
|
|
89
112
|
console.error(launch.warning);
|
|
90
113
|
const env = nativeLaunchEnv("claude", childEnv());
|
|
114
|
+
if (ctx.instance)
|
|
115
|
+
env.AGENTHUB_PEER_ID = peer;
|
|
91
116
|
result = await runClaudeLaunch(launch, { cwd, stateDir, env: { ...env, AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd }, ...launchRecord ? { record: launchRecord } : {} });
|
|
92
117
|
} finally {
|
|
93
118
|
cleanupClaudeSettings(stateDir, launch.settingsFile);
|
|
@@ -96,7 +121,9 @@ async function runClaudeEntry(ctx) {
|
|
|
96
121
|
fail(`cannot run ${launch.cmd}: ${result.error.message}`);
|
|
97
122
|
process.exit(result.status ?? 1);
|
|
98
123
|
}
|
|
99
|
-
var init_native_cli_entry_claude = () => {
|
|
124
|
+
var init_native_cli_entry_claude = __esm(() => {
|
|
125
|
+
init_claude_state();
|
|
126
|
+
});
|
|
100
127
|
|
|
101
128
|
// src/adapters/definitions/native-cli-entry-codex.ts
|
|
102
129
|
async function runCodexEntry(ctx) {
|
|
@@ -142,6 +169,10 @@ function rosterInStatus(status, peer) {
|
|
|
142
169
|
const row = rows.find((r) => !!r && typeof r === "object" && r.peer === peer);
|
|
143
170
|
return typeof row?.on === "boolean" ? row.on : undefined;
|
|
144
171
|
}
|
|
172
|
+
function bundleInStatus(status) {
|
|
173
|
+
const value = status?.pluginBundle;
|
|
174
|
+
return typeof value === "string" && value.startsWith("/") && /[\\/]plugins[\\/]agent-hub[\\/]server\.js$/.test(value) ? value : undefined;
|
|
175
|
+
}
|
|
145
176
|
|
|
146
177
|
// src/adapters/definitions/native-cli-entry-jcode.ts
|
|
147
178
|
import { resolve } from "path";
|
|
@@ -168,7 +199,7 @@ async function runJcodeEntry(ctx) {
|
|
|
168
199
|
fail("cannot read jcode's roster membership from the running hub; upgrade or restart it, then retry (nothing was started)");
|
|
169
200
|
if (!rostered)
|
|
170
201
|
fail("jcode is off roster; add it with ahub roster add jcode");
|
|
171
|
-
const written = configureJcodeMcp(cwd, stateDir, resolve(cliDir, "../../plugins/agent-hub/server.js"));
|
|
202
|
+
const written = configureJcodeMcp(cwd, stateDir, bundleInStatus(snapshot) ?? resolve(cliDir, "../../plugins/agent-hub/server.js"));
|
|
172
203
|
if (written.tracked)
|
|
173
204
|
console.error(".jcode/mcp.json is tracked; review this local machine-path change before committing it");
|
|
174
205
|
const reply = await hub.request({ t: "start", peer: "jcode", args: { mode: "headless", model: one["--model"], providerProfile: one["--provider-profile"], sessionId: one["--session-id"] }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 65000);
|
|
@@ -261,7 +292,7 @@ async function runKimiEntry(ctx) {
|
|
|
261
292
|
fail(admission.error);
|
|
262
293
|
let result;
|
|
263
294
|
try {
|
|
264
|
-
const written = configureKimiMcp(cwd, stateDir, resolve2(cliDir, "../../plugins/agent-hub/server.js"), "tui");
|
|
295
|
+
const written = configureKimiMcp(cwd, stateDir, bundleInStatus(snapshot) ?? resolve2(cliDir, "../../plugins/agent-hub/server.js"), "tui");
|
|
265
296
|
if (written.tracked)
|
|
266
297
|
console.error(".kimi-code/mcp.json is tracked; the agent-hub entry contains machine paths. Review this local change before committing it.");
|
|
267
298
|
result = spawnSync(launch.cmd, launch.args, { cwd, stdio: "inherit", env: { ...nativeLaunchEnv("kimi", childEnv()), AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } });
|
|
@@ -6781,7 +6812,7 @@ function catalogAssignments(raw) {
|
|
|
6781
6812
|
throw new Error("catalog_roles must be a bounded peer map");
|
|
6782
6813
|
const out = Object.create(null);
|
|
6783
6814
|
for (const [peer, ids] of Object.entries(raw)) {
|
|
6784
|
-
if (!
|
|
6815
|
+
if (!PEER2.test(peer) || ["user", "hub", "digest"].includes(peer) || !Array.isArray(ids) || ids.length > 2 || new Set(ids).size !== ids.length || ids.some((id) => typeof id !== "string" || !ID.test(id)))
|
|
6785
6816
|
throw new Error("catalog_roles takes at most two distinct role ids per peer");
|
|
6786
6817
|
out[peer] = [...ids];
|
|
6787
6818
|
}
|
|
@@ -6808,7 +6839,7 @@ function catalogSection(roles) {
|
|
|
6808
6839
|
|
|
6809
6840
|
`);
|
|
6810
6841
|
}
|
|
6811
|
-
var ROLE_SECTIONS, BASES2, CLASSES2, ID,
|
|
6842
|
+
var ROLE_SECTIONS, BASES2, CLASSES2, ID, PEER2;
|
|
6812
6843
|
var init_roles = __esm(() => {
|
|
6813
6844
|
init_envelope();
|
|
6814
6845
|
init_shipped_roles();
|
|
@@ -6816,7 +6847,7 @@ var init_roles = __esm(() => {
|
|
|
6816
6847
|
BASES2 = ["planner", "implementer", "verifier", "reviewer"];
|
|
6817
6848
|
CLASSES2 = ["plan", "implement", "bulk_edit", "test", "review", "summarize", "triage"];
|
|
6818
6849
|
ID = /^[a-z][a-z0-9-]{0,47}$/;
|
|
6819
|
-
|
|
6850
|
+
PEER2 = /^[a-z][a-z0-9-]{0,31}$/;
|
|
6820
6851
|
});
|
|
6821
6852
|
|
|
6822
6853
|
// src/hub/profiles.ts
|
|
@@ -6934,14 +6965,22 @@ function profileTaskTools(profile = "dev", stages, criteria = false) {
|
|
|
6934
6965
|
const selectedPlan = { ...plan, properties: Object.fromEntries(data.plan_keys.map((key) => [key, plan.properties[key] ?? list(key === "sections" ? "document heading anchors, such as report.md#methods" : key === "sources" ? "citation ids, DOI or source URL ids" : "short claim ids to verify")])) };
|
|
6935
6966
|
properties.parts = { ...parts, items: { ...parts.items, properties: { ...parts.items.properties, plan: selectedPlan } } };
|
|
6936
6967
|
}
|
|
6968
|
+
if (criteria && properties.plan) {
|
|
6969
|
+
const current = properties.plan;
|
|
6970
|
+
properties.plan = { ...current, properties: { ...current.properties, ...costPlanProperties } };
|
|
6971
|
+
}
|
|
6972
|
+
if (criteria && tool.name === "hub_backlog_split") {
|
|
6973
|
+
const parts = properties.parts;
|
|
6974
|
+
properties.parts = { ...parts, items: { ...parts.items, properties: { ...parts.items.properties, plan: { ...parts.items.properties.plan, properties: { ...parts.items.properties.plan.properties, ...costPlanProperties } } } } };
|
|
6975
|
+
}
|
|
6937
6976
|
return { ...tool, ...customProfile && tool.name === "hub_task_propose" ? { description: tool.description + ` Selected profile ${data.name}: ${Object.keys(data.classes).join(", ")}. Routing uses their existing base classes.` } : {}, inputSchema: { ...tool.inputSchema, properties } };
|
|
6938
6977
|
});
|
|
6939
6978
|
}
|
|
6940
|
-
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 }), plan, acceptPlan, tool = (name, description, properties, required = []) => ({
|
|
6979
|
+
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 }), costPlanProperties, plan, acceptPlan, tool = (name, description, properties, required = []) => ({
|
|
6941
6980
|
name,
|
|
6942
6981
|
description,
|
|
6943
6982
|
inputSchema: { type: "object", properties, required, additionalProperties: false }
|
|
6944
|
-
}), BOUNDED_TASK_TOOLS, TASK_TOOLS, TASK_TOOL_NAMES, CONDUCTOR_TOOLS, CONDUCTOR_TOOL_NAMES, ROLE_TEXT, DEFAULT_ROLES;
|
|
6983
|
+
}), BOUNDED_TASK_TOOLS, TASK_TOOLS, PLANNER_REVIEW_TOOLS, TASK_TOOL_NAMES, CONDUCTOR_TOOLS, CONDUCTOR_TOOL_NAMES, ROLE_TEXT, DEFAULT_ROLES;
|
|
6945
6984
|
var init_hub_tools = __esm(() => {
|
|
6946
6985
|
init_sdlc();
|
|
6947
6986
|
init_profiles();
|
|
@@ -6956,6 +6995,10 @@ var init_hub_tools = __esm(() => {
|
|
|
6956
6995
|
properties: { branch: str2, commit: str2, paths: { type: "array", items: str2 } },
|
|
6957
6996
|
additionalProperties: false
|
|
6958
6997
|
};
|
|
6998
|
+
costPlanProperties = {
|
|
6999
|
+
review: { type: "object", properties: { scope: { type: "string", enum: ["none", "delta", "full"] }, reviewers: { type: "integer", minimum: 0, maximum: 8 } }, required: ["scope", "reviewers"], additionalProperties: false },
|
|
7000
|
+
ci: { type: "string", enum: ["focused", "full", "reuse"] }
|
|
7001
|
+
};
|
|
6959
7002
|
plan = {
|
|
6960
7003
|
type: "object",
|
|
6961
7004
|
description: "what you will change, before you start. Owners of open tasks on the same files or symbols see it, and get a notice when your task is done.",
|
|
@@ -6988,7 +7031,7 @@ var init_hub_tools = __esm(() => {
|
|
|
6988
7031
|
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"]),
|
|
6989
7032
|
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"]),
|
|
6990
7033
|
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" } }),
|
|
6991
|
-
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" }, 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" } }, ["id", "verdict"]),
|
|
7034
|
+
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" }, 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" }, 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"]),
|
|
6992
7035
|
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"]),
|
|
6993
7036
|
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"]),
|
|
6994
7037
|
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"]),
|
|
@@ -6996,6 +7039,13 @@ var init_hub_tools = __esm(() => {
|
|
|
6996
7039
|
tool("hub_memory_correct", "Propose a correction to a visible memory entry. This creates a machine-local candidate only; a person accepts or rejects it with amem review. It never edits a note. id is an exact id or unique visible prefix; text and reason must each fit 4000 UTF-8 bytes.", { id: str2, text: str2, reason: str2 }, ["id", "text", "reason"]),
|
|
6997
7040
|
tool("hub_remember", "Save a decision, finding, contract or fail to the memory all agents share (the hub's memory and claude-mem); the other agents also get it with their next message. A fail is an approach you tried that does not work, and why: the most useful note, it stops the others spending their quota on it. Do not retry what a fail note rules out without new evidence. Conclusions worth recalling, not chatter. For kind fail, optional approach, symptom, cause, evidence and retry_when strings compose fixed labeled text; text may be omitted when structured fields are supplied. The complete text must fit 4000 UTF-8 bytes and is refused, never truncated, when oversized. A note on an open task is short-term until the task is approved, when its decisions, contracts and fails become long-term; horizon long keeps it now, scope shared lets the person's other projects find it.", { text: str2, approach: str2, symptom: str2, cause: str2, evidence: str2, retry_when: str2, title: str2, kind: { type: "string", enum: [...NOTE_KINDS] }, task: id, horizon: { type: "string", enum: ["short", "long"] }, scope: { type: "string", enum: ["project", "shared"] } })
|
|
6998
7041
|
];
|
|
7042
|
+
PLANNER_REVIEW_TOOLS = [
|
|
7043
|
+
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.", {}),
|
|
7044
|
+
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"]),
|
|
7045
|
+
tool("hub_sdlc_costs", "Read bounded review and CI cost metrics for linked merged PRs. Planner/person only; metadata only, no board writes. S7 owns automatic band proposals.", { repo: str2 }),
|
|
7046
|
+
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"])
|
|
7047
|
+
];
|
|
7048
|
+
TASK_TOOLS.push(...PLANNER_REVIEW_TOOLS);
|
|
6999
7049
|
TASK_TOOL_NAMES = new Set(TASK_TOOLS.map((t) => t.name));
|
|
7000
7050
|
CONDUCTOR_TOOLS = [
|
|
7001
7051
|
tool("hub_status", "Inspect public team state, holds, quota windows, task counts and pending approval peer/tool/age. Only the configured conductor may use this tool.", {}),
|
|
@@ -7516,6 +7566,246 @@ var init_local_worker = __esm(() => {
|
|
|
7516
7566
|
};
|
|
7517
7567
|
});
|
|
7518
7568
|
|
|
7569
|
+
// src/hub/review-brief.ts
|
|
7570
|
+
var LOCKFILES;
|
|
7571
|
+
var init_review_brief = __esm(() => {
|
|
7572
|
+
init_child_process();
|
|
7573
|
+
init_git();
|
|
7574
|
+
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"]);
|
|
7575
|
+
});
|
|
7576
|
+
|
|
7577
|
+
// src/adapters/claude-print.ts
|
|
7578
|
+
import { spawn as spawn6 } from "child_process";
|
|
7579
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
7580
|
+
function claudePrintArgs(sessionId, mcpConfig, systemPrompt, privatePaths = []) {
|
|
7581
|
+
return [
|
|
7582
|
+
"-p",
|
|
7583
|
+
"--output-format",
|
|
7584
|
+
"stream-json",
|
|
7585
|
+
"--verbose",
|
|
7586
|
+
"--no-session-persistence",
|
|
7587
|
+
"--session-id",
|
|
7588
|
+
sessionId,
|
|
7589
|
+
"--strict-mcp-config",
|
|
7590
|
+
"--mcp-config",
|
|
7591
|
+
mcpConfig,
|
|
7592
|
+
"--setting-sources",
|
|
7593
|
+
"",
|
|
7594
|
+
"--tools",
|
|
7595
|
+
"Read,Grep,Glob",
|
|
7596
|
+
"--allowedTools",
|
|
7597
|
+
"mcp__agent-hub__hub_review",
|
|
7598
|
+
"mcp__agent-hub__hub_task_show",
|
|
7599
|
+
"Read",
|
|
7600
|
+
"Grep",
|
|
7601
|
+
"Glob",
|
|
7602
|
+
...privatePaths.length ? ["--disallowedTools", ...readDenyRules(privatePaths)] : [],
|
|
7603
|
+
"--permission-mode",
|
|
7604
|
+
"dontAsk",
|
|
7605
|
+
"--append-system-prompt",
|
|
7606
|
+
systemPrompt
|
|
7607
|
+
];
|
|
7608
|
+
}
|
|
7609
|
+
function readDenyRules(paths) {
|
|
7610
|
+
return paths.flatMap((entry) => {
|
|
7611
|
+
const rule = entry.startsWith("/") ? `/${entry}` : `./${entry.replace(/^\.\//, "")}`;
|
|
7612
|
+
return /[*?]/.test(entry) ? [`Read(${rule})`] : [`Read(${rule})`, `Read(${rule.replace(/\/$/, "")}/**)`];
|
|
7613
|
+
});
|
|
7614
|
+
}
|
|
7615
|
+
var GroupCleanupError, reviewTask = (env) => {
|
|
7616
|
+
if (env.from !== HUB || env.kind !== "review")
|
|
7617
|
+
return;
|
|
7618
|
+
const id = Number(env.refs?.task);
|
|
7619
|
+
return Number.isSafeInteger(id) && id > 0 ? id : undefined;
|
|
7620
|
+
}, ClaudePrintPeer;
|
|
7621
|
+
var init_claude_print = __esm(() => {
|
|
7622
|
+
init_child_process();
|
|
7623
|
+
init_git();
|
|
7624
|
+
init_review_brief();
|
|
7625
|
+
init_envelope();
|
|
7626
|
+
GroupCleanupError = class GroupCleanupError extends Error {
|
|
7627
|
+
};
|
|
7628
|
+
ClaudePrintPeer = class ClaudePrintPeer extends BasePeer {
|
|
7629
|
+
opts;
|
|
7630
|
+
hubNative = true;
|
|
7631
|
+
generation = 0;
|
|
7632
|
+
child;
|
|
7633
|
+
activeDeliveryId;
|
|
7634
|
+
reviewing;
|
|
7635
|
+
reviewSeq = 0;
|
|
7636
|
+
reviewToken(task) {
|
|
7637
|
+
return this.reviewing === task ? this.reviewSeq : undefined;
|
|
7638
|
+
}
|
|
7639
|
+
constructor(id, opts) {
|
|
7640
|
+
super(id, opts.watchdogMs);
|
|
7641
|
+
this.opts = opts;
|
|
7642
|
+
}
|
|
7643
|
+
recoveryMetadata() {
|
|
7644
|
+
return { launch: { kind: "claude-print", peer: this.id } };
|
|
7645
|
+
}
|
|
7646
|
+
async start() {
|
|
7647
|
+
if (this.child)
|
|
7648
|
+
throw new Error(`${this.id}: an earlier review's process group is still running; stop it first`);
|
|
7649
|
+
this.setState("idle");
|
|
7650
|
+
}
|
|
7651
|
+
async stop(reason) {
|
|
7652
|
+
this.generation++;
|
|
7653
|
+
this.settle("needs_review", reason ?? "review stopped before settlement");
|
|
7654
|
+
if (this.child)
|
|
7655
|
+
this.setState("busy");
|
|
7656
|
+
await this.kill();
|
|
7657
|
+
this.setState("offline");
|
|
7658
|
+
}
|
|
7659
|
+
onWatchdog() {
|
|
7660
|
+
this.generation++;
|
|
7661
|
+
this.settle("needs_review", "review watchdog timeout");
|
|
7662
|
+
this.kill().then(() => {
|
|
7663
|
+
if (this.state === "busy")
|
|
7664
|
+
super.onWatchdog();
|
|
7665
|
+
}, (error) => this.opts.log?.(`[${this.id}] watchdog stop failed, staying busy: ${error.message}`));
|
|
7666
|
+
}
|
|
7667
|
+
async deliver(envs, deliveryId) {
|
|
7668
|
+
if (this.state !== "idle") {
|
|
7669
|
+
if (deliveryId)
|
|
7670
|
+
this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} is ${this.state}` });
|
|
7671
|
+
throw new Error(`${this.id} is ${this.state}`);
|
|
7672
|
+
}
|
|
7673
|
+
const generation = ++this.generation;
|
|
7674
|
+
this.activeDeliveryId = deliveryId;
|
|
7675
|
+
this.setState("busy");
|
|
7676
|
+
if (deliveryId)
|
|
7677
|
+
this.delivery({ id: deliveryId, state: "accepted" });
|
|
7678
|
+
this.run(envs, generation).catch((error) => {
|
|
7679
|
+
if (generation !== this.generation)
|
|
7680
|
+
return;
|
|
7681
|
+
this.settle("needs_review", `claude reviewer failed: ${error.message}`);
|
|
7682
|
+
this.kill().then(() => this.setState("idle"), () => {
|
|
7683
|
+
return;
|
|
7684
|
+
});
|
|
7685
|
+
});
|
|
7686
|
+
}
|
|
7687
|
+
async run(envs, generation) {
|
|
7688
|
+
const missing = [];
|
|
7689
|
+
let started = false;
|
|
7690
|
+
let failure;
|
|
7691
|
+
for (const env of envs) {
|
|
7692
|
+
const task = reviewTask(env);
|
|
7693
|
+
if (task === undefined)
|
|
7694
|
+
continue;
|
|
7695
|
+
if (env.private) {
|
|
7696
|
+
failure = { state: "needs_review", reason: "a private review is the person's, never a cloud reviewer's" };
|
|
7697
|
+
break;
|
|
7698
|
+
}
|
|
7699
|
+
const since = Date.now();
|
|
7700
|
+
let code;
|
|
7701
|
+
try {
|
|
7702
|
+
code = await this.review(env, task, generation);
|
|
7703
|
+
} catch (error) {
|
|
7704
|
+
if (generation !== this.generation)
|
|
7705
|
+
return;
|
|
7706
|
+
if (error instanceof GroupCleanupError) {
|
|
7707
|
+
this.opts.log?.(`[${this.id}] review of task #${task}: ${error.message}`);
|
|
7708
|
+
this.settle("needs_review", `review process group still running: ${error.message}`);
|
|
7709
|
+
return;
|
|
7710
|
+
}
|
|
7711
|
+
failure = { state: started ? "needs_review" : "failed_safe", reason: `claude reviewer could not start: ${error.message}` };
|
|
7712
|
+
break;
|
|
7713
|
+
}
|
|
7714
|
+
started = true;
|
|
7715
|
+
if (generation !== this.generation)
|
|
7716
|
+
return;
|
|
7717
|
+
let recorded = false;
|
|
7718
|
+
try {
|
|
7719
|
+
recorded = this.opts.verdictSince(task, since);
|
|
7720
|
+
} catch (error) {
|
|
7721
|
+
this.opts.log?.(`[${this.id}] verdict check for task #${task} failed: ${error.message}`);
|
|
7722
|
+
}
|
|
7723
|
+
if (!recorded)
|
|
7724
|
+
missing.push(task);
|
|
7725
|
+
if (code !== 0)
|
|
7726
|
+
this.opts.log?.(`[${this.id}] review of task #${task} exited ${code}`);
|
|
7727
|
+
}
|
|
7728
|
+
if (generation !== this.generation)
|
|
7729
|
+
return;
|
|
7730
|
+
this.settle(failure?.state ?? (missing.length ? "needs_review" : "completed"), failure?.reason ?? (missing.length ? `no verdict recorded for task ${missing.map((id) => `#${id}`).join(", ")}` : undefined));
|
|
7731
|
+
this.setState("idle");
|
|
7732
|
+
}
|
|
7733
|
+
review(env, task, generation) {
|
|
7734
|
+
const [cmd = "claude", ...prefix] = this.opts.command ?? ["claude"];
|
|
7735
|
+
const args = [...prefix, ...claudePrintArgs(randomUUID5(), this.opts.mcpConfig, this.opts.systemPrompt, this.opts.privatePaths)];
|
|
7736
|
+
return new Promise((resolve, reject) => {
|
|
7737
|
+
const child = spawn6(cmd, args, { cwd: this.opts.cwd, detached: true, env: peerChildEnv(this.id), stdio: ["pipe", "pipe", "ignore"] });
|
|
7738
|
+
this.child = child;
|
|
7739
|
+
child.once("error", (error) => {
|
|
7740
|
+
if (this.child === child)
|
|
7741
|
+
this.child = undefined;
|
|
7742
|
+
reject(error);
|
|
7743
|
+
});
|
|
7744
|
+
child.stdin.on("error", () => {});
|
|
7745
|
+
child.once("spawn", () => {
|
|
7746
|
+
trackGroup(child);
|
|
7747
|
+
if (generation !== this.generation) {
|
|
7748
|
+
child.stdin.destroy();
|
|
7749
|
+
return;
|
|
7750
|
+
}
|
|
7751
|
+
this.reviewing = task;
|
|
7752
|
+
this.reviewSeq++;
|
|
7753
|
+
let facts;
|
|
7754
|
+
try {
|
|
7755
|
+
facts = this.opts.facts?.(env);
|
|
7756
|
+
} catch (error) {
|
|
7757
|
+
this.opts.log?.(`[${this.id}] review facts for task #${task} failed: ${error.message}`);
|
|
7758
|
+
}
|
|
7759
|
+
child.stdin.end(`${frame(env)}${facts ? `
|
|
7760
|
+
|
|
7761
|
+
${facts}` : ""}`);
|
|
7762
|
+
});
|
|
7763
|
+
let buffer = "";
|
|
7764
|
+
child.stdout.on("data", (chunk) => {
|
|
7765
|
+
if (generation !== this.generation)
|
|
7766
|
+
return;
|
|
7767
|
+
this.touch();
|
|
7768
|
+
buffer += chunk.toString("utf8");
|
|
7769
|
+
for (let i;(i = buffer.indexOf(`
|
|
7770
|
+
`)) >= 0; ) {
|
|
7771
|
+
const line = buffer.slice(0, i);
|
|
7772
|
+
buffer = buffer.slice(i + 1);
|
|
7773
|
+
try {
|
|
7774
|
+
const event = JSON.parse(line);
|
|
7775
|
+
if (event.type === "result" && event.usage)
|
|
7776
|
+
this.opts.onUsage?.(event.usage, task);
|
|
7777
|
+
} catch {}
|
|
7778
|
+
}
|
|
7779
|
+
});
|
|
7780
|
+
child.once("exit", (code) => {
|
|
7781
|
+
if (this.child === child)
|
|
7782
|
+
this.reviewing = undefined;
|
|
7783
|
+
stopOwnedProcess(child, { group: true }).then(() => {
|
|
7784
|
+
if (this.child === child)
|
|
7785
|
+
this.child = undefined;
|
|
7786
|
+
resolve(code);
|
|
7787
|
+
}, (error) => reject(new GroupCleanupError(error.message)));
|
|
7788
|
+
});
|
|
7789
|
+
});
|
|
7790
|
+
}
|
|
7791
|
+
settle(state, reason) {
|
|
7792
|
+
const id = this.activeDeliveryId;
|
|
7793
|
+
this.activeDeliveryId = undefined;
|
|
7794
|
+
if (id)
|
|
7795
|
+
this.delivery({ id, state, ...reason ? { reason } : {} });
|
|
7796
|
+
}
|
|
7797
|
+
async kill() {
|
|
7798
|
+
this.reviewing = undefined;
|
|
7799
|
+
const child = this.child;
|
|
7800
|
+
if (!child)
|
|
7801
|
+
return;
|
|
7802
|
+
await stopOwnedProcess(child, { group: true });
|
|
7803
|
+
if (this.child === child)
|
|
7804
|
+
this.child = undefined;
|
|
7805
|
+
}
|
|
7806
|
+
};
|
|
7807
|
+
});
|
|
7808
|
+
|
|
7519
7809
|
// src/adapters/definitions/native-factories.ts
|
|
7520
7810
|
function createCodex(ctx) {
|
|
7521
7811
|
if (!("codex" in ctx.daemon))
|
|
@@ -7557,6 +7847,12 @@ function createLocal(ctx) {
|
|
|
7557
7847
|
init_local_worker();
|
|
7558
7848
|
return new LocalPeer(ctx.instance, ctx.daemon.local);
|
|
7559
7849
|
}
|
|
7850
|
+
function createClaudeReviewer(ctx) {
|
|
7851
|
+
if (!("claudePrint" in ctx.daemon))
|
|
7852
|
+
throw new Error("Claude reviewer requires its print transport options");
|
|
7853
|
+
init_claude_print();
|
|
7854
|
+
return new ClaudePrintPeer(ctx.instance, ctx.daemon.claudePrint);
|
|
7855
|
+
}
|
|
7560
7856
|
var DAEMON_PORTS;
|
|
7561
7857
|
var init_native_factories = __esm(() => {
|
|
7562
7858
|
DAEMON_PORTS = {
|
|
@@ -7564,7 +7860,8 @@ var init_native_factories = __esm(() => {
|
|
|
7564
7860
|
acp: (options) => ({ acp: options }),
|
|
7565
7861
|
"owned-acp": (options) => ({ jcode: options }),
|
|
7566
7862
|
"pi-rpc": (options) => ({ pi: options }),
|
|
7567
|
-
"local-worker": (options) => ({ local: options })
|
|
7863
|
+
"local-worker": (options) => ({ local: options }),
|
|
7864
|
+
"claude-print": (options) => ({ claudePrint: options })
|
|
7568
7865
|
};
|
|
7569
7866
|
});
|
|
7570
7867
|
|
|
@@ -7781,6 +8078,7 @@ function sessionSettings(tee, facts) {
|
|
|
7781
8078
|
}
|
|
7782
8079
|
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, `'\\''`)}'`;
|
|
7783
8080
|
var init_launch2 = __esm(() => {
|
|
8081
|
+
init_claude_state();
|
|
7784
8082
|
init_process_signature();
|
|
7785
8083
|
init_project();
|
|
7786
8084
|
init_child_process();
|
|
@@ -7812,7 +8110,7 @@ var init_native_cli_launch_common = __esm(() => {
|
|
|
7812
8110
|
|
|
7813
8111
|
// src/adapters/definitions/native-cli-claude-launch.ts
|
|
7814
8112
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, renameSync, unlinkSync, readFileSync as readFileSync7 } from "fs";
|
|
7815
|
-
import { randomUUID as
|
|
8113
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
7816
8114
|
import { resolve as resolve11, join as join17 } from "path";
|
|
7817
8115
|
function claudeChannel(userArgs) {
|
|
7818
8116
|
const bundle = resolve11(import.meta.dir, "../../../plugins/agent-hub/server.js");
|
|
@@ -7875,7 +8173,7 @@ function claudeNativeLaunch(ctx) {
|
|
|
7875
8173
|
const stateDir = ctx.facts?.stateDir ?? ctx.statusLine?.stateDir;
|
|
7876
8174
|
if (!stateDir)
|
|
7877
8175
|
throw new Error("Claude session settings require the hub state directory");
|
|
7878
|
-
const file = join17(stateDir, `claude-settings-${
|
|
8176
|
+
const file = join17(stateDir, `claude-settings-${randomUUID6()}.json`);
|
|
7879
8177
|
try {
|
|
7880
8178
|
mkdirSync4(stateDir, { recursive: true, mode: 448 });
|
|
7881
8179
|
writeFileSync2(`${file}.tmp`, settingsArgs[1], { mode: 384, flag: "wx" });
|
|
@@ -8138,7 +8436,7 @@ function sessionId(field) {
|
|
|
8138
8436
|
return typeof value === "string" && value ? value : undefined;
|
|
8139
8437
|
};
|
|
8140
8438
|
}
|
|
8141
|
-
var terminal = (form) => form === "tui", headless = () => false, switches, implementerRoles, dualForms, headlessForms, hubGrants, NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, palette = (fg) => ({ palette: { fg, bg: "transparent" } }), BUILTIN_DEFINITIONS_WITHOUT_QWEN;
|
|
8439
|
+
var terminal = (form) => form === "tui", headless = () => false, switches, implementerRoles, dualForms, headlessForms, hubGrants, CLAUDE_REVIEWER_VARIANT, NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, palette = (fg) => ({ palette: { fg, bg: "transparent" } }), BUILTIN_DEFINITIONS_WITHOUT_QWEN;
|
|
8142
8440
|
var init_builtin_metadata = __esm(() => {
|
|
8143
8441
|
init_native_config_policy();
|
|
8144
8442
|
init_native_policy_aux();
|
|
@@ -8156,6 +8454,27 @@ var init_builtin_metadata = __esm(() => {
|
|
|
8156
8454
|
"ask-when-needed": "write and edit inside the project run without asking; shell and git writes still ask",
|
|
8157
8455
|
"never-ask": "every tool runs without asking, inside the hub sandbox and path guard"
|
|
8158
8456
|
};
|
|
8457
|
+
CLAUDE_REVIEWER_VARIANT = {
|
|
8458
|
+
api: 1,
|
|
8459
|
+
kind: "claude",
|
|
8460
|
+
displayName: "Claude Code reviewer",
|
|
8461
|
+
family: "claude",
|
|
8462
|
+
transport: "hub-native",
|
|
8463
|
+
roles: Object.freeze(["reviewer"]),
|
|
8464
|
+
defaultOnRoster: true,
|
|
8465
|
+
trust: { onPrem: false, hubSandboxed: false },
|
|
8466
|
+
start: { create: createClaudeReviewer, daemonPort: "claude-print", forms: headlessForms, defaultForm: "headless", ownsTerminal: headless, launch: (ctx) => compatibilityLaunch("claude", ctx), conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
8467
|
+
identity: { mcp: "env" },
|
|
8468
|
+
permission: { modes: {}, unmanaged: "each review runs read-only (Read, Grep, Glob) and may call only hub_review and hub_task_show" },
|
|
8469
|
+
session: { idField: "sessionId", currentId: () => {
|
|
8470
|
+
return;
|
|
8471
|
+
}, freshOnRestore: true, launchKind: "claude-print", crashPlan: () => ({ resume: {}, how: "claude reviewer: starts again; every review runs in a new session" }) },
|
|
8472
|
+
turnEnd: "adapter",
|
|
8473
|
+
budget: { account: "claude" },
|
|
8474
|
+
routing: { reviewOnly: true },
|
|
8475
|
+
cli: { command: "claude", startWords: () => ["claude"] },
|
|
8476
|
+
display: CLAUDE_DISPLAY
|
|
8477
|
+
};
|
|
8159
8478
|
NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN = Object.freeze([
|
|
8160
8479
|
{
|
|
8161
8480
|
api: 1,
|
|
@@ -8190,7 +8509,8 @@ var init_builtin_metadata = __esm(() => {
|
|
|
8190
8509
|
budget: { account: "claude", checkpoint: true, reader: { source: "claude status line", read: claudeWindows }, context: { source: "claude_statusline", read: claudeContext } },
|
|
8191
8510
|
cli: { command: "claude", retainedTerminalNotice: claudeRetainedTerminalNotice, stopNotice: () => "this peer belongs to a native terminal; end it in its terminal", startWords: () => ["claude"] },
|
|
8192
8511
|
display: CLAUDE_DISPLAY,
|
|
8193
|
-
hooks: { recallPlatform: "claude", recallSource: "claude", recallInjected: true, pluginInventory: claudePluginInventory }
|
|
8512
|
+
hooks: { recallPlatform: "claude", recallSource: "claude", recallInjected: true, pluginInventory: claudePluginInventory },
|
|
8513
|
+
reviewerVariant: CLAUDE_REVIEWER_VARIANT
|
|
8194
8514
|
},
|
|
8195
8515
|
{
|
|
8196
8516
|
api: 1,
|
|
@@ -8595,6 +8915,12 @@ function immutable(value, seen = new WeakSet) {
|
|
|
8595
8915
|
immutable(member, seen);
|
|
8596
8916
|
return Object.freeze(value);
|
|
8597
8917
|
}
|
|
8918
|
+
function ownField(value, key) {
|
|
8919
|
+
if (!value || typeof value !== "object" || types.isProxy(value) || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
8920
|
+
return;
|
|
8921
|
+
const field = Object.getOwnPropertyDescriptor(value, key);
|
|
8922
|
+
return field && Object.hasOwn(field, "value") && field.enumerable ? field.value : undefined;
|
|
8923
|
+
}
|
|
8598
8924
|
function declaredKind(id, instances) {
|
|
8599
8925
|
if (!instances || typeof instances !== "object" || types.isProxy(instances) || Array.isArray(instances) || ![Object.prototype, null].includes(Object.getPrototypeOf(instances)))
|
|
8600
8926
|
return;
|
|
@@ -8615,7 +8941,8 @@ function kindOf(id, instances) {
|
|
|
8615
8941
|
}
|
|
8616
8942
|
function definitionOf(id, instances) {
|
|
8617
8943
|
const kind = kindOf(id, instances);
|
|
8618
|
-
|
|
8944
|
+
const definition = kind === undefined ? undefined : definitions.get(kind);
|
|
8945
|
+
return definition?.reviewerVariant && kind !== id && ownField(ownField(instances, id), "start_mode") === "headless" ? definition.reviewerVariant : definition;
|
|
8619
8946
|
}
|
|
8620
8947
|
function defaultPeerRoles(includeOptional = false) {
|
|
8621
8948
|
return Object.fromEntries(ROLE_ORDER.filter((peer) => includeOptional || !definitionOf(peer).optional).map((peer) => [peer, [...definitionOf(peer).roles]]));
|
|
@@ -8635,7 +8962,8 @@ function assertNativeCapabilityScope(definition) {
|
|
|
8635
8962
|
["budget.resetSoftTokensOnResume", declared(definition.budget, "resetSoftTokensOnResume")],
|
|
8636
8963
|
["session.crashIdentity", declared(definition.session, "crashIdentity")],
|
|
8637
8964
|
["start.daemonPort pi-rpc", definition.start.daemonPort === "pi-rpc"],
|
|
8638
|
-
["routing", definition.routing !== undefined]
|
|
8965
|
+
["routing", definition.routing !== undefined],
|
|
8966
|
+
["reviewerVariant", definition.reviewerVariant !== undefined]
|
|
8639
8967
|
];
|
|
8640
8968
|
const field = restricted.find(([, present]) => present)?.[0];
|
|
8641
8969
|
if (field)
|
|
@@ -8655,6 +8983,9 @@ var init_registry = __esm(() => {
|
|
|
8655
8983
|
OPTIONAL_PEERS = ["qwen", "jcode"];
|
|
8656
8984
|
SUPPORTED_PEERS = [...KNOWN_PEERS, ...OPTIONAL_PEERS];
|
|
8657
8985
|
fixedBuiltinDefinitions = new Set([...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, QWEN_DEFINITION]);
|
|
8986
|
+
for (const definition of [...fixedBuiltinDefinitions])
|
|
8987
|
+
if (definition.reviewerVariant)
|
|
8988
|
+
fixedBuiltinDefinitions.add(definition.reviewerVariant);
|
|
8658
8989
|
ADAPTERS = immutable([
|
|
8659
8990
|
...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter((definition) => definition.kind !== "jcode"),
|
|
8660
8991
|
QWEN_DEFINITION,
|
|
@@ -8664,6 +8995,10 @@ var init_registry = __esm(() => {
|
|
|
8664
8995
|
for (const definition of ADAPTERS) {
|
|
8665
8996
|
assertDaemonEngine(definition);
|
|
8666
8997
|
assertNativeCapabilityScope(definition);
|
|
8998
|
+
if (definition.reviewerVariant) {
|
|
8999
|
+
assertDaemonEngine(definition.reviewerVariant);
|
|
9000
|
+
assertNativeCapabilityScope(definition.reviewerVariant);
|
|
9001
|
+
}
|
|
8667
9002
|
if (definition.api !== ADAPTER_API || definitions.has(definition.kind))
|
|
8668
9003
|
throw new Error("invalid builtin adapter registry");
|
|
8669
9004
|
definitions.set(definition.kind, definition);
|
|
@@ -15779,10 +16114,17 @@ import { join as join22 } from "path";
|
|
|
15779
16114
|
init_registry();
|
|
15780
16115
|
init_hub_tools();
|
|
15781
16116
|
init_envelope();
|
|
16117
|
+
|
|
16118
|
+
// src/hub/spec-read.ts
|
|
16119
|
+
init_host_executable();
|
|
16120
|
+
init_child_process();
|
|
16121
|
+
var BODY_LIMIT = 64 * 1024;
|
|
16122
|
+
var RESULT_LIMIT = 80 * 1024;
|
|
16123
|
+
var STDOUT_LIMIT = 512 * 1024;
|
|
15782
16124
|
// package.json
|
|
15783
16125
|
var package_default = {
|
|
15784
16126
|
name: "@staix/agent-hub",
|
|
15785
|
-
version: "0.12.
|
|
16127
|
+
version: "0.12.36",
|
|
15786
16128
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
15787
16129
|
license: "MIT",
|
|
15788
16130
|
type: "module",
|
|
@@ -15902,13 +16244,6 @@ init_registry();
|
|
|
15902
16244
|
init_board();
|
|
15903
16245
|
import { join as join20, resolve as resolve12 } from "path";
|
|
15904
16246
|
|
|
15905
|
-
// src/hub/spec-read.ts
|
|
15906
|
-
init_host_executable();
|
|
15907
|
-
init_child_process();
|
|
15908
|
-
var BODY_LIMIT = 64 * 1024;
|
|
15909
|
-
var RESULT_LIMIT = 80 * 1024;
|
|
15910
|
-
var STDOUT_LIMIT = 512 * 1024;
|
|
15911
|
-
|
|
15912
16247
|
// src/models/route/config.ts
|
|
15913
16248
|
var TYPES = new Set(["stage", "plan_execute", "advisor", "escalation"]);
|
|
15914
16249
|
|
|
@@ -15926,7 +16261,13 @@ init_workflow();
|
|
|
15926
16261
|
init_board();
|
|
15927
16262
|
init_envelope();
|
|
15928
16263
|
init_project();
|
|
16264
|
+
init_review_brief();
|
|
15929
16265
|
init_registry();
|
|
16266
|
+
|
|
16267
|
+
// src/hub/task-sweep.ts
|
|
16268
|
+
var GROOMING_CODES = new Set(["tests", "spec", "roster", "pull-only", "not-attached", "excluded", "owner-admission", "offline", "paused", "failing", "pii", "policy", "cancelled", "unavailable", "runner", "approvals", "quota", "dependency"]);
|
|
16269
|
+
|
|
16270
|
+
// src/hub/tasks.ts
|
|
15930
16271
|
init_sdlc();
|
|
15931
16272
|
var DEMOTION_HALF_LIFE_MS = 24 * 3600000;
|
|
15932
16273
|
var CONTRADICTION_WINDOW_MS = 7 * 86400000;
|
|
@@ -16041,8 +16382,23 @@ function validateInstances(raw) {
|
|
|
16041
16382
|
return fail(`${path}.kind`, "unknown native kind");
|
|
16042
16383
|
if (!id.startsWith(prefix) || !/^[a-z0-9][a-z0-9-]*$/.test(id.slice(prefix.length)))
|
|
16043
16384
|
return fail(path, "id must be <kind>-<name>");
|
|
16044
|
-
|
|
16045
|
-
|
|
16385
|
+
const channel = definition.identity.mcp === "channel-plugin";
|
|
16386
|
+
if (!channel && (!definition.start.create || !definition.start.forms.includes("headless") || !["env", "acp-session"].includes(definition.identity.mcp)))
|
|
16387
|
+
return fail(`${path}.kind`, "unsupported; a headless factory with env or ACP-session identity, or a channel-plugin kind, is required, not project-file identity");
|
|
16388
|
+
if (channel) {
|
|
16389
|
+
const declared = entry.roles ? roles(entry.roles.value, `${path}.roles`) : [];
|
|
16390
|
+
if (entry.start_mode && definition.reviewerVariant) {
|
|
16391
|
+
if (entry.worktree)
|
|
16392
|
+
return fail(path, "a headless reviewer reads the project checkout: worktree is not supported");
|
|
16393
|
+
if (declared.length !== 1 || declared[0] !== "reviewer")
|
|
16394
|
+
return fail(`${path}.roles`, 'a headless channel-kind instance is a reviewer: roles must be exactly ["reviewer"]');
|
|
16395
|
+
} else {
|
|
16396
|
+
if (entry.start_mode || entry.worktree)
|
|
16397
|
+
return fail(path, `a channel instance starts only from the person's terminal (ahub <kind> --as <id>): start_mode and worktree are not supported; a headless reviewer declares roles ["reviewer"] and start_mode "headless"`);
|
|
16398
|
+
if (!declared.length || declared.some((role) => role !== "reviewer" && role !== "planner"))
|
|
16399
|
+
return fail(`${path}.roles`, "a channel instance must declare its roles: reviewer and/or planner");
|
|
16400
|
+
}
|
|
16401
|
+
}
|
|
16046
16402
|
const value = { kind };
|
|
16047
16403
|
if (entry.roles) {
|
|
16048
16404
|
value.roles = roles(entry.roles.value, `${path}.roles`);
|