@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.
Files changed (102) hide show
  1. package/README.md +3 -4
  2. package/package.json +3 -2
  3. package/plugins/spectre/.claude-plugin/plugin.json +1 -1
  4. package/plugins/spectre/bin/spectre-register +5 -0
  5. package/plugins/spectre/hooks/hooks.json +3 -14
  6. package/plugins/spectre/hooks/scripts/bootstrap.mjs +98 -0
  7. package/plugins/spectre/hooks/scripts/handoff-resume.mjs +404 -0
  8. package/plugins/spectre/hooks/scripts/lib.mjs +82 -0
  9. package/plugins/spectre/hooks/scripts/load-knowledge.mjs +189 -0
  10. package/plugins/spectre/hooks/scripts/register_learning.mjs +264 -0
  11. package/plugins/spectre/hooks/scripts/{test_bootstrap.cjs → test_bootstrap.mjs} +12 -7
  12. package/plugins/spectre/hooks/scripts/{test_handoff-resume.cjs → test_handoff-resume.mjs} +13 -11
  13. package/plugins/spectre/hooks/scripts/{test_load-knowledge.cjs → test_load-knowledge.mjs} +103 -22
  14. package/plugins/spectre/hooks/scripts/test_register-learning.mjs +335 -0
  15. package/plugins/spectre/skills/apply/SKILL.md +87 -0
  16. package/plugins/spectre/{commands/architecture_review.md → skills/architecture_review/SKILL.md} +9 -0
  17. package/plugins/spectre/{commands/clean.md → skills/clean/SKILL.md} +9 -0
  18. package/plugins/spectre/{commands/code_review.md → skills/code_review/SKILL.md} +9 -0
  19. package/plugins/spectre/{commands/create_plan.md → skills/create_plan/SKILL.md} +9 -0
  20. package/plugins/spectre/{commands/create_tasks.md → skills/create_tasks/SKILL.md} +9 -0
  21. package/plugins/spectre/{commands/create_test_guide.md → skills/create_test_guide/SKILL.md} +9 -0
  22. package/plugins/spectre/{commands/evaluate.md → skills/evaluate/SKILL.md} +11 -2
  23. package/plugins/spectre/{commands/execute.md → skills/execute/SKILL.md} +12 -3
  24. package/plugins/spectre/{commands/fix.md → skills/fix/SKILL.md} +9 -0
  25. package/plugins/spectre/{commands/forget.md → skills/forget/SKILL.md} +9 -0
  26. package/plugins/spectre/skills/{spectre-guide → guide}/SKILL.md +2 -1
  27. package/plugins/spectre/{commands/handoff.md → skills/handoff/SKILL.md} +9 -0
  28. package/plugins/spectre/{commands/kickoff.md → skills/kickoff/SKILL.md} +9 -0
  29. package/plugins/spectre/skills/{spectre-learn → learn}/SKILL.md +19 -59
  30. package/plugins/spectre/skills/learn/references/recall-template.md +34 -0
  31. package/plugins/spectre/{commands/plan.md → skills/plan/SKILL.md} +66 -25
  32. package/plugins/spectre/{commands/plan_review.md → skills/plan_review/SKILL.md} +9 -0
  33. package/plugins/spectre/{commands/quick_dev.md → skills/quick_dev/SKILL.md} +9 -0
  34. package/plugins/spectre/{commands/rebase.md → skills/rebase/SKILL.md} +9 -0
  35. package/plugins/spectre/skills/recall/SKILL.md +17 -0
  36. package/plugins/spectre/{commands/research.md → skills/research/SKILL.md} +9 -0
  37. package/plugins/spectre/{commands/scope.md → skills/scope/SKILL.md} +9 -0
  38. package/plugins/spectre/{commands/ship.md → skills/ship/SKILL.md} +9 -0
  39. package/plugins/spectre/{commands/sweep.md → skills/sweep/SKILL.md} +9 -0
  40. package/plugins/spectre/skills/tdd/SKILL.md +111 -0
  41. package/plugins/spectre/{commands/test.md → skills/test/SKILL.md} +9 -0
  42. package/plugins/spectre/{commands/ux_spec.md → skills/ux_spec/SKILL.md} +9 -0
  43. package/plugins/spectre/{commands/validate.md → skills/validate/SKILL.md} +9 -0
  44. package/plugins/spectre-codex/agents/analyst.toml +117 -0
  45. package/plugins/spectre-codex/agents/dev.toml +65 -0
  46. package/plugins/spectre-codex/agents/finder.toml +101 -0
  47. package/plugins/spectre-codex/agents/patterns.toml +203 -0
  48. package/plugins/spectre-codex/agents/reviewer.toml +123 -0
  49. package/plugins/spectre-codex/agents/sync.toml +146 -0
  50. package/plugins/spectre-codex/agents/tester.toml +205 -0
  51. package/plugins/spectre-codex/agents/web-research.toml +104 -0
  52. package/plugins/spectre-codex/hooks/hooks.json +23 -0
  53. package/plugins/{spectre/hooks/scripts/bootstrap.cjs → spectre-codex/hooks/scripts/bootstrap.mjs} +15 -16
  54. package/plugins/{spectre/hooks/scripts/handoff-resume.cjs → spectre-codex/hooks/scripts/handoff-resume.mjs} +21 -27
  55. package/plugins/{spectre/hooks/scripts/lib.cjs → spectre-codex/hooks/scripts/lib.mjs} +3 -4
  56. package/plugins/spectre-codex/hooks/scripts/load-knowledge.mjs +189 -0
  57. package/plugins/spectre-codex/hooks/scripts/register_learning.mjs +264 -0
  58. package/plugins/spectre-codex/skills/apply/SKILL.md +87 -0
  59. package/plugins/spectre-codex/skills/architecture_review/SKILL.md +129 -0
  60. package/plugins/spectre-codex/skills/clean/SKILL.md +322 -0
  61. package/plugins/spectre-codex/skills/code_review/SKILL.md +417 -0
  62. package/plugins/spectre-codex/skills/create_plan/SKILL.md +126 -0
  63. package/plugins/spectre-codex/skills/create_tasks/SKILL.md +383 -0
  64. package/plugins/spectre-codex/skills/create_test_guide/SKILL.md +129 -0
  65. package/plugins/spectre-codex/skills/evaluate/SKILL.md +59 -0
  66. package/plugins/spectre-codex/skills/execute/SKILL.md +96 -0
  67. package/plugins/spectre-codex/skills/fix/SKILL.md +70 -0
  68. package/plugins/spectre-codex/skills/forget/SKILL.md +67 -0
  69. package/plugins/spectre-codex/skills/guide/SKILL.md +359 -0
  70. package/plugins/spectre-codex/skills/handoff/SKILL.md +170 -0
  71. package/plugins/spectre-codex/skills/kickoff/SKILL.md +124 -0
  72. package/plugins/spectre-codex/skills/learn/SKILL.md +595 -0
  73. package/plugins/{spectre/skills/spectre-learn → spectre-codex/skills/learn}/references/recall-template.md +4 -1
  74. package/plugins/spectre-codex/skills/plan/SKILL.md +211 -0
  75. package/plugins/spectre-codex/skills/plan_review/SKILL.md +42 -0
  76. package/plugins/spectre-codex/skills/quick_dev/SKILL.md +110 -0
  77. package/plugins/spectre-codex/skills/rebase/SKILL.md +82 -0
  78. package/plugins/spectre-codex/skills/recall/SKILL.md +17 -0
  79. package/plugins/spectre-codex/skills/research/SKILL.md +168 -0
  80. package/plugins/spectre-codex/skills/scope/SKILL.md +128 -0
  81. package/plugins/spectre-codex/skills/ship/SKILL.md +181 -0
  82. package/plugins/spectre-codex/skills/sweep/SKILL.md +91 -0
  83. package/plugins/{spectre/skills/spectre-tdd → spectre-codex/skills/tdd}/SKILL.md +1 -1
  84. package/plugins/spectre-codex/skills/test/SKILL.md +389 -0
  85. package/plugins/spectre-codex/skills/ux_spec/SKILL.md +100 -0
  86. package/plugins/spectre-codex/skills/validate/SKILL.md +352 -0
  87. package/src/config.test.js +6 -5
  88. package/src/install.test.js +100 -11
  89. package/src/lib/config.js +107 -54
  90. package/src/lib/constants.js +17 -23
  91. package/src/lib/doctor.js +19 -22
  92. package/src/lib/install.js +98 -313
  93. package/src/lib/knowledge.js +7 -37
  94. package/src/lib/paths.js +0 -12
  95. package/src/pack.test.js +87 -0
  96. package/plugins/spectre/commands/learn.md +0 -15
  97. package/plugins/spectre/commands/recall.md +0 -5
  98. package/plugins/spectre/hooks/scripts/load-knowledge.cjs +0 -120
  99. package/plugins/spectre/hooks/scripts/precompact-warning.cjs +0 -19
  100. package/plugins/spectre/hooks/scripts/register_learning.cjs +0 -144
  101. package/plugins/spectre/hooks/scripts/test_register-learning.cjs +0 -146
  102. 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.cjs SessionStart hook.
4
+ * Tests for load-knowledge.mjs SessionStart hook.
6
5
  *
7
- * Run with: node --test plugins/spectre/hooks/scripts/test_load-knowledge.cjs
6
+ * Run with: node --test plugins/spectre/hooks/scripts/test_load-knowledge.mjs
8
7
  */
9
8
 
10
- const { describe, it } = require('node:test');
11
- const assert = require('node:assert/strict');
12
- const fs = require('fs');
13
- const path = require('path');
14
- const os = require('os');
15
- const { execFileSync } = require('child_process');
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 SCRIPT_PATH = path.join(__dirname, 'load-knowledge.cjs');
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', 'spectre-apply', 'SKILL.md');
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: spectre-apply\n---\n\n# Apply Knowledge\n\n' +
28
- '## Registry Location\n\nThe registry is at somewhere\n\n' +
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('exits silently when no plugin root', () => {
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
- assert.equal(result.stdout.trim(), '');
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('output contains spectre-knowledge tag', () => {
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
- const context = output.hookSpecificOutput.additionalContext;
228
- assert.ok(context.startsWith('<spectre-knowledge>'));
229
- assert.ok(context.endsWith('</spectre-knowledge>'));
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
- const context = output.hookSpecificOutput.additionalContext;
252
- assert.ok(context.includes('embedded-skill|feature|embed|Embedded skill'));
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.
@@ -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