@korso/shepherd 0.11.2 → 0.11.4
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/inboxExtension.js +56 -2
- package/dist/inboxHook.js +11 -2
- package/dist/index.js +282 -147
- package/package.json +1 -1
package/dist/inboxExtension.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
@@ -358,6 +358,7 @@ var ChangeReportEntry = z2.object({
|
|
|
358
358
|
message: z2.string().max(4096).nullable(),
|
|
359
359
|
paths: z2.array(z2.string().min(1).max(1024)).min(1).max(500)
|
|
360
360
|
});
|
|
361
|
+
var CHANGE_REPORT_MAX_BYTES = 48 * 1024;
|
|
361
362
|
var ChangeReport = z2.object({
|
|
362
363
|
branch: z2.string().max(512),
|
|
363
364
|
baseBranch: z2.string().max(512),
|
|
@@ -745,20 +746,52 @@ var EntitlementsStatusResponse = z2.object({
|
|
|
745
746
|
reposUsed: z2.number().int()
|
|
746
747
|
})
|
|
747
748
|
});
|
|
749
|
+
var AnalyticsRange = z2.enum(["24h", "7d", "30d", "90d"]);
|
|
750
|
+
var AnalyticsBucket = z2.enum(["hour", "day"]);
|
|
751
|
+
var PeriodMetric = z2.object({
|
|
752
|
+
current: z2.number().int().nonnegative(),
|
|
753
|
+
previous: z2.number().int().nonnegative(),
|
|
754
|
+
changePct: z2.number().nullable()
|
|
755
|
+
});
|
|
756
|
+
var DurationPercentiles = z2.object({
|
|
757
|
+
p50: z2.number().nonnegative().nullable(),
|
|
758
|
+
p95: z2.number().nonnegative().nullable()
|
|
759
|
+
});
|
|
748
760
|
var TrendPoint = z2.object({
|
|
749
|
-
// `YYYY-MM-DD` (UTC day).
|
|
750
761
|
date: z2.string(),
|
|
751
762
|
count: z2.number()
|
|
752
763
|
});
|
|
764
|
+
var TrendSeries = z2.object({
|
|
765
|
+
current: z2.array(TrendPoint),
|
|
766
|
+
previous: z2.array(TrendPoint)
|
|
767
|
+
});
|
|
753
768
|
var TopWorkspace = z2.object({
|
|
754
769
|
name: z2.string(),
|
|
755
770
|
slug: z2.string(),
|
|
756
|
-
members: z2.number(),
|
|
757
|
-
agents: z2.number(),
|
|
758
|
-
liveSessions: z2.number()
|
|
771
|
+
members: z2.number().int().nonnegative(),
|
|
772
|
+
agents: z2.number().int().nonnegative(),
|
|
773
|
+
liveSessions: z2.number().int().nonnegative(),
|
|
774
|
+
// Distinct agents with any session activity inside the window.
|
|
775
|
+
activeAgents: z2.number().int().nonnegative(),
|
|
776
|
+
sessions: z2.number().int().nonnegative(),
|
|
777
|
+
commits: z2.number().int().nonnegative(),
|
|
778
|
+
claimsReleased: z2.number().int().nonnegative(),
|
|
779
|
+
// Median released-claim duration (created_at -> released_at), seconds.
|
|
780
|
+
medianClaimSeconds: z2.number().nonnegative().nullable(),
|
|
781
|
+
// ISO timestamp of the most recent observed activity, or null if none.
|
|
782
|
+
lastActivityAt: IsoTimestamp.nullable()
|
|
759
783
|
});
|
|
760
784
|
var ShepherdAnalyticsResponse = z2.object({
|
|
761
785
|
generatedAt: IsoTimestamp,
|
|
786
|
+
// Echo of the (validated) requested window plus the bucket granularity and
|
|
787
|
+
// the exact half-open window [windowStart, windowEnd) the hub computed
|
|
788
|
+
// against — clients label charts from these instead of re-deriving time math.
|
|
789
|
+
range: AnalyticsRange,
|
|
790
|
+
bucket: AnalyticsBucket,
|
|
791
|
+
windowStart: IsoTimestamp,
|
|
792
|
+
windowEnd: IsoTimestamp,
|
|
793
|
+
// Current-state totals: whole-platform counts as of `generatedAt`,
|
|
794
|
+
// independent of the requested range.
|
|
762
795
|
totals: z2.object({
|
|
763
796
|
accounts: z2.number(),
|
|
764
797
|
workspaces: z2.number(),
|
|
@@ -778,12 +811,30 @@ var ShepherdAnalyticsResponse = z2.object({
|
|
|
778
811
|
avgMembersPerWorkspace: z2.number(),
|
|
779
812
|
largestWorkspace: z2.number()
|
|
780
813
|
}),
|
|
814
|
+
// Range-scoped KPIs, each with its aligned previous-period comparison.
|
|
815
|
+
period: z2.object({
|
|
816
|
+
activeWorkspaces: PeriodMetric,
|
|
817
|
+
newAccounts: PeriodMetric,
|
|
818
|
+
newSessions: PeriodMetric,
|
|
819
|
+
commits: PeriodMetric,
|
|
820
|
+
claimsReleased: PeriodMetric
|
|
821
|
+
}),
|
|
822
|
+
// Observed timing diagnostics over the current window: session span is
|
|
823
|
+
// created_at -> last_heartbeat_at; claim duration is created_at ->
|
|
824
|
+
// released_at (released claims only).
|
|
825
|
+
timing: z2.object({
|
|
826
|
+
sessionSpanSeconds: DurationPercentiles,
|
|
827
|
+
claimDurationSeconds: DurationPercentiles
|
|
828
|
+
}),
|
|
781
829
|
feedbackByType: z2.array(z2.object({ type: z2.string(), count: z2.number() })),
|
|
830
|
+
// Bucketed activity series (hourly for 24h, daily otherwise), each carrying
|
|
831
|
+
// its aligned previous-period twin for chart overlays.
|
|
782
832
|
trends: z2.object({
|
|
783
|
-
newAccounts:
|
|
784
|
-
newWorkspaces:
|
|
785
|
-
newSessions:
|
|
786
|
-
commits:
|
|
833
|
+
newAccounts: TrendSeries,
|
|
834
|
+
newWorkspaces: TrendSeries,
|
|
835
|
+
newSessions: TrendSeries,
|
|
836
|
+
commits: TrendSeries,
|
|
837
|
+
claimsReleased: TrendSeries
|
|
787
838
|
}),
|
|
788
839
|
topWorkspaces: z2.array(TopWorkspace)
|
|
789
840
|
});
|
|
@@ -1167,13 +1218,30 @@ async function buildChangeReport(cwd, config) {
|
|
|
1167
1218
|
});
|
|
1168
1219
|
}
|
|
1169
1220
|
}
|
|
1170
|
-
return {
|
|
1221
|
+
return fitToBudget({
|
|
1171
1222
|
branch: branch ?? "HEAD",
|
|
1172
1223
|
baseBranch: base ?? UNRESOLVED_BASE,
|
|
1173
1224
|
head: head ?? "",
|
|
1174
1225
|
truncated,
|
|
1175
1226
|
entries
|
|
1176
|
-
};
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
function serializedBytes(report) {
|
|
1230
|
+
return Buffer.byteLength(JSON.stringify(report));
|
|
1231
|
+
}
|
|
1232
|
+
function fitToBudget(report) {
|
|
1233
|
+
if (serializedBytes(report) <= CHANGE_REPORT_MAX_BYTES) return report;
|
|
1234
|
+
report.truncated = true;
|
|
1235
|
+
while (report.entries.length > 0 && report.entries[report.entries.length - 1].kind === "committed" && serializedBytes(report) > CHANGE_REPORT_MAX_BYTES) {
|
|
1236
|
+
report.entries.pop();
|
|
1237
|
+
}
|
|
1238
|
+
const dirty = report.entries[0];
|
|
1239
|
+
if (dirty?.kind === "uncommitted") {
|
|
1240
|
+
while (dirty.paths.length > 1 && serializedBytes(report) > CHANGE_REPORT_MAX_BYTES) {
|
|
1241
|
+
dirty.paths.length = Math.ceil(dirty.paths.length / 2);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return report;
|
|
1177
1245
|
}
|
|
1178
1246
|
|
|
1179
1247
|
// src/inbox.ts
|
|
@@ -1208,6 +1276,12 @@ function sessionMailboxPath(dir, serverPid) {
|
|
|
1208
1276
|
function sessionMetaPath(dir, serverPid) {
|
|
1209
1277
|
return join3(dir, `agent-${serverPid}.json`);
|
|
1210
1278
|
}
|
|
1279
|
+
var HOOK_CHAIN_REACH = { codex: 8 };
|
|
1280
|
+
var DEFAULT_HOOK_CHAIN_REACH = 3;
|
|
1281
|
+
var MAX_HOOK_CHAIN_REACH = Math.max(
|
|
1282
|
+
DEFAULT_HOOK_CHAIN_REACH,
|
|
1283
|
+
...Object.values(HOOK_CHAIN_REACH)
|
|
1284
|
+
);
|
|
1211
1285
|
function normalizeCwd(cwd) {
|
|
1212
1286
|
let normalized = resolve3(cwd);
|
|
1213
1287
|
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
@@ -1220,7 +1294,12 @@ function writeMailboxMeta(dir, serverPid, meta) {
|
|
|
1220
1294
|
const tmp = `${dest}.tmp`;
|
|
1221
1295
|
writeFileSync3(
|
|
1222
1296
|
tmp,
|
|
1223
|
-
JSON.stringify({
|
|
1297
|
+
JSON.stringify({
|
|
1298
|
+
v: 1,
|
|
1299
|
+
cwd: normalizeCwd(meta.cwd),
|
|
1300
|
+
chain: meta.chain,
|
|
1301
|
+
...meta.client === void 0 ? {} : { client: meta.client }
|
|
1302
|
+
})
|
|
1224
1303
|
);
|
|
1225
1304
|
renameSync(tmp, dest);
|
|
1226
1305
|
} catch {
|
|
@@ -2403,131 +2482,6 @@ function createHeartbeat({
|
|
|
2403
2482
|
return { start, stop };
|
|
2404
2483
|
}
|
|
2405
2484
|
|
|
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
2485
|
// src/hookInstall.ts
|
|
2532
2486
|
import {
|
|
2533
2487
|
readFileSync as readFileSync8,
|
|
@@ -2633,11 +2587,25 @@ function planCodexConfig(source, command) {
|
|
|
2633
2587
|
const candidate = installCandidate(source, config, command);
|
|
2634
2588
|
return candidate === null ? { kind: "skip", outcome: "unsupported-shape" } : { kind: "install", candidate };
|
|
2635
2589
|
}
|
|
2636
|
-
function
|
|
2590
|
+
function hasCanonicalHandler(source, event, hookMarker) {
|
|
2591
|
+
const headers = new RegExp("^\\[\\[hooks\\." + event + "\\]\\]$", "gm");
|
|
2592
|
+
const boundary = new RegExp("^\\[(?!\\[hooks\\." + event + "\\.)", "m");
|
|
2593
|
+
const nested = new RegExp("^\\[\\[hooks\\." + event + "\\.hooks\\]\\]$", "m");
|
|
2594
|
+
let header;
|
|
2595
|
+
while ((header = headers.exec(source)) !== null) {
|
|
2596
|
+
const rest = source.slice(header.index + header[0].length);
|
|
2597
|
+
const end = rest.search(boundary);
|
|
2598
|
+
const group = end === -1 ? rest : rest.slice(0, end);
|
|
2599
|
+
if (group.includes(hookMarker) && nested.test(group)) return true;
|
|
2600
|
+
}
|
|
2601
|
+
return false;
|
|
2602
|
+
}
|
|
2603
|
+
function appendMissingCodexHandlers(source, command, hookMarker) {
|
|
2637
2604
|
const handlers = [
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2605
|
+
["UserPromptSubmit", void 0],
|
|
2606
|
+
["SessionStart", void 0],
|
|
2607
|
+
["PreToolUse", "*"]
|
|
2608
|
+
].filter(([event]) => !hasCanonicalHandler(source, event, hookMarker)).map(([event, matcher]) => canonicalHandlerBlock(event, command, matcher));
|
|
2641
2609
|
const candidate = handlers.length === 0 ? source : source + (source.endsWith("\n") ? "" : "\n") + handlers.join("");
|
|
2642
2610
|
return parseConfig2(candidate) === null ? null : candidate;
|
|
2643
2611
|
}
|
|
@@ -2880,7 +2848,7 @@ function ensureMigrationBackup(backupFile, source) {
|
|
|
2880
2848
|
}
|
|
2881
2849
|
|
|
2882
2850
|
// src/codexHookMigration.ts
|
|
2883
|
-
var MIGRATION_VERSION =
|
|
2851
|
+
var MIGRATION_VERSION = 3;
|
|
2884
2852
|
var migrationOutcomeSchema = z5.enum([
|
|
2885
2853
|
"migrated",
|
|
2886
2854
|
"already-canonical",
|
|
@@ -2900,8 +2868,12 @@ function migrationPaths(homeDir) {
|
|
|
2900
2868
|
return {
|
|
2901
2869
|
hooksDir,
|
|
2902
2870
|
recordFile: join6(hooksDir, "codex.json"),
|
|
2903
|
-
lockFile: join6(hooksDir,
|
|
2904
|
-
backupFile: join6(
|
|
2871
|
+
lockFile: join6(hooksDir, `codex-migration-v${MIGRATION_VERSION}.lock`),
|
|
2872
|
+
backupFile: join6(
|
|
2873
|
+
hooksDir,
|
|
2874
|
+
"backups",
|
|
2875
|
+
`codex-config-before-v${MIGRATION_VERSION}.toml`
|
|
2876
|
+
),
|
|
2905
2877
|
configFile: join6(homeDir, ".codex", "config.toml")
|
|
2906
2878
|
};
|
|
2907
2879
|
}
|
|
@@ -2981,8 +2953,8 @@ function exactOwnedLegacyBlock(source, hooksDir) {
|
|
|
2981
2953
|
return exact.length === 1 ? exact[0] : void 0;
|
|
2982
2954
|
}
|
|
2983
2955
|
function migrateLegacy(context, state, sourceBytes, source) {
|
|
2984
|
-
const { paths, command, log } = context;
|
|
2985
|
-
const candidate = appendMissingCodexHandlers(source, command);
|
|
2956
|
+
const { paths, command, hookMarker, log } = context;
|
|
2957
|
+
const candidate = appendMissingCodexHandlers(source, command, hookMarker);
|
|
2986
2958
|
if (candidate === null) {
|
|
2987
2959
|
advanceRecord(paths.recordFile, state, "skipped", "unsupported-shape");
|
|
2988
2960
|
return "skipped";
|
|
@@ -3288,6 +3260,158 @@ function installPi(homeDir, extensionSource, log) {
|
|
|
3288
3260
|
return "installed";
|
|
3289
3261
|
}
|
|
3290
3262
|
|
|
3263
|
+
// src/instructions.ts
|
|
3264
|
+
function sanitizeWorkspace(workspace) {
|
|
3265
|
+
return workspace.replace(/\s+/g, " ").slice(0, 64);
|
|
3266
|
+
}
|
|
3267
|
+
function buildInstructions(state, workspace) {
|
|
3268
|
+
switch (state) {
|
|
3269
|
+
case "linked":
|
|
3270
|
+
return `${INTRO} This repository is linked to the \`${workspace ? sanitizeWorkspace(workspace) : "team"}\` workspace, so coordination is active.
|
|
3271
|
+
|
|
3272
|
+
${PROCEDURE}`;
|
|
3273
|
+
case "declined":
|
|
3274
|
+
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`.";
|
|
3275
|
+
case "unanswered":
|
|
3276
|
+
return `${INTRO}
|
|
3277
|
+
|
|
3278
|
+
${FIRST_RUN_ASK}`;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
var INTRO = "You are connected to Shepherd, the shared coordination hub for a team of agents (human and AI) working in the same repositories.";
|
|
3282
|
+
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.
|
|
3283
|
+
|
|
3284
|
+
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.`;
|
|
3285
|
+
var PROCEDURE = `Follow this procedure on every session, proactively and without being asked:
|
|
3286
|
+
|
|
3287
|
+
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.
|
|
3288
|
+
|
|
3289
|
+
2. Call \`done\` when that unit of work is complete, using its \`workItemId\`, so teammates see the files freed.
|
|
3290
|
+
|
|
3291
|
+
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.)
|
|
3292
|
+
|
|
3293
|
+
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.
|
|
3294
|
+
|
|
3295
|
+
5. Call \`sync\` when you resume, start a new task, or before large changes, to refresh who is doing what.
|
|
3296
|
+
|
|
3297
|
+
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.
|
|
3298
|
+
|
|
3299
|
+
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.`;
|
|
3300
|
+
function clientInjectsInstructions(client) {
|
|
3301
|
+
return client === "claude";
|
|
3302
|
+
}
|
|
3303
|
+
function stageCoordinationBriefing({
|
|
3304
|
+
clientName,
|
|
3305
|
+
linkState,
|
|
3306
|
+
workspace,
|
|
3307
|
+
append
|
|
3308
|
+
}) {
|
|
3309
|
+
if (linkState !== "linked") return false;
|
|
3310
|
+
if (clientInjectsInstructions(detectClient(clientName))) return false;
|
|
3311
|
+
append([coordinationBriefing(workspace)]);
|
|
3312
|
+
return true;
|
|
3313
|
+
}
|
|
3314
|
+
function coordinationBriefing(workspace) {
|
|
3315
|
+
const safeWorkspace = sanitizeWorkspace(workspace ?? "team");
|
|
3316
|
+
return {
|
|
3317
|
+
// Negative, timestamp-derived: the mailbox dedupes by id and the hub's ids
|
|
3318
|
+
// are positive, so a locally-minted id can never collide with a real one.
|
|
3319
|
+
id: -Date.now(),
|
|
3320
|
+
fromAgentName: "shepherd",
|
|
3321
|
+
fromHuman: "shepherd",
|
|
3322
|
+
targetAgentName: null,
|
|
3323
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3324
|
+
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.`
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
// src/processTree.ts
|
|
3329
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
3330
|
+
import { promisify } from "node:util";
|
|
3331
|
+
var execFileAsync = promisify(execFile2);
|
|
3332
|
+
function pidChainFromMap(startPid, parentOf, maxDepth = 32) {
|
|
3333
|
+
const chain = [];
|
|
3334
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3335
|
+
let pid = startPid;
|
|
3336
|
+
while (chain.length < maxDepth && pid > 0 && !seen.has(pid)) {
|
|
3337
|
+
chain.push(pid);
|
|
3338
|
+
seen.add(pid);
|
|
3339
|
+
const parent = parentOf.get(pid);
|
|
3340
|
+
if (parent === void 0) break;
|
|
3341
|
+
pid = parent;
|
|
3342
|
+
}
|
|
3343
|
+
return chain;
|
|
3344
|
+
}
|
|
3345
|
+
function quickChain() {
|
|
3346
|
+
return [process.pid, process.ppid];
|
|
3347
|
+
}
|
|
3348
|
+
function parseWmicProcessList(text) {
|
|
3349
|
+
const map = /* @__PURE__ */ new Map();
|
|
3350
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
3351
|
+
if (lines.length === 0) return map;
|
|
3352
|
+
const header = lines[0].trimStart();
|
|
3353
|
+
let pidFirst;
|
|
3354
|
+
if (header.startsWith("ParentProcessId")) pidFirst = false;
|
|
3355
|
+
else if (header.startsWith("ProcessId")) pidFirst = true;
|
|
3356
|
+
else return map;
|
|
3357
|
+
for (const line of lines.slice(1)) {
|
|
3358
|
+
const nums = line.trim().split(/\s+/).map(Number);
|
|
3359
|
+
if (nums.length !== 2 || nums.some((n) => !Number.isInteger(n))) continue;
|
|
3360
|
+
const [a, b] = nums;
|
|
3361
|
+
const [pid, ppid] = pidFirst ? [a, b] : [b, a];
|
|
3362
|
+
map.set(pid, ppid);
|
|
3363
|
+
}
|
|
3364
|
+
return map;
|
|
3365
|
+
}
|
|
3366
|
+
function parsePidPpidLines(text) {
|
|
3367
|
+
const map = /* @__PURE__ */ new Map();
|
|
3368
|
+
for (const line of text.split(/\r?\n/)) {
|
|
3369
|
+
const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
3370
|
+
if (m) map.set(Number(m[1]), Number(m[2]));
|
|
3371
|
+
}
|
|
3372
|
+
return map;
|
|
3373
|
+
}
|
|
3374
|
+
async function snapshotParentMap() {
|
|
3375
|
+
if (process.platform === "win32") {
|
|
3376
|
+
try {
|
|
3377
|
+
const { stdout: stdout3 } = await execFileAsync(
|
|
3378
|
+
"wmic",
|
|
3379
|
+
["process", "get", "ProcessId,ParentProcessId"],
|
|
3380
|
+
{ windowsHide: true, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
|
|
3381
|
+
);
|
|
3382
|
+
const map = parseWmicProcessList(stdout3);
|
|
3383
|
+
if (map.size > 0) return map;
|
|
3384
|
+
} catch {
|
|
3385
|
+
}
|
|
3386
|
+
const { stdout: stdout2 } = await execFileAsync(
|
|
3387
|
+
"powershell.exe",
|
|
3388
|
+
[
|
|
3389
|
+
"-NoProfile",
|
|
3390
|
+
"-NonInteractive",
|
|
3391
|
+
"-Command",
|
|
3392
|
+
'Get-CimInstance -Query "SELECT ProcessId,ParentProcessId FROM Win32_Process" | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }'
|
|
3393
|
+
],
|
|
3394
|
+
{ windowsHide: true, timeout: 15e3, maxBuffer: 8 * 1024 * 1024 }
|
|
3395
|
+
);
|
|
3396
|
+
return parsePidPpidLines(stdout2);
|
|
3397
|
+
}
|
|
3398
|
+
const { stdout } = await execFileAsync(
|
|
3399
|
+
"ps",
|
|
3400
|
+
["-eo", "pid=,ppid="],
|
|
3401
|
+
{ timeout: 1e4, maxBuffer: 8 * 1024 * 1024 }
|
|
3402
|
+
);
|
|
3403
|
+
return parsePidPpidLines(stdout);
|
|
3404
|
+
}
|
|
3405
|
+
async function ancestorChain(maxDepth = 32, snapshot = snapshotParentMap) {
|
|
3406
|
+
try {
|
|
3407
|
+
const map = await snapshot();
|
|
3408
|
+
const chain = pidChainFromMap(process.pid, map, maxDepth);
|
|
3409
|
+
return chain.length >= 2 ? chain : quickChain();
|
|
3410
|
+
} catch {
|
|
3411
|
+
return quickChain();
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3291
3415
|
// src/index.ts
|
|
3292
3416
|
async function main() {
|
|
3293
3417
|
const config = loadConfig();
|
|
@@ -3300,10 +3424,12 @@ async function main() {
|
|
|
3300
3424
|
const inboxFile = sessionMailboxPath(inboxDir, process.pid);
|
|
3301
3425
|
const launchCwd = process.cwd();
|
|
3302
3426
|
let serverChain = quickChain();
|
|
3427
|
+
let serverClient;
|
|
3303
3428
|
const liveness = {
|
|
3304
3429
|
refresh: () => writeMailboxMeta(inboxDir, process.pid, {
|
|
3305
3430
|
cwd: launchCwd,
|
|
3306
|
-
chain: serverChain
|
|
3431
|
+
chain: serverChain,
|
|
3432
|
+
client: serverClient
|
|
3307
3433
|
}),
|
|
3308
3434
|
remove: () => removeMailboxMeta(inboxDir, process.pid)
|
|
3309
3435
|
};
|
|
@@ -3343,10 +3469,19 @@ async function main() {
|
|
|
3343
3469
|
});
|
|
3344
3470
|
const transport = new StdioServerTransport();
|
|
3345
3471
|
server.server.oninitialized = () => {
|
|
3472
|
+
const clientName = server.server.getClientVersion()?.name;
|
|
3346
3473
|
void autoInstallHooks({
|
|
3347
|
-
clientName
|
|
3474
|
+
clientName,
|
|
3348
3475
|
disabled: config.SHEPHERD_NO_AUTO_HOOKS
|
|
3349
3476
|
});
|
|
3477
|
+
serverClient = detectClient(clientName);
|
|
3478
|
+
liveness.refresh();
|
|
3479
|
+
stageCoordinationBriefing({
|
|
3480
|
+
clientName,
|
|
3481
|
+
linkState: context.linkState,
|
|
3482
|
+
workspace: context.workspace,
|
|
3483
|
+
append: (announcements) => appendAnnouncements(inboxFile, announcements)
|
|
3484
|
+
});
|
|
3350
3485
|
};
|
|
3351
3486
|
let shuttingDown = false;
|
|
3352
3487
|
const shutdown = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@korso/shepherd",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
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": {
|