@korso/shepherd 0.11.2 → 0.11.3

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.
@@ -139,6 +139,15 @@ var MAILBOX_FRESH_MS = 15 * 60 * 1e3;
139
139
  function sessionMailboxPath(dir, serverPid) {
140
140
  return join3(dir, `agent-${serverPid}.jsonl`);
141
141
  }
142
+ var HOOK_CHAIN_REACH = { codex: 8 };
143
+ var DEFAULT_HOOK_CHAIN_REACH = 3;
144
+ var MAX_HOOK_CHAIN_REACH = Math.max(
145
+ DEFAULT_HOOK_CHAIN_REACH,
146
+ ...Object.values(HOOK_CHAIN_REACH)
147
+ );
148
+ function hookChainReach(client) {
149
+ return (client === void 0 ? void 0 : HOOK_CHAIN_REACH[client]) ?? DEFAULT_HOOK_CHAIN_REACH;
150
+ }
142
151
  function normalizeCwd(cwd) {
143
152
  let normalized = resolve3(cwd);
144
153
  if (process.platform === "win32") normalized = normalized.toLowerCase();
@@ -146,7 +155,7 @@ function normalizeCwd(cwd) {
146
155
  }
147
156
  function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH_MS, nowMs = Date.now()) {
148
157
  try {
149
- const chain = hookChain.slice(0, 3);
158
+ const chain = hookChain.slice(0, MAX_HOOK_CHAIN_REACH);
150
159
  const wantedCwd = hookCwd === null ? null : normalizeCwd(hookCwd);
151
160
  const candidates = [];
152
161
  for (const name of readdirSync(dir)) {
@@ -180,7 +189,7 @@ function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH
180
189
  }
181
190
  if (!Array.isArray(meta.chain) || typeof meta.cwd !== "string") continue;
182
191
  const i = chain.findIndex((pid) => meta.chain.includes(pid));
183
- if (i === -1) continue;
192
+ if (i === -1 || i >= hookChainReach(meta.client)) continue;
184
193
  const j = meta.chain.indexOf(chain[i]);
185
194
  if (i >= 2 && (j > 2 || wantedCwd === null || meta.cwd !== wantedCwd))
186
195
  continue;
@@ -280,6 +289,51 @@ function mergeAnnouncements(...lists) {
280
289
  return [...byId.values()].sort((x, y) => x.id - y.id);
281
290
  }
282
291
 
292
+ // src/codexHookMigration.ts
293
+ import { z as z2 } from "zod";
294
+
295
+ // src/codexHookInstall.ts
296
+ import { parse, TomlDate } from "smol-toml";
297
+
298
+ // src/codexHookFs.ts
299
+ import { z } from "zod";
300
+ var lockSchema = z.object({
301
+ pid: z.number().int().positive(),
302
+ createdAt: z.string(),
303
+ owner: z.string().min(1).optional()
304
+ });
305
+
306
+ // src/codexHookMigration.ts
307
+ var migrationOutcomeSchema = z2.enum([
308
+ "migrated",
309
+ "already-canonical",
310
+ "user-removed",
311
+ "ambiguous",
312
+ "opted-out",
313
+ "unsupported-shape"
314
+ ]);
315
+ var recordSchema = z2.object({
316
+ status: z2.string(),
317
+ at: z2.string(),
318
+ migrationVersion: z2.number().int().nonnegative().optional(),
319
+ migrationOutcome: migrationOutcomeSchema.optional()
320
+ }).passthrough();
321
+
322
+ // src/version.ts
323
+ import { createRequire } from "node:module";
324
+ var PACKAGE_VERSION = (() => {
325
+ try {
326
+ const req = createRequire(import.meta.url);
327
+ const pkg = req("../package.json");
328
+ return pkg.version ?? "0.0.0";
329
+ } catch {
330
+ return "0.0.0";
331
+ }
332
+ })();
333
+
334
+ // src/hookInstall.ts
335
+ var HOOK_COMMAND = `npx -y --package=@korso/shepherd@${PACKAGE_VERSION} shepherd-inbox-hook`;
336
+
283
337
  // src/instructions.ts
284
338
  function sanitizeWorkspace(workspace) {
285
339
  return workspace.replace(/\s+/g, " ").slice(0, 64);
package/dist/inboxHook.js CHANGED
@@ -141,6 +141,15 @@ var MAILBOX_FRESH_MS = 15 * 60 * 1e3;
141
141
  function sessionMailboxPath(dir, serverPid) {
142
142
  return join3(dir, `agent-${serverPid}.jsonl`);
143
143
  }
144
+ var HOOK_CHAIN_REACH = { codex: 8 };
145
+ var DEFAULT_HOOK_CHAIN_REACH = 3;
146
+ var MAX_HOOK_CHAIN_REACH = Math.max(
147
+ DEFAULT_HOOK_CHAIN_REACH,
148
+ ...Object.values(HOOK_CHAIN_REACH)
149
+ );
150
+ function hookChainReach(client) {
151
+ return (client === void 0 ? void 0 : HOOK_CHAIN_REACH[client]) ?? DEFAULT_HOOK_CHAIN_REACH;
152
+ }
144
153
  function normalizeCwd(cwd) {
145
154
  let normalized = resolve3(cwd);
146
155
  if (process.platform === "win32") normalized = normalized.toLowerCase();
@@ -163,7 +172,7 @@ function hasFreshSessionMeta(dir, staleMs = MAILBOX_FRESH_MS, nowMs = Date.now()
163
172
  }
164
173
  function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH_MS, nowMs = Date.now()) {
165
174
  try {
166
- const chain = hookChain.slice(0, 3);
175
+ const chain = hookChain.slice(0, MAX_HOOK_CHAIN_REACH);
167
176
  const wantedCwd = hookCwd === null ? null : normalizeCwd(hookCwd);
168
177
  const candidates = [];
169
178
  for (const name of readdirSync(dir)) {
@@ -197,7 +206,7 @@ function selectSessionMailboxes(dir, hookChain, hookCwd, staleMs = MAILBOX_FRESH
197
206
  }
198
207
  if (!Array.isArray(meta.chain) || typeof meta.cwd !== "string") continue;
199
208
  const i = chain.findIndex((pid) => meta.chain.includes(pid));
200
- if (i === -1) continue;
209
+ if (i === -1 || i >= hookChainReach(meta.client)) continue;
201
210
  const j = meta.chain.indexOf(chain[i]);
202
211
  if (i >= 2 && (j > 2 || wantedCwd === null || meta.cwd !== wantedCwd))
203
212
  continue;
package/dist/index.js CHANGED
@@ -745,20 +745,52 @@ var EntitlementsStatusResponse = z2.object({
745
745
  reposUsed: z2.number().int()
746
746
  })
747
747
  });
748
+ var AnalyticsRange = z2.enum(["24h", "7d", "30d", "90d"]);
749
+ var AnalyticsBucket = z2.enum(["hour", "day"]);
750
+ var PeriodMetric = z2.object({
751
+ current: z2.number().int().nonnegative(),
752
+ previous: z2.number().int().nonnegative(),
753
+ changePct: z2.number().nullable()
754
+ });
755
+ var DurationPercentiles = z2.object({
756
+ p50: z2.number().nonnegative().nullable(),
757
+ p95: z2.number().nonnegative().nullable()
758
+ });
748
759
  var TrendPoint = z2.object({
749
- // `YYYY-MM-DD` (UTC day).
750
760
  date: z2.string(),
751
761
  count: z2.number()
752
762
  });
763
+ var TrendSeries = z2.object({
764
+ current: z2.array(TrendPoint),
765
+ previous: z2.array(TrendPoint)
766
+ });
753
767
  var TopWorkspace = z2.object({
754
768
  name: z2.string(),
755
769
  slug: z2.string(),
756
- members: z2.number(),
757
- agents: z2.number(),
758
- liveSessions: z2.number()
770
+ members: z2.number().int().nonnegative(),
771
+ agents: z2.number().int().nonnegative(),
772
+ liveSessions: z2.number().int().nonnegative(),
773
+ // Distinct agents with any session activity inside the window.
774
+ activeAgents: z2.number().int().nonnegative(),
775
+ sessions: z2.number().int().nonnegative(),
776
+ commits: z2.number().int().nonnegative(),
777
+ claimsReleased: z2.number().int().nonnegative(),
778
+ // Median released-claim duration (created_at -> released_at), seconds.
779
+ medianClaimSeconds: z2.number().nonnegative().nullable(),
780
+ // ISO timestamp of the most recent observed activity, or null if none.
781
+ lastActivityAt: IsoTimestamp.nullable()
759
782
  });
760
783
  var ShepherdAnalyticsResponse = z2.object({
761
784
  generatedAt: IsoTimestamp,
785
+ // Echo of the (validated) requested window plus the bucket granularity and
786
+ // the exact half-open window [windowStart, windowEnd) the hub computed
787
+ // against — clients label charts from these instead of re-deriving time math.
788
+ range: AnalyticsRange,
789
+ bucket: AnalyticsBucket,
790
+ windowStart: IsoTimestamp,
791
+ windowEnd: IsoTimestamp,
792
+ // Current-state totals: whole-platform counts as of `generatedAt`,
793
+ // independent of the requested range.
762
794
  totals: z2.object({
763
795
  accounts: z2.number(),
764
796
  workspaces: z2.number(),
@@ -778,12 +810,30 @@ var ShepherdAnalyticsResponse = z2.object({
778
810
  avgMembersPerWorkspace: z2.number(),
779
811
  largestWorkspace: z2.number()
780
812
  }),
813
+ // Range-scoped KPIs, each with its aligned previous-period comparison.
814
+ period: z2.object({
815
+ activeWorkspaces: PeriodMetric,
816
+ newAccounts: PeriodMetric,
817
+ newSessions: PeriodMetric,
818
+ commits: PeriodMetric,
819
+ claimsReleased: PeriodMetric
820
+ }),
821
+ // Observed timing diagnostics over the current window: session span is
822
+ // created_at -> last_heartbeat_at; claim duration is created_at ->
823
+ // released_at (released claims only).
824
+ timing: z2.object({
825
+ sessionSpanSeconds: DurationPercentiles,
826
+ claimDurationSeconds: DurationPercentiles
827
+ }),
781
828
  feedbackByType: z2.array(z2.object({ type: z2.string(), count: z2.number() })),
829
+ // Bucketed activity series (hourly for 24h, daily otherwise), each carrying
830
+ // its aligned previous-period twin for chart overlays.
782
831
  trends: z2.object({
783
- newAccounts: z2.array(TrendPoint),
784
- newWorkspaces: z2.array(TrendPoint),
785
- newSessions: z2.array(TrendPoint),
786
- commits: z2.array(TrendPoint)
832
+ newAccounts: TrendSeries,
833
+ newWorkspaces: TrendSeries,
834
+ newSessions: TrendSeries,
835
+ commits: TrendSeries,
836
+ claimsReleased: TrendSeries
787
837
  }),
788
838
  topWorkspaces: z2.array(TopWorkspace)
789
839
  });
@@ -1208,6 +1258,12 @@ function sessionMailboxPath(dir, serverPid) {
1208
1258
  function sessionMetaPath(dir, serverPid) {
1209
1259
  return join3(dir, `agent-${serverPid}.json`);
1210
1260
  }
1261
+ var HOOK_CHAIN_REACH = { codex: 8 };
1262
+ var DEFAULT_HOOK_CHAIN_REACH = 3;
1263
+ var MAX_HOOK_CHAIN_REACH = Math.max(
1264
+ DEFAULT_HOOK_CHAIN_REACH,
1265
+ ...Object.values(HOOK_CHAIN_REACH)
1266
+ );
1211
1267
  function normalizeCwd(cwd) {
1212
1268
  let normalized = resolve3(cwd);
1213
1269
  if (process.platform === "win32") normalized = normalized.toLowerCase();
@@ -1220,7 +1276,12 @@ function writeMailboxMeta(dir, serverPid, meta) {
1220
1276
  const tmp = `${dest}.tmp`;
1221
1277
  writeFileSync3(
1222
1278
  tmp,
1223
- JSON.stringify({ v: 1, cwd: normalizeCwd(meta.cwd), chain: meta.chain })
1279
+ JSON.stringify({
1280
+ v: 1,
1281
+ cwd: normalizeCwd(meta.cwd),
1282
+ chain: meta.chain,
1283
+ ...meta.client === void 0 ? {} : { client: meta.client }
1284
+ })
1224
1285
  );
1225
1286
  renameSync(tmp, dest);
1226
1287
  } catch {
@@ -2403,131 +2464,6 @@ function createHeartbeat({
2403
2464
  return { start, stop };
2404
2465
  }
2405
2466
 
2406
- // src/instructions.ts
2407
- function sanitizeWorkspace(workspace) {
2408
- return workspace.replace(/\s+/g, " ").slice(0, 64);
2409
- }
2410
- function buildInstructions(state, workspace) {
2411
- switch (state) {
2412
- case "linked":
2413
- return `${INTRO} This repository is linked to the \`${workspace ? sanitizeWorkspace(workspace) : "team"}\` workspace, so coordination is active.
2414
-
2415
- ${PROCEDURE}`;
2416
- case "declined":
2417
- 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`.";
2418
- case "unanswered":
2419
- return `${INTRO}
2420
-
2421
- ${FIRST_RUN_ASK}`;
2422
- }
2423
- }
2424
- var INTRO = "You are connected to Shepherd, the shared coordination hub for a team of agents (human and AI) working in the same repositories.";
2425
- 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.
2426
-
2427
- 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.`;
2428
- var PROCEDURE = `Follow this procedure on every session, proactively and without being asked:
2429
-
2430
- 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.
2431
-
2432
- 2. Call \`done\` when that unit of work is complete, using its \`workItemId\`, so teammates see the files freed.
2433
-
2434
- 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.)
2435
-
2436
- 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.
2437
-
2438
- 5. Call \`sync\` when you resume, start a new task, or before large changes, to refresh who is doing what.
2439
-
2440
- Skip \`work\` entirely for read-only exploration \u2014 reading, searching, or thinking that produces no file. The moment you're going to WRITE something, source or doc, claim it first. These tools are advisory and degrade gracefully if the hub is unreachable \u2014 never block your real work on them.
2441
-
2442
- 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.`;
2443
-
2444
- // src/processTree.ts
2445
- import { execFile as execFile2 } from "node:child_process";
2446
- import { promisify } from "node:util";
2447
- var execFileAsync = promisify(execFile2);
2448
- function pidChainFromMap(startPid, parentOf, maxDepth = 32) {
2449
- const chain = [];
2450
- const seen = /* @__PURE__ */ new Set();
2451
- let pid = startPid;
2452
- while (chain.length < maxDepth && pid > 0 && !seen.has(pid)) {
2453
- chain.push(pid);
2454
- seen.add(pid);
2455
- const parent = parentOf.get(pid);
2456
- if (parent === void 0) break;
2457
- pid = parent;
2458
- }
2459
- return chain;
2460
- }
2461
- function quickChain() {
2462
- return [process.pid, process.ppid];
2463
- }
2464
- function parseWmicProcessList(text) {
2465
- const map = /* @__PURE__ */ new Map();
2466
- const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
2467
- if (lines.length === 0) return map;
2468
- const header = lines[0].trimStart();
2469
- let pidFirst;
2470
- if (header.startsWith("ParentProcessId")) pidFirst = false;
2471
- else if (header.startsWith("ProcessId")) pidFirst = true;
2472
- else return map;
2473
- for (const line of lines.slice(1)) {
2474
- const nums = line.trim().split(/\s+/).map(Number);
2475
- if (nums.length !== 2 || nums.some((n) => !Number.isInteger(n))) continue;
2476
- const [a, b] = nums;
2477
- const [pid, ppid] = pidFirst ? [a, b] : [b, a];
2478
- map.set(pid, ppid);
2479
- }
2480
- return map;
2481
- }
2482
- function parsePidPpidLines(text) {
2483
- const map = /* @__PURE__ */ new Map();
2484
- for (const line of text.split(/\r?\n/)) {
2485
- const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
2486
- if (m) map.set(Number(m[1]), Number(m[2]));
2487
- }
2488
- return map;
2489
- }
2490
- async function snapshotParentMap() {
2491
- if (process.platform === "win32") {
2492
- try {
2493
- const { stdout: stdout3 } = await execFileAsync(
2494
- "wmic",
2495
- ["process", "get", "ProcessId,ParentProcessId"],
2496
- { windowsHide: true, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
2497
- );
2498
- const map = parseWmicProcessList(stdout3);
2499
- if (map.size > 0) return map;
2500
- } catch {
2501
- }
2502
- const { stdout: stdout2 } = await execFileAsync(
2503
- "powershell.exe",
2504
- [
2505
- "-NoProfile",
2506
- "-NonInteractive",
2507
- "-Command",
2508
- 'Get-CimInstance -Query "SELECT ProcessId,ParentProcessId FROM Win32_Process" | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }'
2509
- ],
2510
- { windowsHide: true, timeout: 15e3, maxBuffer: 8 * 1024 * 1024 }
2511
- );
2512
- return parsePidPpidLines(stdout2);
2513
- }
2514
- const { stdout } = await execFileAsync(
2515
- "ps",
2516
- ["-eo", "pid=,ppid="],
2517
- { timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
2518
- );
2519
- return parsePidPpidLines(stdout);
2520
- }
2521
- async function ancestorChain(maxDepth = 32, snapshot = snapshotParentMap) {
2522
- try {
2523
- const map = await snapshot();
2524
- const chain = pidChainFromMap(process.pid, map, maxDepth);
2525
- return chain.length >= 2 ? chain : quickChain();
2526
- } catch {
2527
- return quickChain();
2528
- }
2529
- }
2530
-
2531
2467
  // src/hookInstall.ts
2532
2468
  import {
2533
2469
  readFileSync as readFileSync8,
@@ -2633,11 +2569,25 @@ function planCodexConfig(source, command) {
2633
2569
  const candidate = installCandidate(source, config, command);
2634
2570
  return candidate === null ? { kind: "skip", outcome: "unsupported-shape" } : { kind: "install", candidate };
2635
2571
  }
2636
- function appendMissingCodexHandlers(source, command) {
2572
+ function hasCanonicalHandler(source, event, hookMarker) {
2573
+ const headers = new RegExp("^\\[\\[hooks\\." + event + "\\]\\]$", "gm");
2574
+ const boundary = new RegExp("^\\[(?!\\[hooks\\." + event + "\\.)", "m");
2575
+ const nested = new RegExp("^\\[\\[hooks\\." + event + "\\.hooks\\]\\]$", "m");
2576
+ let header;
2577
+ while ((header = headers.exec(source)) !== null) {
2578
+ const rest = source.slice(header.index + header[0].length);
2579
+ const end = rest.search(boundary);
2580
+ const group = end === -1 ? rest : rest.slice(0, end);
2581
+ if (group.includes(hookMarker) && nested.test(group)) return true;
2582
+ }
2583
+ return false;
2584
+ }
2585
+ function appendMissingCodexHandlers(source, command, hookMarker) {
2637
2586
  const handlers = [
2638
- canonicalHandlerBlock("SessionStart", command),
2639
- canonicalHandlerBlock("PreToolUse", command, "*")
2640
- ].filter((handler) => !source.includes(handler));
2587
+ ["UserPromptSubmit", void 0],
2588
+ ["SessionStart", void 0],
2589
+ ["PreToolUse", "*"]
2590
+ ].filter(([event]) => !hasCanonicalHandler(source, event, hookMarker)).map(([event, matcher]) => canonicalHandlerBlock(event, command, matcher));
2641
2591
  const candidate = handlers.length === 0 ? source : source + (source.endsWith("\n") ? "" : "\n") + handlers.join("");
2642
2592
  return parseConfig2(candidate) === null ? null : candidate;
2643
2593
  }
@@ -2880,7 +2830,7 @@ function ensureMigrationBackup(backupFile, source) {
2880
2830
  }
2881
2831
 
2882
2832
  // src/codexHookMigration.ts
2883
- var MIGRATION_VERSION = 2;
2833
+ var MIGRATION_VERSION = 3;
2884
2834
  var migrationOutcomeSchema = z5.enum([
2885
2835
  "migrated",
2886
2836
  "already-canonical",
@@ -2900,8 +2850,12 @@ function migrationPaths(homeDir) {
2900
2850
  return {
2901
2851
  hooksDir,
2902
2852
  recordFile: join6(hooksDir, "codex.json"),
2903
- lockFile: join6(hooksDir, "codex-migration-v2.lock"),
2904
- backupFile: join6(hooksDir, "backups", "codex-config-before-v2.toml"),
2853
+ lockFile: join6(hooksDir, `codex-migration-v${MIGRATION_VERSION}.lock`),
2854
+ backupFile: join6(
2855
+ hooksDir,
2856
+ "backups",
2857
+ `codex-config-before-v${MIGRATION_VERSION}.toml`
2858
+ ),
2905
2859
  configFile: join6(homeDir, ".codex", "config.toml")
2906
2860
  };
2907
2861
  }
@@ -2981,8 +2935,8 @@ function exactOwnedLegacyBlock(source, hooksDir) {
2981
2935
  return exact.length === 1 ? exact[0] : void 0;
2982
2936
  }
2983
2937
  function migrateLegacy(context, state, sourceBytes, source) {
2984
- const { paths, command, log } = context;
2985
- const candidate = appendMissingCodexHandlers(source, command);
2938
+ const { paths, command, hookMarker, log } = context;
2939
+ const candidate = appendMissingCodexHandlers(source, command, hookMarker);
2986
2940
  if (candidate === null) {
2987
2941
  advanceRecord(paths.recordFile, state, "skipped", "unsupported-shape");
2988
2942
  return "skipped";
@@ -3288,6 +3242,158 @@ function installPi(homeDir, extensionSource, log) {
3288
3242
  return "installed";
3289
3243
  }
3290
3244
 
3245
+ // src/instructions.ts
3246
+ function sanitizeWorkspace(workspace) {
3247
+ return workspace.replace(/\s+/g, " ").slice(0, 64);
3248
+ }
3249
+ function buildInstructions(state, workspace) {
3250
+ switch (state) {
3251
+ case "linked":
3252
+ return `${INTRO} This repository is linked to the \`${workspace ? sanitizeWorkspace(workspace) : "team"}\` workspace, so coordination is active.
3253
+
3254
+ ${PROCEDURE}`;
3255
+ case "declined":
3256
+ 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`.";
3257
+ case "unanswered":
3258
+ return `${INTRO}
3259
+
3260
+ ${FIRST_RUN_ASK}`;
3261
+ }
3262
+ }
3263
+ var INTRO = "You are connected to Shepherd, the shared coordination hub for a team of agents (human and AI) working in the same repositories.";
3264
+ 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.
3265
+
3266
+ 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.`;
3267
+ var PROCEDURE = `Follow this procedure on every session, proactively and without being asked:
3268
+
3269
+ 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.
3270
+
3271
+ 2. Call \`done\` when that unit of work is complete, using its \`workItemId\`, so teammates see the files freed.
3272
+
3273
+ 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.)
3274
+
3275
+ 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.
3276
+
3277
+ 5. Call \`sync\` when you resume, start a new task, or before large changes, to refresh who is doing what.
3278
+
3279
+ Skip \`work\` entirely for read-only exploration \u2014 reading, searching, or thinking that produces no file. The moment you're going to WRITE something, source or doc, claim it first. These tools are advisory and degrade gracefully if the hub is unreachable \u2014 never block your real work on them.
3280
+
3281
+ 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.`;
3282
+ function clientInjectsInstructions(client) {
3283
+ return client === "claude";
3284
+ }
3285
+ function stageCoordinationBriefing({
3286
+ clientName,
3287
+ linkState,
3288
+ workspace,
3289
+ append
3290
+ }) {
3291
+ if (linkState !== "linked") return false;
3292
+ if (clientInjectsInstructions(detectClient(clientName))) return false;
3293
+ append([coordinationBriefing(workspace)]);
3294
+ return true;
3295
+ }
3296
+ function coordinationBriefing(workspace) {
3297
+ const safeWorkspace = sanitizeWorkspace(workspace ?? "team");
3298
+ return {
3299
+ // Negative, timestamp-derived: the mailbox dedupes by id and the hub's ids
3300
+ // are positive, so a locally-minted id can never collide with a real one.
3301
+ id: -Date.now(),
3302
+ fromAgentName: "shepherd",
3303
+ fromHuman: "shepherd",
3304
+ targetAgentName: null,
3305
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3306
+ body: `Shepherd coordination is ACTIVE for this repository (workspace \`${safeWorkspace}\`), and your client does not surface Shepherd's standing instructions \u2014 so they arrive here. Procedure from now on, proactively and without being asked: call \`work\` (a one-line intent plus the \`pathGlobs\` you expect to touch) BEFORE you start changing files in an area \u2014 a plan or design doc you are about to author counts \u2014 and hold that ONE claim across every edit in that area; call \`done\` with its \`workItemId\` when the unit of work is complete; call \`announce\` whenever you find something teammates need; call \`sync\` when you resume or switch tasks. Skip \`work\` for read-only exploration that produces no file. If \`work\` reports a conflict, coordinate or pick different work \u2014 never silently collide. These tools are advisory: never block real work on them.`
3307
+ };
3308
+ }
3309
+
3310
+ // src/processTree.ts
3311
+ import { execFile as execFile2 } from "node:child_process";
3312
+ import { promisify } from "node:util";
3313
+ var execFileAsync = promisify(execFile2);
3314
+ function pidChainFromMap(startPid, parentOf, maxDepth = 32) {
3315
+ const chain = [];
3316
+ const seen = /* @__PURE__ */ new Set();
3317
+ let pid = startPid;
3318
+ while (chain.length < maxDepth && pid > 0 && !seen.has(pid)) {
3319
+ chain.push(pid);
3320
+ seen.add(pid);
3321
+ const parent = parentOf.get(pid);
3322
+ if (parent === void 0) break;
3323
+ pid = parent;
3324
+ }
3325
+ return chain;
3326
+ }
3327
+ function quickChain() {
3328
+ return [process.pid, process.ppid];
3329
+ }
3330
+ function parseWmicProcessList(text) {
3331
+ const map = /* @__PURE__ */ new Map();
3332
+ const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
3333
+ if (lines.length === 0) return map;
3334
+ const header = lines[0].trimStart();
3335
+ let pidFirst;
3336
+ if (header.startsWith("ParentProcessId")) pidFirst = false;
3337
+ else if (header.startsWith("ProcessId")) pidFirst = true;
3338
+ else return map;
3339
+ for (const line of lines.slice(1)) {
3340
+ const nums = line.trim().split(/\s+/).map(Number);
3341
+ if (nums.length !== 2 || nums.some((n) => !Number.isInteger(n))) continue;
3342
+ const [a, b] = nums;
3343
+ const [pid, ppid] = pidFirst ? [a, b] : [b, a];
3344
+ map.set(pid, ppid);
3345
+ }
3346
+ return map;
3347
+ }
3348
+ function parsePidPpidLines(text) {
3349
+ const map = /* @__PURE__ */ new Map();
3350
+ for (const line of text.split(/\r?\n/)) {
3351
+ const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
3352
+ if (m) map.set(Number(m[1]), Number(m[2]));
3353
+ }
3354
+ return map;
3355
+ }
3356
+ async function snapshotParentMap() {
3357
+ if (process.platform === "win32") {
3358
+ try {
3359
+ const { stdout: stdout3 } = await execFileAsync(
3360
+ "wmic",
3361
+ ["process", "get", "ProcessId,ParentProcessId"],
3362
+ { windowsHide: true, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
3363
+ );
3364
+ const map = parseWmicProcessList(stdout3);
3365
+ if (map.size > 0) return map;
3366
+ } catch {
3367
+ }
3368
+ const { stdout: stdout2 } = await execFileAsync(
3369
+ "powershell.exe",
3370
+ [
3371
+ "-NoProfile",
3372
+ "-NonInteractive",
3373
+ "-Command",
3374
+ 'Get-CimInstance -Query "SELECT ProcessId,ParentProcessId FROM Win32_Process" | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }'
3375
+ ],
3376
+ { windowsHide: true, timeout: 15e3, maxBuffer: 8 * 1024 * 1024 }
3377
+ );
3378
+ return parsePidPpidLines(stdout2);
3379
+ }
3380
+ const { stdout } = await execFileAsync(
3381
+ "ps",
3382
+ ["-eo", "pid=,ppid="],
3383
+ { timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
3384
+ );
3385
+ return parsePidPpidLines(stdout);
3386
+ }
3387
+ async function ancestorChain(maxDepth = 32, snapshot = snapshotParentMap) {
3388
+ try {
3389
+ const map = await snapshot();
3390
+ const chain = pidChainFromMap(process.pid, map, maxDepth);
3391
+ return chain.length >= 2 ? chain : quickChain();
3392
+ } catch {
3393
+ return quickChain();
3394
+ }
3395
+ }
3396
+
3291
3397
  // src/index.ts
3292
3398
  async function main() {
3293
3399
  const config = loadConfig();
@@ -3300,10 +3406,12 @@ async function main() {
3300
3406
  const inboxFile = sessionMailboxPath(inboxDir, process.pid);
3301
3407
  const launchCwd = process.cwd();
3302
3408
  let serverChain = quickChain();
3409
+ let serverClient;
3303
3410
  const liveness = {
3304
3411
  refresh: () => writeMailboxMeta(inboxDir, process.pid, {
3305
3412
  cwd: launchCwd,
3306
- chain: serverChain
3413
+ chain: serverChain,
3414
+ client: serverClient
3307
3415
  }),
3308
3416
  remove: () => removeMailboxMeta(inboxDir, process.pid)
3309
3417
  };
@@ -3343,10 +3451,19 @@ async function main() {
3343
3451
  });
3344
3452
  const transport = new StdioServerTransport();
3345
3453
  server.server.oninitialized = () => {
3454
+ const clientName = server.server.getClientVersion()?.name;
3346
3455
  void autoInstallHooks({
3347
- clientName: server.server.getClientVersion()?.name,
3456
+ clientName,
3348
3457
  disabled: config.SHEPHERD_NO_AUTO_HOOKS
3349
3458
  });
3459
+ serverClient = detectClient(clientName);
3460
+ liveness.refresh();
3461
+ stageCoordinationBriefing({
3462
+ clientName,
3463
+ linkState: context.linkState,
3464
+ workspace: context.workspace,
3465
+ append: (announcements) => appendAnnouncements(inboxFile, announcements)
3466
+ });
3350
3467
  };
3351
3468
  let shuttingDown = false;
3352
3469
  const shutdown = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korso/shepherd",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "Shepherd MCP server — gives any MCP-capable agent (Claude Code, Codex, etc.) advisory cross-session coordination tools (work/done/announce/sync, plus link/unlink/decline) backed by the shared Shepherd hub. Joins the workspace automatically and ships standing instructions so the agent self-coordinates.",
5
5
  "homepage": "https://github.com/Korso-AI/shepherd#readme",
6
6
  "bugs": {