@codename_inc/spectre 3.7.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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/bin/spectre.js +8 -0
  4. package/package.json +23 -0
  5. package/plugins/spectre/.claude-plugin/plugin.json +5 -0
  6. package/plugins/spectre/agents/analyst.md +122 -0
  7. package/plugins/spectre/agents/dev.md +70 -0
  8. package/plugins/spectre/agents/finder.md +105 -0
  9. package/plugins/spectre/agents/patterns.md +207 -0
  10. package/plugins/spectre/agents/reviewer.md +128 -0
  11. package/plugins/spectre/agents/sync.md +151 -0
  12. package/plugins/spectre/agents/tester.md +209 -0
  13. package/plugins/spectre/agents/web-research.md +109 -0
  14. package/plugins/spectre/commands/architecture_review.md +120 -0
  15. package/plugins/spectre/commands/clean.md +313 -0
  16. package/plugins/spectre/commands/code_review.md +408 -0
  17. package/plugins/spectre/commands/create_plan.md +117 -0
  18. package/plugins/spectre/commands/create_tasks.md +374 -0
  19. package/plugins/spectre/commands/create_test_guide.md +120 -0
  20. package/plugins/spectre/commands/evaluate.md +50 -0
  21. package/plugins/spectre/commands/execute.md +87 -0
  22. package/plugins/spectre/commands/fix.md +61 -0
  23. package/plugins/spectre/commands/forget.md +58 -0
  24. package/plugins/spectre/commands/handoff.md +161 -0
  25. package/plugins/spectre/commands/kickoff.md +115 -0
  26. package/plugins/spectre/commands/learn.md +15 -0
  27. package/plugins/spectre/commands/plan.md +170 -0
  28. package/plugins/spectre/commands/plan_review.md +33 -0
  29. package/plugins/spectre/commands/quick_dev.md +101 -0
  30. package/plugins/spectre/commands/rebase.md +73 -0
  31. package/plugins/spectre/commands/recall.md +5 -0
  32. package/plugins/spectre/commands/research.md +159 -0
  33. package/plugins/spectre/commands/scope.md +119 -0
  34. package/plugins/spectre/commands/ship.md +172 -0
  35. package/plugins/spectre/commands/sweep.md +82 -0
  36. package/plugins/spectre/commands/test.md +380 -0
  37. package/plugins/spectre/commands/ux_spec.md +91 -0
  38. package/plugins/spectre/commands/validate.md +343 -0
  39. package/plugins/spectre/hooks/hooks.json +34 -0
  40. package/plugins/spectre/hooks/scripts/bootstrap.cjs +99 -0
  41. package/plugins/spectre/hooks/scripts/handoff-resume.cjs +410 -0
  42. package/plugins/spectre/hooks/scripts/lib.cjs +83 -0
  43. package/plugins/spectre/hooks/scripts/load-knowledge.cjs +120 -0
  44. package/plugins/spectre/hooks/scripts/precompact-warning.cjs +19 -0
  45. package/plugins/spectre/hooks/scripts/register_learning.cjs +144 -0
  46. package/plugins/spectre/hooks/scripts/test_bootstrap.cjs +84 -0
  47. package/plugins/spectre/hooks/scripts/test_handoff-resume.cjs +858 -0
  48. package/plugins/spectre/hooks/scripts/test_load-knowledge.cjs +285 -0
  49. package/plugins/spectre/hooks/scripts/test_register-learning.cjs +146 -0
  50. package/plugins/spectre/skills/spectre-apply/SKILL.md +189 -0
  51. package/plugins/spectre/skills/spectre-guide/SKILL.md +358 -0
  52. package/plugins/spectre/skills/spectre-learn/SKILL.md +635 -0
  53. package/plugins/spectre/skills/spectre-learn/references/recall-template.md +31 -0
  54. package/plugins/spectre/skills/spectre-tdd/SKILL.md +111 -0
  55. package/src/config.test.js +134 -0
  56. package/src/install.test.js +273 -0
  57. package/src/lib/config.js +516 -0
  58. package/src/lib/constants.js +60 -0
  59. package/src/lib/doctor.js +168 -0
  60. package/src/lib/install.js +482 -0
  61. package/src/lib/knowledge.js +217 -0
  62. package/src/lib/paths.js +98 -0
  63. package/src/lib/project.js +473 -0
  64. package/src/main.js +150 -0
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * handoff-resume.cjs
6
+ *
7
+ * SessionStart hook that injects context from the last /spectre:handoff.
8
+ * Consolidates the previous session-resume-hook.sh + format-resume-context.py.
9
+ *
10
+ * Outputs JSON for Claude Code hook system:
11
+ * - systemMessage: User-visible notice
12
+ * - hookSpecificOutput.additionalContext: Full session context in <session-context> tags
13
+ *
14
+ * Background modes:
15
+ * - --bg-copy-refs: Copy plugin references (fork #1)
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { fork } = require('child_process');
21
+ const { readStdinWithTimeout, getGitBranch } = require('./lib.cjs');
22
+
23
+ // ──────────────────────────────────────────────────────────────────
24
+ // Plugin reference copying (background fork #1)
25
+ // ──────────────────────────────────────────────────────────────────
26
+
27
+ function copyPluginReferences() {
28
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
29
+ if (!pluginRoot) return;
30
+
31
+ const referencesSrc = path.join(pluginRoot, 'references');
32
+ if (!fs.existsSync(referencesSrc)) return;
33
+
34
+ const referencesDst = path.join('.claude', 'spectre');
35
+ fs.mkdirSync(referencesDst, { recursive: true });
36
+
37
+ const files = fs.readdirSync(referencesSrc).filter(f => f.endsWith('.md'));
38
+ for (const file of files) {
39
+ const dst = path.join(referencesDst, file);
40
+ if (!fs.existsSync(dst)) {
41
+ fs.copyFileSync(path.join(referencesSrc, file), dst);
42
+ }
43
+ }
44
+
45
+ // Append to .gitignore if it exists and .claude/ not already ignored
46
+ const gitignorePath = '.gitignore';
47
+ if (fs.existsSync(gitignorePath)) {
48
+ const content = fs.readFileSync(gitignorePath, 'utf8');
49
+ if (!content.includes('.claude/') && !content.includes('.claude/spectre/')) {
50
+ fs.appendFileSync(gitignorePath, '\n# spectre plugin files\n.claude/spectre/\n');
51
+ }
52
+ }
53
+ }
54
+
55
+ // ──────────────────────────────────────────────────────────────────
56
+ // Session discovery
57
+ // ──────────────────────────────────────────────────────────────────
58
+
59
+ function findLatestHandoff(sessionDir) {
60
+ if (!fs.existsSync(sessionDir)) return null;
61
+
62
+ // Only look at top-level files, not in archive/
63
+ const files = fs.readdirSync(sessionDir)
64
+ .filter(f => f.endsWith('_handoff.json'))
65
+ .map(f => ({
66
+ name: f,
67
+ full: path.join(sessionDir, f),
68
+ mtime: fs.statSync(path.join(sessionDir, f)).mtimeMs
69
+ }))
70
+ .sort((a, b) => b.mtime - a.mtime);
71
+
72
+ return files.length > 0 ? files[0].full : null;
73
+ }
74
+
75
+ // ──────────────────────────────────────────────────────────────────
76
+ // Formatting helpers
77
+ // ──────────────────────────────────────────────────────────────────
78
+
79
+ function formatList(items, prefix) {
80
+ prefix = prefix != null ? prefix : '- ';
81
+ if (!items || !items.length) return `${prefix}None`;
82
+ return items.map(item => `${prefix}${item}`).join('\n');
83
+ }
84
+
85
+ function buildCheckboxTree(tasks) {
86
+ if (!tasks || !tasks.length) return 'No tasks found.';
87
+
88
+ const byParent = {};
89
+ for (const task of tasks) {
90
+ const parent = task.parent || null;
91
+ if (!byParent[parent]) byParent[parent] = [];
92
+ byParent[parent].push(task);
93
+ }
94
+
95
+ function renderTask(task, indent) {
96
+ indent = indent || 0;
97
+ const lines = [];
98
+ const prefix = ' '.repeat(indent);
99
+ const checkbox = task.completed ? '[x]' : '[ ]';
100
+ const status = task.status || 'open';
101
+ const title = task.title || 'Untitled';
102
+ const taskId = task.id || 'unknown';
103
+
104
+ if (task.completed) {
105
+ lines.push(`${prefix}- ${checkbox} ${title} (${taskId}) - COMPLETED`);
106
+ } else {
107
+ const cmd = task.resume_command || `bd update ${taskId} --status in_progress`;
108
+ const statusBadge = status !== 'open' ? `[${status}]` : '';
109
+ lines.push(`${prefix}- ${checkbox} ${title} (${taskId}) ${statusBadge} - \`${cmd}\``);
110
+ }
111
+
112
+ // Render children
113
+ const childrenIds = task.children || [];
114
+ if (childrenIds.length) {
115
+ for (const childTask of tasks) {
116
+ if (childrenIds.includes(childTask.id) || childTask.parent === taskId) {
117
+ lines.push(...renderTask(childTask, indent + 1));
118
+ }
119
+ }
120
+ }
121
+
122
+ return lines;
123
+ }
124
+
125
+ // Start with root tasks (no parent or parent is null)
126
+ const rootTasks = (byParent[null] || []).concat(byParent['null'] || []);
127
+
128
+ // If no root tasks found, just list all tasks flat
129
+ const startTasks = rootTasks.length ? rootTasks : tasks;
130
+
131
+ const lines = [];
132
+ const renderedIds = new Set();
133
+
134
+ for (const task of startTasks) {
135
+ if (!renderedIds.has(task.id)) {
136
+ const taskLines = renderTask(task);
137
+ lines.push(...taskLines);
138
+ renderedIds.add(task.id);
139
+ for (const t of tasks) {
140
+ if (t.parent === task.id) {
141
+ renderedIds.add(t.id);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ return lines.join('\n');
148
+ }
149
+
150
+ // ──────────────────────────────────────────────────────────────────
151
+ // Main context formatter
152
+ // ──────────────────────────────────────────────────────────────────
153
+
154
+ function formatContext(data, opts) {
155
+ opts = opts || {};
156
+ const handoffPath = opts.handoffPath;
157
+
158
+ const taskName = data.task_name || 'unknown';
159
+ const branchName = data.branch_name || 'unknown';
160
+
161
+ // Progress update fields (v1.1 schema)
162
+ const progress = data.progress_update || {};
163
+ const summary = progress.summary || 'No summary available.';
164
+ const goal = progress.goal || '';
165
+ const constraints = progress.constraints || [];
166
+ const decisions = progress.decisions || [];
167
+ const accomplished = progress.accomplished || [];
168
+ const now = progress.now || '';
169
+ const nextSteps = progress.next_steps || [];
170
+ const blockers = progress.blockers || [];
171
+ const openQuestions = progress.open_questions || [];
172
+ const confidence = progress.confidence || 'unknown';
173
+ const risks = progress.risks || [];
174
+
175
+ // Working set (v1.1 schema) - fall back to context.key_files for v1.0
176
+ const workingSet = data.working_set || {};
177
+ let keyFiles = workingSet.key_files || [];
178
+ const activeIds = workingSet.active_ids || [];
179
+ const recentCommands = workingSet.recent_commands || [];
180
+
181
+ // Fall back to old context structure if working_set not present
182
+ if (!keyFiles.length) {
183
+ const ctx = data.context || {};
184
+ keyFiles = ctx.key_files || [];
185
+ }
186
+
187
+ // Beads tasks
188
+ const beads = data.beads || {};
189
+ const beadsAvailable = beads.available != null ? beads.available : true;
190
+ const tasks = beads.tasks || [];
191
+
192
+ // Context
193
+ const context = data.context || {};
194
+ const lastCommit = context.last_commit || 'unknown';
195
+ const wipState = context.wip_state || 'unknown';
196
+
197
+ // Build checkbox tree for beads tasks
198
+ const checkboxTree = (beadsAvailable && tasks.length) ? buildCheckboxTree(tasks) : '';
199
+
200
+ // 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];
209
+ noticeLines.push(`\n\ud83d\udd04 Session Resumed: ${taskName} | Branch: ${branchName}`);
210
+
211
+ if (goal) {
212
+ noticeLines.push(`\n\ud83c\udfaf Goal: ${goal}`);
213
+ }
214
+
215
+ noticeLines.push(`\n\ud83d\udcdd Summary: ${summary}`);
216
+
217
+ if (nextSteps.length) {
218
+ noticeLines.push('\n\u27a1\ufe0f Next Steps:');
219
+ for (const step of nextSteps) {
220
+ noticeLines.push(` - ${step}`);
221
+ }
222
+ }
223
+
224
+ if (handoffPath) {
225
+ noticeLines.push(`\n\ud83d\udcc1 Full details: ${handoffPath}`);
226
+ }
227
+
228
+ noticeLines.push('\n\ud83d\udca1 Run /spectre:forget to clear session memory and start fresh.');
229
+
230
+ const visibleNotice = noticeLines.join('\n');
231
+
232
+ // Build the hidden context sections
233
+ const sections = [];
234
+
235
+ sections.push(`# Session Context: ${taskName}`);
236
+
237
+ // Last session summary
238
+ sections.push(`\n## Last Session Summary\n${summary}`);
239
+
240
+ // Goal (if available - v1.1)
241
+ if (goal) {
242
+ sections.push(`\n### Goal\n${goal}`);
243
+ }
244
+
245
+ // Constraints (if available - v1.1)
246
+ if (constraints.length) {
247
+ sections.push(`\n### Constraints\n${formatList(constraints)}`);
248
+ }
249
+
250
+ // What we accomplished
251
+ sections.push(`\n### What We Accomplished\n${formatList(accomplished)}`);
252
+
253
+ // What we were working on (critical for resume - v1.1)
254
+ if (now) {
255
+ sections.push(`\n### Active Work (Resume Here)\n**${now}**`);
256
+ }
257
+
258
+ // What's next
259
+ sections.push(`\n### What's Next\n${formatList(nextSteps)}`);
260
+
261
+ // Blockers
262
+ if (blockers.length) {
263
+ sections.push(`\n### Blockers\n${formatList(blockers)}`);
264
+ }
265
+
266
+ // Open questions (v1.1)
267
+ if (openQuestions.length) {
268
+ sections.push(`\n### Open Questions\n${formatList(openQuestions)}`);
269
+ }
270
+
271
+ // Decisions
272
+ if (decisions.length) {
273
+ sections.push(`\n### Decisions Made\n${formatList(decisions)}`);
274
+ }
275
+
276
+ // Confidence and risks
277
+ const risksStr = risks.length ? formatList(risks, '') : 'None identified';
278
+ sections.push(`\n**Confidence**: ${confidence} | **Risks**: ${risksStr}`);
279
+
280
+ // Working set (v1.1)
281
+ const wsLines = [];
282
+ if (keyFiles.length) {
283
+ wsLines.push(`- **Key Files**: ${keyFiles.join(', ')}`);
284
+ }
285
+ if (activeIds.length) {
286
+ wsLines.push(`- **Active IDs**: ${activeIds.join(', ')}`);
287
+ }
288
+ if (recentCommands.length) {
289
+ wsLines.push(`- **Recent Commands**: ${recentCommands.join(', ')}`);
290
+ }
291
+
292
+ if (wsLines.length) {
293
+ sections.push('\n### Working Set\n' + wsLines.join('\n'));
294
+ }
295
+
296
+ // Context
297
+ sections.push(
298
+ '\n---\n\n' +
299
+ '## Context\n' +
300
+ `- **Branch**: ${branchName}\n` +
301
+ `- **Last Commit**: ${lastCommit}\n` +
302
+ `- **WIP State**: ${wipState}`
303
+ );
304
+
305
+ // Beads tasks (if available)
306
+ if (beadsAvailable && checkboxTree) {
307
+ sections.push(`\n### Beads Tasks\n${checkboxTree}`);
308
+ }
309
+
310
+ const hiddenContext = `<session-context>\n${sections.join('')}\n</session-context>`;
311
+
312
+ return {
313
+ systemMessage: visibleNotice,
314
+ hookSpecificOutput: {
315
+ hookEventName: 'SessionStart',
316
+ additionalContext: hiddenContext
317
+ }
318
+ };
319
+ }
320
+
321
+ // ──────────────────────────────────────────────────────────────────
322
+ // Main entry point
323
+ // ──────────────────────────────────────────────────────────────────
324
+
325
+ async function main() {
326
+ // Handle background fork modes
327
+ if (process.argv[2] === '--bg-copy-refs') {
328
+ try { copyPluginReferences(); } catch (_) {}
329
+ process.exit(0);
330
+ }
331
+
332
+ // Read stdin
333
+ await readStdinWithTimeout();
334
+
335
+ // Fork to copy plugin references in background (non-blocking)
336
+ const copyChild = fork(__filename, ['--bg-copy-refs'], {
337
+ detached: true,
338
+ stdio: 'ignore'
339
+ });
340
+ copyChild.unref();
341
+
342
+ // Get project directory from environment or cwd
343
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
344
+
345
+ // Get branch name
346
+ const branchName = getGitBranch();
347
+
348
+ // Find session logs directory
349
+ const sessionDir = path.join(projectDir, 'docs', 'tasks', branchName, 'session_logs');
350
+
351
+ // Find latest handoff
352
+ const latestHandoff = findLatestHandoff(sessionDir);
353
+
354
+ if (!latestHandoff) {
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');
362
+ const tips = [
363
+ '',
364
+ 'Getting Started with SPECTRE:',
365
+ '',
366
+ '\u2699\ufe0f Tip: Turn off auto-compact via /config \u2014 SPECTRE works best with manual context management',
367
+ '\ud83d\udcbe Use /spectre:handoff when context is getting full but you\'re still going \u2014 saves state for the next session',
368
+ '\ud83e\uddf9 Use /spectre:forget to clear session memory and start fresh',
369
+ '\ud83d\ude80 Use /spectre:scope to start building features with the full SPECTRE workflow',
370
+ '\ud83c\udf93 Use /spectre:learn to create a documentation skill that your Agent will auto-load when relevant.'
371
+ ].join('\n');
372
+
373
+ const welcome = {
374
+ systemMessage: asciiBanner + '\n' + tips
375
+ };
376
+ process.stdout.write(JSON.stringify(welcome) + '\n');
377
+ process.exit(0);
378
+ }
379
+
380
+ // Read and parse handoff JSON
381
+ let data;
382
+ try {
383
+ data = JSON.parse(fs.readFileSync(latestHandoff, 'utf8'));
384
+ } catch (_) {
385
+ process.exit(0);
386
+ }
387
+
388
+ // Compute relative path to handoff for user reference
389
+ let handoffRelative;
390
+ try {
391
+ handoffRelative = path.relative(projectDir, latestHandoff);
392
+ } catch (_) {
393
+ handoffRelative = latestHandoff;
394
+ }
395
+
396
+ // Format and output context
397
+ const output = formatContext(data, {
398
+ handoffPath: handoffRelative
399
+ });
400
+ process.stdout.write(JSON.stringify(output) + '\n');
401
+
402
+ process.exit(0);
403
+ }
404
+
405
+ // Export for testing
406
+ if (typeof module !== 'undefined') {
407
+ module.exports = { copyPluginReferences, formatContext, buildCheckboxTree };
408
+ }
409
+
410
+ main();
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * lib.cjs
6
+ *
7
+ * Shared utilities for spectre hook scripts.
8
+ */
9
+
10
+ const { execSync } = require('child_process');
11
+
12
+ const STDIN_TIMEOUT = 2000; // milliseconds
13
+
14
+ /**
15
+ * Read stdin with a timeout to avoid blocking indefinitely.
16
+ * @param {number} [timeout=2000] - Timeout in milliseconds
17
+ * @returns {Promise<string|null>} - stdin content or null on timeout
18
+ */
19
+ function readStdinWithTimeout(timeout) {
20
+ timeout = timeout != null ? timeout : STDIN_TIMEOUT;
21
+
22
+ return new Promise((resolve) => {
23
+ // If stdin is a TTY (no piped data), resolve immediately
24
+ if (process.stdin.isTTY) {
25
+ resolve(null);
26
+ return;
27
+ }
28
+
29
+ let data = '';
30
+ let settled = false;
31
+
32
+ const timer = setTimeout(() => {
33
+ if (!settled) {
34
+ settled = true;
35
+ process.stdin.removeAllListeners('data');
36
+ process.stdin.removeAllListeners('end');
37
+ process.stdin.removeAllListeners('error');
38
+ try { process.stdin.pause(); } catch (_) {}
39
+ resolve(data || null);
40
+ }
41
+ }, timeout);
42
+
43
+ process.stdin.setEncoding('utf8');
44
+ process.stdin.on('data', (chunk) => {
45
+ data += chunk;
46
+ });
47
+ process.stdin.on('end', () => {
48
+ if (!settled) {
49
+ settled = true;
50
+ clearTimeout(timer);
51
+ resolve(data || null);
52
+ }
53
+ });
54
+ process.stdin.on('error', () => {
55
+ if (!settled) {
56
+ settled = true;
57
+ clearTimeout(timer);
58
+ resolve(null);
59
+ }
60
+ });
61
+ process.stdin.resume();
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Get current git branch name.
67
+ * @param {string} [cwd] - Working directory for git command
68
+ * @returns {string} Branch name or 'unknown'
69
+ */
70
+ function getGitBranch(cwd) {
71
+ try {
72
+ return execSync('git rev-parse --abbrev-ref HEAD', {
73
+ timeout: 5000,
74
+ encoding: 'utf8',
75
+ cwd: cwd || undefined,
76
+ stdio: ['pipe', 'pipe', 'pipe']
77
+ }).trim();
78
+ } catch (_) {
79
+ return 'unknown';
80
+ }
81
+ }
82
+
83
+ module.exports = { readStdinWithTimeout, getGitBranch, STDIN_TIMEOUT };
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * load-knowledge.cjs
6
+ *
7
+ * SessionStart hook that injects the apply skill content with embedded registry
8
+ * directly into Claude's context.
9
+ *
10
+ * Reads:
11
+ * - Apply skill from plugin: skills/spectre-apply/SKILL.md
12
+ * - Registry from project: .claude/skills/spectre-recall/references/registry.toon
13
+ *
14
+ * Combines them by replacing the Registry Location section with actual registry content.
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ function countRegistryEntries(lines) {
21
+ let count = 0;
22
+ for (const line of lines) {
23
+ if (line.trim() && line.includes('|') && !line.startsWith('#')) {
24
+ count++;
25
+ }
26
+ }
27
+ return count;
28
+ }
29
+
30
+ function stripFrontmatter(content) {
31
+ if (content.startsWith('---')) {
32
+ const end = content.indexOf('---', 3);
33
+ if (end !== -1) {
34
+ return content.slice(end + 3).trim();
35
+ }
36
+ }
37
+ return content;
38
+ }
39
+
40
+ function main() {
41
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
42
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || '';
43
+
44
+ const applySkillPath = path.join(pluginRoot, 'skills', 'spectre-apply', 'SKILL.md');
45
+
46
+ if (!fs.existsSync(applySkillPath)) {
47
+ process.exit(0);
48
+ }
49
+
50
+ // Paths - check new name first, fall back to old names for migration
51
+ let registryPath = path.join(projectDir, '.claude', 'skills', 'spectre-recall', 'references', 'registry.toon');
52
+ const oldRegistryPath = path.join(projectDir, '.claude', 'skills', 'spectre-find', 'references', 'registry.toon');
53
+
54
+ // Support old "spectre-find" path for projects that haven't migrated
55
+ if (!fs.existsSync(registryPath) && fs.existsSync(oldRegistryPath)) {
56
+ registryPath = oldRegistryPath;
57
+ }
58
+
59
+ // Read registry if it exists
60
+ let registryContent = '';
61
+ let entryCount = 0;
62
+ if (fs.existsSync(registryPath)) {
63
+ registryContent = fs.readFileSync(registryPath, 'utf8').trim();
64
+ const lines = registryContent ? registryContent.split('\n') : [];
65
+ entryCount = countRegistryEntries(lines);
66
+ }
67
+
68
+ // Read apply skill and strip frontmatter
69
+ let applyContent = fs.readFileSync(applySkillPath, 'utf8');
70
+ applyContent = stripFrontmatter(applyContent);
71
+
72
+ // Replace the Registry Location section with embedded registry or empty notice
73
+ let registrySection;
74
+ if (entryCount > 0) {
75
+ registrySection =
76
+ '## Registry\n\n' +
77
+ '**Format**: `skill-name|category|triggers|description`\n\n' +
78
+ '```\n' +
79
+ registryContent + '\n' +
80
+ '```\n\n' +
81
+ 'Each entry corresponds to a skill that can be loaded via `Skill({skill-name})`\n\n' +
82
+ '**Categories:** feature, gotchas, patterns, decisions, procedures, integration, performance, testing, ux, strategy';
83
+ } else {
84
+ registrySection =
85
+ '## Registry\n\n' +
86
+ 'No knowledge has been captured for this project yet. The behavioral rules in this document still apply.\n\n' +
87
+ 'To capture knowledge from this session, use `/spectre:learn` after completing significant work.\n\n' +
88
+ '**Categories:** feature, gotchas, patterns, decisions, procedures, integration, performance, testing, ux, strategy';
89
+ }
90
+
91
+ // Replace the Registry Location section
92
+ applyContent = applyContent.replace(
93
+ /## Registry Location[\s\S]*?(?=## Workflow)/,
94
+ registrySection + '\n\n'
95
+ );
96
+
97
+ // Build final context
98
+ const context = `<spectre-knowledge>\n${applyContent}\n</spectre-knowledge>`;
99
+
100
+ // Visible notice
101
+ let visibleNotice;
102
+ if (entryCount > 0) {
103
+ visibleNotice = `\ud83d\udc7b spectre: ${entryCount} knowledge skills available`;
104
+ } else {
105
+ visibleNotice = '\ud83d\udc7b spectre: ready \u2014 capture knowledge with /spectre:learn';
106
+ }
107
+
108
+ const output = {
109
+ systemMessage: visibleNotice,
110
+ hookSpecificOutput: {
111
+ hookEventName: 'SessionStart',
112
+ additionalContext: context
113
+ }
114
+ };
115
+
116
+ process.stdout.write(JSON.stringify(output) + '\n');
117
+ process.exit(0);
118
+ }
119
+
120
+ main();
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * precompact-warning.cjs
6
+ *
7
+ * PreCompact hook that suggests using /spectre:handoff + /clear
8
+ * instead of auto-compact for better context continuity.
9
+ */
10
+
11
+ const output = {
12
+ systemMessage:
13
+ '\u26a0\ufe0f Auto-compact can cause context loss. ' +
14
+ 'For full continuity: /spectre:handoff \u2192 /clear \u2192 new session. ' +
15
+ 'Consider disabling auto-compact in /config.'
16
+ };
17
+
18
+ process.stdout.write(JSON.stringify(output) + '\n');
19
+ process.exit(0);