@korso/shepherd 0.7.0 → 0.8.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.
package/README.md CHANGED
@@ -60,6 +60,7 @@ override only to replace what's detected:
60
60
  | `MODEL` | omitted — **never auto-detected**, so set it if you want it shown | `claude-sonnet-4-6` |
61
61
  | `HEARTBEAT_INTERVAL_SECONDS` | defaults to `60` | `30` |
62
62
  | `SHEPHERD_INBOX_DIR` | defaults to `~/.shepherd/inbox`. Override only to relocate the **announcement-push** inbox (see below); the background heartbeat writes incoming announcements here. If you set it, point your client hook/extension at the **same** dir | `~/.shepherd/inbox` |
63
+ | `SHEPHERD_NO_AUTO_HOOKS` | unset — set to `1`/`true` to stop the server from auto-installing the client delivery hook on first run (see below) | `1` |
63
64
 
64
65
  **Device-identity cache.** Whenever `HUMAN` is unset and git **does** detect a
65
66
  name, that name is cached for your OS user at `~/.shepherd/identity.json`. A
@@ -84,10 +85,37 @@ default `~/.shepherd/inbox`). That file is then drained by two paths:
84
85
  anything sitting in the inbox. So even with no hook configured, no announcement
85
86
  is ever lost — the worst case is the old behaviour (delivered on the next
86
87
  Shepherd tool call), never silent drops.
87
- 2. **Passive client hook/extension (optional, per client).** To get announcements
88
- **without** waiting for a Shepherd tool call — surfaced on the agent's next
89
- action of any kind — wire up your client's hook below. This is the
90
- "a subagent finished" style of notification.
88
+ 2. **Passive client hook/extension (installed automatically, per client).** To
89
+ get announcements **without** waiting for a Shepherd tool call — surfaced on
90
+ the agent's next action of any kind — the client needs its hook wired up.
91
+ **You normally don't do this by hand**: the first time the server runs under
92
+ Claude Code, Codex, or Pi, it installs the hook itself (see *Automatic hook
93
+ install* below). The per-client sections that follow document exactly what
94
+ gets installed, for auditing or manual setup.
95
+
96
+ ### Automatic hook install
97
+
98
+ On its first `initialize` handshake the server detects the connecting client
99
+ and, for Claude Code / Codex / Pi, installs the delivery hook **once per
100
+ machine**:
101
+
102
+ - **Claude Code** — merges the `SessionStart` + `PreToolUse` hook entries into
103
+ `~/.claude/settings.json` (additive JSON merge; an unparseable file is left
104
+ untouched).
105
+ - **Codex** — appends the `[[hooks.UserPromptSubmit]]` block (and
106
+ `features.hooks = true`) to `~/.codex/config.toml` (text-level append; it
107
+ respects an explicit `hooks = false` and never rewrites existing content).
108
+ - **Pi** — copies the bundled extension to
109
+ `~/.pi/agent/extensions/shepherd-inbox.js`.
110
+ - **Cursor** — merges a `beforeSubmitPrompt` entry into `~/.cursor/hooks.json`
111
+ (additive JSON merge). Only that event is wired: it is the one Cursor event
112
+ verified to inject hook output into the agent's context, and wiring an
113
+ unverified event would consume announcements without delivering them.
114
+
115
+ A record under `~/.shepherd/hooks/` guarantees at-most-once: if you remove the
116
+ hook, Shepherd won't re-add it. Everything is fail-open (an error just means no
117
+ hook, never a broken session), and `SHEPHERD_NO_AUTO_HOOKS=1` disables the whole
118
+ mechanism.
91
119
 
92
120
  Both paths read the **same** inbox file and de-duplicate by announcement id, so
93
121
  running both is safe (the hub hands each announcement to exactly one drain; the
@@ -111,8 +139,21 @@ tool-less events like Codex's `UserPromptSubmit` or a Pi turn. The
111
139
  nudge is advisory and self-extinguishing: the moment the repo is linked or
112
140
  declined it goes quiet, and like everything else here it fails open.
113
141
 
142
+ **First-run ask (zero-setup).** Independently of the hook, the server watches an
143
+ unlinked, undeclined repo for its first file edit (a lightweight `git status`
144
+ poll) and — on clients that support MCP elicitation — asks the user directly via
145
+ a popup: *"Coordinate this repo with Shepherd?"* with the workspace choices and
146
+ a "No — don't ask again" option. Only an explicitly **submitted** answer is
147
+ recorded (a dismissed or auto-declined popup means "ask again next session"), so
148
+ the question is answered at most once and never by accident. Linking activates
149
+ coordination live, mid-session; "don't ask again" hides the coordination tools
150
+ for that repo entirely. Clients without elicitation fall back to the
151
+ instructions + hook nudge above.
152
+
114
153
  ### Claude Code — `PreToolUse` + `SessionStart` hooks
115
154
 
155
+ *(Installed automatically on first run — shown for reference/manual setup.)*
156
+
116
157
  `PreToolUse` fires before every tool, giving the most frequent passive delivery;
117
158
  `SessionStart` surfaces the link ask at the top of a session in an unlinked repo.
118
159
  The hook needs no arguments — it resolves the same default inbox dir the server
@@ -152,6 +193,8 @@ uses (override both with `SHEPHERD_INBOX_DIR` if you relocated it):
152
193
 
153
194
  ### Codex — `UserPromptSubmit` hook
154
195
 
196
+ *(Installed automatically on first run — shown for reference/manual setup.)*
197
+
155
198
  Codex uses the **same** hook contract as Claude Code (JSON on stdin, a
156
199
  `hookSpecificOutput.additionalContext` reply), so the **same bin** serves it. Use
157
200
  `UserPromptSubmit` — Codex's `PreToolUse` only fires for Bash, not `apply_patch`
@@ -170,8 +213,10 @@ command = ["npx", "-y", "--package=@korso/shepherd", "shepherd-inbox-hook"]
170
213
 
171
214
  ### Pi — extension
172
215
 
173
- Pi has no stdin/stdout hook; it loads in-process extensions. Ship the bundled
174
- extension into Pi's extensions dir:
216
+ *(Installed automatically on first run — shown for reference/manual setup.)*
217
+
218
+ Pi has no stdin/stdout hook; it loads in-process extensions. The auto-install
219
+ copies the bundled extension into Pi's extensions dir; by hand:
175
220
 
176
221
  ```sh
177
222
  # global, applies everywhere:
@@ -184,6 +229,27 @@ It runs on every user turn (`before_agent_start`), drains the same inbox, and
184
229
  injects pending announcements plus the unlinked-repo nudge. (Or load it ad hoc
185
230
  with `pi -e /abs/path/to/dist/inboxExtension.js`.)
186
231
 
232
+ ### Cursor — `beforeSubmitPrompt` hook
233
+
234
+ *(Installed automatically on first run — shown for reference/manual setup.)*
235
+
236
+ Cursor runs hooks from `~/.cursor/hooks.json` with JSON on stdin and a JSON
237
+ reply on stdout; the same bin detects Cursor's dialect (BOM-prefixed payload,
238
+ `workspace_roots` instead of `cwd`) and answers with the top-level
239
+ `additionalContext` form Cursor injects into the agent's context. Only
240
+ `beforeSubmitPrompt` is used — verified (Cursor 3.9.16) to reach the model:
241
+
242
+ ```json
243
+ {
244
+ "version": 1,
245
+ "hooks": {
246
+ "beforeSubmitPrompt": [
247
+ { "command": "npx -y --package=@korso/shepherd shepherd-inbox-hook" }
248
+ ]
249
+ }
250
+ }
251
+ ```
252
+
187
253
  ### Notes
188
254
 
189
255
  Every path is **fail-open**: a missing dir, unreachable hub, or any error means
@@ -302,9 +368,10 @@ or per-project at `.cursor/mcp.json`:
302
368
  ```
303
369
 
304
370
  Confirm under **Settings → MCP** that `shepherd` is listed with its tools.
305
- Announcement push: Cursor has no hook equivalent (see the hooks above for
306
- Claude Code/Codex/Pi), so announcements arrive via the universal drainer — on
307
- the agent's next Shepherd tool call — rather than pushed between calls.
371
+ Announcement push: handled by the auto-installed `beforeSubmitPrompt` hook in
372
+ `~/.cursor/hooks.json` (see the Cursor hook section above) — announcements and
373
+ the link nudge land on each user turn, plus the universal drainer on every
374
+ Shepherd tool call.
308
375
 
309
376
  ---
310
377
 
@@ -152,6 +152,7 @@ function drainInbox(filePath) {
152
152
  }
153
153
  return out;
154
154
  }
155
+ var REPLY_ROUTING_HINT = "(The senders can't see this chat. If a message needs a reply, send it with the `announce` tool \u2014 directed to the sender by name \u2014 not here.)";
155
156
  function formatInboxAnnouncements(announcements) {
156
157
  if (!announcements || announcements.length === 0) return "";
157
158
  const count = announcements.length;
@@ -162,6 +163,7 @@ function formatInboxAnnouncements(announcements) {
162
163
  const target = a.targetAgentName ? ` \u2192 ${a.targetAgentName}` : " (broadcast)";
163
164
  lines.push(` [${a.fromAgentName}${target}] ${a.body}`);
164
165
  }
166
+ lines.push(REPLY_ROUTING_HINT);
165
167
  return lines.join("\n");
166
168
  }
167
169
 
package/dist/inboxHook.js CHANGED
@@ -154,6 +154,7 @@ function drainInbox(filePath) {
154
154
  }
155
155
  return out;
156
156
  }
157
+ var REPLY_ROUTING_HINT = "(The senders can't see this chat. If a message needs a reply, send it with the `announce` tool \u2014 directed to the sender by name \u2014 not here.)";
157
158
  function formatInboxAnnouncements(announcements) {
158
159
  if (!announcements || announcements.length === 0) return "";
159
160
  const count = announcements.length;
@@ -164,27 +165,42 @@ function formatInboxAnnouncements(announcements) {
164
165
  const target = a.targetAgentName ? ` \u2192 ${a.targetAgentName}` : " (broadcast)";
165
166
  lines.push(` [${a.fromAgentName}${target}] ${a.body}`);
166
167
  }
168
+ lines.push(REPLY_ROUTING_HINT);
167
169
  return lines.join("\n");
168
170
  }
171
+ function nativeWorkspacePath(root) {
172
+ const win = /^\/([A-Za-z]:[/\\].*)$/.exec(root);
173
+ return win ? win[1] : root;
174
+ }
169
175
  function buildHookOutput(rawStdin, inboxDir, drain = drainInbox, nudge = buildLinkNudge) {
170
176
  let input;
171
177
  try {
172
- input = JSON.parse(rawStdin);
178
+ input = JSON.parse(rawStdin.replace(/^\uFEFF/, ""));
173
179
  } catch {
174
180
  return "";
175
181
  }
176
- if (!input || typeof input.cwd !== "string" || input.cwd.length === 0) return "";
182
+ if (!input || typeof input !== "object") return "";
183
+ const isCursor = typeof input.cursor_version === "string" || Array.isArray(input.workspace_roots);
184
+ const firstRoot = Array.isArray(input.workspace_roots) ? input.workspace_roots[0] : void 0;
185
+ const cwd = typeof input.cwd === "string" && input.cwd.length > 0 ? input.cwd : isCursor && typeof firstRoot === "string" && firstRoot.length > 0 ? nativeWorkspacePath(firstRoot) : null;
186
+ if (cwd === null) return "";
177
187
  const parts = [];
178
188
  const nudgeText = nudge(
179
- input.cwd,
189
+ cwd,
180
190
  typeof input.tool_name === "string" ? input.tool_name : void 0
181
191
  );
182
192
  if (nudgeText) parts.push(nudgeText);
183
193
  if (inboxDir) {
184
- const text = formatInboxAnnouncements(drain(inboxFilePath(inboxDir, input.cwd)));
194
+ const text = formatInboxAnnouncements(drain(inboxFilePath(inboxDir, cwd)));
185
195
  if (text) parts.push(text);
186
196
  }
187
197
  if (parts.length === 0) return "";
198
+ if (isCursor) {
199
+ return JSON.stringify({
200
+ continue: true,
201
+ additionalContext: parts.join("\n\n")
202
+ });
203
+ }
188
204
  return JSON.stringify({
189
205
  hookSpecificOutput: {
190
206
  hookEventName: input.hook_event_name || "PreToolUse",
package/dist/index.js CHANGED
@@ -34,7 +34,11 @@ var ConfigSchema = z.object({
34
34
  // context on its next action. Unset → no inbox, announcements flow only via
35
35
  // work/sync/done/announce tool results as before. Both the MCP server and the
36
36
  // hook must agree on this path.
37
- SHEPHERD_INBOX_DIR: z.string().min(1).optional()
37
+ SHEPHERD_INBOX_DIR: z.string().min(1).optional(),
38
+ // Opt-out for the zero-setup hook auto-install (Layer 4). Any of "1", "true",
39
+ // "yes" (case-insensitive) disables it; everything else (including unset)
40
+ // leaves the default-on behavior. See hookInstall.ts and the README.
41
+ SHEPHERD_NO_AUTO_HOOKS: z.string().optional().transform((v) => ["1", "true", "yes"].includes((v ?? "").toLowerCase()))
38
42
  }).refine((c) => Boolean(c.SHEPHERD_TOKEN || c.TEAM_TOKEN), {
39
43
  message: "Either SHEPHERD_TOKEN or TEAM_TOKEN is required",
40
44
  path: ["SHEPHERD_TOKEN"]
@@ -52,7 +56,8 @@ function parseConfig(env) {
52
56
  PROGRAM: env["PROGRAM"],
53
57
  MODEL: env["MODEL"],
54
58
  HEARTBEAT_INTERVAL_SECONDS: env["HEARTBEAT_INTERVAL_SECONDS"],
55
- SHEPHERD_INBOX_DIR: env["SHEPHERD_INBOX_DIR"]
59
+ SHEPHERD_INBOX_DIR: env["SHEPHERD_INBOX_DIR"],
60
+ SHEPHERD_NO_AUTO_HOOKS: env["SHEPHERD_NO_AUTO_HOOKS"]
56
61
  });
57
62
  const authToken = parsed.SHEPHERD_TOKEN ?? parsed.TEAM_TOKEN;
58
63
  return { ...parsed, authToken };
@@ -390,10 +395,16 @@ var WorkspaceAnnouncement = z2.object({
390
395
  // (no agent session). The dashboard renders these as "me" (right-aligned).
391
396
  // Defaulted for version-skew safety with older hubs.
392
397
  fromAdmin: z2.boolean().default(false),
393
- // True when an agent addressed the message TO the operator (the mirror of
394
- // fromAdmin). The dashboard renders these as "<agent> → admin". Not delivered
395
- // to other agents. Defaulted for version-skew safety with older hubs.
398
+ // True when an agent addressed the message TO the operator side (the
399
+ // dashboard) — collectively (legacy) or a specific member (see
400
+ // targetMemberName). Not delivered to other agents. Defaulted for
401
+ // version-skew safety with older hubs.
396
402
  toAdmin: z2.boolean().default(false),
403
+ // When an agent addressed a SPECIFIC workspace member, the member's display
404
+ // name snapshotted at send time — the dashboard renders "→ <name>" instead of
405
+ // the collective "→ admin". Null for legacy/collective operator messages and
406
+ // everything else. Defaulted for version-skew safety with older hubs.
407
+ targetMemberName: z2.string().nullable().default(null),
397
408
  createdAt: IsoTimestamp
398
409
  });
399
410
  var WorkspaceLandscapeResponse = z2.object({
@@ -457,13 +468,20 @@ var DoneResponse = z2.object({
457
468
  var AnnounceRequest = z2.object({
458
469
  sessionId: z2.string().uuid(),
459
470
  body: z2.string().min(1).max(8192),
460
- // absent or null => broadcast to all agents in the workspace
471
+ // THE preferred addressing field: one name that reaches either kind of
472
+ // teammate. The hub resolves it in order — a LIVE AGENT in the sender's repo
473
+ // (exact landscape name, e.g. "alex-rivera-2"), else the operator label
474
+ // ("admin" by default => the dashboard collectively), else a WORKSPACE MEMBER
475
+ // (a dashboard user, matched case-insensitively on display name, GitHub
476
+ // login, or email). No match => 400 listing both sets. Absent/null =>
477
+ // broadcast to all agents. Mutually exclusive with the legacy fields below.
478
+ target: z2.string().min(1).nullable().optional(),
479
+ // LEGACY (kept for older clients; prefer `target`): the exact live-agent name.
461
480
  targetAgentName: z2.string().nullable().optional(),
462
- // true => address the human operator (the dashboard) instead of agents. The
463
- // mirror of the operator's admin → agent DM: it shows in the workspace feed as
464
- // "<agent> → admin" and is NOT delivered to other agents. Mutually exclusive
465
- // with targetAgentName (the hub rejects setting both). Defaulted/optional for
466
- // version skew with older clients.
481
+ // LEGACY (kept for older clients; prefer `target` with a member's name):
482
+ // true => address the human operators (the dashboard) collectively. Shows in
483
+ // the workspace feed as "<agent> → admin" and is NOT delivered to other
484
+ // agents. Mutually exclusive with targetAgentName and target.
467
485
  toAdmin: z2.boolean().optional()
468
486
  });
469
487
  var AnnounceResponse = z2.object({
@@ -1036,6 +1054,7 @@ function drainInbox(filePath) {
1036
1054
  }
1037
1055
  return out;
1038
1056
  }
1057
+ var REPLY_ROUTING_HINT = "(The senders can't see this chat. If a message needs a reply, send it with the `announce` tool \u2014 directed to the sender by name \u2014 not here.)";
1039
1058
  function mergeAnnouncements(...lists) {
1040
1059
  const byId = /* @__PURE__ */ new Map();
1041
1060
  for (const list of lists) {
@@ -1047,6 +1066,125 @@ function mergeAnnouncements(...lists) {
1047
1066
  return [...byId.values()].sort((x, y) => x.id - y.id);
1048
1067
  }
1049
1068
 
1069
+ // src/editTripwire.ts
1070
+ import { execFile } from "child_process";
1071
+ function createEditTripwire({
1072
+ cwd,
1073
+ intervalMs = 3e4,
1074
+ onEdits,
1075
+ runGitStatus = defaultRunGitStatus
1076
+ }) {
1077
+ let timer = null;
1078
+ let baseline = null;
1079
+ let disarmed = false;
1080
+ function stop() {
1081
+ disarmed = true;
1082
+ if (timer !== null) {
1083
+ clearInterval(timer);
1084
+ timer = null;
1085
+ }
1086
+ }
1087
+ async function check() {
1088
+ const out = await runGitStatus(cwd);
1089
+ if (out === null || baseline === null || disarmed) return;
1090
+ for (const path3 of parsePorcelainPaths(out)) {
1091
+ if (!baseline.has(path3)) {
1092
+ stop();
1093
+ try {
1094
+ onEdits();
1095
+ } catch (err) {
1096
+ console.error(
1097
+ `[shepherd] edit-tripwire handler failed: ${err instanceof Error ? err.message : String(err)}`
1098
+ );
1099
+ }
1100
+ return;
1101
+ }
1102
+ }
1103
+ }
1104
+ function start() {
1105
+ stop();
1106
+ disarmed = false;
1107
+ void (async () => {
1108
+ const out = await runGitStatus(cwd);
1109
+ if (out === null || disarmed) return;
1110
+ baseline = new Set(parsePorcelainPaths(out));
1111
+ timer = setInterval(() => {
1112
+ void check().catch(() => {
1113
+ });
1114
+ }, intervalMs);
1115
+ timer.unref();
1116
+ })();
1117
+ }
1118
+ return { start, stop };
1119
+ }
1120
+ function parsePorcelainPaths(out) {
1121
+ return out.split("\n").filter((line) => line.length > 3).map((line) => line.slice(3));
1122
+ }
1123
+ function defaultRunGitStatus(cwd) {
1124
+ return new Promise((resolve4) => {
1125
+ execFile(
1126
+ "git",
1127
+ ["status", "--porcelain"],
1128
+ { cwd, timeout: 5e3, windowsHide: true },
1129
+ (err, stdout) => {
1130
+ resolve4(err ? null : stdout);
1131
+ }
1132
+ );
1133
+ });
1134
+ }
1135
+
1136
+ // src/linkPopup.ts
1137
+ var NEVER_ASK_CHOICE = "No \u2014 don't ask again";
1138
+ async function offerLinkPopup({
1139
+ repoName,
1140
+ elicit,
1141
+ listWorkspaces,
1142
+ linkWorkspace,
1143
+ neverAskAgain
1144
+ }) {
1145
+ let slugs;
1146
+ try {
1147
+ slugs = await listWorkspaces();
1148
+ } catch {
1149
+ return { outcome: "unanswered" };
1150
+ }
1151
+ if (slugs.length === 0) return { outcome: "unanswered" };
1152
+ let response;
1153
+ try {
1154
+ response = await elicit({
1155
+ message: `Shepherd: "${repoName}" isn't linked to a team workspace, so teammates can't see the work happening here. Link it to start coordinating? This writes a small .shepherd file naming the workspace (commit it so teammates inherit the link). Dismiss to decide later \u2014 you'll be asked again next session.`,
1156
+ requestedSchema: {
1157
+ type: "object",
1158
+ properties: {
1159
+ decision: {
1160
+ type: "string",
1161
+ title: "Workspace",
1162
+ description: `Use the arrow keys (\u2192) to view your workspaces. Pick one to link this repo, or "${NEVER_ASK_CHOICE}".`,
1163
+ enum: [...slugs, NEVER_ASK_CHOICE]
1164
+ }
1165
+ },
1166
+ required: ["decision"]
1167
+ }
1168
+ });
1169
+ } catch {
1170
+ return { outcome: "unanswered" };
1171
+ }
1172
+ if (response?.action !== "accept") return { outcome: "unanswered" };
1173
+ const decision = response.content?.decision;
1174
+ if (typeof decision !== "string") return { outcome: "unanswered" };
1175
+ if (decision === NEVER_ASK_CHOICE) {
1176
+ neverAskAgain();
1177
+ return { outcome: "declined" };
1178
+ }
1179
+ if (!slugs.includes(decision)) return { outcome: "unanswered" };
1180
+ try {
1181
+ await linkWorkspace(decision);
1182
+ } catch {
1183
+ return { outcome: "unanswered" };
1184
+ }
1185
+ return { outcome: "linked", workspace: decision };
1186
+ }
1187
+
1050
1188
  // src/tools.ts
1051
1189
  function classifyJoinFailure(err) {
1052
1190
  if (err instanceof HubUnreachable) return "unreachable";
@@ -1116,6 +1254,7 @@ function formatLandscape(landscape) {
1116
1254
  const target = a.targetAgentName ? ` \u2192 ${a.targetAgentName}` : " (broadcast)";
1117
1255
  lines.push(` [${a.fromAgentName}${target}] ${a.body}`);
1118
1256
  }
1257
+ lines.push(REPLY_ROUTING_HINT);
1119
1258
  } else {
1120
1259
  lines.push("ANNOUNCEMENTS: none");
1121
1260
  }
@@ -1128,6 +1267,7 @@ function formatAnnouncements(announcements) {
1128
1267
  const target = a.targetAgentName ? ` \u2192 ${a.targetAgentName}` : " (broadcast)";
1129
1268
  lines.push(` [${a.fromAgentName}${target}] ${a.body}`);
1130
1269
  }
1270
+ lines.push(REPLY_ROUTING_HINT);
1131
1271
  return lines.join("\n");
1132
1272
  }
1133
1273
  function relativeAge(iso) {
@@ -1210,13 +1350,28 @@ function registerTools(server, deps) {
1210
1350
  const dormant = !context.linked || selfHostMismatch;
1211
1351
  let linked = context.linked;
1212
1352
  let declined = context.declined;
1353
+ const gatedTools = [];
1354
+ let surfaceVisible = true;
1355
+ function syncToolSurface() {
1356
+ const visible = linked || !declined;
1357
+ if (visible === surfaceVisible) return;
1358
+ surfaceVisible = visible;
1359
+ for (const tool of gatedTools) {
1360
+ if (visible) tool?.enable();
1361
+ else tool?.disable();
1362
+ }
1363
+ }
1364
+ let tripwire = null;
1213
1365
  function rememberDecline() {
1214
1366
  if (repoRoot !== null) setDeclined(repoRoot, declinedDir);
1215
1367
  declined = true;
1368
+ tripwire?.stop();
1369
+ syncToolSurface();
1216
1370
  }
1217
1371
  function forgetDecline() {
1218
1372
  if (repoRoot !== null) clearDeclined(repoRoot, declinedDir);
1219
1373
  declined = false;
1374
+ syncToolSurface();
1220
1375
  }
1221
1376
  let joinFailure = null;
1222
1377
  let joinInFlight = Promise.resolve();
@@ -1347,7 +1502,7 @@ ${body}` : body;
1347
1502
 
1348
1503
  ${section}` : body;
1349
1504
  }
1350
- server.registerTool(
1505
+ const workTool = server.registerTool(
1351
1506
  "work",
1352
1507
  {
1353
1508
  title: "Claim a unit of work",
@@ -1384,7 +1539,7 @@ You hold this claim until you call done (workItemId: ${result.workItemId}) or it
1384
1539
  }
1385
1540
  }
1386
1541
  );
1387
- server.registerTool(
1542
+ const doneTool = server.registerTool(
1388
1543
  "done",
1389
1544
  {
1390
1545
  title: "Release a work claim",
@@ -1416,11 +1571,11 @@ ${msgs}` : base }
1416
1571
  }
1417
1572
  }
1418
1573
  );
1419
- server.registerTool(
1574
+ const announceTool = server.registerTool(
1420
1575
  "announce",
1421
1576
  {
1422
- title: "Broadcast a message to teammates",
1423
- description: "Broadcast a heads-up to the other agents, direct a finding to a specific agent, or reply to the human operator. This is awareness only \u2014 not a task assignment. To direct it to an agent, pass that agent's EXACT name as shown in the landscape \u2014 including its numeric suffix (e.g. 'alex-rivera-2', NOT the bare handle 'alex-rivera') \u2014 as targetAgentName. Several agents can share one handle (alex-rivera-1, alex-rivera-2, \u2026); the suffix is what picks one, so the bare handle is rejected. The hub rejects any targetAgentName that doesn't match a live agent in your repo \u2014 if you mean the whole team, omit it to broadcast. To reply to the operator (the dashboard), pass toAdmin: true. targetAgentName and toAdmin are mutually exclusive. Delivery is best-effort: the recipient sees it on their next work/sync, once.",
1577
+ title: "Message teammates (agents or humans)",
1578
+ description: "Broadcast a heads-up to the other agents, direct a finding to a specific teammate, or reply to a human on the dashboard. This is awareness only \u2014 not a task assignment. To direct it, pass ONE name as `target`: an agent's EXACT name as shown in the landscape \u2014 including its numeric suffix (e.g. 'alex-rivera-2', NOT the bare handle 'alex-rivera'; several agents can share one handle, the suffix picks one) \u2014 OR a human's name to reach that workspace member on the dashboard (reply to the person a message came from by using their sender name as target), OR 'admin' to reach the dashboard collectively. The hub rejects a target that matches no live agent and no member \u2014 if you mean the whole team, omit target to broadcast. (targetAgentName and toAdmin are deprecated aliases; don't combine them with target.) Delivery is best-effort: agents see it on their next work/sync, once; humans see the feed.",
1424
1579
  inputSchema: AnnounceAgentInput.shape
1425
1580
  },
1426
1581
  async (args) => {
@@ -1448,7 +1603,7 @@ ${msgs}` : base }
1448
1603
  }
1449
1604
  }
1450
1605
  );
1451
- server.registerTool(
1606
+ const syncTool = server.registerTool(
1452
1607
  "sync",
1453
1608
  {
1454
1609
  title: "Sync team landscape",
@@ -1481,10 +1636,16 @@ ${msgs}` : base }
1481
1636
  function advisory(text) {
1482
1637
  return { content: [{ type: "text", text }] };
1483
1638
  }
1639
+ async function linkableSlugs() {
1640
+ if (!isHosted) return config.WORKSPACE ? [config.WORKSPACE] : [];
1641
+ const res = await hubClient.get("/workspaces");
1642
+ return (res.workspaces ?? []).map((w) => w.slug).filter((s) => typeof s === "string" && s.length > 0);
1643
+ }
1484
1644
  async function linkAndActivate(slug) {
1485
1645
  writeMarker(markerCwd, slug);
1486
1646
  forgetDecline();
1487
1647
  linked = true;
1648
+ tripwire?.stop();
1488
1649
  const result = await activate(slug);
1489
1650
  if (result.ok) {
1490
1651
  return advisory(`Linked this repo to \`${slug}\` \u2014 coordinating in \`${slug}\` now.`);
@@ -1520,8 +1681,7 @@ ${msgs}` : base }
1520
1681
  }
1521
1682
  let slugs;
1522
1683
  try {
1523
- const res = await hubClient.get("/workspaces");
1524
- slugs = (res.workspaces ?? []).map((w) => w.slug).filter((s) => typeof s === "string" && s.length > 0);
1684
+ slugs = await linkableSlugs();
1525
1685
  } catch (err) {
1526
1686
  const detail = hubErrorDetail(err);
1527
1687
  return advisory(
@@ -1549,7 +1709,7 @@ ${msgs}` : base }
1549
1709
  return linkAndActivate(requested);
1550
1710
  }
1551
1711
  );
1552
- server.registerTool(
1712
+ const unlinkTool = server.registerTool(
1553
1713
  "unlink",
1554
1714
  {
1555
1715
  title: "Unlink this repo from its Shepherd workspace",
@@ -1560,6 +1720,7 @@ ${msgs}` : base }
1560
1720
  removeMarker(markerCwd);
1561
1721
  rememberDecline();
1562
1722
  linked = false;
1723
+ syncToolSurface();
1563
1724
  if (sessionId !== null) {
1564
1725
  heartbeat.stop();
1565
1726
  await leave();
@@ -1571,7 +1732,7 @@ ${msgs}` : base }
1571
1732
  );
1572
1733
  }
1573
1734
  );
1574
- server.registerTool(
1735
+ const declineTool = server.registerTool(
1575
1736
  "decline",
1576
1737
  {
1577
1738
  title: "Decline Shepherd coordination for this repo",
@@ -1601,8 +1762,64 @@ ${msgs}` : base }
1601
1762
  );
1602
1763
  }
1603
1764
  }
1765
+ gatedTools.push(workTool, doneTool, announceTool, syncTool, unlinkTool, declineTool);
1766
+ surfaceVisible = true;
1767
+ syncToolSurface();
1768
+ async function runFirstRunAsk() {
1769
+ try {
1770
+ if (linked || declined) return;
1771
+ const getCaps = deps.firstRunAsk?.getClientCapabilities ?? (() => server.server.getClientCapabilities());
1772
+ if (!getCaps()?.elicitation) return;
1773
+ const elicit = deps.firstRunAsk?.elicit ?? // Generous timeout: the user may leave the dialog open while they think.
1774
+ // The param cast bridges our structural ElicitParams to the SDK's
1775
+ // stricter schema type — the shapes agree (flat string enum).
1776
+ ((params) => server.server.elicitInput(
1777
+ params,
1778
+ { timeout: 10 * 6e4 }
1779
+ ));
1780
+ const { outcome, workspace } = await offerLinkPopup({
1781
+ repoName: context.repo,
1782
+ elicit,
1783
+ listWorkspaces: linkableSlugs,
1784
+ linkWorkspace: async (slug) => {
1785
+ await linkAndActivate(slug);
1786
+ },
1787
+ neverAskAgain: rememberDecline
1788
+ });
1789
+ if (outcome === "linked" && inboxFile) {
1790
+ appendAnnouncements(inboxFile, [postLinkGuidance(workspace ?? "")]);
1791
+ }
1792
+ if (outcome !== "unanswered") {
1793
+ console.error(`[shepherd] first-run ask answered by the user: ${outcome}`);
1794
+ }
1795
+ } catch (err) {
1796
+ console.error(
1797
+ `[shepherd] first-run ask failed: ${err instanceof Error ? err.message : String(err)}`
1798
+ );
1799
+ }
1800
+ }
1801
+ if (!context.linked && !context.declined) {
1802
+ const createTripwire_ = deps.firstRunAsk?.createTripwire ?? createEditTripwire;
1803
+ tripwire = createTripwire_({
1804
+ cwd: markerCwd,
1805
+ onEdits: () => {
1806
+ void runFirstRunAsk();
1807
+ }
1808
+ });
1809
+ tripwire.start();
1810
+ }
1604
1811
  return { ready: joinInFlight, leave };
1605
1812
  }
1813
+ function postLinkGuidance(workspace) {
1814
+ return {
1815
+ id: -Date.now(),
1816
+ fromAgentName: "shepherd",
1817
+ fromHuman: "shepherd",
1818
+ targetAgentName: null,
1819
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1820
+ body: `The user just linked this repository to the \`${workspace}\` workspace \u2014 coordination is NOW ACTIVE. Standing procedure from here on: call \`work\` (one-line intent + pathGlobs) BEFORE changing files in an area; \`done\` when that unit of work is complete; \`announce\` anything teammates need to know; \`sync\` when you resume or switch tasks. Start by calling \`work\` for the files you're editing right now.`
1821
+ };
1822
+ }
1606
1823
 
1607
1824
  // src/identityCache.ts
1608
1825
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
@@ -1741,11 +1958,25 @@ function createHeartbeat({
1741
1958
  }
1742
1959
 
1743
1960
  // src/instructions.ts
1744
- var SHEPHERD_INSTRUCTIONS = `You are connected to Shepherd, the shared coordination hub for a team of agents (human and AI) working in the same repositories.
1961
+ function buildInstructions(state, workspace) {
1962
+ switch (state) {
1963
+ case "linked":
1964
+ return `${INTRO} This repository is linked to the \`${workspace ?? "team"}\` workspace, so coordination is active.
1965
+
1966
+ ${PROCEDURE}`;
1967
+ case "declined":
1968
+ return "Shepherd (team coordination) is connected, but the user declined coordination for this repository. Do not call Shepherd tools or bring up coordination here. If the user asks to start coordinating this repo, call `link`.";
1969
+ case "unanswered":
1970
+ return `${INTRO}
1745
1971
 
1746
- On your first coordination-relevant action in a repo, if it isn't linked and hasn't been declined, call \`link\` with no argument: it auto-starts coordination if you belong to exactly one workspace, or lists your workspaces \u2014 ask the user "Coordinate this repo with Shepherd? Which workspace?" then call \`link <workspace>\` with their answer, or \`decline\` if they say no. Ask at most once per repo: once linked or declined, do not ask again \u2014 the marker is committed and teammates inherit it, so coordination is automatic from then on.
1972
+ ${FIRST_RUN_ASK}`;
1973
+ }
1974
+ }
1975
+ var INTRO = "You are connected to Shepherd, the shared coordination hub for a team of agents (human and AI) working in the same repositories.";
1976
+ var FIRST_RUN_ASK = `This repository isn't linked to a Shepherd workspace yet, so coordination is dormant. Shepherd normally asks the user directly (a popup) when file edits are detected \u2014 you don't need to raise it yourself.
1747
1977
 
1748
- Once linked, follow this procedure on every session, proactively and without being asked:
1978
+ If the user asks you to set up coordination \u2014 or you're about to change files and no popup or Shepherd message has settled the question \u2014 ask at most once: call \`link\` with no argument. It auto-links when the user belongs to exactly one workspace, or lists the choices; ask the user which workspace, then call \`link\` again with their answer. If they say no, call \`decline\` so they're never asked again. Once linked, the tool results will guide the coordination procedure.`;
1979
+ var PROCEDURE = `Follow this procedure on every session, proactively and without being asked:
1749
1980
 
1750
1981
  1. Before you start producing or changing files in an AREA of the codebase, call \`work\` ONCE. This includes authoring a plan or design doc: claim the doc's path (e.g. ["docs/plans/auth.md"], or the directory you'll write into) BEFORE you write it \u2014 a plan you're about to author counts as a unit of work, not exploration. Pass a one-line \`intent\` and the \`pathGlobs\` covering the files you expect to touch. Scope the globs as specifically as you reasonably can \u2014 tight enough to avoid colliding with unrelated work, broad enough to cover the task (e.g. ["src/auth/**"], not ["src/**"] and not a single file). Hold that one claim across all your edits in that area; do NOT re-claim per file. If it reports a conflict, coordinate or pick different work \u2014 never silently collide.
1751
1982
 
@@ -1753,7 +1984,7 @@ Once linked, follow this procedure on every session, proactively and without bei
1753
1984
 
1754
1985
  3. Re-call \`work\` only when you move to a DIFFERENT area not covered by a live claim. (\`work\` and \`sync\` also renew your existing claims.)
1755
1986
 
1756
- 4. Call \`announce\` whenever you discover something another agent needs \u2014 a shared decision, a gotcha, an API change, a finding. If the landscape shows a specific agent working in the affected area, direct it to them by passing their name as \`targetAgentName\`; otherwise broadcast. Awareness only, not task assignment.
1987
+ 4. Call \`announce\` whenever you discover something another agent needs \u2014 a shared decision, a gotcha, an API change, a finding. If the landscape shows a specific agent working in the affected area, direct it to them by passing their name as \`target\`; otherwise broadcast. A human teammate's name (or \`admin\`) as \`target\` reaches them on the dashboard \u2014 reply to a human's message that way, directed to its sender, never in your own chat. Awareness only, not task assignment.
1757
1988
 
1758
1989
  5. Call \`sync\` when you resume, start a new task, or before large changes, to refresh who is doing what.
1759
1990
 
@@ -1761,6 +1992,209 @@ Skip \`work\` entirely for read-only exploration \u2014 reading, searching, or t
1761
1992
 
1762
1993
  Commit work-in-progress as you go rather than sitting on a large dirty tree: committed work becomes a precise, presence-independent signal to teammates (with line-level detail and automatic resolution once it lands), whereas uncommitted edits are only a best-effort, decaying hint.`;
1763
1994
 
1995
+ // src/hookInstall.ts
1996
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, copyFileSync, existsSync as existsSync4 } from "fs";
1997
+ import { homedir as homedir4 } from "os";
1998
+ import { dirname as dirname5, join as join5 } from "path";
1999
+ import { fileURLToPath } from "url";
2000
+ function detectClient(clientName) {
2001
+ const name = (clientName ?? "").toLowerCase();
2002
+ if (!name) return "unknown";
2003
+ if (name.includes("claude")) return "claude";
2004
+ if (name.includes("codex")) return "codex";
2005
+ if (name.includes("cursor")) return "cursor";
2006
+ if (/(^|[^a-z0-9])pi([^a-z0-9]|$)/.test(name)) return "pi";
2007
+ return "unknown";
2008
+ }
2009
+ var HOOK_COMMAND = "npx -y --package=@korso/shepherd shepherd-inbox-hook";
2010
+ var HOOK_MARKER = "shepherd-inbox-hook";
2011
+ var CODEX_HOOK_BLOCK = [
2012
+ "",
2013
+ "# Added by Shepherd: delivers teammate announcements to the agent. Remove to disable.",
2014
+ "[[hooks.UserPromptSubmit]]",
2015
+ 'command = ["npx", "-y", "--package=@korso/shepherd", "shepherd-inbox-hook"]',
2016
+ ""
2017
+ ].join("\n");
2018
+ async function autoInstallHooks({
2019
+ clientName,
2020
+ homeDir = homedir4(),
2021
+ disabled = false,
2022
+ extensionSource,
2023
+ log = (msg) => console.error(msg)
2024
+ }) {
2025
+ const client = detectClient(clientName);
2026
+ try {
2027
+ if (disabled) return { client, status: "disabled" };
2028
+ if (client === "unknown") {
2029
+ return { client, status: "unsupported" };
2030
+ }
2031
+ const recordFile = join5(homeDir, ".shepherd", "hooks", `${client}.json`);
2032
+ if (existsSync4(recordFile)) return { client, status: "already-attempted" };
2033
+ let status;
2034
+ if (client === "claude") {
2035
+ status = installClaude(homeDir, log);
2036
+ } else if (client === "codex") {
2037
+ status = installCodex(homeDir, log);
2038
+ } else if (client === "cursor") {
2039
+ status = installCursor(homeDir, log);
2040
+ } else {
2041
+ status = installPi(homeDir, extensionSource, log);
2042
+ }
2043
+ mkdirSync4(dirname5(recordFile), { recursive: true });
2044
+ writeFileSync4(
2045
+ recordFile,
2046
+ JSON.stringify({ status, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
2047
+ "utf8"
2048
+ );
2049
+ if (status === "installed") {
2050
+ log(
2051
+ `[shepherd] Installed the announcement-delivery hook for ${client} (disable by removing it, or set SHEPHERD_NO_AUTO_HOOKS=1 to never auto-install).`
2052
+ );
2053
+ }
2054
+ return { client, status };
2055
+ } catch (err) {
2056
+ log(
2057
+ `[shepherd] hook auto-install skipped: ${err instanceof Error ? err.message : String(err)}`
2058
+ );
2059
+ return { client, status: "skipped" };
2060
+ }
2061
+ }
2062
+ function installClaude(homeDir, log) {
2063
+ const settingsFile = join5(homeDir, ".claude", "settings.json");
2064
+ let raw = "";
2065
+ if (existsSync4(settingsFile)) {
2066
+ raw = readFileSync5(settingsFile, "utf8");
2067
+ if (raw.includes(HOOK_MARKER)) return "already-present";
2068
+ }
2069
+ let settings = {};
2070
+ if (raw.trim()) {
2071
+ try {
2072
+ const parsed = JSON.parse(raw);
2073
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2074
+ throw new Error("settings.json is not a JSON object");
2075
+ }
2076
+ settings = parsed;
2077
+ } catch {
2078
+ log(
2079
+ `[shepherd] ${settingsFile} could not be parsed \u2014 not touching it. Add the hook manually (see the dashboard's Connect screen).`
2080
+ );
2081
+ return "skipped";
2082
+ }
2083
+ }
2084
+ const hooks = settings["hooks"] ??= {};
2085
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
2086
+ log(`[shepherd] ${settingsFile} has an unexpected "hooks" shape \u2014 not touching it.`);
2087
+ return "skipped";
2088
+ }
2089
+ const hooksObj = hooks;
2090
+ for (const event of ["SessionStart", "PreToolUse"]) {
2091
+ const existing = hooksObj[event] ??= [];
2092
+ if (!Array.isArray(existing)) {
2093
+ log(`[shepherd] ${settingsFile} has an unexpected hooks.${event} shape \u2014 not touching it.`);
2094
+ return "skipped";
2095
+ }
2096
+ }
2097
+ hooksObj["SessionStart"].push({
2098
+ hooks: [{ type: "command", command: HOOK_COMMAND }]
2099
+ });
2100
+ hooksObj["PreToolUse"].push({
2101
+ matcher: "*",
2102
+ hooks: [{ type: "command", command: HOOK_COMMAND }]
2103
+ });
2104
+ mkdirSync4(dirname5(settingsFile), { recursive: true });
2105
+ writeFileSync4(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
2106
+ return "installed";
2107
+ }
2108
+ function installCodex(homeDir, log) {
2109
+ const configFile = join5(homeDir, ".codex", "config.toml");
2110
+ const manualHint = "Add the hook manually (see the dashboard's Connect screen).";
2111
+ if (!existsSync4(configFile)) {
2112
+ mkdirSync4(dirname5(configFile), { recursive: true });
2113
+ writeFileSync4(configFile, `[features]
2114
+ hooks = true
2115
+ ${CODEX_HOOK_BLOCK}`, "utf8");
2116
+ return "installed";
2117
+ }
2118
+ const toml = readFileSync5(configFile, "utf8");
2119
+ if (toml.includes(HOOK_MARKER)) return "already-present";
2120
+ if (/^\s*\[hooks\.UserPromptSubmit\]\s*$/m.test(toml)) {
2121
+ log(`[shepherd] ${configFile} defines [hooks.UserPromptSubmit] \u2014 not touching it. ${manualHint}`);
2122
+ return "skipped";
2123
+ }
2124
+ if (/^\s*\[features\]/m.test(toml)) {
2125
+ const hooksKey = /^\s*hooks\s*=\s*(.+)$/m.exec(toml);
2126
+ if (hooksKey && hooksKey[1].trim() !== "true") {
2127
+ log(`[shepherd] ${configFile} sets hooks = ${hooksKey[1].trim()} \u2014 respecting it. ${manualHint}`);
2128
+ return "skipped";
2129
+ }
2130
+ let updated = toml;
2131
+ if (!hooksKey) {
2132
+ updated = toml.replace(/^(\s*\[features\]\s*)$/m, `$1
2133
+ hooks = true`);
2134
+ }
2135
+ writeFileSync4(configFile, updated + CODEX_HOOK_BLOCK, "utf8");
2136
+ return "installed";
2137
+ }
2138
+ writeFileSync4(configFile, `${toml}
2139
+ [features]
2140
+ hooks = true
2141
+ ${CODEX_HOOK_BLOCK}`, "utf8");
2142
+ return "installed";
2143
+ }
2144
+ function installCursor(homeDir, log) {
2145
+ const hooksFile = join5(homeDir, ".cursor", "hooks.json");
2146
+ let raw = "";
2147
+ if (existsSync4(hooksFile)) {
2148
+ raw = readFileSync5(hooksFile, "utf8");
2149
+ if (raw.includes(HOOK_MARKER)) return "already-present";
2150
+ }
2151
+ let config = {};
2152
+ if (raw.trim()) {
2153
+ try {
2154
+ const parsed = JSON.parse(raw);
2155
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2156
+ throw new Error("hooks.json is not a JSON object");
2157
+ }
2158
+ config = parsed;
2159
+ } catch {
2160
+ log(
2161
+ `[shepherd] ${hooksFile} could not be parsed \u2014 not touching it. Add the hook manually (see the dashboard's Connect screen).`
2162
+ );
2163
+ return "skipped";
2164
+ }
2165
+ }
2166
+ config["version"] ??= 1;
2167
+ const hooks = config["hooks"] ??= {};
2168
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
2169
+ log(`[shepherd] ${hooksFile} has an unexpected "hooks" shape \u2014 not touching it.`);
2170
+ return "skipped";
2171
+ }
2172
+ const hooksObj = hooks;
2173
+ const entries = hooksObj["beforeSubmitPrompt"] ??= [];
2174
+ if (!Array.isArray(entries)) {
2175
+ log(
2176
+ `[shepherd] ${hooksFile} has an unexpected hooks.beforeSubmitPrompt shape \u2014 not touching it.`
2177
+ );
2178
+ return "skipped";
2179
+ }
2180
+ entries.push({ command: HOOK_COMMAND });
2181
+ mkdirSync4(dirname5(hooksFile), { recursive: true });
2182
+ writeFileSync4(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf8");
2183
+ return "installed";
2184
+ }
2185
+ function installPi(homeDir, extensionSource, log) {
2186
+ const source = extensionSource ?? join5(dirname5(fileURLToPath(import.meta.url)), "inboxExtension.js");
2187
+ const dest = join5(homeDir, ".pi", "agent", "extensions", "shepherd-inbox.js");
2188
+ if (existsSync4(dest)) return "already-present";
2189
+ if (!existsSync4(source)) {
2190
+ log(`[shepherd] bundled Pi extension not found at ${source} \u2014 skipping auto-install.`);
2191
+ return "skipped";
2192
+ }
2193
+ mkdirSync4(dirname5(dest), { recursive: true });
2194
+ copyFileSync(source, dest);
2195
+ return "installed";
2196
+ }
2197
+
1764
2198
  // src/index.ts
1765
2199
  async function main() {
1766
2200
  const config = loadConfig();
@@ -1787,10 +2221,16 @@ async function main() {
1787
2221
  });
1788
2222
  const server = new McpServer(
1789
2223
  { name: "shepherd", version: "0.1.0" },
1790
- { instructions: SHEPHERD_INSTRUCTIONS }
2224
+ { instructions: buildInstructions(context.linkState, context.workspace) }
1791
2225
  );
1792
2226
  const tools = registerTools(server, { hubClient, config, context, heartbeat, inboxFile });
1793
2227
  const transport = new StdioServerTransport();
2228
+ server.server.oninitialized = () => {
2229
+ void autoInstallHooks({
2230
+ clientName: server.server.getClientVersion()?.name,
2231
+ disabled: config.SHEPHERD_NO_AUTO_HOOKS
2232
+ });
2233
+ };
1794
2234
  let shuttingDown = false;
1795
2235
  const shutdown = async () => {
1796
2236
  if (shuttingDown) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korso/shepherd",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Shepherd MCP server — gives any MCP-capable agent (Claude Code, Codex, etc.) four advisory cross-session coordination tools (work/done/announce/sync) backed by the shared Shepherd hub. Joins the workspace automatically and ships standing instructions so the agent self-coordinates.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",