@korso/shepherd 0.5.0 → 0.6.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/dist/inboxHook.js CHANGED
File without changes
package/dist/index.js CHANGED
@@ -558,15 +558,25 @@ var ListTokensResponse = z2.object({
558
558
  });
559
559
  var CreateInviteRequest = z2.object({
560
560
  expiresInDays: z2.number().int().positive().optional(),
561
+ // Omitted = unlimited, redeemable until explicitly revoked. Pass a positive
562
+ // integer to cap it instead.
561
563
  maxUses: z2.number().int().positive().optional()
562
564
  });
563
565
  var InviteResponse = z2.object({
564
566
  code: z2.string(),
565
567
  // ISO timestamp string, or null when the invite never expires.
566
568
  expiresAt: IsoTimestamp.nullable(),
567
- maxUses: z2.number().int().positive(),
569
+ // null = unlimited (redeemable until revoked).
570
+ maxUses: z2.number().int().positive().nullable(),
568
571
  useCount: z2.number().int().nonnegative()
569
572
  });
573
+ var InviteByEmailRequest = z2.object({
574
+ email: z2.string().email()
575
+ });
576
+ var InviteByEmailResponse = z2.object({
577
+ email: z2.string(),
578
+ sentAt: IsoTimestamp
579
+ });
570
580
  var RedeemInviteResponse = z2.object({
571
581
  // The workspace the caller just joined.
572
582
  workspace: WorkspaceSummary
@@ -575,12 +585,23 @@ var MemberSummary = z2.object({
575
585
  accountId: z2.string(),
576
586
  displayName: z2.string().nullable(),
577
587
  githubLogin: z2.string().nullable(),
588
+ email: z2.string().nullable(),
578
589
  avatarUrl: z2.string().nullable(),
579
590
  role: Role
580
591
  });
581
592
  var ListMembersResponse = z2.object({
582
593
  members: z2.array(MemberSummary)
583
594
  });
595
+ var FeedbackType = z2.enum(["bug", "suggestion", "other"]);
596
+ var FeedbackRequest = z2.object({
597
+ type: FeedbackType,
598
+ body: z2.string().trim().min(1).max(4e3)
599
+ });
600
+ var FeedbackResponse = z2.object({
601
+ ok: z2.literal(true),
602
+ // uuid PK (the feedback table, like workspaces, uses gen_random_uuid()).
603
+ id: z2.string()
604
+ });
584
605
 
585
606
  // src/marker.ts
586
607
  import * as fs from "fs";
@@ -634,6 +655,66 @@ function removeMarker(cwd = process.cwd()) {
634
655
  }
635
656
  }
636
657
 
658
+ // src/declined.ts
659
+ import { createHash } from "crypto";
660
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
661
+ import { homedir, tmpdir } from "os";
662
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
663
+ function defaultDeclinedDir() {
664
+ let base = "";
665
+ try {
666
+ base = homedir();
667
+ } catch {
668
+ base = "";
669
+ }
670
+ if (!base) base = tmpdir();
671
+ return join2(base, ".shepherd", "declined");
672
+ }
673
+ function declinedFilePath(repoRoot, dir = defaultDeclinedDir()) {
674
+ let normalized = resolve2(repoRoot);
675
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
676
+ const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
677
+ return join2(dir, hash);
678
+ }
679
+ function isDeclined(repoRoot, dir = defaultDeclinedDir()) {
680
+ const file = declinedFilePath(repoRoot, dir);
681
+ let raw;
682
+ try {
683
+ raw = readFileSync2(file, "utf8");
684
+ } catch {
685
+ return false;
686
+ }
687
+ try {
688
+ const parsed = JSON.parse(raw);
689
+ return typeof parsed?.declinedAt === "string";
690
+ } catch {
691
+ return false;
692
+ }
693
+ }
694
+ function setDeclined(repoRoot, dir = defaultDeclinedDir()) {
695
+ const file = declinedFilePath(repoRoot, dir);
696
+ try {
697
+ mkdirSync(dirname2(file), { recursive: true });
698
+ const payload = JSON.stringify({ declinedAt: (/* @__PURE__ */ new Date()).toISOString() });
699
+ writeFileSync2(file, payload + "\n", "utf8");
700
+ } catch (err) {
701
+ console.error(
702
+ `[shepherd] declined-state write failed: ${err instanceof Error ? err.message : String(err)}`
703
+ );
704
+ }
705
+ }
706
+ function clearDeclined(repoRoot, dir = defaultDeclinedDir()) {
707
+ const file = declinedFilePath(repoRoot, dir);
708
+ if (!existsSync2(file)) return;
709
+ try {
710
+ rmSync2(file, { force: true });
711
+ } catch (err) {
712
+ console.error(
713
+ `[shepherd] declined-state clear failed: ${err instanceof Error ? err.message : String(err)}`
714
+ );
715
+ }
716
+ }
717
+
637
718
  // src/gitContext.ts
638
719
  import { execFileSync } from "child_process";
639
720
  import * as path2 from "path";
@@ -879,37 +960,37 @@ async function buildChangeReport(cwd, config) {
879
960
  }
880
961
 
881
962
  // src/inbox.ts
882
- import { createHash } from "crypto";
963
+ import { createHash as createHash2 } from "crypto";
883
964
  import {
884
965
  appendFileSync,
885
- mkdirSync,
886
- readFileSync as readFileSync2,
966
+ mkdirSync as mkdirSync2,
967
+ readFileSync as readFileSync3,
887
968
  renameSync,
888
- rmSync as rmSync2,
889
- existsSync as existsSync2
969
+ rmSync as rmSync3,
970
+ existsSync as existsSync3
890
971
  } from "fs";
891
- import { homedir, tmpdir } from "os";
892
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
972
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
973
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
893
974
  function defaultInboxDir() {
894
975
  let base = "";
895
976
  try {
896
- base = homedir();
977
+ base = homedir2();
897
978
  } catch {
898
979
  base = "";
899
980
  }
900
- if (!base) base = tmpdir();
901
- return join2(base, ".shepherd", "inbox");
981
+ if (!base) base = tmpdir2();
982
+ return join3(base, ".shepherd", "inbox");
902
983
  }
903
984
  function inboxFilePath(dir, cwd) {
904
- let normalized = resolve2(cwd);
985
+ let normalized = resolve3(cwd);
905
986
  if (process.platform === "win32") normalized = normalized.toLowerCase();
906
- const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
907
- return join2(dir, `${hash}.jsonl`);
987
+ const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
988
+ return join3(dir, `${hash}.jsonl`);
908
989
  }
909
990
  function appendAnnouncements(filePath, announcements) {
910
991
  if (!announcements || announcements.length === 0) return;
911
992
  try {
912
- mkdirSync(dirname2(filePath), { recursive: true });
993
+ mkdirSync2(dirname3(filePath), { recursive: true });
913
994
  const payload = announcements.map((a) => JSON.stringify(a)).join("\n") + "\n";
914
995
  appendFileSync(filePath, payload, "utf8");
915
996
  } catch {
@@ -919,17 +1000,17 @@ function drainInbox(filePath) {
919
1000
  const tmp = `${filePath}.draining`;
920
1001
  let raw = "";
921
1002
  try {
922
- if (existsSync2(tmp)) {
923
- raw += readFileSync2(tmp, "utf8");
924
- rmSync2(tmp, { force: true });
1003
+ if (existsSync3(tmp)) {
1004
+ raw += readFileSync3(tmp, "utf8");
1005
+ rmSync3(tmp, { force: true });
925
1006
  }
926
1007
  } catch {
927
1008
  }
928
1009
  try {
929
- if (existsSync2(filePath)) {
1010
+ if (existsSync3(filePath)) {
930
1011
  renameSync(filePath, tmp);
931
- raw += readFileSync2(tmp, "utf8");
932
- rmSync2(tmp, { force: true });
1012
+ raw += readFileSync3(tmp, "utf8");
1013
+ rmSync3(tmp, { force: true });
933
1014
  }
934
1015
  } catch {
935
1016
  }
@@ -970,6 +1051,12 @@ function classifyJoinFailure(err) {
970
1051
  }
971
1052
  return "unknown";
972
1053
  }
1054
+ function classifyActivateFailure(err) {
1055
+ if (err instanceof HubRequestError && (err.status === 403 || err.status === 404)) {
1056
+ return "workspaceRejected";
1057
+ }
1058
+ return classifyJoinFailure(err);
1059
+ }
973
1060
  function joinFailureCause(reason) {
974
1061
  switch (reason) {
975
1062
  case "unreachable":
@@ -1107,57 +1194,88 @@ function degradedResult(err) {
1107
1194
  function registerTools(server, deps) {
1108
1195
  const { hubClient, config, context, heartbeat, inboxFile } = deps;
1109
1196
  const markerCwd = deps.cwd ?? process.cwd();
1197
+ const declinedDir = deps.declinedDir;
1198
+ const repoRoot = findRepoRoot(markerCwd);
1110
1199
  let sessionId = null;
1111
1200
  let agentName = null;
1112
1201
  const isHosted = Boolean(config.SHEPHERD_TOKEN);
1113
1202
  const selfHostMismatch = !isHosted && context.linked && config.WORKSPACE !== void 0 && config.WORKSPACE !== context.workspace;
1114
1203
  let hostedWorkspaceRejected = false;
1115
1204
  const dormant = !context.linked || selfHostMismatch;
1205
+ let linked = context.linked;
1206
+ let declined = context.declined;
1207
+ function rememberDecline() {
1208
+ if (repoRoot !== null) setDeclined(repoRoot, declinedDir);
1209
+ declined = true;
1210
+ }
1211
+ function forgetDecline() {
1212
+ if (repoRoot !== null) clearDeclined(repoRoot, declinedDir);
1213
+ declined = false;
1214
+ }
1116
1215
  let joinFailure = null;
1117
- const joinBody = {
1118
- workspace: context.workspace,
1119
- repo: context.repo,
1120
- branch: context.branch,
1121
- human: context.human,
1122
- program: context.program
1123
- };
1124
- if (context.model !== void 0) {
1125
- joinBody.model = context.model;
1216
+ let joinInFlight = Promise.resolve();
1217
+ async function activate(workspaceSlug) {
1218
+ const joinBody = {
1219
+ workspace: workspaceSlug,
1220
+ repo: context.repo,
1221
+ branch: context.branch,
1222
+ human: context.human,
1223
+ program: context.program
1224
+ };
1225
+ if (context.model !== void 0) {
1226
+ joinBody.model = context.model;
1227
+ }
1228
+ const attempt = (async () => {
1229
+ try {
1230
+ const raw = await hubClient.post("/join", joinBody);
1231
+ const parsed = JoinResponse.safeParse(raw);
1232
+ if (!parsed.success || !parsed.data.sessionId) {
1233
+ joinFailure = "validation";
1234
+ console.error(
1235
+ "[shepherd] join failed (validation): hub returned a malformed join response (no usable sessionId)"
1236
+ );
1237
+ return { ok: false, reason: "validation" };
1238
+ }
1239
+ const newSessionId = parsed.data.sessionId;
1240
+ heartbeat.start(newSessionId);
1241
+ sessionId = newSessionId;
1242
+ agentName = parsed.data.agentName;
1243
+ linked = true;
1244
+ hostedWorkspaceRejected = false;
1245
+ joinFailure = null;
1246
+ return { ok: true };
1247
+ } catch (err) {
1248
+ const reason = classifyActivateFailure(err);
1249
+ joinFailure = classifyJoinFailure(err);
1250
+ if (reason === "workspaceRejected") {
1251
+ hostedWorkspaceRejected = true;
1252
+ console.error(
1253
+ `[shepherd] This repo is linked to workspace "${workspaceSlug}" but your configured token is for a different workspace \u2014 coordination disabled.`
1254
+ );
1255
+ } else {
1256
+ console.error(
1257
+ `[shepherd] join failed (${reason}): ${err instanceof Error ? err.message : String(err)}`
1258
+ );
1259
+ }
1260
+ return { ok: false, reason };
1261
+ }
1262
+ })();
1263
+ joinInFlight = attempt.then(() => void 0);
1264
+ return attempt;
1126
1265
  }
1127
1266
  if (!context.linked) {
1128
1267
  console.error(
1129
- "[shepherd] This repo isn't linked to a Shepherd workspace \u2014 staying uncoordinated. Run `link` to choose one."
1268
+ context.declined ? "[shepherd] This repo was declined \u2014 staying uncoordinated. Run `link` to change your mind." : "[shepherd] This repo isn't linked to a Shepherd workspace \u2014 staying uncoordinated. Run `link` to choose one."
1130
1269
  );
1131
1270
  } else if (selfHostMismatch) {
1132
1271
  console.error(
1133
1272
  `[shepherd] This repo is linked to workspace "${context.workspace}" but your configured token is for a different workspace \u2014 coordination disabled.`
1134
1273
  );
1135
1274
  }
1136
- const joinInFlight = dormant ? Promise.resolve() : hubClient.post("/join", joinBody).then((raw) => {
1137
- const parsed = JoinResponse.safeParse(raw);
1138
- if (!parsed.success || !parsed.data.sessionId) {
1139
- joinFailure = "validation";
1140
- console.error(
1141
- "[shepherd] join failed (validation): hub returned a malformed join response (no usable sessionId)"
1142
- );
1143
- return;
1144
- }
1145
- sessionId = parsed.data.sessionId;
1146
- agentName = parsed.data.agentName;
1147
- heartbeat.start(parsed.data.sessionId);
1148
- }).catch((err) => {
1149
- joinFailure = classifyJoinFailure(err);
1150
- if (err instanceof HubRequestError && (err.status === 403 || err.status === 404)) {
1151
- hostedWorkspaceRejected = true;
1152
- console.error(
1153
- `[shepherd] This repo is linked to workspace "${context.workspace}" but your configured token is for a different workspace \u2014 coordination disabled.`
1154
- );
1155
- } else {
1156
- console.error(
1157
- `[shepherd] join failed (${joinFailure}): ${err instanceof Error ? err.message : String(err)}`
1158
- );
1159
- }
1160
- });
1275
+ if (!dormant) {
1276
+ forgetDecline();
1277
+ void activate(context.workspace);
1278
+ }
1161
1279
  async function awaitJoin() {
1162
1280
  await joinInFlight;
1163
1281
  }
@@ -1172,14 +1290,8 @@ function registerTools(server, deps) {
1172
1290
  };
1173
1291
  }
1174
1292
  function notLinked() {
1175
- return {
1176
- content: [
1177
- {
1178
- type: "text",
1179
- text: "This repo isn't linked to a Shepherd workspace \u2014 run `link` to choose one, or ignore to stay uncoordinated."
1180
- }
1181
- ]
1182
- };
1293
+ const text = declined ? "Not coordinating this repo \u2014 you declined. Run `link` anytime to change your mind." : "This repo isn't linked to a Shepherd workspace \u2014 run `link` to choose one, or `decline` to stay uncoordinated and not be asked again.";
1294
+ return { content: [{ type: "text", text }] };
1183
1295
  }
1184
1296
  function workspaceMismatch() {
1185
1297
  return {
@@ -1193,7 +1305,7 @@ function registerTools(server, deps) {
1193
1305
  }
1194
1306
  async function coordinationGate() {
1195
1307
  await awaitJoin();
1196
- if (!context.linked) return notLinked();
1308
+ if (!linked) return notLinked();
1197
1309
  if (selfHostMismatch || hostedWorkspaceRejected) return workspaceMismatch();
1198
1310
  if (sessionId === null) return sessionNotReady();
1199
1311
  return null;
@@ -1363,13 +1475,25 @@ ${msgs}` : base }
1363
1475
  function advisory(text) {
1364
1476
  return { content: [{ type: "text", text }] };
1365
1477
  }
1478
+ async function linkAndActivate(slug) {
1479
+ writeMarker(markerCwd, slug);
1480
+ forgetDecline();
1481
+ linked = true;
1482
+ const result = await activate(slug);
1483
+ if (result.ok) {
1484
+ return advisory(`Linked this repo to \`${slug}\` \u2014 coordinating in \`${slug}\` now.`);
1485
+ }
1486
+ return advisory(
1487
+ `Linked this repo to \`${slug}\`, but coordination couldn't start just now (${joinFailureCause(joinFailure)}). It'll connect on your next tool call or session.`
1488
+ );
1489
+ }
1366
1490
  server.registerTool(
1367
1491
  "link",
1368
1492
  {
1369
1493
  title: "Link this repo to a Shepherd workspace",
1370
- description: "Opt this repository into Shepherd coordination by writing a committed `.shepherd` marker naming the workspace. Call with no argument to see the workspaces you can link to, then call again with one. You can only link to a workspace you are a member of. Takes effect on the next session (restart to coordinate now). Use `unlink` to opt out.",
1494
+ description: "Opt this repository into Shepherd coordination by writing a committed `.shepherd` marker naming the workspace. Call with no argument: if you belong to exactly one workspace it is linked and coordination starts immediately; if you belong to several, the choices are listed for you to confirm one with the agent's user, then call `link` again with that `workspace`. You can only link to a workspace you are a member of. Takes effect immediately \u2014 no restart. Use `unlink` to opt out, or `decline` to stay uncoordinated without linking.",
1371
1495
  inputSchema: z3.object({
1372
- workspace: z3.string().min(1).optional().describe("The workspace slug to link this repo to. Omit to list your choices.")
1496
+ workspace: z3.string().min(1).optional().describe("The workspace slug to link this repo to. Omit to auto-pick or list choices.")
1373
1497
  }).shape
1374
1498
  },
1375
1499
  async (args) => {
@@ -1381,21 +1505,12 @@ ${msgs}` : base }
1381
1505
  "Self-host mode has no configured workspace (WORKSPACE is unset) \u2014 cannot link."
1382
1506
  );
1383
1507
  }
1384
- if (requested === void 0) {
1385
- return advisory(
1386
- `This is a self-host deployment with a single workspace: \`${allowed}\`.
1387
- Run \`link\` again with workspace "${allowed}" to opt this repo in.`
1388
- );
1389
- }
1390
- if (requested !== allowed) {
1508
+ if (requested !== void 0 && requested !== allowed) {
1391
1509
  return advisory(
1392
1510
  `This self-host deployment only serves the workspace \`${allowed}\`; you asked for \`${requested}\`. Choose: ${allowed}`
1393
1511
  );
1394
1512
  }
1395
- writeMarker(markerCwd, allowed);
1396
- return advisory(
1397
- `Linked this repo to \`${allowed}\` \u2014 restart the session for it to take effect (it applies on the next launch).`
1398
- );
1513
+ return linkAndActivate(allowed);
1399
1514
  }
1400
1515
  let slugs;
1401
1516
  try {
@@ -1409,12 +1524,15 @@ Run \`link\` again with workspace "${allowed}" to opt this repo in.`
1409
1524
  }
1410
1525
  if (slugs.length === 0) {
1411
1526
  return advisory(
1412
- "Your account isn't a member of any workspaces yet \u2014 nothing to link to. Create or join a workspace first, then run `link` again."
1527
+ "Your account isn't a member of any workspaces yet \u2014 nothing to link to. Create one in the Shepherd dashboard, then run `link` again."
1413
1528
  );
1414
1529
  }
1415
1530
  if (requested === void 0) {
1531
+ if (slugs.length === 1) {
1532
+ return linkAndActivate(slugs[0]);
1533
+ }
1416
1534
  return advisory(
1417
- "You can link this repo to one of these workspaces:\n" + slugs.map((s) => ` - ${s}`).join("\n") + "\n\nRun `link` again with one of them as the `workspace` argument."
1535
+ "You're a member of multiple Shepherd workspaces:\n" + slugs.map((s) => ` - ${s}`).join("\n") + "\n\nAsk the user which one to use for this repo, then call `link` again with it as the `workspace` argument."
1418
1536
  );
1419
1537
  }
1420
1538
  if (!slugs.includes(requested)) {
@@ -1422,23 +1540,47 @@ Run \`link\` again with workspace "${allowed}" to opt this repo in.`
1422
1540
  `You're not a member of \`${requested}\`; choose one of: ${slugs.join(", ")}`
1423
1541
  );
1424
1542
  }
1425
- writeMarker(markerCwd, requested);
1426
- return advisory(
1427
- `Linked this repo to \`${requested}\` \u2014 restart the session for it to take effect (it applies on the next launch).`
1428
- );
1543
+ return linkAndActivate(requested);
1429
1544
  }
1430
1545
  );
1431
1546
  server.registerTool(
1432
1547
  "unlink",
1433
1548
  {
1434
1549
  title: "Unlink this repo from its Shepherd workspace",
1435
- description: "Opt this repository OUT of Shepherd coordination by removing its `.shepherd` marker. The repo stays uncoordinated (no claims, no presence) until you `link` it again.",
1550
+ description: "Opt this repository OUT of Shepherd coordination by removing its `.shepherd` marker. Also records a local decline so you aren't re-prompted to link on the next session. The repo stays uncoordinated (no claims, no presence) until you `link` it again.",
1436
1551
  inputSchema: z3.object({}).shape
1437
1552
  },
1438
1553
  async () => {
1439
1554
  removeMarker(markerCwd);
1555
+ rememberDecline();
1556
+ linked = false;
1557
+ if (sessionId !== null) {
1558
+ heartbeat.stop();
1559
+ await leave();
1560
+ sessionId = null;
1561
+ agentName = null;
1562
+ }
1440
1563
  return advisory(
1441
- "Unlinked \u2014 this repo will stay uncoordinated until re-linked."
1564
+ "Unlinked \u2014 this repo will stay uncoordinated and won't ask again. Run `link` to re-enable."
1565
+ );
1566
+ }
1567
+ );
1568
+ server.registerTool(
1569
+ "decline",
1570
+ {
1571
+ title: "Decline Shepherd coordination for this repo",
1572
+ description: "Opt out of Shepherd for this repo WITHOUT linking: records a local, per-user 'don't ask again' so this repo stays uncoordinated and you aren't prompted to link on future sessions. This is local only \u2014 it is never committed, so a teammate on the same repo can still link it. Run `link` anytime to change your mind.",
1573
+ inputSchema: z3.object({}).shape
1574
+ },
1575
+ async () => {
1576
+ if (linked) {
1577
+ return advisory(
1578
+ `Already coordinating \`${context.workspace}\` \u2014 run \`unlink\` to stop coordinating this repo.`
1579
+ );
1580
+ }
1581
+ rememberDecline();
1582
+ return advisory(
1583
+ "Won't coordinate this repo or ask again. Run `link` anytime to change your mind."
1442
1584
  );
1443
1585
  }
1444
1586
  );
@@ -1457,23 +1599,23 @@ Run \`link\` again with workspace "${allowed}" to opt this repo in.`
1457
1599
  }
1458
1600
 
1459
1601
  // src/identityCache.ts
1460
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1461
- import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1462
- import { dirname as dirname3, join as join3 } from "path";
1602
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1603
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
1604
+ import { dirname as dirname4, join as join4 } from "path";
1463
1605
  function defaultIdentityCachePath() {
1464
1606
  let base = "";
1465
1607
  try {
1466
- base = homedir2();
1608
+ base = homedir3();
1467
1609
  } catch {
1468
1610
  base = "";
1469
1611
  }
1470
- if (!base) base = tmpdir2();
1471
- return join3(base, ".shepherd", "identity.json");
1612
+ if (!base) base = tmpdir3();
1613
+ return join4(base, ".shepherd", "identity.json");
1472
1614
  }
1473
1615
  function readCachedHuman(filePath = defaultIdentityCachePath()) {
1474
1616
  let raw;
1475
1617
  try {
1476
- raw = readFileSync3(filePath, "utf8");
1618
+ raw = readFileSync4(filePath, "utf8");
1477
1619
  } catch {
1478
1620
  return null;
1479
1621
  }
@@ -1488,9 +1630,9 @@ function readCachedHuman(filePath = defaultIdentityCachePath()) {
1488
1630
  function writeCachedHuman(human, filePath = defaultIdentityCachePath()) {
1489
1631
  if (typeof human !== "string" || human.trim().length === 0) return;
1490
1632
  try {
1491
- mkdirSync2(dirname3(filePath), { recursive: true });
1633
+ mkdirSync3(dirname4(filePath), { recursive: true });
1492
1634
  const payload = JSON.stringify({ human });
1493
- writeFileSync2(filePath, payload + "\n", "utf8");
1635
+ writeFileSync3(filePath, payload + "\n", "utf8");
1494
1636
  } catch {
1495
1637
  }
1496
1638
  }
@@ -1501,6 +1643,8 @@ var defaultDeps = {
1501
1643
  detectBranch,
1502
1644
  detectHuman,
1503
1645
  readMarker,
1646
+ findRepoRoot,
1647
+ isDeclined: (repoRoot) => isDeclined(repoRoot),
1504
1648
  readCachedHuman,
1505
1649
  writeCachedHuman
1506
1650
  };
@@ -1516,7 +1660,10 @@ async function resolveContext(config, cwd = process.cwd(), deps = defaultDeps) {
1516
1660
  const marker = deps.readMarker(cwd);
1517
1661
  const linked = marker !== null;
1518
1662
  const workspace = marker?.workspace ?? config.WORKSPACE ?? DEFAULT_WORKSPACE;
1519
- return { workspace, repo, branch, human, program, model, linked };
1663
+ const repoRoot = deps.findRepoRoot(cwd);
1664
+ const declined = repoRoot !== null ? deps.isDeclined(repoRoot) : false;
1665
+ const linkState = linked ? "linked" : declined ? "declined" : "unanswered";
1666
+ return { workspace, repo, branch, human, program, model, linked, declined, linkState };
1520
1667
  }
1521
1668
  function resolveHuman(config, cwd, deps) {
1522
1669
  if (config.HUMAN) return config.HUMAN;
@@ -1588,9 +1735,11 @@ function createHeartbeat({
1588
1735
  }
1589
1736
 
1590
1737
  // src/instructions.ts
1591
- 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.
1738
+ 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.
1739
+
1740
+ 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.
1592
1741
 
1593
- Follow this procedure on every session, proactively and without being asked:
1742
+ Once linked, follow this procedure on every session, proactively and without being asked:
1594
1743
 
1595
1744
  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.
1596
1745
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korso/shepherd",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -15,10 +15,10 @@
15
15
  "engines": {
16
16
  "node": ">=18"
17
17
  },
18
- "license": "UNLICENSED",
18
+ "license": "AGPL-3.0-only",
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "git+https://github.com/Korsoai/shepherd.git",
21
+ "url": "git+https://github.com/Korso-AI/shepherd.git",
22
22
  "directory": "packages/mcp-server"
23
23
  },
24
24
  "keywords": [