patchcord 0.5.130 → 0.5.132
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/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -0
- package/bin/patchcord.mjs +141 -97
- package/package.json +1 -1
- package/skills/inbox/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,9 @@ npx patchcord@latest --token <token> --server https://patchcord.yourdomain.com
|
|
|
23
23
|
- **Stop hook** — checks inbox between turns, notifies of pending messages
|
|
24
24
|
- **Slash commands** — `/patchcord` and `/patchcord-wait` for Codex and Gemini CLI
|
|
25
25
|
- **MCP config** — per-project or global config depending on tool
|
|
26
|
+
- **Cursor CLI** — bearer-only MCP endpoint, `patchcord:*` pre-allowed in
|
|
27
|
+
`~/.cursor/permissions.json`, and inbox/wait skills installed when
|
|
28
|
+
`cursor-agent` is detected
|
|
26
29
|
- **Kimi CLI** — global MCP config plus skills in `~/.kimi/skills/`
|
|
27
30
|
|
|
28
31
|
## How it works
|
package/bin/patchcord.mjs
CHANGED
|
@@ -74,72 +74,76 @@ if (
|
|
|
74
74
|
cmd === "help" || cmd === "--help" || cmd === "-h" ||
|
|
75
75
|
process.argv.slice(3).some((a) => a === "--help" || a === "-h")
|
|
76
76
|
) {
|
|
77
|
-
console.log(`patchcord
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
patchcord
|
|
88
|
-
patchcord --
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
patchcord
|
|
94
|
-
patchcord
|
|
95
|
-
patchcord
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
77
|
+
console.log(`usage: patchcord [command] [options]
|
|
78
|
+
|
|
79
|
+
Cross-agent messaging + team orchestration for AI coding agents.
|
|
80
|
+
|
|
81
|
+
Two layers: THIS AGENT (a per-project identity, by bearer token) and your
|
|
82
|
+
ACCOUNT (user-level — provisions agents, runs teams + schedules). \`patchcord
|
|
83
|
+
login\` authenticates the CLI once; account/team/schedule commands then use
|
|
84
|
+
that token and never open a browser on their own.
|
|
85
|
+
|
|
86
|
+
setup:
|
|
87
|
+
patchcord set up this project's agent (browser)
|
|
88
|
+
patchcord --token <t> [--server <url>] headless / self-hosted setup
|
|
89
|
+
patchcord --full same, with full statusline
|
|
90
|
+
patchcord --rename <name> [--agent-type <t>] rename this agent
|
|
91
|
+
|
|
92
|
+
agent:
|
|
93
|
+
patchcord whoami this agent's identity + project
|
|
94
|
+
patchcord whoami --propose "<text>" set self-description (two-shot confirm)
|
|
95
|
+
patchcord agents [name] list agents in your namespace, or show one
|
|
96
|
+
patchcord subscribe [interval] start the realtime listener
|
|
97
|
+
patchcord upload <file> [--mime <t>] [--as <name>] share a file as an attachment
|
|
98
|
+
|
|
99
|
+
account:
|
|
100
|
+
patchcord login authenticate the CLI (your account)
|
|
101
|
+
|
|
102
|
+
orchestrator:
|
|
101
103
|
patchcord orchestrator [--namespace <ns>] [--tool <harness>] [--project <name>]
|
|
102
|
-
|
|
103
|
-
lead
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
patchcord provision <agent> --tool <X> --role <Y> --namespace <ns>
|
|
108
|
-
[--project <name>] [--lead]
|
|
109
|
-
|
|
110
|
-
--lead marks it as a cross-namespace lead
|
|
111
|
-
(--role lead does the same); --project
|
|
112
|
-
assigns its namespace (see PROJECTS below)
|
|
104
|
+
provision the orchestrator here (always
|
|
105
|
+
a lead); launch it to adopt this project
|
|
106
|
+
or build a new team
|
|
107
|
+
|
|
108
|
+
team (run from the project root):
|
|
109
|
+
patchcord provision <agent> --tool <X> --role <Y> --namespace <ns>
|
|
110
|
+
[--dir <sub/>] [--project <name>] [--lead]
|
|
111
|
+
create a worker agent (identity + config)
|
|
113
112
|
patchcord pull <agent> --tool <X> --namespace <ns> [--dir <sub/>]
|
|
114
|
-
|
|
115
|
-
(same identity, new token
|
|
113
|
+
move an existing agent into a folder
|
|
114
|
+
(same identity, new token)
|
|
116
115
|
patchcord provision revoke <agent> --namespace <ns>
|
|
117
|
-
|
|
118
|
-
patchcord team
|
|
119
|
-
patchcord team
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
patchcord schedule create <name> --namespace <ns> --to <agent> --content "..."
|
|
132
|
-
(--at <ISO> | --cron "<expr>" | --every <sec>)
|
|
116
|
+
revoke a worker agent
|
|
117
|
+
patchcord team list [--namespace <ns>] provisioned agents (server view)
|
|
118
|
+
patchcord team status reconcile folder ↔ identity ↔ tmux ↔ token
|
|
119
|
+
patchcord team launch launch each worker in mux
|
|
120
|
+
|
|
121
|
+
projects (cloud-only — a free-text label grouping your own namespaces):
|
|
122
|
+
patchcord namespace set-project <namespace> <project> assign a project label
|
|
123
|
+
patchcord namespace set-project <namespace> clear it
|
|
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.)
|
|
128
|
+
|
|
129
|
+
schedules:
|
|
130
|
+
patchcord schedule create <name> --namespace <ns> --to <agent> --content "..."
|
|
131
|
+
(--at <ISO> | --cron "<expr>" | --every <sec>)
|
|
133
132
|
[--timezone <tz>] [--thread <slug>] [--max-runs N] [--expires <ISO>]
|
|
134
133
|
patchcord schedule list [--namespace <ns>]
|
|
135
134
|
patchcord schedule cancel|test|pause|resume <id>
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
patchcord update
|
|
139
|
-
patchcord --version
|
|
140
|
-
patchcord --help
|
|
136
|
+
misc:
|
|
137
|
+
patchcord update update to the latest version
|
|
138
|
+
patchcord --version show installed version
|
|
139
|
+
patchcord --help show this help
|
|
141
140
|
|
|
142
|
-
|
|
141
|
+
examples:
|
|
142
|
+
patchcord provision codex --tool codex --role maker --namespace myproj
|
|
143
|
+
patchcord schedule create standup --namespace myproj --to lead \\
|
|
144
|
+
--content "status?" --cron "0 9 * * *"
|
|
145
|
+
patchcord team status
|
|
146
|
+
`);
|
|
143
147
|
process.exit(0);
|
|
144
148
|
}
|
|
145
149
|
|
|
@@ -1004,6 +1008,19 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1004
1008
|
o.mcp.patchcord = { type: "remote", url: `${baseUrl}/mcp`, headers: hdr };
|
|
1005
1009
|
});
|
|
1006
1010
|
}
|
|
1011
|
+
if (tool === "cursor") {
|
|
1012
|
+
// Cursor IDE + Cursor CLI (cursor-agent) share the same project config —
|
|
1013
|
+
// same file, same schema (see patchcord.dev/console/connect writer).
|
|
1014
|
+
const cdir = join(dir, ".cursor"); mkdirSync(cdir, { recursive: true });
|
|
1015
|
+
return writeJson(join(cdir, "mcp.json"), (o) => {
|
|
1016
|
+
o.mcpServers = o.mcpServers || {};
|
|
1017
|
+
o.mcpServers.patchcord = {
|
|
1018
|
+
type: "streamable-http",
|
|
1019
|
+
url: `${baseUrl}/mcp/bearer`,
|
|
1020
|
+
headers: hdr,
|
|
1021
|
+
};
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1007
1024
|
if (tool === "antigravity" || tool === "agy") {
|
|
1008
1025
|
// Antigravity CLI: PROJECT-scoped config at .agents/mcp_config.json (one
|
|
1009
1026
|
// project = one namespace = one agent). Remote HTTP uses `serverUrl`.
|
|
@@ -1071,11 +1088,13 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1071
1088
|
const ns = flagVal("namespace");
|
|
1072
1089
|
const subdir = flagVal("dir", arg);
|
|
1073
1090
|
const project = flagVal("project", "");
|
|
1074
|
-
// --lead marks this token as
|
|
1091
|
+
// --lead marks this token as eligible for cross-namespace lead chat (server
|
|
1075
1092
|
// trusts ONLY this explicit, main-token-authenticated request — see
|
|
1076
|
-
// patchcord-cloud's api_provision).
|
|
1077
|
-
//
|
|
1078
|
-
//
|
|
1093
|
+
// patchcord-cloud's api_provision). Reaching another namespace's lead also
|
|
1094
|
+
// requires an approved namespace_links pair (via request_namespace_link /
|
|
1095
|
+
// respond_namespace_link — no CLI, agent-to-agent only). `--role lead` also
|
|
1096
|
+
// sets is_lead server-side (role==="lead" is treated the same), but `--lead`
|
|
1097
|
+
// is the explicit, discoverable way to ask for it without overloading the
|
|
1079
1098
|
// free-text --role field.
|
|
1080
1099
|
const lead = process.argv.includes("--lead");
|
|
1081
1100
|
if (!ns) { console.error("--namespace <project-namespace> required"); process.exit(1); }
|
|
@@ -1100,12 +1119,12 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1100
1119
|
writeFileSync(tj, JSON.stringify(man, null, 2) + "\n");
|
|
1101
1120
|
}
|
|
1102
1121
|
} catch {}
|
|
1103
|
-
// --project assigns this namespace to a project —
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1122
|
+
// --project assigns this namespace to a project — an organizational label
|
|
1123
|
+
// ONLY, does not affect cross-namespace messaging (that's namespace_links,
|
|
1124
|
+
// agent-to-agent, no CLI). Separate call: provisioning creates the agent
|
|
1125
|
+
// identity, project assignment groups the NAMESPACE — safe to retry
|
|
1126
|
+
// independently, and a namespace already in a project just gets reassigned
|
|
1127
|
+
// (last call wins, server-side upsert).
|
|
1109
1128
|
if (project) {
|
|
1110
1129
|
const { status: pStatus, json: pJson } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project });
|
|
1111
1130
|
if (pStatus !== "200") {
|
|
@@ -1119,22 +1138,22 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1119
1138
|
}
|
|
1120
1139
|
|
|
1121
1140
|
if (cmd === "namespace") {
|
|
1122
|
-
// Cloud-only: group namespaces into a "project" —
|
|
1123
|
-
// cross-namespace lead chat
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1141
|
+
// Cloud-only: group namespaces into a "project" — a free-text organizational
|
|
1142
|
+
// label only. Does NOT gate cross-namespace lead chat — that's decided by
|
|
1143
|
+
// per-pair namespace_links, approved lead-to-lead via the MCP tools
|
|
1144
|
+
// request_namespace_link / respond_namespace_link (no CLI for that; see
|
|
1145
|
+
// --help's CROSS-NAMESPACE LINKS section).
|
|
1127
1146
|
const sub = process.argv[3];
|
|
1128
1147
|
if (sub === "set-project") {
|
|
1129
1148
|
const ns = process.argv[4];
|
|
1130
1149
|
const project = process.argv[5];
|
|
1131
|
-
if (!ns) { console.error("Usage: patchcord namespace set-project <namespace> [project-name]\n Omit project-name to clear
|
|
1150
|
+
if (!ns) { console.error("Usage: patchcord namespace set-project <namespace> [project-name]\n Omit project-name to clear the label."); process.exit(1); }
|
|
1132
1151
|
const { status, json } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project: project || "" });
|
|
1133
1152
|
if (status !== "200") { console.error(`set-project failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
1134
1153
|
if (json.status === "cleared") {
|
|
1135
|
-
console.log(`✓ ${ns} un-grouped — no project
|
|
1154
|
+
console.log(`✓ ${ns} un-grouped — no project label`);
|
|
1136
1155
|
} else {
|
|
1137
|
-
console.log(`✓ ${ns} assigned to project "${json.project}"`);
|
|
1156
|
+
console.log(`✓ ${ns} assigned to project "${json.project}" (label only — does not affect cross-namespace messaging)`);
|
|
1138
1157
|
}
|
|
1139
1158
|
process.exit(0);
|
|
1140
1159
|
}
|
|
@@ -1188,8 +1207,10 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1188
1207
|
const project = flagVal("project", "");
|
|
1189
1208
|
const hostname = run("hostname -s") || run("hostname") || "unknown";
|
|
1190
1209
|
// is_lead: true always — the orchestrator IS the one identity per namespace
|
|
1191
|
-
// meant to coordinate across teams. Harmless
|
|
1192
|
-
//
|
|
1210
|
+
// meant to coordinate across teams. Harmless by itself: a lead only reaches
|
|
1211
|
+
// another namespace's lead after an approved namespace_links pair (agent-to-
|
|
1212
|
+
// agent via request_namespace_link/respond_namespace_link — fails closed
|
|
1213
|
+
// until both leads explicitly consent).
|
|
1193
1214
|
const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: "orchestrator", tool, role: "orchestrator", label: "orchestrator:self", is_lead: true });
|
|
1194
1215
|
if (status !== "200" || !json?.token) { console.error(`could not set up orchestrator (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
1195
1216
|
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
@@ -1722,9 +1743,14 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1722
1743
|
}
|
|
1723
1744
|
}
|
|
1724
1745
|
|
|
1725
|
-
// Cursor
|
|
1746
|
+
// Cursor. Cursor CLI (cursor-agent) can be installed without creating the
|
|
1747
|
+
// IDE's skills directory, so detect the executable as well as the directory.
|
|
1748
|
+
// The bearer-only endpoint prevents Cursor from incorrectly starting OAuth
|
|
1749
|
+
// discovery when the installer has supplied a static bearer header.
|
|
1726
1750
|
const cursorSkillsRoot = join(HOME, ".cursor", "skills-cursor");
|
|
1727
|
-
|
|
1751
|
+
const hasCursorAgent = run("which cursor-agent");
|
|
1752
|
+
if (hasCursorAgent || existsSync(cursorSkillsRoot)) {
|
|
1753
|
+
mkdirSync(cursorSkillsRoot, { recursive: true });
|
|
1728
1754
|
const cursorSkillDir = join(cursorSkillsRoot, "patchcord");
|
|
1729
1755
|
const cursorWaitDir = join(cursorSkillsRoot, "patchcord-wait");
|
|
1730
1756
|
let cursorChanged = false;
|
|
@@ -1739,6 +1765,27 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1739
1765
|
cursorChanged = true;
|
|
1740
1766
|
}
|
|
1741
1767
|
if (cursorChanged) globalChanges.push("Cursor skills installed");
|
|
1768
|
+
|
|
1769
|
+
if (hasCursorAgent) {
|
|
1770
|
+
const cursorPermissionsPath = join(HOME, ".cursor", "permissions.json");
|
|
1771
|
+
let allowlistChanged = false;
|
|
1772
|
+
const permissionsOk = updateJsonConfig(cursorPermissionsPath, (obj) => {
|
|
1773
|
+
if (obj.mcpAllowlist === undefined) obj.mcpAllowlist = [];
|
|
1774
|
+
if (!Array.isArray(obj.mcpAllowlist)) {
|
|
1775
|
+
console.log(`\n ${yellow}⚠${r} Skipped Cursor MCP allowlist — ${cursorPermissionsPath} has a non-array mcpAllowlist.`);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (!obj.mcpAllowlist.includes("patchcord:*")) {
|
|
1779
|
+
obj.mcpAllowlist.push("patchcord:*");
|
|
1780
|
+
allowlistChanged = true;
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
if (!permissionsOk) {
|
|
1784
|
+
globalChanges.push("✗ Cursor permissions error");
|
|
1785
|
+
} else if (allowlistChanged) {
|
|
1786
|
+
globalChanges.push("Cursor MCP allowlist configured");
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1742
1789
|
}
|
|
1743
1790
|
|
|
1744
1791
|
// Windsurf
|
|
@@ -2074,8 +2121,8 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2074
2121
|
}
|
|
2075
2122
|
}
|
|
2076
2123
|
|
|
2077
|
-
if (!hasClaude && !existsSync(codexConfig) && !hasKimi) {
|
|
2078
|
-
console.log(`${dim}No Claude Code, Codex CLI, or Kimi Code detected — skipping global setup.${r}`);
|
|
2124
|
+
if (!hasClaude && !existsSync(codexConfig) && !hasKimi && !hasCursorAgent && !existsSync(cursorSkillsRoot)) {
|
|
2125
|
+
console.log(`${dim}No Claude Code, Codex CLI, Cursor, or Kimi Code detected — skipping global setup.${r}`);
|
|
2079
2126
|
console.log(`${dim} Kimi Code: https://kimi.moonshot.cn/download${r}`);
|
|
2080
2127
|
}
|
|
2081
2128
|
|
|
@@ -2142,21 +2189,17 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2142
2189
|
|| (flags.includes("--token") ? flags[flags.indexOf("--token") + 1] : "");
|
|
2143
2190
|
|
|
2144
2191
|
if (tokenFlag) {
|
|
2145
|
-
// --token bypass
|
|
2146
|
-
//
|
|
2192
|
+
// --token bypass requires --tool=<slug> up front — no terminal picker.
|
|
2193
|
+
// The web connect page (patchcord.dev/console/connect/<platform>) is
|
|
2194
|
+
// where the tool is chosen; it hands back the full npx command with
|
|
2195
|
+
// --tool=<slug> already baked in. If someone lands here without one,
|
|
2196
|
+
// point them at the web flow instead of prompting.
|
|
2147
2197
|
if (!choice) {
|
|
2148
|
-
console.
|
|
2149
|
-
console.
|
|
2150
|
-
console.
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
console.log(` ${cyan}13.${r} Hermes\n`);
|
|
2154
|
-
choice = (await ask(`${dim}Choose (1-13):${r} `)).trim();
|
|
2155
|
-
if (!["1","2","3","4","5","6","7","8","9","10","11","12","13"].includes(choice)) {
|
|
2156
|
-
console.error("Invalid choice.");
|
|
2157
|
-
rl.close();
|
|
2158
|
-
process.exit(1);
|
|
2159
|
-
}
|
|
2198
|
+
console.error(`${bold}--tool=<slug> is required with --token.${r}`);
|
|
2199
|
+
console.error(`${dim}Pick your tool at https://patchcord.dev/console/connect — it gives you the exact command.${r}`);
|
|
2200
|
+
console.error(`${dim}Valid slugs: ${Object.keys(CLIENT_TYPE_MAP).join(", ")}${r}`);
|
|
2201
|
+
rl.close();
|
|
2202
|
+
process.exit(1);
|
|
2160
2203
|
}
|
|
2161
2204
|
token = tokenFlag.trim();
|
|
2162
2205
|
if (!isSafeToken(token)) {
|
|
@@ -2506,7 +2549,8 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2506
2549
|
const cursorConfig = {
|
|
2507
2550
|
mcpServers: {
|
|
2508
2551
|
patchcord: {
|
|
2509
|
-
|
|
2552
|
+
type: "streamable-http",
|
|
2553
|
+
url: `${serverUrl}/mcp/bearer`,
|
|
2510
2554
|
headers: {
|
|
2511
2555
|
Authorization: `Bearer ${token}`,
|
|
2512
2556
|
"X-Patchcord-Machine": hostname,
|
package/package.json
CHANGED
package/skills/inbox/SKILL.md
CHANGED
|
@@ -184,7 +184,7 @@ Named threads group related messages between a pair of agents. Use them for mult
|
|
|
184
184
|
- Never show raw JSON to the human - summarize naturally.
|
|
185
185
|
- **Cross-namespace addressing (`agent@namespace`)**: the syntax always exists in `send_message`/`reply`, but what it actually reaches depends on who you are:
|
|
186
186
|
- **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
|
|
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
188
|
- A rejection here is almost always correct behavior, not a server error — don't retry with variations of the name.
|
|
189
189
|
- **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
190
|
- **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.
|