@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.
- package/LICENSE +21 -0
- package/README.md +411 -0
- package/bin/spectre.js +8 -0
- package/package.json +23 -0
- package/plugins/spectre/.claude-plugin/plugin.json +5 -0
- package/plugins/spectre/agents/analyst.md +122 -0
- package/plugins/spectre/agents/dev.md +70 -0
- package/plugins/spectre/agents/finder.md +105 -0
- package/plugins/spectre/agents/patterns.md +207 -0
- package/plugins/spectre/agents/reviewer.md +128 -0
- package/plugins/spectre/agents/sync.md +151 -0
- package/plugins/spectre/agents/tester.md +209 -0
- package/plugins/spectre/agents/web-research.md +109 -0
- package/plugins/spectre/commands/architecture_review.md +120 -0
- package/plugins/spectre/commands/clean.md +313 -0
- package/plugins/spectre/commands/code_review.md +408 -0
- package/plugins/spectre/commands/create_plan.md +117 -0
- package/plugins/spectre/commands/create_tasks.md +374 -0
- package/plugins/spectre/commands/create_test_guide.md +120 -0
- package/plugins/spectre/commands/evaluate.md +50 -0
- package/plugins/spectre/commands/execute.md +87 -0
- package/plugins/spectre/commands/fix.md +61 -0
- package/plugins/spectre/commands/forget.md +58 -0
- package/plugins/spectre/commands/handoff.md +161 -0
- package/plugins/spectre/commands/kickoff.md +115 -0
- package/plugins/spectre/commands/learn.md +15 -0
- package/plugins/spectre/commands/plan.md +170 -0
- package/plugins/spectre/commands/plan_review.md +33 -0
- package/plugins/spectre/commands/quick_dev.md +101 -0
- package/plugins/spectre/commands/rebase.md +73 -0
- package/plugins/spectre/commands/recall.md +5 -0
- package/plugins/spectre/commands/research.md +159 -0
- package/plugins/spectre/commands/scope.md +119 -0
- package/plugins/spectre/commands/ship.md +172 -0
- package/plugins/spectre/commands/sweep.md +82 -0
- package/plugins/spectre/commands/test.md +380 -0
- package/plugins/spectre/commands/ux_spec.md +91 -0
- package/plugins/spectre/commands/validate.md +343 -0
- package/plugins/spectre/hooks/hooks.json +34 -0
- package/plugins/spectre/hooks/scripts/bootstrap.cjs +99 -0
- package/plugins/spectre/hooks/scripts/handoff-resume.cjs +410 -0
- package/plugins/spectre/hooks/scripts/lib.cjs +83 -0
- package/plugins/spectre/hooks/scripts/load-knowledge.cjs +120 -0
- package/plugins/spectre/hooks/scripts/precompact-warning.cjs +19 -0
- package/plugins/spectre/hooks/scripts/register_learning.cjs +144 -0
- package/plugins/spectre/hooks/scripts/test_bootstrap.cjs +84 -0
- package/plugins/spectre/hooks/scripts/test_handoff-resume.cjs +858 -0
- package/plugins/spectre/hooks/scripts/test_load-knowledge.cjs +285 -0
- package/plugins/spectre/hooks/scripts/test_register-learning.cjs +146 -0
- package/plugins/spectre/skills/spectre-apply/SKILL.md +189 -0
- package/plugins/spectre/skills/spectre-guide/SKILL.md +358 -0
- package/plugins/spectre/skills/spectre-learn/SKILL.md +635 -0
- package/plugins/spectre/skills/spectre-learn/references/recall-template.md +31 -0
- package/plugins/spectre/skills/spectre-tdd/SKILL.md +111 -0
- package/src/config.test.js +134 -0
- package/src/install.test.js +273 -0
- package/src/lib/config.js +516 -0
- package/src/lib/constants.js +60 -0
- package/src/lib/doctor.js +168 -0
- package/src/lib/install.js +482 -0
- package/src/lib/knowledge.js +217 -0
- package/src/lib/paths.js +98 -0
- package/src/lib/project.js +473 -0
- 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);
|