patchcord 0.6.2 → 0.6.5

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "patchcord",
3
3
  "description": "Cross-machine agent messaging. Messages from other agents land in the inbox and wake the agent to reply.",
4
- "version": "0.6.2",
4
+ "version": "0.6.5",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
package/bin/patchcord.mjs CHANGED
@@ -93,7 +93,7 @@ agent:
93
93
  patchcord whoami this agent's identity + project
94
94
  patchcord whoami --propose "<text>" set self-description (two-shot confirm)
95
95
  patchcord agents [name] list agents in your namespace, or show one
96
- patchcord subscribe [interval] start the realtime listener
96
+ patchcord subscribe [interval] [--kimi] realtime listener (Kimi: poll + exit)
97
97
  patchcord upload <file> [--mime <t>] [--as <name>] share a file as an attachment
98
98
 
99
99
  account:
@@ -122,9 +122,10 @@ projects (cloud-only — a free-text label grouping your own namespaces):
122
122
  patchcord namespace set-project <namespace> <project> assign a project label
123
123
  patchcord namespace set-project <namespace> clear it
124
124
 
125
- cross-namespace links (cloud-only, no CLI — leads request/approve via MCP:
126
- request_namespace_link / respond_namespace_link / list_my_namespace_links.
127
- Both namespaces need a --lead agent; links are opt-in, not project-based.)
125
+ cross-namespace links (cloud-only, no CLI — agent-to-agent via MCP:
126
+ request_namespace_link(ns, agent) / respond_namespace_link(ns, agent, approve)
127
+ / list_my_namespace_links. Any agent, no lead role but each link is to ONE
128
+ named agent, never the whole namespace; opt-in, not project-based.)
128
129
 
129
130
  schedules:
130
131
  patchcord schedule create <name> --namespace <ns> --to <agent> --content "..."
@@ -166,6 +167,45 @@ function _kimiContextRequested(options = {}) {
166
167
  || Boolean(process.env.KIMI_MCP_CONFIG_FILE);
167
168
  }
168
169
 
170
+ // Strip legacy Python kimi-cli per-project artifacts. Kimi Code (Node) uses
171
+ // .kimi-code/mcp.json only — a leftover .kimi/mcp.json makes subscribe and
172
+ // docs lie about which identity is active.
173
+ function _purgeLegacyKimiProjectConfig(dir) {
174
+ const legacyMcp = join(dir, ".kimi", "mcp.json");
175
+ if (existsSync(legacyMcp)) {
176
+ try {
177
+ const obj = JSON.parse(readFileSync(legacyMcp, "utf-8"));
178
+ if (obj?.mcpServers?.patchcord) {
179
+ delete obj.mcpServers.patchcord;
180
+ if (Object.keys(obj.mcpServers).length === 0) delete obj.mcpServers;
181
+ }
182
+ if (!obj.mcpServers || Object.keys(obj).length === 0) {
183
+ rmSync(legacyMcp, { force: true });
184
+ } else {
185
+ writeFileSync(legacyMcp, JSON.stringify(obj, null, 2) + "\n");
186
+ }
187
+ } catch {
188
+ try { rmSync(legacyMcp, { force: true }); } catch {}
189
+ }
190
+ }
191
+ const legacySkills = join(dir, ".kimi", "skills");
192
+ if (existsSync(legacySkills)) {
193
+ for (const stale of ["patchcord", "patchcord-inbox", "patchcord-wait", "patchcord-subscribe", "patchcord:inbox", "patchcord:wait", "patchcord:subscribe"]) {
194
+ const d = join(legacySkills, stale);
195
+ if (existsSync(d)) { try { rmSync(d, { recursive: true, force: true }); } catch {} }
196
+ }
197
+ try {
198
+ if (readdirSync(legacySkills).length === 0) rmSync(legacySkills, { recursive: true, force: true });
199
+ } catch {}
200
+ }
201
+ try {
202
+ const legacyKimi = join(dir, ".kimi");
203
+ if (existsSync(legacyKimi) && readdirSync(legacyKimi).length === 0) {
204
+ rmSync(legacyKimi, { recursive: true, force: true });
205
+ }
206
+ } catch {}
207
+ }
208
+
169
209
  // Insert-or-update mcp_servers.patchcord in a Hermes ~/.hermes/config.yaml,
170
210
  // preserving the rest of the user's YAML. Block-style only (what Hermes writes).
171
211
  function upsertHermesConfig(existing, url, token) {
@@ -667,9 +707,14 @@ if (cmd === "subscribe") {
667
707
  process.exit(1);
668
708
  }
669
709
  const { spawnSync } = await import("child_process");
710
+ const spawnEnv = {
711
+ ...process.env,
712
+ PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
713
+ };
714
+ if (forceKimi) spawnEnv.PATCHCORD_KIMI_REQUIRED = "1";
670
715
  const result = spawnSync("bash", [kimiSubScript, intervalArg], {
671
716
  stdio: "inherit",
672
- env: { ...process.env, PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin" },
717
+ env: spawnEnv,
673
718
  });
674
719
  process.exit(result.status ?? (result.signal ? 1 : 0));
675
720
  }
@@ -1034,6 +1079,7 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1034
1079
  }
1035
1080
  if (tool === "kimi" || tool === "kimi-code") {
1036
1081
  const kdir = join(dir, ".kimi-code"); mkdirSync(kdir, { recursive: true });
1082
+ _purgeLegacyKimiProjectConfig(dir);
1037
1083
  return writeJson(join(kdir, "mcp.json"), (o) => {
1038
1084
  o.mcpServers = o.mcpServers || {};
1039
1085
  o.mcpServers.patchcord = { url: `${baseUrl}/mcp/bearer`, headers: hdr };
@@ -1856,20 +1902,11 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1856
1902
  if (windsurfChanged) globalChanges.push("Windsurf skills installed");
1857
1903
  }
1858
1904
 
1859
- // Gemini CLI + Antigravity SHARE ~/.gemini/skills and ~/.gemini/commands, but
1860
- // their MCP config is NOT shared: Gemini CLI stores patchcord globally
1861
- // (~/.gemini/settings.json) while Antigravity is PER-PROJECT
1862
- // (<project>/.agents/mcp_config.json). So the shared global skill must be
1863
- // seeded ONLY off Gemini CLI's global config — never off an Antigravity config.
1864
- // A patchcord entry in the global ~/.gemini/antigravity(-cli)/mcp_config.json or
1865
- // ~/.gemini/config/mcp_config.json (Antigravity's post-migration global path —
1866
- // confirmed live via the sibling ~/.gemini/config/.migrated marker; the older
1867
- // two paths are its predecessors) is a leaked per-project token: it makes agy
1868
- // load the patchcord MCP (and its "check inbox first" instruction) in EVERY
1869
- // project, and it kept this skill seeded everywhere. We strip that leak below
1870
- // and gate the skill on Gemini only. Keep all three paths in sync — a prior
1871
- // fix (a36809c) covered only the first two and missed the migrated path,
1872
- // leaving leaks there uncleaned.
1905
+ // Gemini CLI and Antigravity CLI SHARE ~/.gemini/skills/ Antigravity reads it
1906
+ // globally in EVERY project. NEVER seed patchcord skills there (v0.6.2 still did
1907
+ // when Gemini had MCP configured, poisoning bare agy sessions). Gemini CLI uses
1908
+ // ~/.gemini/commands/ slash commands only. Antigravity gets per-project skills
1909
+ // at <project>/.agents/skills/ during provision (see isAntigravity below).
1873
1910
  if (existsSync(join(HOME, ".gemini"))) {
1874
1911
  // Remove any leaked patchcord server from Antigravity's GLOBAL configs so a
1875
1912
  // per-project token stops hijacking every agy session. Preserve other MCP
@@ -1889,27 +1926,28 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1889
1926
  }
1890
1927
  } catch {}
1891
1928
  }
1892
- // Older installers seeded Antigravity-only skills globally, and the
1893
- // Antigravity CLI caches each MCP server's tools/instructions globally.
1894
- // Both survive removal of the global mcp_config entry and can make a bare
1895
- // agy session synthesize an unconditional Patchcord rule in every folder.
1896
- // Antigravity is project-scoped, so remove only Patchcord's stale global
1897
- // artifacts; other MCP servers and Antigravity state stay untouched.
1898
- const staleAntigravityPatchcord = [
1929
+ // Unconditionally purge every global Patchcord skill/instruction path Antigravity
1930
+ // reads. Older installers seeded ~/.gemini/skills/patchcord for Gemini; agy-cli
1931
+ // inherits that shared dir and auto-loads it in unrelated projects.
1932
+ const staleGlobalPatchcord = [
1933
+ join(HOME, ".gemini", "skills", "patchcord"),
1934
+ join(HOME, ".gemini", "skills", "patchcord-wait"),
1899
1935
  join(HOME, ".gemini", "antigravity", "skills", "patchcord"),
1900
1936
  join(HOME, ".gemini", "antigravity", "skills", "patchcord-wait"),
1901
1937
  join(HOME, ".gemini", "antigravity-cli", "skills", "patchcord"),
1902
1938
  join(HOME, ".gemini", "antigravity-cli", "skills", "patchcord-wait"),
1939
+ join(HOME, ".gemini", "config", "skills", "patchcord"),
1940
+ join(HOME, ".gemini", "config", "skills", "patchcord-wait"),
1903
1941
  join(HOME, ".gemini", "antigravity-cli", "mcp", "patchcord"),
1904
1942
  ];
1905
- let removedStaleAntigravity = false;
1906
- for (const stale of staleAntigravityPatchcord) {
1943
+ let removedStaleGlobal = false;
1944
+ for (const stale of staleGlobalPatchcord) {
1907
1945
  if (existsSync(stale)) {
1908
- try { rmSync(stale, { recursive: true, force: true }); removedStaleAntigravity = true; } catch {}
1946
+ try { rmSync(stale, { recursive: true, force: true }); removedStaleGlobal = true; } catch {}
1909
1947
  }
1910
1948
  }
1911
- if (removedStaleAntigravity) {
1912
- globalChanges.push("Removed stale global Antigravity Patchcord instructions (agy is per-project)");
1949
+ if (removedStaleGlobal) {
1950
+ globalChanges.push("Removed global Patchcord skills/instructions (agy reads ~/.gemini/skills — per-project only)");
1913
1951
  }
1914
1952
  // true = configured, false = file absent (definitely not), null = unparseable (unknown)
1915
1953
  const hasPatchcordMcp = (p, keyPath) => {
@@ -1922,38 +1960,27 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1922
1960
  ];
1923
1961
  const mcpConfigured = checks.some((c) => c === true);
1924
1962
  const mcpUnknown = checks.some((c) => c === null);
1925
- const geminiSkillDir = join(HOME, ".gemini", "skills", "patchcord");
1926
- const geminiWaitDir = join(HOME, ".gemini", "skills", "patchcord-wait");
1927
1963
  const geminiCmdDir = join(HOME, ".gemini", "commands");
1928
1964
  const geminiLegacyCmd = join(geminiCmdDir, "patchcord.toml"); // single-command file from older installs
1929
1965
  if (mcpConfigured) {
1930
1966
  let geminiChanged = false;
1931
- // Refresh to the CURRENT plugin version (overwrite, not create-if-absent)
1932
- // so skill/gate fixes reach machines that already have an older copy.
1933
1967
  const seedFile = (dest, src) => {
1934
1968
  mkdirSync(dirname(dest), { recursive: true });
1935
1969
  const want = readFileSync(src, "utf-8");
1936
1970
  let cur = ""; try { cur = readFileSync(dest, "utf-8"); } catch {}
1937
1971
  if (cur !== want) { writeFileSync(dest, want); geminiChanged = true; }
1938
1972
  };
1939
- seedFile(join(geminiSkillDir, "SKILL.md"), join(pluginRoot, "skills", "inbox", "SKILL.md"));
1940
- seedFile(join(geminiWaitDir, "SKILL.md"), join(pluginRoot, "skills", "wait", "SKILL.md"));
1973
+ // Gemini CLI only — slash commands, NOT ~/.gemini/skills (shared with agy-cli).
1941
1974
  seedFile(join(geminiCmdDir, "inbox.toml"), join(pluginRoot, "commands", "inbox.toml"));
1942
1975
  seedFile(join(geminiCmdDir, "wait.toml"), join(pluginRoot, "commands", "wait.toml"));
1943
1976
  if (existsSync(geminiLegacyCmd)) { try { unlinkSync(geminiLegacyCmd); geminiChanged = true; } catch {} }
1944
- if (geminiChanged) globalChanges.push("Gemini/Antigravity patchcord skills + commands refreshed");
1977
+ if (geminiChanged) globalChanges.push("Gemini CLI patchcord commands refreshed");
1945
1978
  } else if (!mcpUnknown) {
1946
- // Definitively no patchcord MCP under ~/.gemini → remove any previously
1947
- // seeded skill/commands so a bare Gemini/Antigravity session has nothing
1948
- // to act on (incl. the legacy patchcord.toml command).
1949
1979
  let removed = false;
1950
- for (const d of [geminiSkillDir, geminiWaitDir]) {
1951
- if (existsSync(d)) { try { rmSync(d, { recursive: true, force: true }); removed = true; } catch {} }
1952
- }
1953
1980
  for (const f of [join(geminiCmdDir, "inbox.toml"), join(geminiCmdDir, "wait.toml"), geminiLegacyCmd]) {
1954
1981
  if (existsSync(f)) { try { unlinkSync(f); removed = true; } catch {} }
1955
1982
  }
1956
- if (removed) globalChanges.push("Removed stale Gemini/Antigravity patchcord skills (no patchcord MCP configured there)");
1983
+ if (removed) globalChanges.push("Removed stale Gemini CLI patchcord commands (no patchcord MCP configured there)");
1957
1984
  }
1958
1985
  }
1959
1986
 
@@ -2100,6 +2127,13 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2100
2127
  const p = join(legacyKimiHome, f);
2101
2128
  if (existsSync(p)) { try { rmSync(p, { force: true }); } catch {} }
2102
2129
  }
2130
+ const legacySkillsRoot = join(legacyKimiHome, "skills");
2131
+ if (existsSync(legacySkillsRoot)) {
2132
+ for (const stale of ["patchcord", "patchcord-inbox", "patchcord-wait", "patchcord-subscribe", "patchcord:inbox", "patchcord:wait", "patchcord:subscribe"]) {
2133
+ const d = join(legacySkillsRoot, stale);
2134
+ if (existsSync(d)) { try { rmSync(d, { recursive: true, force: true }); } catch {} }
2135
+ }
2136
+ }
2103
2137
  const legacyConfigPath = join(legacyKimiHome, "config.toml");
2104
2138
  if (existsSync(legacyConfigPath)) {
2105
2139
  try {
@@ -2843,9 +2877,8 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2843
2877
  // Antigravity CLI: PROJECT-scoped config at <project>/.agents/mcp_config.json
2844
2878
  // (one project = one namespace = one agent identity — never global, which
2845
2879
  // would give one shared agent cross-project visibility). Remote HTTP uses
2846
- // the `serverUrl` key. Skills are NOT seeded globally here: a stray skill in
2847
- // the shared ~/.gemini/skills makes bare agy sessions improvise. The MCP
2848
- // server injects its own instructions once connected.
2880
+ // the `serverUrl` key. Skills are per-project at .agents/skills/ only never
2881
+ // ~/.gemini/skills (agy-cli reads that globally in every folder).
2849
2882
  const agPath = join(cwd, ".agents", "mcp_config.json");
2850
2883
  const agOk = updateJsonConfig(agPath, (obj) => {
2851
2884
  obj.mcpServers = obj.mcpServers || {};
@@ -2857,8 +2890,15 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2857
2890
  },
2858
2891
  };
2859
2892
  });
2893
+ const agInboxDir = join(cwd, ".agents", "skills", "patchcord");
2894
+ const agWaitDir = join(cwd, ".agents", "skills", "patchcord-wait");
2895
+ mkdirSync(agInboxDir, { recursive: true });
2896
+ mkdirSync(agWaitDir, { recursive: true });
2897
+ cpSync(join(pluginRoot, "per-project-skills", "antigravity", "inbox", "SKILL.md"), join(agInboxDir, "SKILL.md"));
2898
+ cpSync(join(pluginRoot, "per-project-skills", "antigravity", "wait", "SKILL.md"), join(agWaitDir, "SKILL.md"));
2860
2899
  if (agOk) {
2861
2900
  console.log(`\n ${green}✓${r} Antigravity CLI configured: ${dim}${agPath}${r}`);
2901
+ console.log(` ${green}✓${r} Skills installed: ${dim}.agents/skills/patchcord${r}, ${dim}patchcord-wait${r}`);
2862
2902
  console.log(` ${yellow}Project config — this agent is scoped to ${cwd}.${r}`);
2863
2903
  console.log(` ${dim}Verify in agy with the /mcp slash command.${r}`);
2864
2904
  }
@@ -2937,6 +2977,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2937
2977
  };
2938
2978
  });
2939
2979
  if (kcOk) {
2980
+ _purgeLegacyKimiProjectConfig(cwd);
2940
2981
  console.log(`\n ${green}✓${r} Kimi Code configured: ${dim}${kcPath}${r}`);
2941
2982
  console.log(` ${dim}Kimi Code auto-loads project-local .kimi-code/mcp.json — just run ${r}${bold}kimi${r}${dim} in this project (no wrapper needed).${r}`);
2942
2983
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.6.2",
3
+ "version": "0.6.5",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "scripts": {
6
6
  "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json"
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: patchcord-inbox
3
+ description: Patchcord MCP inbox workflow for this project's Antigravity agent
4
+ ---
5
+
6
+ ## Applies only when Patchcord MCP tools are loaded
7
+
8
+ If the `inbox` MCP tool is not available in this session, this skill does not
9
+ apply. Do nothing: do not run the Patchcord CLI, read a token from a config
10
+ file, or call the HTTP API. A missing project MCP config is a normal state.
11
+
12
+ Invoke with `/patchcord-inbox` when Patchcord MCP is connected in this project.
13
+
14
+ Call the `inbox` MCP tool now. In its response, the first header line is YOUR own identity (the recipient); the real sender of each message is on a `From X` line — never confuse the two.
15
+
16
+ For each pending message, classify it and act:
17
+ - **ACK** — short signals like thanks, noted, works, great, ok, 👍 with no task → close it silently: `reply(message_id, resolve=true)` with NO content. Never send a text reply to an ack (it creates infinite ack chains).
18
+ - **BLOCKED** — you cannot do the work right now (busy, missing credentials, ambiguous target) → `reply(message_id, "<reason>", defer=true)` so it stays in your inbox as a reminder. Never silently skip a message.
19
+ - **ACTIONABLE** — do the work the message asks for FIRST (edit the file, run the command, write the code), THEN `reply(message_id, "<concrete summary of what you did, with file paths and line numbers>")`. Never reply "will do" / "understood" before doing the work.
20
+
21
+ After handling every message, tell the human who wrote, what they asked, and what you did.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: patchcord-wait
3
+ description: Block until one Patchcord message arrives (this project's Antigravity agent)
4
+ ---
5
+
6
+ ## Applies only when Patchcord MCP tools are loaded
7
+
8
+ If the `wait_for_message` MCP tool is not available in this session, this skill
9
+ does not apply. Do nothing: do not run the Patchcord CLI, read a token from a
10
+ config file, or call the HTTP API. A missing project MCP config is a normal state.
11
+
12
+ Invoke with `/patchcord-wait` when Patchcord MCP is connected in this project.
13
+
14
+ Call `wait_for_message()` to block until a message arrives (up to 5 minutes).
15
+
16
+ When a message arrives:
17
+ 1. Do the work described in the message first.
18
+ 2. `reply(message_id, "<concrete summary of what you did>")`.
19
+ 3. Tell the human who wrote and what you did.
20
+ 4. Call `wait_for_message()` again to keep listening.
21
+
22
+ If `wait_for_message()` errors, poll `inbox()` every 10–15 seconds instead.
@@ -15,13 +15,11 @@ set -euo pipefail
15
15
  command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 1; }
16
16
 
17
17
  # Resolve MCP config: PROJECT-scoped ONLY (walk up from cwd for .kimi-code/mcp.json).
18
- # Kimi Code (the current product) uses .kimi-code/the legacy .kimi/ path is
19
- # deprecated and MUST NOT be used; it also mismatched where `patchcord provision`
20
- # writes the config (.kimi-code/mcp.json), so the listener never found it.
18
+ # Legacy .kimi/mcp.json (Python kimi-cli) is NOT read reinstall/provision removes it.
21
19
  # NO global fallback: auto-launched by the (global) SessionStart hook on EVERY
22
20
  # Kimi session, so a global fallback would start a listener for some leftover
23
- # identity in every folder. No project config in the cwd tree => no patchcord
24
- # agent here => exit quietly (0, not 1: must not spam stderr in non-patchcord dirs).
21
+ # identity in every folder. No project config in the cwd tree => exit quietly
22
+ # (0) unless PATCHCORD_KIMI_REQUIRED=1 (explicit `patchcord subscribe --kimi`).
25
23
  KIMI_MCP=""
26
24
  dir="$PWD"
27
25
  while [ "$dir" != "/" ]; do
@@ -33,6 +31,11 @@ while [ "$dir" != "/" ]; do
33
31
  done
34
32
 
35
33
  if [ -z "$KIMI_MCP" ] || [ ! -f "$KIMI_MCP" ]; then
34
+ if [ "${PATCHCORD_KIMI_REQUIRED:-}" = "1" ]; then
35
+ echo "No Kimi Code Patchcord config in this project tree (.kimi-code/mcp.json)." >&2
36
+ echo "Run patchcord provision <agent> --tool kimi --namespace <ns> from the project root." >&2
37
+ exit 1
38
+ fi
36
39
  exit 0
37
40
  fi
38
41
 
@@ -46,10 +49,16 @@ fi
46
49
 
47
50
  BASE_URL=$(echo "$URL" | sed 's|/mcp$||; s|/mcp/bearer$||')
48
51
 
49
- # Derive agent identity for pidfile
52
+ # Poll loop: count_only + no registry write — same cost model as subscribe.mjs
53
+ # drainQueueOnce (id SELECT only). Kimi cannot hold a WebSocket/Realtime
54
+ # listener (no Monitor; task must exit to wake), so polling is the transport;
55
+ # the bug was fetching full message bodies every tick.
56
+ INBOX_POLL_QS="status=pending&limit=5&count_only=1&touch_presence=0"
57
+
58
+ # Derive agent identity for pidfile (count_only, no registry write)
50
59
  IDENTITY_RESP=$(curl -s --max-time 5 \
51
60
  -H "Authorization: Bearer ${TOKEN}" \
52
- "${BASE_URL}/api/inbox?limit=0" 2>/dev/null || echo "{}")
61
+ "${BASE_URL}/api/inbox?limit=0&count_only=1&touch_presence=0" 2>/dev/null || echo "{}")
53
62
  NAMESPACE_ID=$(echo "$IDENTITY_RESP" | jq -r '.namespace_id // empty' 2>/dev/null || true)
54
63
  AGENT_ID=$(echo "$IDENTITY_RESP" | jq -r '.agent_id // empty' 2>/dev/null || true)
55
64
 
@@ -59,7 +68,7 @@ if [ -z "$NAMESPACE_ID" ] || [ -z "$AGENT_ID" ]; then
59
68
  fi
60
69
 
61
70
  PIDFILE="/tmp/patchcord_subscribe_${NAMESPACE_ID}_${AGENT_ID}.pid"
62
- NOTIFY_FILE="${HOME}/.kimi/patchcord-subscribe-notify.txt"
71
+ NOTIFY_FILE="${KIMI_CODE_HOME:-${HOME}/.kimi-code}/patchcord-subscribe-notify.txt"
63
72
 
64
73
  # Always replace any existing listener for this project. Otherwise the agent
65
74
  # sees an instant exit ("already running") and narrates it as "listener failed
@@ -96,7 +105,7 @@ echo "PATCHCORD: subscribe started (poll ${INTERVAL}s) for ${AGENT_ID}@${NAMESPA
96
105
  while true; do
97
106
  RESP=$(curl -s --max-time 10 \
98
107
  -H "Authorization: Bearer ${TOKEN}" \
99
- "${BASE_URL}/api/inbox?status=pending&limit=5" 2>/dev/null || echo "{}")
108
+ "${BASE_URL}/api/inbox?${INBOX_POLL_QS}" 2>/dev/null || echo "{}")
100
109
 
101
110
  COUNT=$(echo "$RESP" | jq -r '.pending_count // 0' 2>/dev/null || echo "0")
102
111
 
@@ -1,10 +1,9 @@
1
1
  ---
2
2
  name: patchcord:inbox
3
3
  description: >
4
- Cross-agent messaging across MCP-connected agents. Use when user mentions
5
- other agents, patchcord, check inbox, send message, who's online, agent
6
- coordination, or when you receive additionalContext about pending patchcord
7
- messages.
4
+ MCP tool workflow for Patchcord cross-agent messaging. Active ONLY when
5
+ mcp_patchcord_* tools are loaded in this session ignore this skill
6
+ otherwise.
8
7
  ---
9
8
  # patchcord
10
9
 
@@ -42,7 +41,9 @@ The user can undo any change in 3 seconds with git. A wrong action costs nothing
42
41
 
43
42
  **If you can't do it right now** (busy with current task): use `reply(message_id, "reason why deferred", defer=true)`. This keeps the message visible in your inbox so you will come back to it. Never silently skip a message - you will forget it. If you don't act and don't defer, the message is lost forever.
44
43
 
45
- ## On session start or when prompted by a hook
44
+ ## When MCP tools are loaded: session start or hook
45
+
46
+ The following applies only when the patchcord MCP tools are present in this session (see the gate above). If they are absent, skip this entire section.
46
47
 
47
48
  Call inbox(). It returns pending messages, recently active agents, and your own push-receiving state via `self_subscribed`. Note that value — it determines whether you should call `wait_for_message` after sends for the rest of the session.
48
49
 
@@ -184,8 +185,8 @@ Named threads group related messages between a pair of agents. Use them for mult
184
185
  - Never show raw JSON to the human - summarize naturally.
185
186
  - **Cross-namespace addressing (`agent@namespace`)**: the syntax always exists in `send_message`/`reply`, but what it actually reaches depends on who you are:
186
187
  - **Ordinary agents (the default):** `agent@namespace` only ever works for YOUR OWN namespace — same as a bare name. Targeting any other namespace is rejected. This is the isolation model, not a bug or a missing feature — don't loop on it or try creative addressing to work around it.
187
- - **Lead agents (cloud only a small number of specially-provisioned identities per namespace, usually the orchestrator):** may reach a peer lead in a DIFFERENT namespace, but only after that specific pair of namespaces has an `approved` `namespace_links` row. You set this up yourself no human step: call `request_namespace_link(peer_namespace)`, the peer namespace's lead gets a patchcord message and calls `respond_namespace_link(peer_namespace, approve=True)`, then `agent@namespace` works both ways. Check `list_my_namespace_links()` to see pending/approved/denied status. If you don't know whether you're a lead, you're not one this isn't discoverable by trial and error; ask the human. (Namespace "projects" still exist but are just an organizational label now — they do NOT gate this.)
188
- - A rejection here is almost always correct behavior, not a server error — don't retry with variations of the name.
188
+ - **Any agent (cloud only, no lead role required)** may reach ONE SPECIFIC agent in a DIFFERENT namespace, but only after an `approved` link between exactly that agent pair never a whole-namespace grant. Set it up yourself, agent-to-agent, no human step: call `request_namespace_link(peer_namespace, peer_agent)` naming the exact agent you want to reach; that one agent (and only that one) gets a patchcord message and calls `respond_namespace_link(peer_namespace, peer_agent, approve=True)` naming you back. Once approved, ONLY that pair can message each other — it does NOT open reach to any other agent in either namespace, even in the same namespace as one you've already linked. Want to reach a second agent? Request a separate link for that specific pair. Check `list_my_namespace_links()` to see your own pending/approved/denied links. (Namespace "projects" still exist but are just an organizational label now — they do NOT gate this.)
189
+ - A rejection here is almost always correct behavior, not a server error — don't retry with variations of the name. If you're reaching for `agent@namespace` and haven't specifically linked to THAT agent, request the link first — don't try a different agent name in the same namespace hoping it's already open.
189
190
  - **Do not reply to acks.** "ok", "noted", "seen", "thanks", "good progress", "keep running", thumbs up — anything that is clearly a conversation-ending signal. Just read them and move on. If you must close the thread, use `reply(id, resolve=true)` with NO content. Never send a text reply to an ack.
190
191
  - **resolve=true with ack-only content is an anti-pattern.** `reply(id, "Noted, thanks", resolve=true)` creates a new pending message the other side feels compelled to answer — producing ack chains. If you have nothing substantive to add, omit content entirely: `reply(id, resolve=true)`. Only include content with resolve when it carries new information the recipient needs.
191
192
  - **When you receive an ack**, close it silently: `reply(id, resolve=true)`. No content. This stops the chain.