@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
@@ -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
+ }
@@ -1,18 +1,23 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
- const { describe, it } = require('node:test');
5
- const assert = require('node:assert');
6
- const fs = require('fs');
7
- const path = require('path');
8
- const os = require('os');
9
- const { cleanupStalePaths, STALE_PATHS } = require('./bootstrap.cjs');
3
+ import { describe, it } from 'node:test';
4
+ import assert from 'node:assert';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { cleanupStalePaths, STALE_PATHS } from './bootstrap.mjs';
9
+ import { readStdinWithTimeout, STDIN_TIMEOUT } from './lib.mjs';
10
10
 
11
11
  function createTempDir() {
12
12
  return fs.mkdtempSync(path.join(os.tmpdir(), 'spectre-bootstrap-test-'));
13
13
  }
14
14
 
15
15
  describe('bootstrap', () => {
16
+ it('imports shared hook helpers through ESM', () => {
17
+ assert.strictEqual(STDIN_TIMEOUT, 2000);
18
+ assert.strictEqual(typeof readStdinWithTimeout, 'function');
19
+ });
20
+
16
21
  it('STALE_PATHS is a non-empty array', () => {
17
22
  assert.ok(Array.isArray(STALE_PATHS));
18
23
  assert.ok(STALE_PATHS.length > 0);
@@ -1,20 +1,22 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
2
 
4
3
  /**
5
- * Tests for handoff-resume.cjs
4
+ * Tests for handoff-resume.mjs
6
5
  *
7
- * Run with: node --test plugins/spectre/hooks/scripts/test_handoff-resume.cjs
6
+ * Run with: node --test plugins/spectre/hooks/scripts/test_handoff-resume.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, execSync } = require('child_process');
16
-
17
- const SCRIPT_PATH = path.join(__dirname, 'handoff-resume.cjs');
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, execSync } 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, 'handoff-resume.mjs');
18
20
 
19
21
  function createTmpDir() {
20
22
  return fs.mkdtempSync(path.join(os.tmpdir(), 'spectre-hr-'));