herdr-link 0.3.1 → 0.4.1

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.
@@ -3,7 +3,9 @@ import { tool } from "@opencode-ai/plugin";
3
3
 
4
4
  // src/herdr.ts
5
5
  import { execFile } from "node:child_process";
6
+ import { readFile } from "node:fs/promises";
6
7
  import { randomBytes } from "node:crypto";
8
+ import { resolve } from "node:path";
7
9
 
8
10
  // src/protocol.ts
9
11
  var PROTOCOL_ID = "herdr-link/1";
@@ -19,6 +21,7 @@ function toAgentState(value) {
19
21
  }
20
22
  return "unknown";
21
23
  }
24
+ var START_TOOL_DESCRIPTION = "Start a new Herdr Agent in an existing pane. Provide name and pane, then choose exactly one complete parameter source: config_agent for .agents/agent_config.json, or kind plus args for explicit Herdr start parameters. These modes are mutually exclusive; partial overrides are not supported. This operation does not create panes or retry/fallback after failure.";
22
25
  var HerdrLinkError = class extends Error {
23
26
  code;
24
27
  constructor(code, detail) {
@@ -32,7 +35,12 @@ var AGENT_ERROR_DETAILS = {
32
35
  SELF_UNNAMED: "Herdr Link could not establish a stable Agent Name",
33
36
  PEER_NOT_FOUND: "target agent is not a live peer",
34
37
  SEND_FAILED: "Herdr did not accept message delivery",
35
- CLOSE_FAILED: "Herdr pane close failed"
38
+ CLOSE_FAILED: "Herdr pane close failed",
39
+ START_CONFIG_NOT_FOUND: "configured Agent start configuration was not found",
40
+ START_AGENT_NOT_FOUND: "configured Agent start entry was not found",
41
+ START_CONFIG_INVALID: "configured Agent start configuration is invalid",
42
+ START_INPUT_INVALID: "Agent start input is invalid",
43
+ START_FAILED: "Herdr did not accept Agent start"
36
44
  };
37
45
  function formatAgentFacingError(error, fallbackCode) {
38
46
  const code = error instanceof HerdrLinkError ? error.code : fallbackCode;
@@ -104,14 +112,14 @@ var COMMUNICATION_CONTRACT = `Herdr Link is the standard interoperability channe
104
112
  function attachCliOutput(error, stdout, stderr) {
105
113
  Object.assign(error, { stdout, stderr });
106
114
  }
107
- var defaultHerdrRunner = (file, args) => new Promise((resolve, reject) => {
115
+ var defaultHerdrRunner = (file, args) => new Promise((resolve2, reject) => {
108
116
  execFile(file, args, { encoding: "utf8", shell: false }, (error, stdout, stderr) => {
109
117
  if (error) {
110
118
  attachCliOutput(error, String(stdout), String(stderr));
111
119
  reject(error);
112
120
  return;
113
121
  }
114
- resolve({ stdout: String(stdout), stderr: String(stderr) });
122
+ resolve2({ stdout: String(stdout), stderr: String(stderr) });
115
123
  });
116
124
  });
117
125
  var herdrRunner = defaultHerdrRunner;
@@ -160,6 +168,180 @@ async function runFor(args, failureCode) {
160
168
  throw operationError(error, failureCode);
161
169
  }
162
170
  }
171
+ var startCursors = /* @__PURE__ */ new Map();
172
+ var startLocks = /* @__PURE__ */ new Map();
173
+ var START_CONFIG_PATH_PARTS = [".agents", "agent_config.json"];
174
+ var START_INPUT_KEYS = /* @__PURE__ */ new Set(["name", "pane", "config_agent", "kind", "args"]);
175
+ function hasOwn(value, key) {
176
+ return Object.prototype.hasOwnProperty.call(value, key);
177
+ }
178
+ function startInputError(detail) {
179
+ return new HerdrLinkError("START_INPUT_INVALID", detail);
180
+ }
181
+ function startConfigError(code, detail) {
182
+ return new HerdrLinkError(code, detail);
183
+ }
184
+ function validateStartInput(input) {
185
+ const value = asRecord(input);
186
+ if (!value) throw startInputError("start input must be an object");
187
+ for (const key of Object.keys(value)) {
188
+ if (!START_INPUT_KEYS.has(key)) throw startInputError(`unknown start field "${key}"`);
189
+ }
190
+ const name = value.name;
191
+ if (typeof name !== "string" || !isValidAgentName(name)) {
192
+ throw startInputError('"name" must be a valid Herdr Agent Name');
193
+ }
194
+ const pane = value.pane;
195
+ if (typeof pane !== "string" || pane.trim() === "") {
196
+ throw startInputError('"pane" must be a non-empty pane id');
197
+ }
198
+ const hasConfigAgent = hasOwn(value, "config_agent");
199
+ const hasKind = hasOwn(value, "kind");
200
+ const hasArgs = hasOwn(value, "args");
201
+ if (hasConfigAgent && (hasKind || hasArgs)) {
202
+ throw startInputError("config_agent cannot be combined with kind or args");
203
+ }
204
+ if (hasConfigAgent) {
205
+ const configAgent = value.config_agent;
206
+ if (typeof configAgent !== "string" || configAgent.trim() === "") {
207
+ throw startInputError('"config_agent" must be a non-empty string');
208
+ }
209
+ return { mode: "configured", name, pane, configAgent };
210
+ }
211
+ if (!hasKind || !hasArgs) {
212
+ throw startInputError("explicit start requires both kind and args");
213
+ }
214
+ const kind = value.kind;
215
+ if (typeof kind !== "string" || kind.trim() === "") {
216
+ throw startInputError('"kind" must be a non-empty string');
217
+ }
218
+ const args = value.args;
219
+ if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string")) {
220
+ throw startInputError('"args" must be an array of strings');
221
+ }
222
+ return { mode: "explicit", name, pane, variant: { kind, args: [...args] } };
223
+ }
224
+ function assertAllowedKeys(value, allowed, label) {
225
+ const allowedSet = new Set(allowed);
226
+ for (const key of Object.keys(value)) {
227
+ if (!allowedSet.has(key)) throw startConfigError("START_CONFIG_INVALID", `${label} contains unknown field "${key}"`);
228
+ }
229
+ }
230
+ function validateConfiguredDocument(document) {
231
+ const root = asRecord(document);
232
+ if (!root) throw startConfigError("START_CONFIG_INVALID", "configuration root must be an object");
233
+ assertAllowedKeys(root, ["version", "agents"], "configuration root");
234
+ if (root.version !== 1) throw startConfigError("START_CONFIG_INVALID", "configuration version must be 1");
235
+ const agents = asRecord(root.agents);
236
+ if (!agents) throw startConfigError("START_CONFIG_INVALID", "agents must be an object");
237
+ const result = /* @__PURE__ */ new Map();
238
+ for (const [configAgent, rawEntry] of Object.entries(agents)) {
239
+ if (configAgent.trim() === "") {
240
+ throw startConfigError("START_CONFIG_INVALID", "agents contains an empty configuration key");
241
+ }
242
+ const entry = asRecord(rawEntry);
243
+ if (!entry) throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent} must be an object`);
244
+ assertAllowedKeys(entry, ["strategy", "variants"], `agents.${configAgent}`);
245
+ const rawVariants = entry.variants;
246
+ if (!Array.isArray(rawVariants) || rawVariants.length === 0) {
247
+ throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.variants must be non-empty`);
248
+ }
249
+ const hasStrategy = hasOwn(entry, "strategy");
250
+ if (hasStrategy && entry.strategy !== "round-robin") {
251
+ throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.strategy is unsupported`);
252
+ }
253
+ if (rawVariants.length > 1 && entry.strategy !== "round-robin") {
254
+ throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent} requires strategy round-robin for multiple variants`);
255
+ }
256
+ const variants = rawVariants.map((rawVariant, index) => {
257
+ const variant = asRecord(rawVariant);
258
+ if (!variant) throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.variants[${index}] must be an object`);
259
+ assertAllowedKeys(variant, ["kind", "args"], `agents.${configAgent}.variants[${index}]`);
260
+ const kind = variant.kind;
261
+ if (typeof kind !== "string" || kind.trim() === "") {
262
+ throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.variants[${index}].kind must be non-empty`);
263
+ }
264
+ const args = variant.args;
265
+ if (hasOwn(variant, "args") && (!Array.isArray(args) || !args.every((arg) => typeof arg === "string"))) {
266
+ throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.variants[${index}].args must be an array of strings`);
267
+ }
268
+ return { kind, args: Array.isArray(args) ? [...args] : [] };
269
+ });
270
+ result.set(configAgent, {
271
+ ...hasStrategy ? { strategy: "round-robin" } : {},
272
+ variants
273
+ });
274
+ }
275
+ return result;
276
+ }
277
+ async function loadConfiguredStartAgents(configPath) {
278
+ let text;
279
+ try {
280
+ text = await readFile(configPath, "utf8");
281
+ } catch (error) {
282
+ const code = asRecord(error)?.code;
283
+ if (code === "ENOENT") {
284
+ throw startConfigError("START_CONFIG_NOT_FOUND", "agent_config.json was not found");
285
+ }
286
+ throw startConfigError("START_CONFIG_INVALID", "agent_config.json could not be read");
287
+ }
288
+ let document;
289
+ try {
290
+ document = JSON.parse(text);
291
+ } catch {
292
+ throw startConfigError("START_CONFIG_INVALID", "agent_config.json is not valid JSON");
293
+ }
294
+ return validateConfiguredDocument(document);
295
+ }
296
+ async function withStartCursorLock(key, operation) {
297
+ const previous = startLocks.get(key) ?? Promise.resolve();
298
+ let release;
299
+ const current = new Promise((resolve2) => {
300
+ release = resolve2;
301
+ });
302
+ startLocks.set(key, current);
303
+ await previous;
304
+ try {
305
+ return await operation();
306
+ } finally {
307
+ release();
308
+ if (startLocks.get(key) === current) startLocks.delete(key);
309
+ }
310
+ }
311
+ async function runStart(name, pane, variant) {
312
+ try {
313
+ await runHerdr(["agent", "start", name, "--kind", variant.kind, "--pane", pane, "--", ...variant.args]);
314
+ } catch (error) {
315
+ if (error instanceof HerdrLinkError && error.code === "NOT_IN_HERDR") throw error;
316
+ throw operationError(error, "START_FAILED");
317
+ }
318
+ }
319
+ async function startAgent(input, options = {}) {
320
+ assertHerdrEnvironment();
321
+ const validated = validateStartInput(input);
322
+ if (validated.mode === "explicit") {
323
+ await runStart(validated.name, validated.pane, validated.variant);
324
+ return { status: "started", agent: validated.name, kind: validated.variant.kind };
325
+ }
326
+ const projectRoot = typeof options.cwd === "string" && options.cwd.trim() !== "" ? options.cwd : process.cwd();
327
+ const configPath = resolve(projectRoot, ...START_CONFIG_PATH_PARTS);
328
+ const cursorKey = `${configPath}\0${validated.configAgent}`;
329
+ return withStartCursorLock(cursorKey, async () => {
330
+ const configuredAgents = await loadConfiguredStartAgents(configPath);
331
+ const configured = configuredAgents.get(validated.configAgent);
332
+ if (!configured) {
333
+ throw startConfigError("START_AGENT_NOT_FOUND", `configured Agent "${validated.configAgent}" was not found`);
334
+ }
335
+ const current = startCursors.get(cursorKey) ?? 0;
336
+ const variantIndex = current % configured.variants.length;
337
+ const variant = configured.variants[variantIndex];
338
+ await runStart(validated.name, validated.pane, variant);
339
+ if (configured.variants.length > 1) {
340
+ startCursors.set(cursorKey, (variantIndex + 1) % configured.variants.length);
341
+ }
342
+ return { status: "started", agent: validated.name, kind: variant.kind };
343
+ });
344
+ }
163
345
  var CLI_ERROR_CODE_MAP = {
164
346
  agent_not_found: "PEER_NOT_FOUND",
165
347
  not_in_herdr: "NOT_IN_HERDR"
@@ -259,7 +441,7 @@ function stableName(record) {
259
441
  }
260
442
  var SELF_PROBE_ATTEMPTS = 3;
261
443
  var SELF_PROBE_DELAY_MS = 100;
262
- var sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
444
+ var sleepMs = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
263
445
  async function fetchSelfRecord(pane) {
264
446
  for (let attempt = 1; ; attempt += 1) {
265
447
  try {
@@ -422,6 +604,7 @@ async function closeAgentPane(agentName) {
422
604
 
423
605
  // src/opencode.ts
424
606
  var GATEWAY_PRESENTATION_APPENDIX = `In this runtime the active Herdr Link capabilities are dispatched through the single herdr_link gateway.
607
+ - Use herdr_link with action "start": ${START_TOOL_DESCRIPTION}
425
608
  - Use herdr_link with action "peers" to list live same-workspace agents.
426
609
  - Use herdr_link with action "send" with to and message to deliver an inter-agent message or ordinary reply.
427
610
  - Use herdr_link with action "close" and an Agent Name only after any final send returns status "sent", in a later tool step.`;
@@ -439,7 +622,7 @@ function failWith(error, fallbackCode) {
439
622
  }
440
623
  function failInvalidAction(action) {
441
624
  throw new Error(
442
- `INVALID_ACTION: herdr_link action "${action}" is not supported; use "peers", "send", "close", or omit action (call with {}) to activate.`
625
+ `INVALID_ACTION: herdr_link action "${action}" is not supported; use "start", "peers", "send", "close", or omit action (call with {}) to activate.`
443
626
  );
444
627
  }
445
628
  var herdrLinkPlugin = async () => {
@@ -452,21 +635,34 @@ var herdrLinkPlugin = async () => {
452
635
  return {
453
636
  tool: {
454
637
  [HERDR_LINK_GATEWAY]: tool({
455
- description: `Herdr Link cross-agent communication gateway (herdr-link/1). Activate only when the user explicitly asks to use Herdr or when handling an inbound Herdr Link message. Call once with no arguments {} to activate Herdr Link for this session; the response lists capabilities. Then pass action "peers" to list live same-workspace agents, "send" with to + message to deliver an inter-agent message or ordinary reply, or "close" with agent to close a named agent's pane \u2014 only after any final send has returned status "sent", and in a later tool step.`,
638
+ description: `Herdr Link cross-agent control gateway (herdr-link/1). Activate only when the user explicitly asks to use Herdr or when handling an inbound Herdr Link message. Call once with no arguments {} to activate Herdr Link for this session; the response lists capabilities. Then pass action "start" with name + pane and either config_agent or complete kind + args, action "peers" to list live same-workspace agents, action "send" with to + message to deliver an inter-agent message or ordinary reply, or action "close" with agent to close a named agent's pane \u2014 start modes are mutually exclusive and close is only after any final send has returned status "sent", in a later tool step.`,
456
639
  args: {
457
- action: tool.schema.enum(["peers", "send", "close"]).optional().describe(
458
- 'Operation to run: "peers" | "send" | "close". Omit action entirely (call with {}) to activate Herdr Link for this session.'
640
+ action: tool.schema.enum(["start", "peers", "send", "close"]).optional().describe(
641
+ 'Operation to run: "start", "peers", "send", or "close". Omit action entirely (call with {}) to activate Herdr Link for this session.'
459
642
  ),
460
643
  to: tool.schema.string().optional().describe('Target agent name; required for action "send".'),
461
644
  message: tool.schema.string().optional().describe('Message payload; required for action "send".'),
462
- agent: tool.schema.string().optional().describe('Target agent name; required for action "close".')
645
+ agent: tool.schema.string().optional().describe('Target agent name; required for action "close".'),
646
+ name: tool.schema.string().optional().describe('New Agent Name; required for action "start".'),
647
+ pane: tool.schema.string().optional().describe('Existing pane id; required for action "start".'),
648
+ config_agent: tool.schema.string().optional().describe('Configured Agent key for action "start"; do not combine with kind or args.'),
649
+ kind: tool.schema.string().optional().describe('Herdr Agent kind for explicit action "start".'),
650
+ args: tool.schema.array(tool.schema.string()).optional().describe('Complete Herdr Agent arguments for explicit action "start".')
463
651
  },
464
652
  async execute(args, context) {
465
653
  if (args.action === void 0) {
466
654
  activatedSessions.add(context.sessionID);
467
- return jsonResult({ status: "active", capabilities: ["peers", "send", "close"] });
655
+ return jsonResult({ status: "active", capabilities: ["start", "peers", "send", "close"] });
468
656
  }
469
657
  activatedSessions.add(context.sessionID);
658
+ if (args.action === "start") {
659
+ const startInput = Object.fromEntries(Object.entries(args).filter(([key]) => key !== "action"));
660
+ try {
661
+ return jsonResult(await startAgent(startInput, { cwd: context.directory }));
662
+ } catch (error) {
663
+ failWith(error, "START_FAILED");
664
+ }
665
+ }
470
666
  if (args.action === "peers") {
471
667
  try {
472
668
  return jsonResult(await listPeers());
@@ -1,22 +1,22 @@
1
1
  # MCP Adapter 接线指南(Claude Code / Codex / AGY)
2
2
 
3
- 适用对象:没有原生自定义工具注册面的 Runtime(Claude Code、Codex、AGY)。三者共用同一个零依赖 stdio MCP server(决策见蓝图 ADR-013/ADR-014);契约注入不经 MCP 通道,按 Runtime 分治。
3
+ 适用对象:没有原生自定义工具注册面的 Runtime(Claude Code、Codex、AGY)。三者共用同一个 stdio MCP server(不依赖 MCP SDK,配置使用 Node 原生 `JSON.parse`);契约注入不经 MCP 通道,按 Runtime 分治。
4
4
 
5
5
  ## 形态总览
6
6
 
7
7
  ```text
8
8
  dist/herdr-link.mcp.js 单文件 bundle(esbuild 产出,行分隔 JSON-RPC over stdio)
9
- 能力面 Tier 0 gateway herdr_link + Tier 1 herdr_link_peers / herdr_link_send / herdr_link_close(canonical 名)
9
+ 能力面 Tier 0 gateway herdr_link + Tier 1 herdr_link_start / herdr_link_peers / herdr_link_send / herdr_link_close(canonical 名)
10
10
  lazy 呈现 非 Herdr:tools/list = []
11
11
  Herdr dormant:tools/list = [herdr_link]
12
12
  激活(tools/call herdr_link {})后:发射一次 notifications/tools/list_changed,
13
- 本连接内 tools/list = [herdr_link, peers, send, close];activation 随 stdio 连接存亡
13
+ 本连接内 tools/list = [herdr_link, start, peers, send, close];activation 随 stdio 连接存亡
14
14
  listChanged fallback 不响应刷新的 Host 继续用 gateway 显式 action 分发:
15
- {"action":"peers"} / {"action":"send","arguments":{...}} /
15
+ {"action":"start","arguments":{...}} / {"action":"peers"} / {"action":"send","arguments":{...}} /
16
16
  {"action":"close","arguments":{...}}
17
17
  呈现方式 Claude Code / Codex = mcp__herdr_link__<tool>
18
18
  AGY = call_mcp_tool(ServerName/ToolName/Arguments)
19
- 错误语义 五个 Link error 一律 isError:true + "CODE: detail" 文本(PROTOCOL §7);
19
+ 错误语义 十个 Link error 一律 isError:true + "CODE: detail" 文本(PROTOCOL §7);
20
20
  环境/transport 失败归类 NOT_IN_HERDR 且不被重包装
21
21
  零副作用 非 Herdr 环境 tools/list 返回 [],模型无感知;stale host 注册表直接调用也返回 NOT_IN_HERDR
22
22
  契约/提示注入 dormant 启动只允许短 Tier-0 hint:
@@ -42,7 +42,7 @@ BUNDLE=$(pwd)/dist/herdr-link.mcp.js # 后文统一引用
42
42
 
43
43
  运行只依赖 Node ≥ 22.6。server 由各 Runtime 在 Herdr managed pane 内拉起;**`HERDR_*` 环境变量是否透传给 MCP 子进程由各 Runtime 决定**——Codex 必须显式 `env_vars` 转发(§3),AGY 实测原生透传(§4),其余 Runtime 接入时先实测。
44
44
 
45
- **命名约束**:三家 Runtime 的 host registration namespace 均使用 `herdr_link`(下划线;连字符名在 Codex code-mode 工具面有兼容性问题)。Claude Code / Codex 以 prefix 形式呈现、AGY 以 wrapper 形式呈现(PROTOCOL §4.5),契约附录分别由 `buildMcpPrefixedCommunicationContract("herdr_link")` 与 `buildMcpWrapperCommunicationContract("call_mcp_tool", "herdr_link")` 生成(见 §1.2 / §1.3)。
45
+ **命名约束**:三家 Runtime 的 host registration namespace 均使用 `herdr_link`(下划线;连字符名在 Codex code-mode 工具面有兼容性问题)。Claude Code / Codex 以 prefix 形式呈现、AGY 以 wrapper 形式呈现(PROTOCOL §4.6),契约附录分别由 `buildMcpPrefixedCommunicationContract("herdr_link")` 与 `buildMcpWrapperCommunicationContract("call_mcp_tool", "herdr_link")` 生成(见 §1.2 / §1.3)。
46
46
 
47
47
  ## 1. Active-state Contract 文本(canonical compact 共享 + Runtime presentation 附录)
48
48
 
@@ -73,8 +73,10 @@ Herdr Link is the standard interoperability channel between agents running in th
73
73
  ```text
74
74
  In this runtime Herdr Link starts dormant: only the mcp__herdr_link__herdr_link gateway tool is listed until it is activated.
75
75
  - Call mcp__herdr_link__herdr_link once with no arguments ({}); the host then receives notifications/tools/list_changed and the cross-agent tools become available.
76
- - If the host did not refresh its tool list, keep dispatching through the gateway: {"action":"peers"}, {"action":"send","arguments":{...}}, {"action":"close","arguments":{...}}.
76
+ - If the host did not refresh its tool list, keep dispatching through the gateway: {"action":"start","arguments":{...}}, {"action":"peers"}, {"action":"send","arguments":{...}}, {"action":"close","arguments":{...}}.
77
+ - Start a new Herdr Agent in an existing pane. Provide name and pane, then choose exactly one complete parameter source: config_agent for .agents/agent_config.json, or kind plus args for explicit Herdr start parameters. These modes are mutually exclusive; partial overrides are not supported. This operation does not create panes or retry/fallback after failure.
77
78
  The tools are presented under MCP-prefixed names (the canonical name is always the suffix):
79
+ - herdr_link_start -> mcp__herdr_link__herdr_link_start
78
80
  - herdr_link_peers -> mcp__herdr_link__herdr_link_peers
79
81
  - herdr_link_send -> mcp__herdr_link__herdr_link_send
80
82
  - herdr_link_close -> mcp__herdr_link__herdr_link_close
@@ -87,13 +89,14 @@ AGY 的 model-facing 调用是单一原生 wrapper 携带 ServerName/ToolName/Ar
87
89
  ```text
88
90
  In this runtime Herdr Link starts dormant: only the Tier 0 gateway (herdr_link) is listed until it is activated.
89
91
  - Invoke the gateway once with empty Arguments {} (ToolName "herdr_link"); the host then receives notifications/tools/list_changed and the cross-agent tools become available.
90
- - If the host did not refresh its tool list, keep dispatching through the gateway with ToolName "herdr_link" and an Arguments object carrying {"action":"peers"|"send"|"close", ...}.
92
+ - If the host did not refresh its tool list, keep dispatching through the gateway with ToolName "herdr_link" and an Arguments object carrying {"action":"start"|"peers"|"send"|"close", ...}.
93
+ - Start a new Herdr Agent in an existing pane. Provide name and pane, then choose exactly one complete parameter source: config_agent for .agents/agent_config.json, or kind plus args for explicit Herdr start parameters. These modes are mutually exclusive; partial overrides are not supported. This operation does not create panes or retry/fallback after failure.
91
94
 
92
95
  After activation, Herdr Link MCP tools are invoked through call_mcp_tool.
93
96
 
94
97
  Use:
95
98
  - ServerName: "herdr_link"
96
- - ToolName: "herdr_link_peers", "herdr_link_send", or "herdr_link_close"
99
+ - ToolName: "herdr_link_start", "herdr_link_peers", "herdr_link_send", or "herdr_link_close"
97
100
  - Arguments: the canonical input object for that Herdr Link tool
98
101
  ```
99
102
 
@@ -195,7 +198,7 @@ printf '%s\n' 'Herdr Link gateway: activate only when the user explicitly asks t
195
198
  }
196
199
  ```
197
200
 
198
- 契约提示以 PreInvocation `ephemeralMessage` hook 为主通道:它只注入短 Tier-0 hint,不在 dormant 阶段生成完整 Contract。激活后由 wrapper 的 active tools/list descriptions 与 gateway dispatch 提供完整语义。实测注记(2026-08-23 transcript 取证):AGY 的 model-facing MCP 调用是 **wrapper 形态**——`ServerName:"herdr_link"` + `ToolName` 参数化调用(非 `mcp__` 前缀独立函数),符合协议 §4.5 的 wrapper 条款。hooks 配置:
201
+ 契约提示以 PreInvocation `ephemeralMessage` hook 为主通道:它只注入短 Tier-0 hint,不在 dormant 阶段生成完整 Contract。激活后由 wrapper 的 active tools/list descriptions 与 gateway dispatch 提供完整语义。实测注记(2026-08-23 transcript 取证):AGY 的 model-facing MCP 调用是 **wrapper 形态**——`ServerName:"herdr_link"` + `ToolName` 参数化调用(非 `mcp__` 前缀独立函数),符合协议 §4.6 的 wrapper 条款。hooks 配置:
199
202
 
200
203
  ```json
201
204
  {
@@ -252,15 +255,15 @@ printf '%s\n%s\n%s\n%s\n' \
252
255
  '{"jsonrpc":"2.0","id":4,"method":"tools/list"}' | node "$BUNDLE"
253
256
  ```
254
257
 
255
- 预期:`initialize` 结果含 `"capabilities":{"tools":{"listChanged":true}}`;dormant list 仅 `herdr_link`;激活调用返回 `{"status":"active",...}` 且 stdout 在其后出现一行 `"method":"notifications/tools/list_changed"`;再次 `tools/list` 含 gateway + 三个 canonical 工具。对不存在的 peer 调用 `herdr_link_send`(或 gateway action 分发等价形式)应得到 `"isError":true` 且文本以 `PEER_NOT_FOUND:` 开头;跨 workspace 目标同样表现为 `PEER_NOT_FOUND`,不得泄漏目标存在于其他 workspace。
258
+ 预期:`initialize` 结果含 `"capabilities":{"tools":{"listChanged":true}}`;dormant list 仅 `herdr_link`;激活调用返回 `{"status":"active",...}` 且 stdout 在其后出现一行 `"method":"notifications/tools/list_changed"`;再次 `tools/list` 含 gateway + 四个 canonical 工具。对不存在的 peer 调用 `herdr_link_send`(或 gateway action 分发等价形式)应得到 `"isError":true` 且文本以 `PEER_NOT_FOUND:` 开头;跨 workspace 目标同样表现为 `PEER_NOT_FOUND`,不得泄漏目标存在于其他 workspace。
256
259
 
257
260
  ## 6. 行为边界(与 PROTOCOL.md 一致)
258
261
 
259
- - 工具失败是本地 tool failure:`NOT_IN_HERDR` / `SELF_UNNAMED` / `PEER_NOT_FOUND` / `SEND_FAILED` / `CLOSE_FAILED`,一律 `isError:true` 文本返回,进程不崩溃、不自动重试、不 fallback;
260
- - activation 是本 stdio 连接内的内存状态:连接断开即回到 dormant,不持久化、不跨连接共享;JSON-RPC 保留错误码(-32700 等)只用于 transport 层,五个 Link 错误码永不映射其上;
261
- - server 只调用 `agent get` / `agent list` / `agent prompt` / `pane close`,外加仅限 self identity bootstrap(PROTOCOL.md §6.3,目标只能是当前 pane 的未命名 occupant)的 `agent rename <self-pane>`,共五个 CLI 面,argv 数组执行,无 shell;每次通信调用实时解析 live identity/workspace 并强制 same-workspace guard;
262
+ - 工具失败是本地 tool failure:`NOT_IN_HERDR` / `SELF_UNNAMED` / `PEER_NOT_FOUND` / `SEND_FAILED` / `CLOSE_FAILED` / `START_CONFIG_NOT_FOUND` / `START_AGENT_NOT_FOUND` / `START_CONFIG_INVALID` / `START_INPUT_INVALID` / `START_FAILED`,一律 `isError:true` 文本返回,进程不崩溃、不自动重试、不 fallback;
263
+ - activation 是本 stdio 连接内的内存状态:连接断开即回到 dormant,不持久化、不跨连接共享;JSON-RPC 保留错误码(-32700 等)只用于 transport 层,Link 错误码永不映射其上;
264
+ - server 只调用 `agent get` / `agent list` / `agent prompt` / `agent start` / `pane close`,外加仅限 self identity bootstrap(PROTOCOL §6.3,目标只能是当前 pane 的未命名 occupant)的 `agent rename <self-pane>`,共六个 CLI 面,argv 数组执行,无 shell;每次通信调用实时解析 live identity/workspace 并强制 same-workspace guard;
262
265
  - server 启动时执行一次 `ensureSelfName()`(fire-and-forget,失败静默、稍后以 `SELF_UNNAMED` 呈现):手动启动且未命名的 agent 无需人工 rename 即可成为可发现 peer;
263
- - 不提供 agent 创建/调度/回收、workspace/topology 控制等任何 Non-goals 能力;worker 生命周期仍由调用方决定;正常协作不依赖外部 `AGENTS.md` / Skill 补充 Contract。
266
+ - 不提供业务调度、Agent 创建/回收、workspace/topology 控制等任何 Non-goals 能力;`herdr_link_start` 只执行调用方明确的 configured/explicit 启动选择;worker 生命周期其余部分仍由调用方决定;正常协作不依赖外部 `AGENTS.md` / Skill 补充 Contract。
264
267
 
265
268
  ## 7. 已知坑(Codex/AGY 真机实测)
266
269
 
@@ -0,0 +1,41 @@
1
+ {
2
+ "version": 1,
3
+ "agents": {
4
+ "example-single": {
5
+ "variants": [
6
+ {
7
+ "kind": "pi",
8
+ "args": [
9
+ "--model",
10
+ "your-provider/your-model",
11
+ "--thinking",
12
+ "high"
13
+ ]
14
+ }
15
+ ]
16
+ },
17
+ "example-round-robin": {
18
+ "strategy": "round-robin",
19
+ "variants": [
20
+ {
21
+ "kind": "pi",
22
+ "args": [
23
+ "--model",
24
+ "provider-a/model-a",
25
+ "--thinking",
26
+ "high"
27
+ ]
28
+ },
29
+ {
30
+ "kind": "pi",
31
+ "args": [
32
+ "--model",
33
+ "provider-b/model-b",
34
+ "--thinking",
35
+ "high"
36
+ ]
37
+ }
38
+ ]
39
+ }
40
+ }
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "herdr-link",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "pi": {
5
5
  "extensions": [
6
6
  "./src/pi.ts"
@@ -11,12 +11,13 @@
11
11
  "herdr",
12
12
  "agent-interop"
13
13
  ],
14
- "description": "Herdr Link — cross-agent interoperability layer for Herdr sessions. V1: Pi / OpenCode native adapters + shared stdio MCP server for Claude Code / Codex / AGY.",
14
+ "description": "Herdr Link — cross-agent interoperability layer for Herdr sessions. Pi / OpenCode native adapters + shared stdio MCP server for Claude Code / Codex / AGY.",
15
15
  "files": [
16
16
  "PROTOCOL.md",
17
17
  "README.md",
18
18
  "CHANGELOG.md",
19
19
  "docs/mcp-wiring.md",
20
+ "examples/",
20
21
  "dist/",
21
22
  "src/",
22
23
  "scripts/mcp-probe.mjs"
@@ -40,7 +41,8 @@
40
41
  "typecheck": "tsc --noEmit",
41
42
  "test": "node --experimental-strip-types --test test/*.test.ts",
42
43
  "build:opencode": "esbuild src/opencode.ts --bundle --format=esm --platform=node --external:@opencode-ai/plugin --outfile=dist/herdr-link.opencode.js",
43
- "build:mcp": "esbuild src/mcp.ts --bundle --format=esm --platform=node --target=node22 --banner:js='#!/usr/bin/env node' --outfile=dist/herdr-link.mcp.js"
44
+ "build:mcp": "esbuild src/mcp.ts --bundle --format=esm --platform=node --target=node22 --banner:js='#!/usr/bin/env node' --outfile=dist/herdr-link.mcp.js",
45
+ "check:mcp-bundle": "node scripts/check-mcp-bundle.mjs"
44
46
  },
45
47
  "peerDependencies": {
46
48
  "@earendil-works/pi-coding-agent": "*",