@codename_inc/spectre 3.7.0 → 4.0.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 +3 -4
- package/package.json +3 -2
- package/plugins/spectre/.claude-plugin/plugin.json +1 -1
- package/plugins/spectre/bin/spectre-register +5 -0
- package/plugins/spectre/hooks/hooks.json +3 -14
- package/plugins/spectre/hooks/scripts/bootstrap.mjs +98 -0
- package/plugins/spectre/hooks/scripts/handoff-resume.mjs +404 -0
- package/plugins/spectre/hooks/scripts/lib.mjs +82 -0
- package/plugins/spectre/hooks/scripts/load-knowledge.mjs +189 -0
- package/plugins/spectre/hooks/scripts/register_learning.mjs +264 -0
- package/plugins/spectre/hooks/scripts/{test_bootstrap.cjs → test_bootstrap.mjs} +12 -7
- package/plugins/spectre/hooks/scripts/{test_handoff-resume.cjs → test_handoff-resume.mjs} +13 -11
- package/plugins/spectre/hooks/scripts/{test_load-knowledge.cjs → test_load-knowledge.mjs} +103 -22
- package/plugins/spectre/hooks/scripts/test_register-learning.mjs +335 -0
- package/plugins/spectre/skills/apply/SKILL.md +87 -0
- package/plugins/spectre/{commands/architecture_review.md → skills/architecture_review/SKILL.md} +9 -0
- package/plugins/spectre/{commands/clean.md → skills/clean/SKILL.md} +9 -0
- package/plugins/spectre/{commands/code_review.md → skills/code_review/SKILL.md} +9 -0
- package/plugins/spectre/{commands/create_plan.md → skills/create_plan/SKILL.md} +9 -0
- package/plugins/spectre/{commands/create_tasks.md → skills/create_tasks/SKILL.md} +9 -0
- package/plugins/spectre/{commands/create_test_guide.md → skills/create_test_guide/SKILL.md} +9 -0
- package/plugins/spectre/{commands/evaluate.md → skills/evaluate/SKILL.md} +11 -2
- package/plugins/spectre/{commands/execute.md → skills/execute/SKILL.md} +12 -3
- package/plugins/spectre/{commands/fix.md → skills/fix/SKILL.md} +9 -0
- package/plugins/spectre/{commands/forget.md → skills/forget/SKILL.md} +9 -0
- package/plugins/spectre/skills/{spectre-guide → guide}/SKILL.md +2 -1
- package/plugins/spectre/{commands/handoff.md → skills/handoff/SKILL.md} +9 -0
- package/plugins/spectre/{commands/kickoff.md → skills/kickoff/SKILL.md} +9 -0
- package/plugins/spectre/skills/{spectre-learn → learn}/SKILL.md +19 -59
- package/plugins/spectre/skills/learn/references/recall-template.md +34 -0
- package/plugins/spectre/{commands/plan.md → skills/plan/SKILL.md} +66 -25
- package/plugins/spectre/{commands/plan_review.md → skills/plan_review/SKILL.md} +9 -0
- package/plugins/spectre/{commands/quick_dev.md → skills/quick_dev/SKILL.md} +9 -0
- package/plugins/spectre/{commands/rebase.md → skills/rebase/SKILL.md} +9 -0
- package/plugins/spectre/skills/recall/SKILL.md +17 -0
- package/plugins/spectre/{commands/research.md → skills/research/SKILL.md} +9 -0
- package/plugins/spectre/{commands/scope.md → skills/scope/SKILL.md} +9 -0
- package/plugins/spectre/{commands/ship.md → skills/ship/SKILL.md} +9 -0
- package/plugins/spectre/{commands/sweep.md → skills/sweep/SKILL.md} +9 -0
- package/plugins/spectre/skills/tdd/SKILL.md +111 -0
- package/plugins/spectre/{commands/test.md → skills/test/SKILL.md} +9 -0
- package/plugins/spectre/{commands/ux_spec.md → skills/ux_spec/SKILL.md} +9 -0
- package/plugins/spectre/{commands/validate.md → skills/validate/SKILL.md} +9 -0
- package/plugins/spectre-codex/agents/analyst.toml +117 -0
- package/plugins/spectre-codex/agents/dev.toml +65 -0
- package/plugins/spectre-codex/agents/finder.toml +101 -0
- package/plugins/spectre-codex/agents/patterns.toml +203 -0
- package/plugins/spectre-codex/agents/reviewer.toml +123 -0
- package/plugins/spectre-codex/agents/sync.toml +146 -0
- package/plugins/spectre-codex/agents/tester.toml +205 -0
- package/plugins/spectre-codex/agents/web-research.toml +104 -0
- package/plugins/spectre-codex/hooks/hooks.json +23 -0
- package/plugins/{spectre/hooks/scripts/bootstrap.cjs → spectre-codex/hooks/scripts/bootstrap.mjs} +15 -16
- package/plugins/{spectre/hooks/scripts/handoff-resume.cjs → spectre-codex/hooks/scripts/handoff-resume.mjs} +21 -27
- package/plugins/{spectre/hooks/scripts/lib.cjs → spectre-codex/hooks/scripts/lib.mjs} +3 -4
- package/plugins/spectre-codex/hooks/scripts/load-knowledge.mjs +189 -0
- package/plugins/spectre-codex/hooks/scripts/register_learning.mjs +264 -0
- package/plugins/spectre-codex/skills/apply/SKILL.md +87 -0
- package/plugins/spectre-codex/skills/architecture_review/SKILL.md +129 -0
- package/plugins/spectre-codex/skills/clean/SKILL.md +322 -0
- package/plugins/spectre-codex/skills/code_review/SKILL.md +417 -0
- package/plugins/spectre-codex/skills/create_plan/SKILL.md +126 -0
- package/plugins/spectre-codex/skills/create_tasks/SKILL.md +383 -0
- package/plugins/spectre-codex/skills/create_test_guide/SKILL.md +129 -0
- package/plugins/spectre-codex/skills/evaluate/SKILL.md +59 -0
- package/plugins/spectre-codex/skills/execute/SKILL.md +96 -0
- package/plugins/spectre-codex/skills/fix/SKILL.md +70 -0
- package/plugins/spectre-codex/skills/forget/SKILL.md +67 -0
- package/plugins/spectre-codex/skills/guide/SKILL.md +359 -0
- package/plugins/spectre-codex/skills/handoff/SKILL.md +170 -0
- package/plugins/spectre-codex/skills/kickoff/SKILL.md +124 -0
- package/plugins/spectre-codex/skills/learn/SKILL.md +595 -0
- package/plugins/{spectre/skills/spectre-learn → spectre-codex/skills/learn}/references/recall-template.md +4 -1
- package/plugins/spectre-codex/skills/plan/SKILL.md +211 -0
- package/plugins/spectre-codex/skills/plan_review/SKILL.md +42 -0
- package/plugins/spectre-codex/skills/quick_dev/SKILL.md +110 -0
- package/plugins/spectre-codex/skills/rebase/SKILL.md +82 -0
- package/plugins/spectre-codex/skills/recall/SKILL.md +17 -0
- package/plugins/spectre-codex/skills/research/SKILL.md +168 -0
- package/plugins/spectre-codex/skills/scope/SKILL.md +128 -0
- package/plugins/spectre-codex/skills/ship/SKILL.md +181 -0
- package/plugins/spectre-codex/skills/sweep/SKILL.md +91 -0
- package/plugins/{spectre/skills/spectre-tdd → spectre-codex/skills/tdd}/SKILL.md +1 -1
- package/plugins/spectre-codex/skills/test/SKILL.md +389 -0
- package/plugins/spectre-codex/skills/ux_spec/SKILL.md +100 -0
- package/plugins/spectre-codex/skills/validate/SKILL.md +352 -0
- package/src/config.test.js +6 -5
- package/src/install.test.js +100 -11
- package/src/lib/config.js +107 -54
- package/src/lib/constants.js +17 -23
- package/src/lib/doctor.js +19 -22
- package/src/lib/install.js +98 -313
- package/src/lib/knowledge.js +7 -37
- package/src/lib/paths.js +0 -12
- package/src/pack.test.js +87 -0
- package/plugins/spectre/commands/learn.md +0 -15
- package/plugins/spectre/commands/recall.md +0 -5
- package/plugins/spectre/hooks/scripts/load-knowledge.cjs +0 -120
- package/plugins/spectre/hooks/scripts/precompact-warning.cjs +0 -19
- package/plugins/spectre/hooks/scripts/register_learning.cjs +0 -144
- package/plugins/spectre/hooks/scripts/test_register-learning.cjs +0 -146
- package/plugins/spectre/skills/spectre-apply/SKILL.md +0 -189
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* Tests for load-knowledge.
|
|
4
|
+
* Tests for load-knowledge.mjs SessionStart hook.
|
|
6
5
|
*
|
|
7
|
-
* Run with: node --test plugins/spectre/hooks/scripts/test_load-knowledge.
|
|
6
|
+
* Run with: node --test plugins/spectre/hooks/scripts/test_load-knowledge.mjs
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
import { describe, it } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
const SCRIPT_PATH = path.join(__dirname, 'load-knowledge.mjs');
|
|
18
20
|
|
|
19
21
|
function createTmpDir() {
|
|
20
22
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'spectre-lk-'));
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function createApplySkill(pluginDir) {
|
|
24
|
-
const skillPath = path.join(pluginDir, 'skills', '
|
|
26
|
+
const skillPath = path.join(pluginDir, 'skills', 'apply', 'SKILL.md');
|
|
25
27
|
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
26
28
|
fs.writeFileSync(skillPath,
|
|
27
|
-
'---\nname:
|
|
28
|
-
'##
|
|
29
|
+
'---\nname: apply\n---\n\n# Apply Knowledge\n\n' +
|
|
30
|
+
'## How to Find Skills\n\nScan available skills.\n\n' +
|
|
31
|
+
'## Workflow\n\nDo things.\n'
|
|
32
|
+
);
|
|
33
|
+
return skillPath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createCodexApplySkill(codexHome) {
|
|
37
|
+
const skillPath = path.join(codexHome, 'skills', 'apply', 'SKILL.md');
|
|
38
|
+
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
39
|
+
fs.writeFileSync(skillPath,
|
|
40
|
+
'---\nname: apply\n---\n\n# Apply Knowledge\n\n' +
|
|
41
|
+
'## How to Find Skills\n\nScan available skills.\n\n' +
|
|
29
42
|
'## Workflow\n\nDo things.\n'
|
|
30
43
|
);
|
|
31
44
|
return skillPath;
|
|
@@ -41,6 +54,13 @@ function createRegistry(projectDir, entries, subdir) {
|
|
|
41
54
|
return registryPath;
|
|
42
55
|
}
|
|
43
56
|
|
|
57
|
+
function createManifest(projectDir) {
|
|
58
|
+
const manifestPath = path.join(projectDir, '.spectre', 'manifest.json');
|
|
59
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
60
|
+
fs.writeFileSync(manifestPath, '{"version":1}\n');
|
|
61
|
+
return manifestPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
function runHook(opts) {
|
|
45
65
|
opts = opts || {};
|
|
46
66
|
const env = Object.assign({}, process.env);
|
|
@@ -73,12 +93,15 @@ function cleanup(dir) {
|
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
describe('LoadKnowledge - Core behavior', () => {
|
|
76
|
-
it('
|
|
96
|
+
it('falls back to script-relative plugin root when no plugin root env is set', () => {
|
|
77
97
|
const tmp = createTmpDir();
|
|
78
98
|
try {
|
|
79
99
|
const result = runHook({ pluginRoot: '', cwd: tmp });
|
|
80
100
|
assert.equal(result.exitCode, 0);
|
|
81
|
-
|
|
101
|
+
const output = JSON.parse(result.stdout.trim());
|
|
102
|
+
assert.match(output.systemMessage, /spectre: ready/);
|
|
103
|
+
assert.deepEqual(output.hookSpecificOutput, { hookEventName: 'SessionStart' });
|
|
104
|
+
assert.ok(!fs.existsSync(path.join(tmp, 'AGENTS.override.md')));
|
|
82
105
|
} finally {
|
|
83
106
|
cleanup(tmp);
|
|
84
107
|
}
|
|
@@ -100,6 +123,7 @@ describe('LoadKnowledge - Core behavior', () => {
|
|
|
100
123
|
const pluginDir = path.join(tmp, 'plugin');
|
|
101
124
|
fs.mkdirSync(pluginDir);
|
|
102
125
|
createApplySkill(pluginDir);
|
|
126
|
+
createManifest(tmp);
|
|
103
127
|
|
|
104
128
|
try {
|
|
105
129
|
const result = runHook({ pluginRoot: pluginDir, cwd: tmp });
|
|
@@ -107,6 +131,30 @@ describe('LoadKnowledge - Core behavior', () => {
|
|
|
107
131
|
const output = JSON.parse(result.stdout);
|
|
108
132
|
assert.ok(output.systemMessage.includes('ready'));
|
|
109
133
|
assert.ok(output.systemMessage.includes('/spectre:learn'));
|
|
134
|
+
assert.deepEqual(output.hookSpecificOutput, { hookEventName: 'SessionStart' });
|
|
135
|
+
const overrideContent = fs.readFileSync(path.join(tmp, 'AGENTS.override.md'), 'utf8');
|
|
136
|
+
assert.match(overrideContent, /<!-- spectre-knowledge:start -->/);
|
|
137
|
+
assert.match(overrideContent, /# Apply Knowledge/);
|
|
138
|
+
assert.doesNotMatch(overrideContent, /additionalContext/);
|
|
139
|
+
} finally {
|
|
140
|
+
cleanup(tmp);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('resolves apply skill from Codex home when hooks are under spectre runtime', () => {
|
|
145
|
+
const tmp = createTmpDir();
|
|
146
|
+
const codexHome = path.join(tmp, 'codex-home');
|
|
147
|
+
const runtimeRoot = path.join(codexHome, 'spectre');
|
|
148
|
+
fs.mkdirSync(path.join(runtimeRoot, 'hooks', 'scripts'), { recursive: true });
|
|
149
|
+
createCodexApplySkill(codexHome);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const result = runHook({ pluginRoot: runtimeRoot, cwd: tmp });
|
|
153
|
+
assert.equal(result.exitCode, 0);
|
|
154
|
+
const output = JSON.parse(result.stdout);
|
|
155
|
+
assert.ok(output.systemMessage.includes('ready'));
|
|
156
|
+
assert.deepEqual(output.hookSpecificOutput, { hookEventName: 'SessionStart' });
|
|
157
|
+
assert.ok(!fs.existsSync(path.join(tmp, 'AGENTS.override.md')));
|
|
110
158
|
} finally {
|
|
111
159
|
cleanup(tmp);
|
|
112
160
|
}
|
|
@@ -136,6 +184,10 @@ describe('LoadKnowledge - Core behavior', () => {
|
|
|
136
184
|
assert.equal(result.exitCode, 0);
|
|
137
185
|
const output = JSON.parse(result.stdout);
|
|
138
186
|
assert.ok(output.systemMessage.includes('2 knowledge skills'));
|
|
187
|
+
assert.deepEqual(output.hookSpecificOutput, { hookEventName: 'SessionStart' });
|
|
188
|
+
const overrideContent = fs.readFileSync(path.join(projectDir, 'AGENTS.override.md'), 'utf8');
|
|
189
|
+
assert.match(overrideContent, /# Apply Knowledge/);
|
|
190
|
+
assert.doesNotMatch(overrideContent, /feature-auth\|feature\|auth, login\|Auth system knowledge/);
|
|
139
191
|
} finally {
|
|
140
192
|
cleanup(tmp);
|
|
141
193
|
}
|
|
@@ -214,25 +266,50 @@ describe('LoadKnowledge - Core behavior', () => {
|
|
|
214
266
|
}
|
|
215
267
|
});
|
|
216
268
|
|
|
217
|
-
it('
|
|
269
|
+
it('writes spectre-knowledge block into AGENTS.override.md', () => {
|
|
270
|
+
const tmp = createTmpDir();
|
|
271
|
+
const pluginDir = path.join(tmp, 'plugin');
|
|
272
|
+
fs.mkdirSync(pluginDir);
|
|
273
|
+
createApplySkill(pluginDir);
|
|
274
|
+
createManifest(tmp);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const result = runHook({ pluginRoot: pluginDir, cwd: tmp });
|
|
278
|
+
assert.equal(result.exitCode, 0);
|
|
279
|
+
const output = JSON.parse(result.stdout);
|
|
280
|
+
assert.deepEqual(output.hookSpecificOutput, { hookEventName: 'SessionStart' });
|
|
281
|
+
const overrideContent = fs.readFileSync(path.join(tmp, 'AGENTS.override.md'), 'utf8');
|
|
282
|
+
assert.match(overrideContent, /<!-- spectre-knowledge:start -->/);
|
|
283
|
+
assert.match(overrideContent, /## SPECTRE Knowledge Context/);
|
|
284
|
+
assert.match(overrideContent, /# Apply Knowledge/);
|
|
285
|
+
assert.match(overrideContent, /<!-- spectre-knowledge:end -->/);
|
|
286
|
+
} finally {
|
|
287
|
+
cleanup(tmp);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('does not append to unrelated AGENTS.override.md outside a Spectre project', () => {
|
|
218
292
|
const tmp = createTmpDir();
|
|
219
293
|
const pluginDir = path.join(tmp, 'plugin');
|
|
220
294
|
fs.mkdirSync(pluginDir);
|
|
221
295
|
createApplySkill(pluginDir);
|
|
296
|
+
fs.writeFileSync(path.join(tmp, 'AGENTS.override.md'), 'User-owned override content.\n');
|
|
222
297
|
|
|
223
298
|
try {
|
|
224
299
|
const result = runHook({ pluginRoot: pluginDir, cwd: tmp });
|
|
225
300
|
assert.equal(result.exitCode, 0);
|
|
226
301
|
const output = JSON.parse(result.stdout);
|
|
227
|
-
|
|
228
|
-
assert.
|
|
229
|
-
|
|
302
|
+
assert.deepEqual(output.hookSpecificOutput, { hookEventName: 'SessionStart' });
|
|
303
|
+
assert.equal(
|
|
304
|
+
fs.readFileSync(path.join(tmp, 'AGENTS.override.md'), 'utf8'),
|
|
305
|
+
'User-owned override content.\n'
|
|
306
|
+
);
|
|
230
307
|
} finally {
|
|
231
308
|
cleanup(tmp);
|
|
232
309
|
}
|
|
233
310
|
});
|
|
234
311
|
|
|
235
|
-
it('registry content embedded in context', () => {
|
|
312
|
+
it('registry content is NOT embedded in context', () => {
|
|
236
313
|
const tmp = createTmpDir();
|
|
237
314
|
const pluginDir = path.join(tmp, 'plugin');
|
|
238
315
|
fs.mkdirSync(pluginDir);
|
|
@@ -248,8 +325,12 @@ describe('LoadKnowledge - Core behavior', () => {
|
|
|
248
325
|
});
|
|
249
326
|
assert.equal(result.exitCode, 0);
|
|
250
327
|
const output = JSON.parse(result.stdout);
|
|
251
|
-
|
|
252
|
-
|
|
328
|
+
assert.deepEqual(output.hookSpecificOutput, { hookEventName: 'SessionStart' });
|
|
329
|
+
const overrideContent = fs.readFileSync(path.join(tmp, 'AGENTS.override.md'), 'utf8');
|
|
330
|
+
assert.ok(!overrideContent.includes('embedded-skill|feature|embed|Embedded skill'),
|
|
331
|
+
'Registry entries should not be embedded in context');
|
|
332
|
+
assert.ok(overrideContent.includes('# Apply Knowledge'),
|
|
333
|
+
'Scaffold content should still be present');
|
|
253
334
|
} finally {
|
|
254
335
|
cleanup(tmp);
|
|
255
336
|
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for register_learning.mjs
|
|
5
|
+
*
|
|
6
|
+
* Run with: node --test plugins/spectre/hooks/scripts/test_register-learning.mjs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
const SCRIPT_PATH = path.join(__dirname, 'register_learning.mjs');
|
|
20
|
+
|
|
21
|
+
function createTmpDir() {
|
|
22
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'spectre-rl-'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cleanup(dir) {
|
|
26
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runScript(args, opts) {
|
|
30
|
+
opts = opts || {};
|
|
31
|
+
const env = Object.assign({}, process.env);
|
|
32
|
+
if (opts.pluginRoot) {
|
|
33
|
+
env.CLAUDE_PLUGIN_ROOT = opts.pluginRoot;
|
|
34
|
+
} else {
|
|
35
|
+
delete env.CLAUDE_PLUGIN_ROOT;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const stdout = execFileSync(process.execPath, [SCRIPT_PATH, ...args], {
|
|
40
|
+
env,
|
|
41
|
+
timeout: 10000,
|
|
42
|
+
encoding: 'utf8'
|
|
43
|
+
});
|
|
44
|
+
return { stdout, exitCode: 0 };
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { stdout: err.stdout || '', stderr: err.stderr || '', exitCode: err.status };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('register_learning', () => {
|
|
51
|
+
it('creates new registry with entry', () => {
|
|
52
|
+
const tmp = createTmpDir();
|
|
53
|
+
try {
|
|
54
|
+
const result = runScript([
|
|
55
|
+
'--project-root', tmp,
|
|
56
|
+
'--skill-name', 'feature-auth',
|
|
57
|
+
'--category', 'feature',
|
|
58
|
+
'--triggers', 'auth, login',
|
|
59
|
+
'--description', 'Use when working on authentication'
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
assert.equal(result.exitCode, 0);
|
|
63
|
+
assert.ok(result.stdout.includes('Registered:'));
|
|
64
|
+
|
|
65
|
+
const registryPath = path.join(tmp, '.claude', 'skills', 'spectre-recall', 'references', 'registry.toon');
|
|
66
|
+
assert.ok(fs.existsSync(registryPath));
|
|
67
|
+
|
|
68
|
+
const content = fs.readFileSync(registryPath, 'utf8');
|
|
69
|
+
assert.ok(content.includes('# SPECTRE Knowledge Registry'));
|
|
70
|
+
assert.ok(content.includes('feature-auth|feature|auth, login|Use when working on authentication'));
|
|
71
|
+
} finally {
|
|
72
|
+
cleanup(tmp);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('updates existing entry by skill name', () => {
|
|
77
|
+
const tmp = createTmpDir();
|
|
78
|
+
try {
|
|
79
|
+
// First registration
|
|
80
|
+
runScript([
|
|
81
|
+
'--project-root', tmp,
|
|
82
|
+
'--skill-name', 'feature-auth',
|
|
83
|
+
'--category', 'feature',
|
|
84
|
+
'--triggers', 'auth',
|
|
85
|
+
'--description', 'Old description'
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Second registration with same skill name
|
|
89
|
+
runScript([
|
|
90
|
+
'--project-root', tmp,
|
|
91
|
+
'--skill-name', 'feature-auth',
|
|
92
|
+
'--category', 'feature',
|
|
93
|
+
'--triggers', 'auth, login, oauth',
|
|
94
|
+
'--description', 'Updated description'
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const registryPath = path.join(tmp, '.claude', 'skills', 'spectre-recall', 'references', 'registry.toon');
|
|
98
|
+
const content = fs.readFileSync(registryPath, 'utf8');
|
|
99
|
+
|
|
100
|
+
// Should have the updated entry, not the old one
|
|
101
|
+
assert.ok(content.includes('Updated description'));
|
|
102
|
+
assert.ok(!content.includes('Old description'));
|
|
103
|
+
|
|
104
|
+
// Should only have one entry for feature-auth
|
|
105
|
+
const entries = content.split('\n').filter(l => l.startsWith('feature-auth|'));
|
|
106
|
+
assert.equal(entries.length, 1);
|
|
107
|
+
} finally {
|
|
108
|
+
cleanup(tmp);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('generates recall skill with template', () => {
|
|
113
|
+
const tmp = createTmpDir();
|
|
114
|
+
const pluginRoot = path.join(tmp, 'plugin');
|
|
115
|
+
|
|
116
|
+
// Create template
|
|
117
|
+
const templateDir = path.join(pluginRoot, 'skills', 'learn', 'references');
|
|
118
|
+
fs.mkdirSync(templateDir, { recursive: true });
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
path.join(templateDir, 'recall-template.md'),
|
|
121
|
+
'# Recall Skill\n\nRegistry:\n{{REGISTRY}}\n\nEnd.\n'
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
runScript([
|
|
126
|
+
'--project-root', tmp,
|
|
127
|
+
'--skill-name', 'feature-test',
|
|
128
|
+
'--category', 'feature',
|
|
129
|
+
'--triggers', 'test',
|
|
130
|
+
'--description', 'Test skill'
|
|
131
|
+
], { pluginRoot });
|
|
132
|
+
|
|
133
|
+
const skillPath = path.join(tmp, '.claude', 'skills', 'spectre-recall', 'SKILL.md');
|
|
134
|
+
assert.ok(fs.existsSync(skillPath));
|
|
135
|
+
|
|
136
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
137
|
+
assert.ok(content.includes('# Recall Skill'));
|
|
138
|
+
assert.ok(content.includes('feature-test|feature|test|Test skill'));
|
|
139
|
+
} finally {
|
|
140
|
+
cleanup(tmp);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('resolves recall template from Codex home when hooks are under spectre runtime', () => {
|
|
145
|
+
const tmp = createTmpDir();
|
|
146
|
+
const codexHome = path.join(tmp, 'codex-home');
|
|
147
|
+
const runtimeRoot = path.join(codexHome, 'spectre');
|
|
148
|
+
|
|
149
|
+
const templateDir = path.join(codexHome, 'skills', 'learn', 'references');
|
|
150
|
+
fs.mkdirSync(templateDir, { recursive: true });
|
|
151
|
+
fs.writeFileSync(
|
|
152
|
+
path.join(templateDir, 'recall-template.md'),
|
|
153
|
+
'# Codex Recall Skill\n\nRegistry:\n{{REGISTRY}}\n\nEnd.\n'
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
runScript([
|
|
158
|
+
'--project-root', tmp,
|
|
159
|
+
'--skill-name', 'feature-codex',
|
|
160
|
+
'--category', 'feature',
|
|
161
|
+
'--triggers', 'codex',
|
|
162
|
+
'--description', 'Codex skill'
|
|
163
|
+
], { pluginRoot: runtimeRoot });
|
|
164
|
+
|
|
165
|
+
const skillPath = path.join(tmp, '.claude', 'skills', 'spectre-recall', 'SKILL.md');
|
|
166
|
+
assert.ok(fs.existsSync(skillPath));
|
|
167
|
+
|
|
168
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
169
|
+
assert.ok(content.includes('# Codex Recall Skill'));
|
|
170
|
+
assert.ok(content.includes('feature-codex|feature|codex|Codex skill'));
|
|
171
|
+
} finally {
|
|
172
|
+
cleanup(tmp);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('fails with missing required arguments', () => {
|
|
177
|
+
const result = runScript(['--project-root', '/tmp/fake']);
|
|
178
|
+
assert.notEqual(result.exitCode, 0);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('register_learning - trigger migration', () => {
|
|
183
|
+
it('injects TRIGGER into single-line description', () => {
|
|
184
|
+
const tmp = createTmpDir();
|
|
185
|
+
try {
|
|
186
|
+
const skillDir = path.join(tmp, '.claude', 'skills', 'feature-auth');
|
|
187
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
188
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'),
|
|
189
|
+
'---\nname: feature-auth\ndescription: Use when working on authentication\nuser-invocable: false\n---\n\n# Auth Knowledge\n'
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
runScript([
|
|
193
|
+
'--project-root', tmp,
|
|
194
|
+
'--skill-name', 'feature-auth',
|
|
195
|
+
'--category', 'feature',
|
|
196
|
+
'--triggers', 'auth, login',
|
|
197
|
+
'--description', 'Use when working on authentication'
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
const content = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
|
|
201
|
+
assert.ok(content.includes('description: Use when working on authentication TRIGGER when: auth, login'),
|
|
202
|
+
'Should be single-line with description and trigger');
|
|
203
|
+
assert.ok(!content.includes('description: |'), 'Should NOT use block scalar');
|
|
204
|
+
assert.ok(content.includes('# Auth Knowledge'), 'Body should be preserved');
|
|
205
|
+
} finally {
|
|
206
|
+
cleanup(tmp);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('injects TRIGGER into block scalar description', () => {
|
|
211
|
+
const tmp = createTmpDir();
|
|
212
|
+
try {
|
|
213
|
+
const skillDir = path.join(tmp, '.claude', 'skills', 'feature-release');
|
|
214
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
215
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'),
|
|
216
|
+
'---\nname: feature-release\ndescription: |\n Use when releasing the plugin or bumping versions.\nuser-invocable: true\n---\n\n# Release\n'
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
runScript([
|
|
220
|
+
'--project-root', tmp,
|
|
221
|
+
'--skill-name', 'feature-release',
|
|
222
|
+
'--category', 'procedures',
|
|
223
|
+
'--triggers', 'release, version',
|
|
224
|
+
'--description', 'Use when releasing the plugin or bumping versions.'
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const content = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
|
|
228
|
+
assert.ok(content.includes('description: Use when releasing the plugin or bumping versions. TRIGGER when: release, version'),
|
|
229
|
+
'Should collapse block scalar to single-line with trigger');
|
|
230
|
+
assert.ok(!content.includes('description: |'), 'Block scalar should be removed');
|
|
231
|
+
assert.ok(content.includes('# Release'), 'Body should be preserved');
|
|
232
|
+
} finally {
|
|
233
|
+
cleanup(tmp);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('trigger injection is idempotent', () => {
|
|
238
|
+
const tmp = createTmpDir();
|
|
239
|
+
try {
|
|
240
|
+
const skillDir = path.join(tmp, '.claude', 'skills', 'feature-auth');
|
|
241
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
242
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'),
|
|
243
|
+
'---\nname: feature-auth\ndescription: Use when working on authentication\nuser-invocable: false\n---\n\n# Auth\n'
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const args = [
|
|
247
|
+
'--project-root', tmp,
|
|
248
|
+
'--skill-name', 'feature-auth',
|
|
249
|
+
'--category', 'feature',
|
|
250
|
+
'--triggers', 'auth, login',
|
|
251
|
+
'--description', 'Use when working on authentication'
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
runScript(args);
|
|
255
|
+
runScript(args);
|
|
256
|
+
|
|
257
|
+
const content = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
|
|
258
|
+
const matches = content.match(/TRIGGER when:/g);
|
|
259
|
+
assert.equal(matches.length, 1, 'Should have exactly one TRIGGER line');
|
|
260
|
+
} finally {
|
|
261
|
+
cleanup(tmp);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('skips missing skill files gracefully', () => {
|
|
266
|
+
const tmp = createTmpDir();
|
|
267
|
+
try {
|
|
268
|
+
// Register a skill with no SKILL.md on disk
|
|
269
|
+
const result = runScript([
|
|
270
|
+
'--project-root', tmp,
|
|
271
|
+
'--skill-name', 'feature-missing',
|
|
272
|
+
'--category', 'feature',
|
|
273
|
+
'--triggers', 'missing',
|
|
274
|
+
'--description', 'Nonexistent skill'
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
assert.equal(result.exitCode, 0, 'Should not error on missing skill files');
|
|
278
|
+
} finally {
|
|
279
|
+
cleanup(tmp);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('migrates all registry entries on any registration', () => {
|
|
284
|
+
const tmp = createTmpDir();
|
|
285
|
+
try {
|
|
286
|
+
// Create two existing skills without triggers
|
|
287
|
+
for (const name of ['feature-alpha', 'feature-beta']) {
|
|
288
|
+
const dir = path.join(tmp, '.claude', 'skills', name);
|
|
289
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
290
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'),
|
|
291
|
+
`---\nname: ${name}\ndescription: Use when working on ${name}\n---\n\n# ${name}\n`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Seed registry with alpha
|
|
296
|
+
const registryDir = path.join(tmp, '.claude', 'skills', 'spectre-recall', 'references');
|
|
297
|
+
fs.mkdirSync(registryDir, { recursive: true });
|
|
298
|
+
fs.writeFileSync(path.join(registryDir, 'registry.toon'),
|
|
299
|
+
'# SPECTRE Knowledge Registry\n# Format: skill-name|category|triggers|description\n\n' +
|
|
300
|
+
'feature-alpha|feature|alpha, first|Use when working on feature-alpha\n' +
|
|
301
|
+
'feature-beta|feature|beta, second|Use when working on feature-beta\n'
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Register a third skill — should trigger migration of alpha and beta too
|
|
305
|
+
const gammaDir = path.join(tmp, '.claude', 'skills', 'feature-gamma');
|
|
306
|
+
fs.mkdirSync(gammaDir, { recursive: true });
|
|
307
|
+
fs.writeFileSync(path.join(gammaDir, 'SKILL.md'),
|
|
308
|
+
'---\nname: feature-gamma\ndescription: Use when working on gamma\n---\n\n# Gamma\n'
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
runScript([
|
|
312
|
+
'--project-root', tmp,
|
|
313
|
+
'--skill-name', 'feature-gamma',
|
|
314
|
+
'--category', 'feature',
|
|
315
|
+
'--triggers', 'gamma, third',
|
|
316
|
+
'--description', 'Use when working on gamma'
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
// All three should now have triggers
|
|
320
|
+
for (const [name, triggers] of [
|
|
321
|
+
['feature-alpha', 'alpha, first'],
|
|
322
|
+
['feature-beta', 'beta, second'],
|
|
323
|
+
['feature-gamma', 'gamma, third']
|
|
324
|
+
]) {
|
|
325
|
+
const content = fs.readFileSync(
|
|
326
|
+
path.join(tmp, '.claude', 'skills', name, 'SKILL.md'), 'utf8'
|
|
327
|
+
);
|
|
328
|
+
assert.ok(content.includes(`TRIGGER when: ${triggers}`),
|
|
329
|
+
`${name} should have triggers: ${triggers}`);
|
|
330
|
+
}
|
|
331
|
+
} finally {
|
|
332
|
+
cleanup(tmp);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apply
|
|
3
|
+
description: Use when starting implementation, debugging, or feature work on a project with captured knowledge.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Apply Knowledge
|
|
8
|
+
|
|
9
|
+
## Why This Exists
|
|
10
|
+
|
|
11
|
+
SPECTRE captures knowledge — patterns, gotchas, decisions, and feature context — across sessions. Loading it first prevents repeated mistakes, maintains consistency, and tells you WHERE to look before searching.
|
|
12
|
+
|
|
13
|
+
## The Rule
|
|
14
|
+
|
|
15
|
+
<CRITICAL>
|
|
16
|
+
If ANY skill's triggers or description match your current task, you MUST load the skill FIRST using the Skill tool.
|
|
17
|
+
|
|
18
|
+
**Trigger matches are sufficient.** If a trigger word appears in the user's request, load the skill — you don't need the description to also match. Don't reframe the user's request to avoid triggers.
|
|
19
|
+
|
|
20
|
+
DO NOT search the codebase or dispatch agents BEFORE loading relevant knowledge — even if you think you already have enough context. Partial context from Read results or error messages is not a substitute for the complete picture in the skill.
|
|
21
|
+
|
|
22
|
+
**When a command explicitly tells you to load a skill, you MUST call the Skill tool to load it.** Do not improvise the workflow based on what you think the skill does. The skill defines a specific workflow with precise steps, output formats, file locations, and integrations. Your improvised version will be wrong.
|
|
23
|
+
|
|
24
|
+
**You are also responsible for keeping knowledge current.** After completing significant work, proactively check whether loaded skills need updating and whether new skills should be captured via `Skill(learn)`. Do NOT wait for the user to ask.
|
|
25
|
+
</CRITICAL>
|
|
26
|
+
|
|
27
|
+
## Path Convention
|
|
28
|
+
|
|
29
|
+
`{{project_root}}` refers to **the current working directory** (`$PWD`). NEVER traverse up to a parent git root or main worktree. If in a git worktree, `{{project_root}}` is the worktree — not the main repository.
|
|
30
|
+
|
|
31
|
+
## How to Find Skills
|
|
32
|
+
|
|
33
|
+
Your available skills are listed in context at the start of every session. Each skill's description includes `TRIGGER when:` keywords.
|
|
34
|
+
|
|
35
|
+
Scan the skill list for trigger matches against your current task. Load matches with `Skill({skill-name})`.
|
|
36
|
+
|
|
37
|
+
The registry at `{{project_root}}/.claude/skills/spectre-recall/references/registry.toon` remains the source of truth for registration, but you do NOT need to read it for discovery — the skill list already has what you need.
|
|
38
|
+
|
|
39
|
+
## Workflow
|
|
40
|
+
|
|
41
|
+
1. **Scan available skills** in your context — match trigger keywords or descriptions to your task
|
|
42
|
+
2. **For each match**, load the skill: `Skill({skill-name})`
|
|
43
|
+
3. **Apply the knowledge** — use it to guide your approach, know where to look
|
|
44
|
+
4. **Then proceed** — now you can search/implement with context
|
|
45
|
+
5. **No matches?** Proceed normally
|
|
46
|
+
|
|
47
|
+
## Keeping Knowledge Current
|
|
48
|
+
|
|
49
|
+
After completing work, check:
|
|
50
|
+
|
|
51
|
+
1. **Loaded skill now outdated?** → Update it immediately
|
|
52
|
+
2. **Discovered something capture-worthy?** (gotcha, pattern, decision) → Capture via `Skill(learn)`
|
|
53
|
+
3. **Changed key files, flows, or architecture?** → Update the relevant feature skill
|
|
54
|
+
4. **Made a decision with non-obvious rationale?** → Capture before the session ends
|
|
55
|
+
|
|
56
|
+
Stale knowledge is worse than no knowledge — it actively misleads future sessions. Update skills before moving to the next task.
|
|
57
|
+
|
|
58
|
+
## Red Flags
|
|
59
|
+
|
|
60
|
+
| Thought | Reality |
|
|
61
|
+
|---------|---------|
|
|
62
|
+
| "Let me search the codebase first" | Knowledge tells you WHERE to search. Load the skill first. |
|
|
63
|
+
| "I already have context from a Read/system message" | Partial context is dangerous. The skill has the full picture — including related changes you don't know about yet. |
|
|
64
|
+
| "This seems simple / the edit is surgical" | Simple tasks benefit from captured patterns. Skills reveal if similar changes are needed elsewhere. |
|
|
65
|
+
| "I understand the intent, I don't need the skill" | Understanding intent ≠ knowing the implementation. Skills define WHERE files go, WHAT format to use, and HOW to register outputs. |
|
|
66
|
+
| "The command says to load a skill, but I can handle it directly" | When a command tells you to load a skill, that is a mandatory Skill tool call, not a suggestion. |
|
|
67
|
+
| "I'll update the skill later" | Later never comes. Update before moving to the next task. |
|
|
68
|
+
|
|
69
|
+
## Failure Pattern
|
|
70
|
+
|
|
71
|
+
**Common scenario**: Task matches triggers (e.g., "spectre", "release", "learn"), but agent rationalizes skipping the skill load.
|
|
72
|
+
|
|
73
|
+
**Rationalizations that fail**: "I already have the file contents", "The error points to the exact path", "This is really about X not Y", "I can figure this out faster by searching."
|
|
74
|
+
|
|
75
|
+
**What skills provide that context doesn't**: Architectural relationships, related files you don't know about, exact workflows with registration steps, correct output paths. Partial context from Read results is not a substitute.
|
|
76
|
+
|
|
77
|
+
**The cost**: Extra tool calls, wrong output locations, missed registration steps, inconsistent changes.
|
|
78
|
+
|
|
79
|
+
## Example
|
|
80
|
+
|
|
81
|
+
User: "How does /spectre work?"
|
|
82
|
+
|
|
83
|
+
Skill list shows: `feature-spectre-plugin` with trigger `spectre`
|
|
84
|
+
|
|
85
|
+
Action: `Skill(feature-spectre-plugin)`
|
|
86
|
+
|
|
87
|
+
Then: Use the key files and patterns from that knowledge to guide your work.
|
package/plugins/spectre/{commands/architecture_review.md → skills/architecture_review/SKILL.md}
RENAMED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
+
name: architecture_review
|
|
2
3
|
description: 👻 | Conduct principal architecture review
|
|
4
|
+
user-invocable: true
|
|
3
5
|
---
|
|
4
6
|
|
|
7
|
+
# architecture_review
|
|
8
|
+
|
|
9
|
+
## Input Handling
|
|
10
|
+
|
|
11
|
+
Treat the current command arguments as this workflow's input. When invoked from a slash command, use the forwarded `$ARGUMENTS` value.
|
|
12
|
+
|
|
13
|
+
|
|
5
14
|
# architecture_review: Technical review of completed features
|
|
6
15
|
|
|
7
16
|
## Description
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
+
name: clean
|
|
2
3
|
description: 👻 | Complete cleanup flow - clean, inspect, lint, test - primary agent
|
|
4
|
+
user-invocable: true
|
|
3
5
|
---
|
|
6
|
+
|
|
7
|
+
# clean
|
|
8
|
+
|
|
9
|
+
## Input Handling
|
|
10
|
+
|
|
11
|
+
Treat the current command arguments as this workflow's input. When invoked from a slash command, use the forwarded `$ARGUMENTS` value.
|
|
12
|
+
|
|
4
13
|
# clean: Analyze recent changes for dead code and artifacts from failed branches
|
|
5
14
|
|
|
6
15
|
## Description
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
+
name: code_review
|
|
2
3
|
description: 👻 | Independent LLM Code Review - subagent
|
|
4
|
+
user-invocable: true
|
|
3
5
|
---
|
|
6
|
+
|
|
7
|
+
# code_review
|
|
8
|
+
|
|
9
|
+
## Input Handling
|
|
10
|
+
|
|
11
|
+
Treat the current command arguments as this workflow's input. When invoked from a slash command, use the forwarded `$ARGUMENTS` value.
|
|
12
|
+
|
|
4
13
|
# code_review: Comprehensive Code Analysis
|
|
5
14
|
|
|
6
15
|
## Description
|