@korso/shepherd 0.5.0 → 0.6.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.
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
@@ -581,6 +591,16 @@ var MemberSummary = z2.object({
581
591
  var ListMembersResponse = z2.object({
582
592
  members: z2.array(MemberSummary)
583
593
  });
594
+ var FeedbackType = z2.enum(["bug", "suggestion", "other"]);
595
+ var FeedbackRequest = z2.object({
596
+ type: FeedbackType,
597
+ body: z2.string().trim().min(1).max(4e3)
598
+ });
599
+ var FeedbackResponse = z2.object({
600
+ ok: z2.literal(true),
601
+ // uuid PK (the feedback table, like workspaces, uses gen_random_uuid()).
602
+ id: z2.string()
603
+ });
584
604
 
585
605
  // src/marker.ts
586
606
  import * as fs from "fs";
@@ -634,6 +654,66 @@ function removeMarker(cwd = process.cwd()) {
634
654
  }
635
655
  }
636
656
 
657
+ // src/declined.ts
658
+ import { createHash } from "crypto";
659
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
660
+ import { homedir, tmpdir } from "os";
661
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
662
+ function defaultDeclinedDir() {
663
+ let base = "";
664
+ try {
665
+ base = homedir();
666
+ } catch {
667
+ base = "";
668
+ }
669
+ if (!base) base = tmpdir();
670
+ return join2(base, ".shepherd", "declined");
671
+ }
672
+ function declinedFilePath(repoRoot, dir = defaultDeclinedDir()) {
673
+ let normalized = resolve2(repoRoot);
674
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
675
+ const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
676
+ return join2(dir, hash);
677
+ }
678
+ function isDeclined(repoRoot, dir = defaultDeclinedDir()) {
679
+ const file = declinedFilePath(repoRoot, dir);
680
+ let raw;
681
+ try {
682
+ raw = readFileSync2(file, "utf8");
683
+ } catch {
684
+ return false;
685
+ }
686
+ try {
687
+ const parsed = JSON.parse(raw);
688
+ return typeof parsed?.declinedAt === "string";
689
+ } catch {
690
+ return false;
691
+ }
692
+ }
693
+ function setDeclined(repoRoot, dir = defaultDeclinedDir()) {
694
+ const file = declinedFilePath(repoRoot, dir);
695
+ try {
696
+ mkdirSync(dirname2(file), { recursive: true });
697
+ const payload = JSON.stringify({ declinedAt: (/* @__PURE__ */ new Date()).toISOString() });
698
+ writeFileSync2(file, payload + "\n", "utf8");
699
+ } catch (err) {
700
+ console.error(
701
+ `[shepherd] declined-state write failed: ${err instanceof Error ? err.message : String(err)}`
702
+ );
703
+ }
704
+ }
705
+ function clearDeclined(repoRoot, dir = defaultDeclinedDir()) {
706
+ const file = declinedFilePath(repoRoot, dir);
707
+ if (!existsSync2(file)) return;
708
+ try {
709
+ rmSync2(file, { force: true });
710
+ } catch (err) {
711
+ console.error(
712
+ `[shepherd] declined-state clear failed: ${err instanceof Error ? err.message : String(err)}`
713
+ );
714
+ }
715
+ }
716
+
637
717
  // src/gitContext.ts
638
718
  import { execFileSync } from "child_process";
639
719
  import * as path2 from "path";
@@ -879,37 +959,37 @@ async function buildChangeReport(cwd, config) {
879
959
  }
880
960
 
881
961
  // src/inbox.ts
882
- import { createHash } from "crypto";
962
+ import { createHash as createHash2 } from "crypto";
883
963
  import {
884
964
  appendFileSync,
885
- mkdirSync,
886
- readFileSync as readFileSync2,
965
+ mkdirSync as mkdirSync2,
966
+ readFileSync as readFileSync3,
887
967
  renameSync,
888
- rmSync as rmSync2,
889
- existsSync as existsSync2
968
+ rmSync as rmSync3,
969
+ existsSync as existsSync3
890
970
  } from "fs";
891
- import { homedir, tmpdir } from "os";
892
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
971
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
972
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
893
973
  function defaultInboxDir() {
894
974
  let base = "";
895
975
  try {
896
- base = homedir();
976
+ base = homedir2();
897
977
  } catch {
898
978
  base = "";
899
979
  }
900
- if (!base) base = tmpdir();
901
- return join2(base, ".shepherd", "inbox");
980
+ if (!base) base = tmpdir2();
981
+ return join3(base, ".shepherd", "inbox");
902
982
  }
903
983
  function inboxFilePath(dir, cwd) {
904
- let normalized = resolve2(cwd);
984
+ let normalized = resolve3(cwd);
905
985
  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`);
986
+ const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
987
+ return join3(dir, `${hash}.jsonl`);
908
988
  }
909
989
  function appendAnnouncements(filePath, announcements) {
910
990
  if (!announcements || announcements.length === 0) return;
911
991
  try {
912
- mkdirSync(dirname2(filePath), { recursive: true });
992
+ mkdirSync2(dirname3(filePath), { recursive: true });
913
993
  const payload = announcements.map((a) => JSON.stringify(a)).join("\n") + "\n";
914
994
  appendFileSync(filePath, payload, "utf8");
915
995
  } catch {
@@ -919,17 +999,17 @@ function drainInbox(filePath) {
919
999
  const tmp = `${filePath}.draining`;
920
1000
  let raw = "";
921
1001
  try {
922
- if (existsSync2(tmp)) {
923
- raw += readFileSync2(tmp, "utf8");
924
- rmSync2(tmp, { force: true });
1002
+ if (existsSync3(tmp)) {
1003
+ raw += readFileSync3(tmp, "utf8");
1004
+ rmSync3(tmp, { force: true });
925
1005
  }
926
1006
  } catch {
927
1007
  }
928
1008
  try {
929
- if (existsSync2(filePath)) {
1009
+ if (existsSync3(filePath)) {
930
1010
  renameSync(filePath, tmp);
931
- raw += readFileSync2(tmp, "utf8");
932
- rmSync2(tmp, { force: true });
1011
+ raw += readFileSync3(tmp, "utf8");
1012
+ rmSync3(tmp, { force: true });
933
1013
  }
934
1014
  } catch {
935
1015
  }
@@ -970,6 +1050,12 @@ function classifyJoinFailure(err) {
970
1050
  }
971
1051
  return "unknown";
972
1052
  }
1053
+ function classifyActivateFailure(err) {
1054
+ if (err instanceof HubRequestError && (err.status === 403 || err.status === 404)) {
1055
+ return "workspaceRejected";
1056
+ }
1057
+ return classifyJoinFailure(err);
1058
+ }
973
1059
  function joinFailureCause(reason) {
974
1060
  switch (reason) {
975
1061
  case "unreachable":
@@ -1107,57 +1193,88 @@ function degradedResult(err) {
1107
1193
  function registerTools(server, deps) {
1108
1194
  const { hubClient, config, context, heartbeat, inboxFile } = deps;
1109
1195
  const markerCwd = deps.cwd ?? process.cwd();
1196
+ const declinedDir = deps.declinedDir;
1197
+ const repoRoot = findRepoRoot(markerCwd);
1110
1198
  let sessionId = null;
1111
1199
  let agentName = null;
1112
1200
  const isHosted = Boolean(config.SHEPHERD_TOKEN);
1113
1201
  const selfHostMismatch = !isHosted && context.linked && config.WORKSPACE !== void 0 && config.WORKSPACE !== context.workspace;
1114
1202
  let hostedWorkspaceRejected = false;
1115
1203
  const dormant = !context.linked || selfHostMismatch;
1204
+ let linked = context.linked;
1205
+ let declined = context.declined;
1206
+ function rememberDecline() {
1207
+ if (repoRoot !== null) setDeclined(repoRoot, declinedDir);
1208
+ declined = true;
1209
+ }
1210
+ function forgetDecline() {
1211
+ if (repoRoot !== null) clearDeclined(repoRoot, declinedDir);
1212
+ declined = false;
1213
+ }
1116
1214
  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;
1215
+ let joinInFlight = Promise.resolve();
1216
+ async function activate(workspaceSlug) {
1217
+ const joinBody = {
1218
+ workspace: workspaceSlug,
1219
+ repo: context.repo,
1220
+ branch: context.branch,
1221
+ human: context.human,
1222
+ program: context.program
1223
+ };
1224
+ if (context.model !== void 0) {
1225
+ joinBody.model = context.model;
1226
+ }
1227
+ const attempt = (async () => {
1228
+ try {
1229
+ const raw = await hubClient.post("/join", joinBody);
1230
+ const parsed = JoinResponse.safeParse(raw);
1231
+ if (!parsed.success || !parsed.data.sessionId) {
1232
+ joinFailure = "validation";
1233
+ console.error(
1234
+ "[shepherd] join failed (validation): hub returned a malformed join response (no usable sessionId)"
1235
+ );
1236
+ return { ok: false, reason: "validation" };
1237
+ }
1238
+ const newSessionId = parsed.data.sessionId;
1239
+ heartbeat.start(newSessionId);
1240
+ sessionId = newSessionId;
1241
+ agentName = parsed.data.agentName;
1242
+ linked = true;
1243
+ hostedWorkspaceRejected = false;
1244
+ joinFailure = null;
1245
+ return { ok: true };
1246
+ } catch (err) {
1247
+ const reason = classifyActivateFailure(err);
1248
+ joinFailure = classifyJoinFailure(err);
1249
+ if (reason === "workspaceRejected") {
1250
+ hostedWorkspaceRejected = true;
1251
+ console.error(
1252
+ `[shepherd] This repo is linked to workspace "${workspaceSlug}" but your configured token is for a different workspace \u2014 coordination disabled.`
1253
+ );
1254
+ } else {
1255
+ console.error(
1256
+ `[shepherd] join failed (${reason}): ${err instanceof Error ? err.message : String(err)}`
1257
+ );
1258
+ }
1259
+ return { ok: false, reason };
1260
+ }
1261
+ })();
1262
+ joinInFlight = attempt.then(() => void 0);
1263
+ return attempt;
1126
1264
  }
1127
1265
  if (!context.linked) {
1128
1266
  console.error(
1129
- "[shepherd] This repo isn't linked to a Shepherd workspace \u2014 staying uncoordinated. Run `link` to choose one."
1267
+ 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
1268
  );
1131
1269
  } else if (selfHostMismatch) {
1132
1270
  console.error(
1133
1271
  `[shepherd] This repo is linked to workspace "${context.workspace}" but your configured token is for a different workspace \u2014 coordination disabled.`
1134
1272
  );
1135
1273
  }
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
- });
1274
+ if (!dormant) {
1275
+ forgetDecline();
1276
+ void activate(context.workspace);
1277
+ }
1161
1278
  async function awaitJoin() {
1162
1279
  await joinInFlight;
1163
1280
  }
@@ -1172,14 +1289,8 @@ function registerTools(server, deps) {
1172
1289
  };
1173
1290
  }
1174
1291
  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
- };
1292
+ 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.";
1293
+ return { content: [{ type: "text", text }] };
1183
1294
  }
1184
1295
  function workspaceMismatch() {
1185
1296
  return {
@@ -1193,7 +1304,7 @@ function registerTools(server, deps) {
1193
1304
  }
1194
1305
  async function coordinationGate() {
1195
1306
  await awaitJoin();
1196
- if (!context.linked) return notLinked();
1307
+ if (!linked) return notLinked();
1197
1308
  if (selfHostMismatch || hostedWorkspaceRejected) return workspaceMismatch();
1198
1309
  if (sessionId === null) return sessionNotReady();
1199
1310
  return null;
@@ -1363,13 +1474,25 @@ ${msgs}` : base }
1363
1474
  function advisory(text) {
1364
1475
  return { content: [{ type: "text", text }] };
1365
1476
  }
1477
+ async function linkAndActivate(slug) {
1478
+ writeMarker(markerCwd, slug);
1479
+ forgetDecline();
1480
+ linked = true;
1481
+ const result = await activate(slug);
1482
+ if (result.ok) {
1483
+ return advisory(`Linked this repo to \`${slug}\` \u2014 coordinating in \`${slug}\` now.`);
1484
+ }
1485
+ return advisory(
1486
+ `Linked this repo to \`${slug}\`, but coordination couldn't start just now (${joinFailureCause(joinFailure)}). It'll connect on your next tool call or session.`
1487
+ );
1488
+ }
1366
1489
  server.registerTool(
1367
1490
  "link",
1368
1491
  {
1369
1492
  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.",
1493
+ 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
1494
  inputSchema: z3.object({
1372
- workspace: z3.string().min(1).optional().describe("The workspace slug to link this repo to. Omit to list your choices.")
1495
+ workspace: z3.string().min(1).optional().describe("The workspace slug to link this repo to. Omit to auto-pick or list choices.")
1373
1496
  }).shape
1374
1497
  },
1375
1498
  async (args) => {
@@ -1381,21 +1504,12 @@ ${msgs}` : base }
1381
1504
  "Self-host mode has no configured workspace (WORKSPACE is unset) \u2014 cannot link."
1382
1505
  );
1383
1506
  }
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) {
1507
+ if (requested !== void 0 && requested !== allowed) {
1391
1508
  return advisory(
1392
1509
  `This self-host deployment only serves the workspace \`${allowed}\`; you asked for \`${requested}\`. Choose: ${allowed}`
1393
1510
  );
1394
1511
  }
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
- );
1512
+ return linkAndActivate(allowed);
1399
1513
  }
1400
1514
  let slugs;
1401
1515
  try {
@@ -1409,12 +1523,15 @@ Run \`link\` again with workspace "${allowed}" to opt this repo in.`
1409
1523
  }
1410
1524
  if (slugs.length === 0) {
1411
1525
  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."
1526
+ "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
1527
  );
1414
1528
  }
1415
1529
  if (requested === void 0) {
1530
+ if (slugs.length === 1) {
1531
+ return linkAndActivate(slugs[0]);
1532
+ }
1416
1533
  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."
1534
+ "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
1535
  );
1419
1536
  }
1420
1537
  if (!slugs.includes(requested)) {
@@ -1422,23 +1539,47 @@ Run \`link\` again with workspace "${allowed}" to opt this repo in.`
1422
1539
  `You're not a member of \`${requested}\`; choose one of: ${slugs.join(", ")}`
1423
1540
  );
1424
1541
  }
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
- );
1542
+ return linkAndActivate(requested);
1429
1543
  }
1430
1544
  );
1431
1545
  server.registerTool(
1432
1546
  "unlink",
1433
1547
  {
1434
1548
  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.",
1549
+ 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
1550
  inputSchema: z3.object({}).shape
1437
1551
  },
1438
1552
  async () => {
1439
1553
  removeMarker(markerCwd);
1554
+ rememberDecline();
1555
+ linked = false;
1556
+ if (sessionId !== null) {
1557
+ heartbeat.stop();
1558
+ await leave();
1559
+ sessionId = null;
1560
+ agentName = null;
1561
+ }
1440
1562
  return advisory(
1441
- "Unlinked \u2014 this repo will stay uncoordinated until re-linked."
1563
+ "Unlinked \u2014 this repo will stay uncoordinated and won't ask again. Run `link` to re-enable."
1564
+ );
1565
+ }
1566
+ );
1567
+ server.registerTool(
1568
+ "decline",
1569
+ {
1570
+ title: "Decline Shepherd coordination for this repo",
1571
+ 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.",
1572
+ inputSchema: z3.object({}).shape
1573
+ },
1574
+ async () => {
1575
+ if (linked) {
1576
+ return advisory(
1577
+ `Already coordinating \`${context.workspace}\` \u2014 run \`unlink\` to stop coordinating this repo.`
1578
+ );
1579
+ }
1580
+ rememberDecline();
1581
+ return advisory(
1582
+ "Won't coordinate this repo or ask again. Run `link` anytime to change your mind."
1442
1583
  );
1443
1584
  }
1444
1585
  );
@@ -1457,23 +1598,23 @@ Run \`link\` again with workspace "${allowed}" to opt this repo in.`
1457
1598
  }
1458
1599
 
1459
1600
  // 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";
1601
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1602
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
1603
+ import { dirname as dirname4, join as join4 } from "path";
1463
1604
  function defaultIdentityCachePath() {
1464
1605
  let base = "";
1465
1606
  try {
1466
- base = homedir2();
1607
+ base = homedir3();
1467
1608
  } catch {
1468
1609
  base = "";
1469
1610
  }
1470
- if (!base) base = tmpdir2();
1471
- return join3(base, ".shepherd", "identity.json");
1611
+ if (!base) base = tmpdir3();
1612
+ return join4(base, ".shepherd", "identity.json");
1472
1613
  }
1473
1614
  function readCachedHuman(filePath = defaultIdentityCachePath()) {
1474
1615
  let raw;
1475
1616
  try {
1476
- raw = readFileSync3(filePath, "utf8");
1617
+ raw = readFileSync4(filePath, "utf8");
1477
1618
  } catch {
1478
1619
  return null;
1479
1620
  }
@@ -1488,9 +1629,9 @@ function readCachedHuman(filePath = defaultIdentityCachePath()) {
1488
1629
  function writeCachedHuman(human, filePath = defaultIdentityCachePath()) {
1489
1630
  if (typeof human !== "string" || human.trim().length === 0) return;
1490
1631
  try {
1491
- mkdirSync2(dirname3(filePath), { recursive: true });
1632
+ mkdirSync3(dirname4(filePath), { recursive: true });
1492
1633
  const payload = JSON.stringify({ human });
1493
- writeFileSync2(filePath, payload + "\n", "utf8");
1634
+ writeFileSync3(filePath, payload + "\n", "utf8");
1494
1635
  } catch {
1495
1636
  }
1496
1637
  }
@@ -1501,6 +1642,8 @@ var defaultDeps = {
1501
1642
  detectBranch,
1502
1643
  detectHuman,
1503
1644
  readMarker,
1645
+ findRepoRoot,
1646
+ isDeclined: (repoRoot) => isDeclined(repoRoot),
1504
1647
  readCachedHuman,
1505
1648
  writeCachedHuman
1506
1649
  };
@@ -1516,7 +1659,10 @@ async function resolveContext(config, cwd = process.cwd(), deps = defaultDeps) {
1516
1659
  const marker = deps.readMarker(cwd);
1517
1660
  const linked = marker !== null;
1518
1661
  const workspace = marker?.workspace ?? config.WORKSPACE ?? DEFAULT_WORKSPACE;
1519
- return { workspace, repo, branch, human, program, model, linked };
1662
+ const repoRoot = deps.findRepoRoot(cwd);
1663
+ const declined = repoRoot !== null ? deps.isDeclined(repoRoot) : false;
1664
+ const linkState = linked ? "linked" : declined ? "declined" : "unanswered";
1665
+ return { workspace, repo, branch, human, program, model, linked, declined, linkState };
1520
1666
  }
1521
1667
  function resolveHuman(config, cwd, deps) {
1522
1668
  if (config.HUMAN) return config.HUMAN;
@@ -1588,9 +1734,11 @@ function createHeartbeat({
1588
1734
  }
1589
1735
 
1590
1736
  // 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.
1737
+ 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.
1738
+
1739
+ 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
1740
 
1593
- Follow this procedure on every session, proactively and without being asked:
1741
+ Once linked, follow this procedure on every session, proactively and without being asked:
1594
1742
 
1595
1743
  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
1744
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korso/shepherd",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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": [