@korso/shepherd 0.6.3 → 0.7.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/README.md +25 -5
- package/dist/inboxExtension.js +110 -18
- package/dist/inboxHook.js +122 -23
- package/dist/index.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,9 +99,22 @@ It delivers to an agent **while it's active**; an idle agent picks messages up t
|
|
|
99
99
|
moment it next does anything. (Waking a fully-idle agent is out of scope — for
|
|
100
100
|
Claude Code that needs Channels; Codex/Pi have no equivalent.)
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
**The hook also carries the unlinked-repo nudge.** The server instructions tell
|
|
103
|
+
the agent to ask about linking on its first write in a new repo, but instructions
|
|
104
|
+
sitting passively in context don't trigger themselves — an agent focused on the
|
|
105
|
+
task can skip straight to editing. So on every invocation the hook also checks
|
|
106
|
+
this repo's link state and, when the repo is **neither linked (`.shepherd`
|
|
107
|
+
marker) nor declined**, injects a reminder to run `link`/`decline` — on
|
|
108
|
+
`SessionStart` (front-loads the ask), right before a file-writing tool
|
|
109
|
+
(`Edit`/`Write`/`MultiEdit`/`NotebookEdit`; read-only tools never nudge), and on
|
|
110
|
+
tool-less events like Codex's `UserPromptSubmit` or a Pi turn. The
|
|
111
|
+
nudge is advisory and self-extinguishing: the moment the repo is linked or
|
|
112
|
+
declined it goes quiet, and like everything else here it fails open.
|
|
113
|
+
|
|
114
|
+
### Claude Code — `PreToolUse` + `SessionStart` hooks
|
|
115
|
+
|
|
116
|
+
`PreToolUse` fires before every tool, giving the most frequent passive delivery;
|
|
117
|
+
`SessionStart` surfaces the link ask at the top of a session in an unlinked repo.
|
|
105
118
|
The hook needs no arguments — it resolves the same default inbox dir the server
|
|
106
119
|
uses (override both with `SHEPHERD_INBOX_DIR` if you relocated it):
|
|
107
120
|
|
|
@@ -118,6 +131,13 @@ uses (override both with `SHEPHERD_INBOX_DIR` if you relocated it):
|
|
|
118
131
|
}
|
|
119
132
|
},
|
|
120
133
|
"hooks": {
|
|
134
|
+
"SessionStart": [
|
|
135
|
+
{
|
|
136
|
+
"hooks": [
|
|
137
|
+
{ "type": "command", "command": "npx -y --package=@korso/shepherd shepherd-inbox-hook" }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
],
|
|
121
141
|
"PreToolUse": [
|
|
122
142
|
{
|
|
123
143
|
"matcher": "*",
|
|
@@ -161,8 +181,8 @@ cp "$(npm root -g)/@korso/shepherd/dist/inboxExtension.js" ~/.pi/agent/extension
|
|
|
161
181
|
```
|
|
162
182
|
|
|
163
183
|
It runs on every user turn (`before_agent_start`), drains the same inbox, and
|
|
164
|
-
injects pending announcements. (Or load it ad hoc
|
|
165
|
-
`pi -e /abs/path/to/dist/inboxExtension.js`.)
|
|
184
|
+
injects pending announcements plus the unlinked-repo nudge. (Or load it ad hoc
|
|
185
|
+
with `pi -e /abs/path/to/dist/inboxExtension.js`.)
|
|
166
186
|
|
|
167
187
|
### Notes
|
|
168
188
|
|
package/dist/inboxExtension.js
CHANGED
|
@@ -1,16 +1,59 @@
|
|
|
1
1
|
// src/inbox.ts
|
|
2
|
-
import { createHash } from "crypto";
|
|
2
|
+
import { createHash as createHash2 } from "crypto";
|
|
3
3
|
import {
|
|
4
4
|
appendFileSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readFileSync,
|
|
5
|
+
mkdirSync as mkdirSync2,
|
|
6
|
+
readFileSync as readFileSync3,
|
|
7
7
|
renameSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
existsSync
|
|
8
|
+
rmSync as rmSync3,
|
|
9
|
+
existsSync as existsSync3
|
|
10
10
|
} from "fs";
|
|
11
|
+
import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
|
|
12
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
|
|
13
|
+
|
|
14
|
+
// src/marker.ts
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
var MARKER_FILENAME = ".shepherd";
|
|
18
|
+
function findRepoRoot(cwd) {
|
|
19
|
+
let dir = path.resolve(cwd);
|
|
20
|
+
for (; ; ) {
|
|
21
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir) return null;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function markerPath(cwd) {
|
|
28
|
+
const root = findRepoRoot(cwd);
|
|
29
|
+
return root === null ? null : path.join(root, MARKER_FILENAME);
|
|
30
|
+
}
|
|
31
|
+
function readMarker(cwd = process.cwd()) {
|
|
32
|
+
const file = markerPath(cwd);
|
|
33
|
+
if (file === null) return null;
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = fs.readFileSync(file, "utf8");
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.workspace === "string" && parsed.workspace.length > 0) {
|
|
43
|
+
return { workspace: parsed.workspace };
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/declined.ts
|
|
52
|
+
import { createHash } from "crypto";
|
|
53
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
11
54
|
import { homedir, tmpdir } from "os";
|
|
12
|
-
import { dirname, join, resolve } from "path";
|
|
13
|
-
function
|
|
55
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
56
|
+
function defaultDeclinedDir() {
|
|
14
57
|
let base = "";
|
|
15
58
|
try {
|
|
16
59
|
base = homedir();
|
|
@@ -18,29 +61,78 @@ function defaultInboxDir() {
|
|
|
18
61
|
base = "";
|
|
19
62
|
}
|
|
20
63
|
if (!base) base = tmpdir();
|
|
21
|
-
return
|
|
64
|
+
return join2(base, ".shepherd", "declined");
|
|
22
65
|
}
|
|
23
|
-
function
|
|
24
|
-
let normalized =
|
|
66
|
+
function declinedFilePath(repoRoot, dir = defaultDeclinedDir()) {
|
|
67
|
+
let normalized = resolve2(repoRoot);
|
|
25
68
|
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
26
69
|
const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
27
|
-
return
|
|
70
|
+
return join2(dir, hash);
|
|
71
|
+
}
|
|
72
|
+
function isDeclined(repoRoot, dir = defaultDeclinedDir()) {
|
|
73
|
+
const file = declinedFilePath(repoRoot, dir);
|
|
74
|
+
let raw;
|
|
75
|
+
try {
|
|
76
|
+
raw = readFileSync2(file, "utf8");
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
return typeof parsed?.declinedAt === "string";
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/linkNudge.ts
|
|
89
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
90
|
+
var LINK_NUDGE_TEXT = '[Shepherd] This repo is not linked to a Shepherd workspace and has not been declined. Before writing files here, call the shepherd `link` tool with no argument: it auto-links if the user belongs to exactly one workspace, or lists workspaces \u2014 then ask the user "Coordinate this repo with Shepherd? Which workspace?" and call `link <workspace>` with their answer, or `decline` if they say no. Ask at most once per repo.';
|
|
91
|
+
function buildLinkNudge(cwd, toolName, deps = {}) {
|
|
92
|
+
try {
|
|
93
|
+
if (toolName !== void 0 && !WRITE_TOOLS.has(toolName)) return "";
|
|
94
|
+
const repoRoot = findRepoRoot(cwd);
|
|
95
|
+
if (repoRoot === null) return "";
|
|
96
|
+
if (readMarker(cwd) !== null) return "";
|
|
97
|
+
if (isDeclined(repoRoot, deps.declinedDir)) return "";
|
|
98
|
+
return LINK_NUDGE_TEXT;
|
|
99
|
+
} catch {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/inbox.ts
|
|
105
|
+
function defaultInboxDir() {
|
|
106
|
+
let base = "";
|
|
107
|
+
try {
|
|
108
|
+
base = homedir2();
|
|
109
|
+
} catch {
|
|
110
|
+
base = "";
|
|
111
|
+
}
|
|
112
|
+
if (!base) base = tmpdir2();
|
|
113
|
+
return join3(base, ".shepherd", "inbox");
|
|
114
|
+
}
|
|
115
|
+
function inboxFilePath(dir, cwd) {
|
|
116
|
+
let normalized = resolve3(cwd);
|
|
117
|
+
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
118
|
+
const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
119
|
+
return join3(dir, `${hash}.jsonl`);
|
|
28
120
|
}
|
|
29
121
|
function drainInbox(filePath) {
|
|
30
122
|
const tmp = `${filePath}.draining`;
|
|
31
123
|
let raw = "";
|
|
32
124
|
try {
|
|
33
|
-
if (
|
|
34
|
-
raw +=
|
|
35
|
-
|
|
125
|
+
if (existsSync3(tmp)) {
|
|
126
|
+
raw += readFileSync3(tmp, "utf8");
|
|
127
|
+
rmSync3(tmp, { force: true });
|
|
36
128
|
}
|
|
37
129
|
} catch {
|
|
38
130
|
}
|
|
39
131
|
try {
|
|
40
|
-
if (
|
|
132
|
+
if (existsSync3(filePath)) {
|
|
41
133
|
renameSync(filePath, tmp);
|
|
42
|
-
raw +=
|
|
43
|
-
|
|
134
|
+
raw += readFileSync3(tmp, "utf8");
|
|
135
|
+
rmSync3(tmp, { force: true });
|
|
44
136
|
}
|
|
45
137
|
} catch {
|
|
46
138
|
}
|
|
@@ -80,7 +172,7 @@ function shepherdInbox(pi) {
|
|
|
80
172
|
const dir = process.env["SHEPHERD_INBOX_DIR"] || defaultInboxDir();
|
|
81
173
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
82
174
|
const announcements = drainInbox(inboxFilePath(dir, cwd));
|
|
83
|
-
const content = formatInboxAnnouncements(announcements);
|
|
175
|
+
const content = [buildLinkNudge(cwd), formatInboxAnnouncements(announcements)].filter(Boolean).join("\n\n");
|
|
84
176
|
if (!content) return void 0;
|
|
85
177
|
return { message: { customType: "shepherd-inbox", content, display: true } };
|
|
86
178
|
} catch {
|
package/dist/inboxHook.js
CHANGED
|
@@ -1,18 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/inbox.ts
|
|
4
|
-
import { createHash } from "crypto";
|
|
4
|
+
import { createHash as createHash2 } from "crypto";
|
|
5
5
|
import {
|
|
6
6
|
appendFileSync,
|
|
7
|
-
mkdirSync,
|
|
8
|
-
readFileSync,
|
|
7
|
+
mkdirSync as mkdirSync2,
|
|
8
|
+
readFileSync as readFileSync3,
|
|
9
9
|
renameSync,
|
|
10
|
-
rmSync,
|
|
11
|
-
existsSync
|
|
10
|
+
rmSync as rmSync3,
|
|
11
|
+
existsSync as existsSync3
|
|
12
12
|
} from "fs";
|
|
13
|
+
import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
|
|
14
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
|
|
15
|
+
|
|
16
|
+
// src/marker.ts
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
var MARKER_FILENAME = ".shepherd";
|
|
20
|
+
function findRepoRoot(cwd) {
|
|
21
|
+
let dir = path.resolve(cwd);
|
|
22
|
+
for (; ; ) {
|
|
23
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
24
|
+
const parent = path.dirname(dir);
|
|
25
|
+
if (parent === dir) return null;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function markerPath(cwd) {
|
|
30
|
+
const root = findRepoRoot(cwd);
|
|
31
|
+
return root === null ? null : path.join(root, MARKER_FILENAME);
|
|
32
|
+
}
|
|
33
|
+
function readMarker(cwd = process.cwd()) {
|
|
34
|
+
const file = markerPath(cwd);
|
|
35
|
+
if (file === null) return null;
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = fs.readFileSync(file, "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.workspace === "string" && parsed.workspace.length > 0) {
|
|
45
|
+
return { workspace: parsed.workspace };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/declined.ts
|
|
54
|
+
import { createHash } from "crypto";
|
|
55
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
13
56
|
import { homedir, tmpdir } from "os";
|
|
14
|
-
import { dirname, join, resolve } from "path";
|
|
15
|
-
function
|
|
57
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
58
|
+
function defaultDeclinedDir() {
|
|
16
59
|
let base = "";
|
|
17
60
|
try {
|
|
18
61
|
base = homedir();
|
|
@@ -20,29 +63,78 @@ function defaultInboxDir() {
|
|
|
20
63
|
base = "";
|
|
21
64
|
}
|
|
22
65
|
if (!base) base = tmpdir();
|
|
23
|
-
return
|
|
66
|
+
return join2(base, ".shepherd", "declined");
|
|
24
67
|
}
|
|
25
|
-
function
|
|
26
|
-
let normalized =
|
|
68
|
+
function declinedFilePath(repoRoot, dir = defaultDeclinedDir()) {
|
|
69
|
+
let normalized = resolve2(repoRoot);
|
|
27
70
|
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
28
71
|
const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
29
|
-
return
|
|
72
|
+
return join2(dir, hash);
|
|
73
|
+
}
|
|
74
|
+
function isDeclined(repoRoot, dir = defaultDeclinedDir()) {
|
|
75
|
+
const file = declinedFilePath(repoRoot, dir);
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = readFileSync2(file, "utf8");
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
return typeof parsed?.declinedAt === "string";
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/linkNudge.ts
|
|
91
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
92
|
+
var LINK_NUDGE_TEXT = '[Shepherd] This repo is not linked to a Shepherd workspace and has not been declined. Before writing files here, call the shepherd `link` tool with no argument: it auto-links if the user belongs to exactly one workspace, or lists workspaces \u2014 then ask the user "Coordinate this repo with Shepherd? Which workspace?" and call `link <workspace>` with their answer, or `decline` if they say no. Ask at most once per repo.';
|
|
93
|
+
function buildLinkNudge(cwd, toolName, deps = {}) {
|
|
94
|
+
try {
|
|
95
|
+
if (toolName !== void 0 && !WRITE_TOOLS.has(toolName)) return "";
|
|
96
|
+
const repoRoot = findRepoRoot(cwd);
|
|
97
|
+
if (repoRoot === null) return "";
|
|
98
|
+
if (readMarker(cwd) !== null) return "";
|
|
99
|
+
if (isDeclined(repoRoot, deps.declinedDir)) return "";
|
|
100
|
+
return LINK_NUDGE_TEXT;
|
|
101
|
+
} catch {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/inbox.ts
|
|
107
|
+
function defaultInboxDir() {
|
|
108
|
+
let base = "";
|
|
109
|
+
try {
|
|
110
|
+
base = homedir2();
|
|
111
|
+
} catch {
|
|
112
|
+
base = "";
|
|
113
|
+
}
|
|
114
|
+
if (!base) base = tmpdir2();
|
|
115
|
+
return join3(base, ".shepherd", "inbox");
|
|
116
|
+
}
|
|
117
|
+
function inboxFilePath(dir, cwd) {
|
|
118
|
+
let normalized = resolve3(cwd);
|
|
119
|
+
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
120
|
+
const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
121
|
+
return join3(dir, `${hash}.jsonl`);
|
|
30
122
|
}
|
|
31
123
|
function drainInbox(filePath) {
|
|
32
124
|
const tmp = `${filePath}.draining`;
|
|
33
125
|
let raw = "";
|
|
34
126
|
try {
|
|
35
|
-
if (
|
|
36
|
-
raw +=
|
|
37
|
-
|
|
127
|
+
if (existsSync3(tmp)) {
|
|
128
|
+
raw += readFileSync3(tmp, "utf8");
|
|
129
|
+
rmSync3(tmp, { force: true });
|
|
38
130
|
}
|
|
39
131
|
} catch {
|
|
40
132
|
}
|
|
41
133
|
try {
|
|
42
|
-
if (
|
|
134
|
+
if (existsSync3(filePath)) {
|
|
43
135
|
renameSync(filePath, tmp);
|
|
44
|
-
raw +=
|
|
45
|
-
|
|
136
|
+
raw += readFileSync3(tmp, "utf8");
|
|
137
|
+
rmSync3(tmp, { force: true });
|
|
46
138
|
}
|
|
47
139
|
} catch {
|
|
48
140
|
}
|
|
@@ -74,8 +166,7 @@ function formatInboxAnnouncements(announcements) {
|
|
|
74
166
|
}
|
|
75
167
|
return lines.join("\n");
|
|
76
168
|
}
|
|
77
|
-
function buildHookOutput(rawStdin, inboxDir, drain = drainInbox) {
|
|
78
|
-
if (!inboxDir) return "";
|
|
169
|
+
function buildHookOutput(rawStdin, inboxDir, drain = drainInbox, nudge = buildLinkNudge) {
|
|
79
170
|
let input;
|
|
80
171
|
try {
|
|
81
172
|
input = JSON.parse(rawStdin);
|
|
@@ -83,13 +174,21 @@ function buildHookOutput(rawStdin, inboxDir, drain = drainInbox) {
|
|
|
83
174
|
return "";
|
|
84
175
|
}
|
|
85
176
|
if (!input || typeof input.cwd !== "string" || input.cwd.length === 0) return "";
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
177
|
+
const parts = [];
|
|
178
|
+
const nudgeText = nudge(
|
|
179
|
+
input.cwd,
|
|
180
|
+
typeof input.tool_name === "string" ? input.tool_name : void 0
|
|
181
|
+
);
|
|
182
|
+
if (nudgeText) parts.push(nudgeText);
|
|
183
|
+
if (inboxDir) {
|
|
184
|
+
const text = formatInboxAnnouncements(drain(inboxFilePath(inboxDir, input.cwd)));
|
|
185
|
+
if (text) parts.push(text);
|
|
186
|
+
}
|
|
187
|
+
if (parts.length === 0) return "";
|
|
89
188
|
return JSON.stringify({
|
|
90
189
|
hookSpecificOutput: {
|
|
91
190
|
hookEventName: input.hook_event_name || "PreToolUse",
|
|
92
|
-
additionalContext:
|
|
191
|
+
additionalContext: parts.join("\n\n")
|
|
93
192
|
}
|
|
94
193
|
});
|
|
95
194
|
}
|
package/dist/index.js
CHANGED
|
@@ -783,7 +783,10 @@ function detectHuman(cwd = process.cwd()) {
|
|
|
783
783
|
return null;
|
|
784
784
|
}
|
|
785
785
|
const name = runGit(cwd, ["config", "user.name"]);
|
|
786
|
-
if (name)
|
|
786
|
+
if (name) {
|
|
787
|
+
const local = name.includes("@") ? name.split("@")[0] : name;
|
|
788
|
+
if (local) return local;
|
|
789
|
+
}
|
|
787
790
|
const email = runGit(cwd, ["config", "user.email"]);
|
|
788
791
|
if (email) {
|
|
789
792
|
const local = email.split("@")[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@korso/shepherd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|