@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,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  /**
5
- * handoff-resume.cjs
4
+ * handoff-resume.mjs
6
5
  *
7
6
  * SessionStart hook that injects context from the last /spectre:handoff.
8
7
  * Consolidates the previous session-resume-hook.sh + format-resume-context.py.
@@ -15,17 +14,25 @@
15
14
  * - --bg-copy-refs: Copy plugin references (fork #1)
16
15
  */
17
16
 
18
- const fs = require('fs');
19
- const path = require('path');
20
- const { fork } = require('child_process');
21
- const { readStdinWithTimeout, getGitBranch } = require('./lib.cjs');
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fork } from 'node:child_process';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { readStdinWithTimeout, getGitBranch } from './lib.mjs';
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = path.dirname(__filename);
25
+
26
+ function getPluginRoot() {
27
+ return process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..', '..');
28
+ }
22
29
 
23
30
  // ──────────────────────────────────────────────────────────────────
24
31
  // Plugin reference copying (background fork #1)
25
32
  // ──────────────────────────────────────────────────────────────────
26
33
 
27
34
  function copyPluginReferences() {
28
- const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
35
+ const pluginRoot = getPluginRoot();
29
36
  if (!pluginRoot) return;
30
37
 
31
38
  const referencesSrc = path.join(pluginRoot, 'references');
@@ -198,14 +205,7 @@ function formatContext(data, opts) {
198
205
  const checkboxTree = (beadsAvailable && tasks.length) ? buildCheckboxTree(tasks) : '';
199
206
 
200
207
  // User-visible notice
201
- const asciiBanner = [
202
- '',
203
- '\u2591\u2588\u2580\u2580\u2591\u2588\u2580\u2588\u2591\u2588\u2580\u2580\u2591\u2588\u2580\u2580\u2591\u2580\u2588\u2580\u2591\u2588\u2580\u2584\u2591\u2588\u2580\u2580',
204
- '\u2591\u2580\u2580\u2588\u2591\u2588\u2580\u2580\u2591\u2588\u2580\u2580\u2591\u2588\u2591\u2591\u2591\u2591\u2588\u2591\u2591\u2588\u2580\u2584\u2591\u2588\u2580\u2580',
205
- '\u2591\u2580\u2580\u2580\u2591\u2580\u2591\u2591\u2591\u2580\u2580\u2580\u2591\u2580\u2580\u2580\u2591\u2591\u2580\u2591\u2591\u2580\u2591\u2580\u2591\u2580\u2580\u2580'
206
- ].join('\n');
207
-
208
- const noticeLines = [asciiBanner];
208
+ const noticeLines = ['\ud83d\udc7b SPECTRE'];
209
209
  noticeLines.push(`\n\ud83d\udd04 Session Resumed: ${taskName} | Branch: ${branchName}`);
210
210
 
211
211
  if (goal) {
@@ -353,12 +353,7 @@ async function main() {
353
353
 
354
354
  if (!latestHandoff) {
355
355
  // No session to resume - show welcome banner with tips
356
- const asciiBanner = [
357
- '',
358
- '\u2591\u2588\u2580\u2580\u2591\u2588\u2580\u2588\u2591\u2588\u2580\u2580\u2591\u2588\u2580\u2580\u2591\u2580\u2588\u2580\u2591\u2588\u2580\u2584\u2591\u2588\u2580\u2580',
359
- '\u2591\u2580\u2580\u2588\u2591\u2588\u2580\u2580\u2591\u2588\u2580\u2580\u2591\u2588\u2591\u2591\u2591\u2591\u2588\u2591\u2591\u2588\u2580\u2584\u2591\u2588\u2580\u2580',
360
- '\u2591\u2580\u2580\u2580\u2591\u2580\u2591\u2591\u2591\u2580\u2580\u2580\u2591\u2580\u2580\u2580\u2591\u2591\u2580\u2591\u2591\u2580\u2591\u2580\u2591\u2580\u2580\u2580'
361
- ].join('\n');
356
+ const banner = '\ud83d\udc7b SPECTRE';
362
357
  const tips = [
363
358
  '',
364
359
  'Getting Started with SPECTRE:',
@@ -371,7 +366,7 @@ async function main() {
371
366
  ].join('\n');
372
367
 
373
368
  const welcome = {
374
- systemMessage: asciiBanner + '\n' + tips
369
+ systemMessage: banner + '\n' + tips
375
370
  };
376
371
  process.stdout.write(JSON.stringify(welcome) + '\n');
377
372
  process.exit(0);
@@ -402,9 +397,8 @@ async function main() {
402
397
  process.exit(0);
403
398
  }
404
399
 
405
- // Export for testing
406
- if (typeof module !== 'undefined') {
407
- module.exports = { copyPluginReferences, formatContext, buildCheckboxTree };
408
- }
400
+ export { copyPluginReferences, formatContext, buildCheckboxTree };
409
401
 
410
- main();
402
+ if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fs.realpathSync(__filename)) {
403
+ main();
404
+ }
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  /**
5
- * lib.cjs
4
+ * lib.mjs
6
5
  *
7
6
  * Shared utilities for spectre hook scripts.
8
7
  */
9
8
 
10
- const { execSync } = require('child_process');
9
+ import { execSync } from 'node:child_process';
11
10
 
12
11
  const STDIN_TIMEOUT = 2000; // milliseconds
13
12
 
@@ -80,4 +79,4 @@ function getGitBranch(cwd) {
80
79
  }
81
80
  }
82
81
 
83
- module.exports = { readStdinWithTimeout, getGitBranch, STDIN_TIMEOUT };
82
+ export { readStdinWithTimeout, getGitBranch, STDIN_TIMEOUT };
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * load-knowledge.mjs
5
+ *
6
+ * SessionStart hook that refreshes the managed SPECTRE knowledge block in
7
+ * AGENTS.override.md and returns a short visible status line.
8
+ *
9
+ * Reads:
10
+ * - Apply skill from plugin: skills/apply/SKILL.md
11
+ * - Registry from project: .agents/skills/spectre-recall/references/registry.toon
12
+ */
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+
21
+ function getPluginRoot() {
22
+ return process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..', '..');
23
+ }
24
+
25
+ function resolvePluginSkillPath(pluginRoot, skillName, ...parts) {
26
+ const candidates = [
27
+ path.join(pluginRoot, 'skills', skillName, ...parts),
28
+ path.join(pluginRoot, '..', 'skills', skillName, ...parts),
29
+ ];
30
+
31
+ for (const candidate of candidates) {
32
+ if (fs.existsSync(candidate)) {
33
+ return candidate;
34
+ }
35
+ }
36
+
37
+ return candidates[0];
38
+ }
39
+
40
+ function skillBaseDir(projectDir) {
41
+ const agentsDir = path.join(projectDir, '.agents', 'skills');
42
+ if (fs.existsSync(agentsDir)) return agentsDir;
43
+ return path.join(projectDir, '.claude', 'skills');
44
+ }
45
+
46
+ function countRegistryEntries(lines) {
47
+ let count = 0;
48
+ for (const line of lines) {
49
+ if (line.trim() && line.includes('|') && !line.startsWith('#')) {
50
+ count++;
51
+ }
52
+ }
53
+ return count;
54
+ }
55
+
56
+ function stripFrontmatter(content) {
57
+ if (content.startsWith('---')) {
58
+ const end = content.indexOf('---', 3);
59
+ if (end !== -1) {
60
+ return content.slice(end + 3).trim();
61
+ }
62
+ }
63
+ return content;
64
+ }
65
+
66
+ function normalizeOverrideFile(content) {
67
+ return content
68
+ .replace(/\n{3,}/g, '\n\n')
69
+ .trim();
70
+ }
71
+
72
+ function escapeRegExp(value) {
73
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
74
+ }
75
+
76
+ function managedOverridePattern(startMarker, endMarker) {
77
+ return new RegExp(`\\n?${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, 'm');
78
+ }
79
+
80
+ function writeManagedOverride(overridePath, startMarker, endMarker, bodyContent) {
81
+ const current = fs.existsSync(overridePath) ? fs.readFileSync(overridePath, 'utf8') : '';
82
+ const pattern = managedOverridePattern(startMarker, endMarker);
83
+ const blockContent = `${startMarker}\n${bodyContent}\n${endMarker}`;
84
+ let updated;
85
+
86
+ if (pattern.test(current)) {
87
+ updated = current.replace(pattern, `${blockContent}\n`);
88
+ } else if (current.trim()) {
89
+ updated = `${current.trimEnd()}\n\n${blockContent}\n`;
90
+ } else {
91
+ updated = `${blockContent}\n`;
92
+ }
93
+
94
+ const normalized = normalizeOverrideFile(updated);
95
+ fs.writeFileSync(overridePath, normalized ? `${normalized}\n` : '');
96
+ }
97
+
98
+ function hasProjectKnowledgeSurface(projectDir, registryPath) {
99
+ const overridePath = path.join(projectDir, 'AGENTS.override.md');
100
+ const overrideContent = fs.existsSync(overridePath) ? fs.readFileSync(overridePath, 'utf8') : '';
101
+ return fs.existsSync(path.join(projectDir, '.spectre', 'manifest.json'))
102
+ || fs.existsSync(registryPath)
103
+ || overrideContent.includes('<!-- spectre-knowledge:start -->')
104
+ || overrideContent.includes('<!-- spectre-session:start -->');
105
+ }
106
+
107
+ function buildKnowledgeOverrideBody(applyContent) {
108
+ return [
109
+ '## SPECTRE Knowledge Context',
110
+ '',
111
+ 'This block is managed by SPECTRE and replaced automatically on session start.',
112
+ 'Use it before searching or implementing work in this repository.',
113
+ '',
114
+ applyContent.trim()
115
+ ].join('\n');
116
+ }
117
+
118
+ function main() {
119
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
120
+ const pluginRoot = getPluginRoot();
121
+
122
+ const applySkillPath = resolvePluginSkillPath(pluginRoot, 'apply', 'SKILL.md');
123
+
124
+ if (!fs.existsSync(applySkillPath)) {
125
+ process.exit(0);
126
+ }
127
+
128
+ // Paths - check new name first, fall back to old names for migration
129
+ let registryPath = path.join(skillBaseDir(projectDir), 'spectre-recall', 'references', 'registry.toon');
130
+ const oldRegistryPath = path.join(projectDir, '.claude', 'skills', 'spectre-find', 'references', 'registry.toon');
131
+
132
+ // Support old "spectre-find" path for projects that haven't migrated
133
+ if (!fs.existsSync(registryPath) && fs.existsSync(oldRegistryPath)) {
134
+ registryPath = oldRegistryPath;
135
+ }
136
+
137
+ // Read registry if it exists
138
+ let registryContent = '';
139
+ let entryCount = 0;
140
+ if (fs.existsSync(registryPath)) {
141
+ registryContent = fs.readFileSync(registryPath, 'utf8').trim();
142
+ const lines = registryContent ? registryContent.split('\n') : [];
143
+ entryCount = countRegistryEntries(lines);
144
+ }
145
+
146
+ // Read apply skill and strip frontmatter
147
+ let applyContent = fs.readFileSync(applySkillPath, 'utf8');
148
+ applyContent = stripFrontmatter(applyContent);
149
+ applyContent = applyContent.replaceAll('.claude/skills/', '.agents/skills/').replaceAll('/spectre:', '');
150
+
151
+ if (hasProjectKnowledgeSurface(projectDir, registryPath)) {
152
+ writeManagedOverride(
153
+ path.join(projectDir, 'AGENTS.override.md'),
154
+ '<!-- spectre-knowledge:start -->',
155
+ '<!-- spectre-knowledge:end -->',
156
+ buildKnowledgeOverrideBody(applyContent)
157
+ );
158
+ }
159
+
160
+ // Visible notice
161
+ let visibleNotice;
162
+ if (entryCount > 0) {
163
+ visibleNotice = `\ud83d\udc7b spectre: ${entryCount} knowledge skills available`;
164
+ } else {
165
+ visibleNotice = '\ud83d\udc7b spectre: ready \u2014 capture knowledge with /spectre:learn';
166
+ }
167
+
168
+ const output = {
169
+ systemMessage: visibleNotice,
170
+ hookSpecificOutput: {
171
+ hookEventName: 'SessionStart'
172
+ }
173
+ };
174
+
175
+ process.stdout.write(JSON.stringify(output) + '\n');
176
+ process.exit(0);
177
+ }
178
+
179
+ export {
180
+ buildKnowledgeOverrideBody,
181
+ countRegistryEntries,
182
+ hasProjectKnowledgeSurface,
183
+ resolvePluginSkillPath,
184
+ stripFrontmatter
185
+ };
186
+
187
+ if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fs.realpathSync(__filename)) {
188
+ main();
189
+ }
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * register_learning.mjs
5
+ *
6
+ * Registers a spectre learning and manages the project-level recall skill.
7
+ *
8
+ * Responsibilities:
9
+ * 1. Create/update registry at .claude/skills/spectre-recall/references/registry.toon
10
+ * 2. Read recall-template.md from plugin
11
+ * 3. Generate .claude/skills/spectre-recall/SKILL.md with embedded registry
12
+ *
13
+ * Usage:
14
+ * node register_learning.mjs \
15
+ * --project-root "/path/to/project" \
16
+ * --skill-name "feature-my-feature" \
17
+ * --category "feature" \
18
+ * --triggers "keyword1, keyword2" \
19
+ * --description "Use when doing X or Y"
20
+ */
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ const __filename = fileURLToPath(import.meta.url);
27
+ const __dirname = path.dirname(__filename);
28
+
29
+ function getRegistryHeader() {
30
+ return [
31
+ '# SPECTRE Knowledge Registry',
32
+ '# Format: skill-name|category|triggers|description',
33
+ ''
34
+ ];
35
+ }
36
+
37
+ function updateRegistry(registryPath, entry, skillName) {
38
+ const entryPrefix = skillName + '|';
39
+ let lines;
40
+
41
+ if (fs.existsSync(registryPath)) {
42
+ const content = fs.readFileSync(registryPath, 'utf8').trim();
43
+ lines = content ? content.split('\n') : [];
44
+ } else {
45
+ lines = getRegistryHeader();
46
+ }
47
+
48
+ let entryExists = false;
49
+ const updatedLines = [];
50
+
51
+ for (const line of lines) {
52
+ if (line.startsWith(entryPrefix)) {
53
+ updatedLines.push(entry);
54
+ entryExists = true;
55
+ } else {
56
+ updatedLines.push(line);
57
+ }
58
+ }
59
+
60
+ if (!entryExists) {
61
+ updatedLines.push(entry);
62
+ }
63
+
64
+ let content = updatedLines.join('\n');
65
+ if (!content.endsWith('\n')) {
66
+ content += '\n';
67
+ }
68
+
69
+ fs.writeFileSync(registryPath, content);
70
+ return content;
71
+ }
72
+
73
+ function generateFindSkill(findSkillPath, templatePath, registryContent) {
74
+ if (!fs.existsSync(templatePath)) {
75
+ process.stderr.write(`Warning: Template not found at ${templatePath}\n`);
76
+ return;
77
+ }
78
+
79
+ const template = fs.readFileSync(templatePath, 'utf8');
80
+ const skillContent = template.replace('{{REGISTRY}}', registryContent.trim());
81
+
82
+ fs.mkdirSync(path.dirname(findSkillPath), { recursive: true });
83
+ fs.writeFileSync(findSkillPath, skillContent);
84
+ }
85
+
86
+ function parseFrontmatter(content) {
87
+ if (!content.startsWith('---')) return null;
88
+ const end = content.indexOf('\n---', 3);
89
+ if (end === -1) return null;
90
+ return {
91
+ fmBlock: content.slice(4, end),
92
+ body: content.slice(end + 4)
93
+ };
94
+ }
95
+
96
+ function injectTriggerIntoSkill(skillPath, triggers) {
97
+ if (!fs.existsSync(skillPath)) return;
98
+
99
+ const content = fs.readFileSync(skillPath, 'utf8');
100
+ const parsed = parseFrontmatter(content);
101
+ if (!parsed) return;
102
+
103
+ const { fmBlock, body } = parsed;
104
+
105
+ const lines = fmBlock.split('\n');
106
+ const descIdx = lines.findIndex(l => /^description:\s*/.test(l));
107
+ if (descIdx === -1) return;
108
+
109
+ const descLine = lines[descIdx];
110
+ const rawValue = descLine.replace(/^description:\s*/, '').trim();
111
+
112
+ // Extract the plain description text and count lines to replace
113
+ let descText;
114
+ let linesToRemove = 1;
115
+
116
+ if (rawValue === '|' || rawValue === '>') {
117
+ // Block scalar — collect indented continuation lines
118
+ const continuationParts = [];
119
+ for (let i = descIdx + 1; i < lines.length; i++) {
120
+ if (/^\s+/.test(lines[i])) {
121
+ continuationParts.push(lines[i].trim());
122
+ linesToRemove++;
123
+ } else {
124
+ break;
125
+ }
126
+ }
127
+ descText = continuationParts.join(' ');
128
+ } else {
129
+ descText = rawValue;
130
+ // Strip surrounding quotes
131
+ if ((descText.startsWith('"') && descText.endsWith('"')) ||
132
+ (descText.startsWith("'") && descText.endsWith("'"))) {
133
+ descText = descText.slice(1, -1);
134
+ }
135
+ }
136
+
137
+ // Strip any existing TRIGGER clause so we can re-append with fresh triggers
138
+ descText = descText.replace(/\s*TRIGGER when:.*$/, '').trim();
139
+
140
+ // Single-line description with trigger appended
141
+ const newDesc = `description: ${descText} TRIGGER when: ${triggers}`;
142
+
143
+ // Idempotent: if the line is already exactly right, skip the write
144
+ if (linesToRemove === 1 && lines[descIdx] === newDesc) return;
145
+
146
+ lines.splice(descIdx, linesToRemove, newDesc);
147
+
148
+ const newFmBlock = lines.join('\n');
149
+ fs.writeFileSync(skillPath, `---\n${newFmBlock}\n---${body}`);
150
+ }
151
+
152
+ function migrateAllTriggers(projectRoot, registryContent) {
153
+ const skillsDir = skillBaseDir(projectRoot);
154
+ const lines = registryContent.trim().split('\n');
155
+ for (const line of lines) {
156
+ if (!line.trim() || line.startsWith('#')) continue;
157
+ const parts = line.split('|');
158
+ if (parts.length < 3) continue;
159
+ const skillName = parts[0];
160
+ const triggers = parts[2];
161
+ const skillPath = path.join(skillsDir, skillName, 'SKILL.md');
162
+ injectTriggerIntoSkill(skillPath, triggers);
163
+ }
164
+ }
165
+
166
+ function skillBaseDir(projectRoot) {
167
+ const agentsDir = path.join(projectRoot, '.agents', 'skills');
168
+ if (fs.existsSync(agentsDir)) return agentsDir;
169
+ return path.join(projectRoot, '.claude', 'skills');
170
+ }
171
+
172
+ function resolvePluginSkillPath(pluginRoot, skillName, ...parts) {
173
+ const candidates = [
174
+ path.join(pluginRoot, 'skills', skillName, ...parts),
175
+ path.join(pluginRoot, '..', 'skills', skillName, ...parts),
176
+ ];
177
+
178
+ for (const candidate of candidates) {
179
+ if (fs.existsSync(candidate)) {
180
+ return candidate;
181
+ }
182
+ }
183
+
184
+ return candidates[0];
185
+ }
186
+
187
+ function parseArgs(argv) {
188
+ const args = {};
189
+ const flags = ['--project-root', '--skill-name', '--category', '--triggers', '--description'];
190
+
191
+ for (let i = 0; i < argv.length; i++) {
192
+ if (flags.includes(argv[i]) && i + 1 < argv.length) {
193
+ // Convert --project-root to projectRoot
194
+ const key = argv[i].slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
195
+ args[key] = argv[i + 1];
196
+ i++;
197
+ }
198
+ }
199
+
200
+ return args;
201
+ }
202
+
203
+ function main() {
204
+ const args = parseArgs(process.argv.slice(2));
205
+
206
+ const required = ['projectRoot', 'skillName', 'category', 'triggers', 'description'];
207
+ for (const key of required) {
208
+ if (!args[key]) {
209
+ process.stderr.write(`Error: missing required argument --${key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}\n`);
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ const projectRoot = args.projectRoot;
215
+
216
+ // New paths: registry lives inside spectre-recall skill
217
+ const recallDir = path.join(skillBaseDir(projectRoot), 'spectre-recall');
218
+ const registryDir = path.join(recallDir, 'references');
219
+ const registryPath = path.join(registryDir, 'registry.toon');
220
+ const recallSkillPath = path.join(recallDir, 'SKILL.md');
221
+
222
+ // Template is in the plugin — resolve via env var (hooks) or this script (manual invocation)
223
+ let pluginRoot;
224
+ if (process.env.CLAUDE_PLUGIN_ROOT) {
225
+ pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
226
+ } else {
227
+ // Fallback: resolve relative to this script
228
+ // Script is at: <plugin_root>/hooks/scripts/register_learning.mjs
229
+ pluginRoot = path.resolve(__dirname, '..', '..');
230
+ }
231
+ const templatePath = resolvePluginSkillPath(pluginRoot, 'learn', 'references', 'recall-template.md');
232
+
233
+ // Ensure directories exist
234
+ fs.mkdirSync(registryDir, { recursive: true });
235
+
236
+ // Build the registry entry
237
+ const entry = `${args.skillName}|${args.category}|${args.triggers}|${args.description}`;
238
+
239
+ // Update registry and get full content
240
+ const registryContent = updateRegistry(registryPath, entry, args.skillName);
241
+
242
+ // Generate recall skill with embedded registry
243
+ generateFindSkill(recallSkillPath, templatePath, registryContent);
244
+
245
+ // Reconcile triggers into all skill frontmatter descriptions
246
+ migrateAllTriggers(projectRoot, registryContent);
247
+
248
+ process.stdout.write(`Registered: ${entry}\n`);
249
+ }
250
+
251
+ export {
252
+ getRegistryHeader,
253
+ updateRegistry,
254
+ generateFindSkill,
255
+ parseFrontmatter,
256
+ injectTriggerIntoSkill,
257
+ migrateAllTriggers,
258
+ resolvePluginSkillPath,
259
+ parseArgs
260
+ };
261
+
262
+ if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fs.realpathSync(__filename)) {
263
+ main();
264
+ }
@@ -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}}/.agents/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.