@korso/shepherd 0.1.0 → 0.2.0

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.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/dist/index.js +67 -66
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @korso/shepherd — Shepherd MCP Server
2
2
 
3
- Shepherd's stdio MCP server. Gives any MCP-capable agent (Claude Code, Codex, etc.) five advisory coordination tools backed by the shared hub: `join`, `work`, `done`, `announce`, and `sync`.
3
+ Shepherd's stdio MCP server. Gives any MCP-capable agent (Claude Code, Codex, etc.) four advisory coordination tools backed by the shared hub: `work`, `done`, `announce`, and `sync`. The agent **joins the workspace automatically** on startup (no `join` tool), and the server ships standing instructions so the agent self-coordinates without the user prompting it.
4
4
 
5
5
  > **New here?** The [developer quickstart](https://github.com/Korsoai/shepherd/blob/main/docs/shepherd-mcp-quickstart.md) is the fastest path. TL;DR: `npx -y @korso/shepherd` with the env vars below.
6
6
 
@@ -10,7 +10,7 @@ Shepherd's stdio MCP server. Gives any MCP-capable agent (Claude Code, Codex, et
10
10
 
11
11
  > **Everyone must set `WORKSPACE` to the identical string, and that string must equal the hub's `ALLOWED_WORKSPACE` env var.**
12
12
 
13
- If `WORKSPACE` does not match, the server's `join` call to the hub returns HTTP 400 immediately. The error is loud on purpose — you will see it in stderr the moment the first tool is called. This is the most common silent onboarding mistake; the guard turns it into a loud, immediate failure.
13
+ If `WORKSPACE` does not match, the server's automatic `join` call to the hub (fired at startup) returns HTTP 400. Coordination then degrades: every tool reports "session not ready … proceeding uncoordinated" instead of a landscape. This is the most common silent onboarding mistake — if your agent never sees teammates, check `WORKSPACE` first.
14
14
 
15
15
  ---
16
16
 
@@ -156,7 +156,7 @@ No stderr output and the process blocking on stdin = healthy. Press Ctrl+C to ex
156
156
 
157
157
  and the process exits 1 immediately. This is by design.
158
158
 
159
- **Wrong WORKSPACE:** the server starts and connects, but the first tool call (`work`, `sync`, etc.) returns a 400 from the hub. Check that your `WORKSPACE` value exactly matches the hub's `ALLOWED_WORKSPACE`.
159
+ **Wrong WORKSPACE:** the server starts and connects, but the startup auto-join is rejected (400), so every tool call (`work`, `sync`, etc.) reports "session not ready … proceeding uncoordinated". Check that your `WORKSPACE` value exactly matches the hub's `ALLOWED_WORKSPACE`, then restart.
160
160
 
161
161
  ---
162
162
 
@@ -198,6 +198,6 @@ npm publish --workspace=@korso/shepherd # prepublishOnly runs tsup automatical
198
198
  | Symptom | Likely cause | Fix |
199
199
  |---|---|---|
200
200
  | `Configuration error — missing or invalid env vars` | One or more of the 8 env vars is absent | Add the missing vars to your client's `env` block |
201
- | Hub returns 400 on first tool call | `WORKSPACE` mismatch between client and hub | Set `WORKSPACE` to exactly match the hub's `ALLOWED_WORKSPACE` |
201
+ | Tools report "session not ready … proceeding uncoordinated" | Startup auto-join rejected — usually `WORKSPACE` mismatch (or stale `TEAM_TOKEN`) | Set `WORKSPACE` to exactly match the hub's `ALLOWED_WORKSPACE`; re-check `TEAM_TOKEN`; restart |
202
202
  | `npm error 404 … @korso/shepherd` | Package not published yet, or name typo | `npm view @korso/shepherd version` to confirm it's live |
203
203
  | Process exits immediately with no error | Rare; check for node version incompatibility | Requires Node 18+ (ESM support) |
package/dist/index.js CHANGED
@@ -247,71 +247,60 @@ function degradedResult(err) {
247
247
  function registerTools(server, deps) {
248
248
  const { hubClient, config } = deps;
249
249
  let sessionId = null;
250
- let joinInFlight = null;
250
+ let agentName = null;
251
+ const joinBody = {
252
+ workspace: config.WORKSPACE,
253
+ repo: config.REPO,
254
+ branch: config.BRANCH,
255
+ human: config.HUMAN,
256
+ program: config.PROGRAM,
257
+ model: config.MODEL
258
+ };
259
+ const joinInFlight = hubClient.post("/join", joinBody).then((r) => {
260
+ sessionId = r.sessionId;
261
+ agentName = r.agentName;
262
+ }).catch(() => {
263
+ });
251
264
  async function awaitJoin() {
252
- if (joinInFlight) await joinInFlight;
265
+ await joinInFlight;
253
266
  }
254
- server.registerTool(
255
- "join",
256
- {
257
- title: "Join coordination hub",
258
- description: "Register yourself with the team coordination hub at the start of a session. Call once before any other coordination tool. Returns your assigned agent name and session ID that the other tools use automatically.",
259
- inputSchema: JoinAgentInput.shape
260
- },
261
- async (_args) => {
262
- try {
263
- const body = {
264
- workspace: config.WORKSPACE,
265
- repo: config.REPO,
266
- branch: config.BRANCH,
267
- human: config.HUMAN,
268
- program: config.PROGRAM,
269
- model: config.MODEL
270
- };
271
- const pending = hubClient.post("/join", body);
272
- joinInFlight = pending.then((r) => {
273
- sessionId = r.sessionId;
274
- }).catch(() => {
275
- });
276
- const result = await pending;
277
- sessionId = result.sessionId;
278
- return {
279
- content: [
280
- {
281
- type: "text",
282
- text: `Joined as ${result.agentName}.`
283
- }
284
- ]
285
- };
286
- } catch (err) {
287
- if (err instanceof HubUnreachable || err instanceof HubRequestError) {
288
- return degradedResult(err);
267
+ function sessionNotReady() {
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: "Shepherd coordination session not ready (hub unreachable at startup) \u2014 proceeding uncoordinated."
289
273
  }
290
- throw err;
291
- }
292
- }
293
- );
274
+ ]
275
+ };
276
+ }
277
+ function withIdentity(body) {
278
+ return agentName ? `You are ${agentName}.
279
+
280
+ ${body}` : body;
281
+ }
294
282
  server.registerTool(
295
283
  "work",
296
284
  {
297
285
  title: "Claim a unit of work",
298
- description: "Call this BEFORE you start a unit of work. It atomically checks whether any teammate's agent is already touching the same files and claims the work for you, returning any conflicts and what others are working on. Always call this before editing files.",
286
+ description: 'Claim a unit of work BEFORE you start editing files in an area of the codebase (per unit of work, NOT per edit). Pass a one-line `intent` and the `pathGlobs` covering the files you expect to touch \u2014 scope them as specifically as you reasonably can (e.g. ["src/auth/**"], not ["src/**"] and not a single file). It atomically checks whether a teammate is already in those files and claims them for you, returning any conflicts and what others are working on. Hold one claim across all edits in that area; don\'t re-claim per file.',
299
287
  inputSchema: WorkAgentInput.shape
300
288
  },
301
289
  async (args) => {
302
290
  await awaitJoin();
303
291
  if (sessionId === null) {
304
- return {
305
- isError: true,
306
- content: [{ type: "text", text: "Call join first \u2014 no active session." }]
307
- };
292
+ return sessionNotReady();
308
293
  }
309
294
  try {
310
295
  const body = { sessionId, ...args };
311
296
  const result = await hubClient.post("/work", body);
312
- const text = `Work claimed (workItemId: ${result.workItemId})
297
+ const text = withIdentity(
298
+ `Work claimed (workItemId: ${result.workItemId})
299
+
300
+ ` + formatLandscape(result.landscape) + `
313
301
 
314
- ` + formatLandscape(result.landscape);
302
+ You hold this claim until you call done (workItemId: ${result.workItemId}) or it expires (~30 min). Calling work or sync renews it.`
303
+ );
315
304
  return { content: [{ type: "text", text }] };
316
305
  } catch (err) {
317
306
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
@@ -325,16 +314,13 @@ function registerTools(server, deps) {
325
314
  "done",
326
315
  {
327
316
  title: "Release a work claim",
328
- description: "Call when you finish a unit of work to release your claim so teammates know the files are free. Pass the workItemId returned by the work tool.",
317
+ description: "Call when a unit of work is complete to release your claim so teammates know the files are free. Pass the workItemId returned by the work tool.",
329
318
  inputSchema: DoneAgentInput.shape
330
319
  },
331
320
  async (args) => {
332
321
  await awaitJoin();
333
322
  if (sessionId === null) {
334
- return {
335
- isError: true,
336
- content: [{ type: "text", text: "Call join first \u2014 no active session." }]
337
- };
323
+ return sessionNotReady();
338
324
  }
339
325
  try {
340
326
  const body = { sessionId, ...args };
@@ -343,7 +329,7 @@ function registerTools(server, deps) {
343
329
  content: [
344
330
  {
345
331
  type: "text",
346
- text: "Work item released."
332
+ text: "Work item released. Call work again before your next edit in a new area."
347
333
  }
348
334
  ]
349
335
  };
@@ -359,16 +345,13 @@ function registerTools(server, deps) {
359
345
  "announce",
360
346
  {
361
347
  title: "Broadcast a message to teammates",
362
- description: "Broadcast a heads-up to the other agents (or hand a specific agent a finding). This is awareness only \u2014 not a task assignment. Omit targetAgentName to broadcast to everyone in the workspace.",
348
+ description: "Broadcast a heads-up to the other agents, or direct a finding to a specific agent. This is awareness only \u2014 not a task assignment. To direct it, pass that agent's name (exactly as shown in the landscape) as targetAgentName; omit it to broadcast to everyone in the workspace. Delivery is best-effort: the recipient sees it on their next work/sync, once.",
363
349
  inputSchema: AnnounceAgentInput.shape
364
350
  },
365
351
  async (args) => {
366
352
  await awaitJoin();
367
353
  if (sessionId === null) {
368
- return {
369
- isError: true,
370
- content: [{ type: "text", text: "Call join first \u2014 no active session." }]
371
- };
354
+ return sessionNotReady();
372
355
  }
373
356
  try {
374
357
  const body = { sessionId, ...args };
@@ -393,21 +376,18 @@ function registerTools(server, deps) {
393
376
  "sync",
394
377
  {
395
378
  title: "Sync team landscape",
396
- description: "Optional periodic pull of the latest team landscape (who's working on what, any messages for you). Also keeps your claims alive. Call this if you want to check for teammate activity without starting new work.",
379
+ description: "Pull the latest team landscape (who's working on what, any messages for you) and renew your active claims. Call when you resume, start a new task, or before large changes \u2014 or any time you want to check for teammate activity without claiming work.",
397
380
  inputSchema: SyncAgentInput.shape
398
381
  },
399
382
  async (_args) => {
400
383
  await awaitJoin();
401
384
  if (sessionId === null) {
402
- return {
403
- isError: true,
404
- content: [{ type: "text", text: "Call join first \u2014 no active session." }]
405
- };
385
+ return sessionNotReady();
406
386
  }
407
387
  try {
408
388
  const body = { sessionId };
409
389
  const result = await hubClient.post("/sync", body);
410
- const text = formatLandscape(result.landscape);
390
+ const text = withIdentity(formatLandscape(result.landscape));
411
391
  return { content: [{ type: "text", text }] };
412
392
  } catch (err) {
413
393
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
@@ -417,13 +397,34 @@ function registerTools(server, deps) {
417
397
  }
418
398
  }
419
399
  );
400
+ return { ready: joinInFlight };
420
401
  }
421
402
 
403
+ // src/instructions.ts
404
+ 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. You have ALREADY been joined to the workspace automatically \u2014 there is no setup step, and you must not ask the user how to connect.
405
+
406
+ Follow this procedure on every session, proactively and without being asked:
407
+
408
+ 1. Before you start working on an AREA of the codebase, call \`work\` ONCE. 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.
409
+
410
+ 2. Call \`done\` when that unit of work is complete, using its \`workItemId\`, so teammates see the files freed.
411
+
412
+ 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.)
413
+
414
+ 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.
415
+
416
+ 5. Call \`sync\` when you resume, start a new task, or before large changes, to refresh who is doing what.
417
+
418
+ Skip \`work\` entirely for read-only exploration. These tools are advisory and degrade gracefully if the hub is unreachable \u2014 never block your real work on them.`;
419
+
422
420
  // src/index.ts
423
421
  async function main() {
424
422
  const config = loadConfig();
425
423
  const hubClient = createHubClient({ hubUrl: config.HUB_URL, teamToken: config.TEAM_TOKEN });
426
- const server = new McpServer({ name: "shepherd", version: "0.1.0" });
424
+ const server = new McpServer(
425
+ { name: "shepherd", version: "0.1.0" },
426
+ { instructions: SHEPHERD_INSTRUCTIONS }
427
+ );
427
428
  registerTools(server, { hubClient, config });
428
429
  const transport = new StdioServerTransport();
429
430
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@korso/shepherd",
3
- "version": "0.1.0",
4
- "description": "Shepherd MCP server — gives any MCP-capable agent (Claude Code, Codex, etc.) five advisory cross-session coordination tools (join/work/done/announce/sync) backed by the shared Shepherd hub.",
3
+ "version": "0.2.0",
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",
7
7
  "bin": {