@jaggerxtrm/specialists 3.0.2 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -257
- package/bin/install.js +156 -266
- package/dist/index.js +2049 -262
- package/hooks/specialists-session-start.mjs +105 -0
- package/package.json +1 -1
- package/specialists/bug-hunt.specialist.yaml +53 -20
- package/specialists/codebase-explorer.specialist.yaml +43 -24
- package/specialists/feature-design.specialist.yaml +48 -29
- package/specialists/planner.specialist.yaml +87 -0
- package/specialists/specialist-author.specialist.yaml +56 -0
- package/specialists/sync-docs.specialist.yaml +53 -0
- package/specialists/xt-merge.specialist.yaml +78 -0
- package/hooks/beads-close-memory-prompt.mjs +0 -47
- package/hooks/beads-commit-gate.mjs +0 -58
- package/hooks/beads-edit-gate.mjs +0 -53
- package/hooks/beads-stop-gate.mjs +0 -52
- package/hooks/specialists-main-guard.mjs +0 -90
package/bin/install.js
CHANGED
|
@@ -1,319 +1,209 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Specialists Installer
|
|
3
|
-
// Usage: npx --package=@jaggerxtrm/specialists install
|
|
4
2
|
|
|
5
3
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
chmodSync,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
7
13
|
import { homedir } from 'node:os';
|
|
8
14
|
import { join } from 'node:path';
|
|
9
15
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const MCP_NAME = 'specialists';
|
|
17
|
-
const GITHUB_PKG = '@jaggerxtrm/specialists';
|
|
18
|
-
|
|
19
|
-
// Bundled specialists dir — resolved relative to this file (bin/../specialists/)
|
|
16
|
+
const CWD = process.cwd();
|
|
17
|
+
const CLAUDE_DIR = join(CWD, '.claude');
|
|
18
|
+
const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
|
|
19
|
+
const SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
|
|
20
|
+
const MCP_FILE = join(CWD, '.mcp.json');
|
|
21
|
+
const BUNDLED_HOOKS_DIR = new URL('../hooks', import.meta.url).pathname;
|
|
20
22
|
const BUNDLED_SPECIALISTS_DIR = new URL('../specialists', import.meta.url).pathname;
|
|
21
|
-
const
|
|
23
|
+
const USER_SPECIALISTS_DIR = join(homedir(), '.agents', 'specialists');
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
25
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
26
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
26
27
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
27
|
-
const bold
|
|
28
|
-
const red
|
|
28
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
29
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
29
30
|
|
|
30
31
|
function section(label) {
|
|
31
|
-
const line = '─'.repeat(Math.max(0,
|
|
32
|
+
const line = '─'.repeat(Math.max(0, 44 - label.length));
|
|
32
33
|
console.log(`\n${bold(`── ${label} ${line}`)}`);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function ok(label)
|
|
36
|
+
function ok(label) { console.log(` ${green('✓')} ${label}`); }
|
|
36
37
|
function skip(label) { console.log(` ${yellow('○')} ${label}`); }
|
|
37
38
|
function info(label) { console.log(` ${dim(label)}`); }
|
|
38
39
|
function fail(label) { console.log(` ${red('✗')} ${label}`); }
|
|
39
40
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
function run(cmd, args, options = {}) {
|
|
42
|
+
return spawnSync(cmd, args, {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
stdio: 'pipe',
|
|
45
|
+
...options,
|
|
46
|
+
});
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
49
|
+
function commandOk(cmd, args = ['--version']) {
|
|
50
|
+
const result = run(cmd, args);
|
|
51
|
+
return result.status === 0 && !result.error;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
function
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
info('Installing dolt (requires sudo)...');
|
|
57
|
-
const r = spawnSync(
|
|
58
|
-
'sudo', ['bash', '-c', 'curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash'],
|
|
59
|
-
{ stdio: 'inherit', encoding: 'utf8' }
|
|
60
|
-
);
|
|
61
|
-
if (r.status === 0) {
|
|
62
|
-
ok('dolt installed');
|
|
63
|
-
} else {
|
|
64
|
-
fail('dolt install failed — install manually:');
|
|
65
|
-
info(" sudo bash -c 'curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash'");
|
|
66
|
-
}
|
|
54
|
+
function loadJson(path, fallback) {
|
|
55
|
+
if (!existsSync(path)) return structuredClone(fallback);
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
58
|
+
} catch {
|
|
59
|
+
return structuredClone(fallback);
|
|
67
60
|
}
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
if (check.status === 0) return false;
|
|
73
|
-
|
|
74
|
-
npmInstallGlobal(GITHUB_PKG);
|
|
75
|
-
|
|
76
|
-
const r = spawnSync('claude', [
|
|
77
|
-
'mcp', 'add', '--scope', 'user', MCP_NAME, '--', MCP_NAME,
|
|
78
|
-
], { stdio: 'inherit', encoding: 'utf8' });
|
|
79
|
-
if (r.status !== 0) throw new Error('claude mcp add failed');
|
|
80
|
-
return true;
|
|
63
|
+
function saveJson(path, value) {
|
|
64
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
81
65
|
}
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const HOOK_ENTRY = {
|
|
87
|
-
matcher: 'Edit|Write|MultiEdit|NotebookEdit|Bash',
|
|
88
|
-
hooks: [{ type: 'command', command: HOOK_FILE }],
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const BEADS_EDIT_GATE_FILE = join(HOOKS_DIR, 'beads-edit-gate.mjs');
|
|
93
|
-
const BEADS_COMMIT_GATE_FILE = join(HOOKS_DIR, 'beads-commit-gate.mjs');
|
|
94
|
-
const BEADS_STOP_GATE_FILE = join(HOOKS_DIR, 'beads-stop-gate.mjs');
|
|
95
|
-
|
|
96
|
-
const BEADS_EDIT_GATE_ENTRY = {
|
|
97
|
-
matcher: 'Edit|Write|MultiEdit|NotebookEdit|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol',
|
|
98
|
-
hooks: [{ type: 'command', command: BEADS_EDIT_GATE_FILE, timeout: 10000 }],
|
|
99
|
-
};
|
|
100
|
-
const BEADS_COMMIT_GATE_ENTRY = {
|
|
101
|
-
matcher: 'Bash',
|
|
102
|
-
hooks: [{ type: 'command', command: BEADS_COMMIT_GATE_FILE, timeout: 10000 }],
|
|
103
|
-
};
|
|
104
|
-
const BEADS_STOP_GATE_ENTRY = {
|
|
105
|
-
hooks: [{ type: 'command', command: BEADS_STOP_GATE_FILE, timeout: 10000 }],
|
|
106
|
-
};
|
|
107
|
-
const BEADS_CLOSE_MEMORY_PROMPT_FILE = join(HOOKS_DIR, 'beads-close-memory-prompt.mjs');
|
|
108
|
-
const BEADS_CLOSE_MEMORY_PROMPT_ENTRY = {
|
|
109
|
-
matcher: 'Bash',
|
|
110
|
-
hooks: [{ type: 'command', command: BEADS_CLOSE_MEMORY_PROMPT_FILE, timeout: 10000 }],
|
|
111
|
-
};
|
|
112
|
-
const SPECIALISTS_COMPLETE_FILE = join(HOOKS_DIR, 'specialists-complete.mjs');
|
|
113
|
-
const SPECIALISTS_COMPLETE_ENTRY = {
|
|
114
|
-
hooks: [{ type: 'command', command: SPECIALISTS_COMPLETE_FILE, timeout: 5000 }],
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
function promptYN(question) {
|
|
118
|
-
if (!process.stdin.isTTY) return true; // non-interactive: default yes
|
|
119
|
-
process.stdout.write(`${question} [Y/n]: `);
|
|
120
|
-
const r = spawnSync('/bin/sh', ['-c', 'read ans; printf "%s" "$ans"'], {
|
|
121
|
-
stdio: ['inherit', 'pipe', 'inherit'],
|
|
122
|
-
encoding: 'utf8',
|
|
123
|
-
});
|
|
124
|
-
const ans = (r.stdout ?? '').trim().toLowerCase();
|
|
125
|
-
return ans === '' || ans === 'y' || ans === 'yes';
|
|
67
|
+
function sameFileContent(a, b) {
|
|
68
|
+
if (!existsSync(a) || !existsSync(b)) return false;
|
|
69
|
+
return readFileSync(a, 'utf8') === readFileSync(b, 'utf8');
|
|
126
70
|
}
|
|
127
71
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
72
|
+
const HOOKS = [
|
|
73
|
+
{
|
|
74
|
+
event: 'UserPromptSubmit',
|
|
75
|
+
file: 'specialists-complete.mjs',
|
|
76
|
+
timeout: 5000,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
event: 'SessionStart',
|
|
80
|
+
file: 'specialists-session-start.mjs',
|
|
81
|
+
timeout: 8000,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
function findHookCommands(settings, event, fileName) {
|
|
86
|
+
const entries = settings?.hooks?.[event];
|
|
87
|
+
if (!Array.isArray(entries)) return [];
|
|
88
|
+
return entries.flatMap((entry) =>
|
|
89
|
+
(entry.hooks ?? [])
|
|
90
|
+
.map((hook) => hook.command)
|
|
91
|
+
.filter((command) => typeof command === 'string' && command.includes(fileName)),
|
|
92
|
+
);
|
|
147
93
|
}
|
|
148
94
|
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
chmodSync(HOOK_FILE, 0o755);
|
|
155
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-edit-gate.mjs'), BEADS_EDIT_GATE_FILE);
|
|
156
|
-
chmodSync(BEADS_EDIT_GATE_FILE, 0o755);
|
|
157
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-commit-gate.mjs'), BEADS_COMMIT_GATE_FILE);
|
|
158
|
-
chmodSync(BEADS_COMMIT_GATE_FILE, 0o755);
|
|
159
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-stop-gate.mjs'), BEADS_STOP_GATE_FILE);
|
|
160
|
-
chmodSync(BEADS_STOP_GATE_FILE, 0o755);
|
|
161
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-close-memory-prompt.mjs'), BEADS_CLOSE_MEMORY_PROMPT_FILE);
|
|
162
|
-
chmodSync(BEADS_CLOSE_MEMORY_PROMPT_FILE, 0o755);
|
|
163
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'specialists-complete.mjs'), SPECIALISTS_COMPLETE_FILE);
|
|
164
|
-
chmodSync(SPECIALISTS_COMPLETE_FILE, 0o755);
|
|
95
|
+
function ensureHook(settings, hook) {
|
|
96
|
+
const dest = join(HOOKS_DIR, hook.file);
|
|
97
|
+
const source = join(BUNDLED_HOOKS_DIR, hook.file);
|
|
98
|
+
const existingCommands = findHookCommands(settings, hook.event, hook.file);
|
|
99
|
+
const externalOwner = existingCommands.find((command) => command !== dest);
|
|
165
100
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
101
|
+
if (externalOwner) {
|
|
102
|
+
skip(`${hook.file} already managed externally — deferring`);
|
|
103
|
+
info(`existing command: ${externalOwner}`);
|
|
104
|
+
return false;
|
|
169
105
|
}
|
|
170
106
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
);
|
|
182
|
-
settings.hooks.PreToolUse.push(HOOK_ENTRY);
|
|
183
|
-
settings.hooks.PreToolUse.push(BEADS_EDIT_GATE_ENTRY);
|
|
184
|
-
settings.hooks.PreToolUse.push(BEADS_COMMIT_GATE_ENTRY);
|
|
185
|
-
|
|
186
|
-
// PostToolUse — replace any existing beads-close-memory-prompt entry
|
|
187
|
-
if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
|
|
188
|
-
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(e =>
|
|
189
|
-
!e.hooks?.some(h => h.command?.includes('beads-close-memory-prompt'))
|
|
190
|
-
);
|
|
191
|
-
settings.hooks.PostToolUse.push(BEADS_CLOSE_MEMORY_PROMPT_ENTRY);
|
|
192
|
-
|
|
193
|
-
// Stop — replace any existing beads-stop-gate entry
|
|
194
|
-
if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
|
|
195
|
-
settings.hooks.Stop = settings.hooks.Stop.filter(e =>
|
|
196
|
-
!e.hooks?.some(h => h.command?.includes('beads-stop-gate'))
|
|
197
|
-
);
|
|
198
|
-
settings.hooks.Stop.push(BEADS_STOP_GATE_ENTRY);
|
|
107
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
108
|
+
const changed = !sameFileContent(source, dest);
|
|
109
|
+
if (changed) {
|
|
110
|
+
copyFileSync(source, dest);
|
|
111
|
+
chmodSync(dest, 0o755);
|
|
112
|
+
ok(`${hook.file} installed in .claude/hooks/`);
|
|
113
|
+
} else {
|
|
114
|
+
skip(`${hook.file} already up to date`);
|
|
115
|
+
}
|
|
199
116
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
settings.hooks.
|
|
203
|
-
!
|
|
117
|
+
settings.hooks ??= {};
|
|
118
|
+
settings.hooks[hook.event] ??= [];
|
|
119
|
+
settings.hooks[hook.event] = settings.hooks[hook.event].filter(
|
|
120
|
+
(entry) => !(entry.hooks ?? []).some((h) => h.command === dest),
|
|
204
121
|
);
|
|
205
|
-
settings.hooks.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
122
|
+
settings.hooks[hook.event].push({
|
|
123
|
+
hooks: [{ type: 'command', command: dest, timeout: hook.timeout }],
|
|
124
|
+
});
|
|
125
|
+
ok(`${hook.file} registered for ${hook.event}`);
|
|
126
|
+
return true;
|
|
209
127
|
}
|
|
210
128
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
129
|
+
function installBundledSpecialists() {
|
|
130
|
+
if (!existsSync(BUNDLED_SPECIALISTS_DIR)) {
|
|
131
|
+
skip('bundled specialists dir not found — skipping');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
mkdirSync(USER_SPECIALISTS_DIR, { recursive: true });
|
|
135
|
+
const files = readdirSync(BUNDLED_SPECIALISTS_DIR).filter(f => f.endsWith('.specialist.yaml'));
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const source = join(BUNDLED_SPECIALISTS_DIR, file);
|
|
138
|
+
const dest = join(USER_SPECIALISTS_DIR, file);
|
|
139
|
+
if (sameFileContent(source, dest)) {
|
|
140
|
+
skip(`${file} already up to date`);
|
|
141
|
+
} else {
|
|
142
|
+
copyFileSync(source, dest);
|
|
143
|
+
ok(`${file} installed in ~/.agents/specialists/`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
222
146
|
}
|
|
223
147
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
148
|
+
function ensureMcpRegistration() {
|
|
149
|
+
const mcp = loadJson(MCP_FILE, { mcpServers: {} });
|
|
150
|
+
mcp.mcpServers ??= {};
|
|
151
|
+
const existing = mcp.mcpServers.specialists;
|
|
152
|
+
const desired = { command: 'specialists', args: [] };
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
existing &&
|
|
156
|
+
existing.command === desired.command &&
|
|
157
|
+
Array.isArray(existing.args) &&
|
|
158
|
+
existing.args.length === 0
|
|
159
|
+
) {
|
|
160
|
+
skip('.mcp.json already registers specialists');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
233
163
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
skip('dolt already installed');
|
|
238
|
-
} else {
|
|
239
|
-
installDolt();
|
|
164
|
+
mcp.mcpServers.specialists = desired;
|
|
165
|
+
saveJson(MCP_FILE, mcp);
|
|
166
|
+
ok('registered specialists in .mcp.json');
|
|
240
167
|
}
|
|
241
168
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const registered = registerMCP();
|
|
245
|
-
registered
|
|
246
|
-
? ok(`MCP '${MCP_NAME}' registered at user scope`)
|
|
247
|
-
: skip(`MCP '${MCP_NAME}' already registered`);
|
|
248
|
-
|
|
249
|
-
// 5. Scaffold + copy built-in specialists
|
|
250
|
-
section('Specialists');
|
|
251
|
-
mkdirSync(SPECIALISTS_DIR, { recursive: true });
|
|
169
|
+
console.log(`\n${bold('Specialists installer')}`);
|
|
170
|
+
console.log(dim('Project scope: prerequisite check, bundled specialists, hooks, MCP registration'));
|
|
252
171
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
: []
|
|
172
|
+
section('Prerequisite check');
|
|
173
|
+
const prereqs = [
|
|
174
|
+
{ name: 'pi', ok: commandOk('pi', ['--version']), required: true, help: 'Install pi first.' },
|
|
175
|
+
{ name: 'bd', ok: commandOk('bd', ['--version']), required: true, help: 'Install beads (bd) first.' },
|
|
176
|
+
{ name: 'xt', ok: commandOk('xt', ['--version']), required: true, help: 'xtrm-tools is required for hooks and workflow integration.' },
|
|
177
|
+
];
|
|
256
178
|
|
|
257
|
-
let
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (existsSync(dest)) {
|
|
262
|
-
skipped++;
|
|
179
|
+
let prereqFailed = false;
|
|
180
|
+
for (const prereq of prereqs) {
|
|
181
|
+
if (prereq.ok) {
|
|
182
|
+
ok(`${prereq.name} available`);
|
|
263
183
|
} else {
|
|
264
|
-
|
|
265
|
-
|
|
184
|
+
prereqFailed = prereqFailed || prereq.required;
|
|
185
|
+
fail(`${prereq.name} not found`);
|
|
186
|
+
info(prereq.help);
|
|
266
187
|
}
|
|
267
188
|
}
|
|
268
189
|
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
190
|
+
if (prereqFailed) {
|
|
191
|
+
console.log(`\n${red('Install aborted: required prerequisites are missing.')}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
273
194
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const
|
|
195
|
+
section('Specialists hooks');
|
|
196
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
197
|
+
const settings = loadJson(SETTINGS_FILE, {});
|
|
198
|
+
for (const hook of HOOKS) ensureHook(settings, hook);
|
|
199
|
+
saveJson(SETTINGS_FILE, settings);
|
|
200
|
+
ok(`updated ${SETTINGS_FILE}`);
|
|
278
201
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
ok('hooks installed → ~/.claude/hooks/');
|
|
282
|
-
} else if (drift.length === 0) {
|
|
283
|
-
skip('hooks up to date');
|
|
284
|
-
} else {
|
|
285
|
-
const label = (h) => h.missing ? red('missing') : yellow('updated');
|
|
286
|
-
console.log(` ${yellow('○')} ${drift.length} of 6 hook(s) have changes:`);
|
|
287
|
-
for (const h of drift) info(` ${h.name} ${label(h)}`);
|
|
288
|
-
console.log();
|
|
289
|
-
const confirmed = promptYN(' Update hooks?');
|
|
290
|
-
if (confirmed) {
|
|
291
|
-
installHook();
|
|
292
|
-
ok('hooks updated');
|
|
293
|
-
} else {
|
|
294
|
-
skip('hooks update skipped');
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
info('main-guard: blocks file edits and direct master pushes (enforces PR workflow)');
|
|
298
|
-
info('beads-edit-gate: requires in_progress bead before editing files');
|
|
299
|
-
info('beads-commit-gate: requires issues closed before git commit');
|
|
300
|
-
info('beads-stop-gate: requires issues closed before session end');
|
|
301
|
-
info('beads-close-memory-prompt: nudges knowledge capture after bd close');
|
|
302
|
-
info('specialists-complete: injects completion banners for background jobs');
|
|
202
|
+
section('Bundled specialists');
|
|
203
|
+
installBundledSpecialists();
|
|
303
204
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (isInstalled('pi')) {
|
|
307
|
-
const r = spawnSync('pi', ['--list-models'], { encoding: 'utf8' });
|
|
308
|
-
r.status === 0
|
|
309
|
-
? ok('pi has at least one active provider')
|
|
310
|
-
: skip('No active provider — run pi config to set one up');
|
|
311
|
-
}
|
|
205
|
+
section('MCP registration');
|
|
206
|
+
ensureMcpRegistration();
|
|
312
207
|
|
|
313
|
-
|
|
314
|
-
console.log('
|
|
315
|
-
console.log('\n' + bold(' Next steps:'));
|
|
316
|
-
console.log(` 1. ${bold('Configure pi:')} run ${yellow('pi')} then ${yellow('pi config')} to enable model providers`);
|
|
317
|
-
console.log(` 2. ${bold('Restart Claude Code')} to load the MCP and hooks`);
|
|
318
|
-
console.log(` 3. ${bold('Customise specialists:')} edit files in ${yellow('~/.agents/specialists/')}`);
|
|
319
|
-
console.log(` 4. ${bold('Update later:')} re-run this installer (existing specialists preserved)\n`);
|
|
208
|
+
console.log(`\n${bold(green('Done!'))}`);
|
|
209
|
+
console.log(` ${dim('Hooks are project-local in .claude/, and MCP is project-local in .mcp.json.')}`);
|