@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.
- package/README.md +3 -4
- package/package.json +3 -2
- package/plugins/spectre/.claude-plugin/plugin.json +1 -1
- package/plugins/spectre/bin/spectre-register +5 -0
- package/plugins/spectre/hooks/hooks.json +3 -14
- package/plugins/spectre/hooks/scripts/bootstrap.mjs +98 -0
- package/plugins/spectre/hooks/scripts/handoff-resume.mjs +404 -0
- package/plugins/spectre/hooks/scripts/lib.mjs +82 -0
- package/plugins/spectre/hooks/scripts/load-knowledge.mjs +189 -0
- package/plugins/spectre/hooks/scripts/register_learning.mjs +264 -0
- package/plugins/spectre/hooks/scripts/{test_bootstrap.cjs → test_bootstrap.mjs} +12 -7
- package/plugins/spectre/hooks/scripts/{test_handoff-resume.cjs → test_handoff-resume.mjs} +13 -11
- package/plugins/spectre/hooks/scripts/{test_load-knowledge.cjs → test_load-knowledge.mjs} +103 -22
- package/plugins/spectre/hooks/scripts/test_register-learning.mjs +335 -0
- package/plugins/spectre/skills/apply/SKILL.md +87 -0
- package/plugins/spectre/{commands/architecture_review.md → skills/architecture_review/SKILL.md} +9 -0
- package/plugins/spectre/{commands/clean.md → skills/clean/SKILL.md} +9 -0
- package/plugins/spectre/{commands/code_review.md → skills/code_review/SKILL.md} +9 -0
- package/plugins/spectre/{commands/create_plan.md → skills/create_plan/SKILL.md} +9 -0
- package/plugins/spectre/{commands/create_tasks.md → skills/create_tasks/SKILL.md} +9 -0
- package/plugins/spectre/{commands/create_test_guide.md → skills/create_test_guide/SKILL.md} +9 -0
- package/plugins/spectre/{commands/evaluate.md → skills/evaluate/SKILL.md} +11 -2
- package/plugins/spectre/{commands/execute.md → skills/execute/SKILL.md} +12 -3
- package/plugins/spectre/{commands/fix.md → skills/fix/SKILL.md} +9 -0
- package/plugins/spectre/{commands/forget.md → skills/forget/SKILL.md} +9 -0
- package/plugins/spectre/skills/{spectre-guide → guide}/SKILL.md +2 -1
- package/plugins/spectre/{commands/handoff.md → skills/handoff/SKILL.md} +9 -0
- package/plugins/spectre/{commands/kickoff.md → skills/kickoff/SKILL.md} +9 -0
- package/plugins/spectre/skills/{spectre-learn → learn}/SKILL.md +19 -59
- package/plugins/spectre/skills/learn/references/recall-template.md +34 -0
- package/plugins/spectre/{commands/plan.md → skills/plan/SKILL.md} +66 -25
- package/plugins/spectre/{commands/plan_review.md → skills/plan_review/SKILL.md} +9 -0
- package/plugins/spectre/{commands/quick_dev.md → skills/quick_dev/SKILL.md} +9 -0
- package/plugins/spectre/{commands/rebase.md → skills/rebase/SKILL.md} +9 -0
- package/plugins/spectre/skills/recall/SKILL.md +17 -0
- package/plugins/spectre/{commands/research.md → skills/research/SKILL.md} +9 -0
- package/plugins/spectre/{commands/scope.md → skills/scope/SKILL.md} +9 -0
- package/plugins/spectre/{commands/ship.md → skills/ship/SKILL.md} +9 -0
- package/plugins/spectre/{commands/sweep.md → skills/sweep/SKILL.md} +9 -0
- package/plugins/spectre/skills/tdd/SKILL.md +111 -0
- package/plugins/spectre/{commands/test.md → skills/test/SKILL.md} +9 -0
- package/plugins/spectre/{commands/ux_spec.md → skills/ux_spec/SKILL.md} +9 -0
- package/plugins/spectre/{commands/validate.md → skills/validate/SKILL.md} +9 -0
- package/plugins/spectre-codex/agents/analyst.toml +117 -0
- package/plugins/spectre-codex/agents/dev.toml +65 -0
- package/plugins/spectre-codex/agents/finder.toml +101 -0
- package/plugins/spectre-codex/agents/patterns.toml +203 -0
- package/plugins/spectre-codex/agents/reviewer.toml +123 -0
- package/plugins/spectre-codex/agents/sync.toml +146 -0
- package/plugins/spectre-codex/agents/tester.toml +205 -0
- package/plugins/spectre-codex/agents/web-research.toml +104 -0
- package/plugins/spectre-codex/hooks/hooks.json +23 -0
- package/plugins/{spectre/hooks/scripts/bootstrap.cjs → spectre-codex/hooks/scripts/bootstrap.mjs} +15 -16
- package/plugins/{spectre/hooks/scripts/handoff-resume.cjs → spectre-codex/hooks/scripts/handoff-resume.mjs} +21 -27
- package/plugins/{spectre/hooks/scripts/lib.cjs → spectre-codex/hooks/scripts/lib.mjs} +3 -4
- package/plugins/spectre-codex/hooks/scripts/load-knowledge.mjs +189 -0
- package/plugins/spectre-codex/hooks/scripts/register_learning.mjs +264 -0
- package/plugins/spectre-codex/skills/apply/SKILL.md +87 -0
- package/plugins/spectre-codex/skills/architecture_review/SKILL.md +129 -0
- package/plugins/spectre-codex/skills/clean/SKILL.md +322 -0
- package/plugins/spectre-codex/skills/code_review/SKILL.md +417 -0
- package/plugins/spectre-codex/skills/create_plan/SKILL.md +126 -0
- package/plugins/spectre-codex/skills/create_tasks/SKILL.md +383 -0
- package/plugins/spectre-codex/skills/create_test_guide/SKILL.md +129 -0
- package/plugins/spectre-codex/skills/evaluate/SKILL.md +59 -0
- package/plugins/spectre-codex/skills/execute/SKILL.md +96 -0
- package/plugins/spectre-codex/skills/fix/SKILL.md +70 -0
- package/plugins/spectre-codex/skills/forget/SKILL.md +67 -0
- package/plugins/spectre-codex/skills/guide/SKILL.md +359 -0
- package/plugins/spectre-codex/skills/handoff/SKILL.md +170 -0
- package/plugins/spectre-codex/skills/kickoff/SKILL.md +124 -0
- package/plugins/spectre-codex/skills/learn/SKILL.md +595 -0
- package/plugins/{spectre/skills/spectre-learn → spectre-codex/skills/learn}/references/recall-template.md +4 -1
- package/plugins/spectre-codex/skills/plan/SKILL.md +211 -0
- package/plugins/spectre-codex/skills/plan_review/SKILL.md +42 -0
- package/plugins/spectre-codex/skills/quick_dev/SKILL.md +110 -0
- package/plugins/spectre-codex/skills/rebase/SKILL.md +82 -0
- package/plugins/spectre-codex/skills/recall/SKILL.md +17 -0
- package/plugins/spectre-codex/skills/research/SKILL.md +168 -0
- package/plugins/spectre-codex/skills/scope/SKILL.md +128 -0
- package/plugins/spectre-codex/skills/ship/SKILL.md +181 -0
- package/plugins/spectre-codex/skills/sweep/SKILL.md +91 -0
- package/plugins/{spectre/skills/spectre-tdd → spectre-codex/skills/tdd}/SKILL.md +1 -1
- package/plugins/spectre-codex/skills/test/SKILL.md +389 -0
- package/plugins/spectre-codex/skills/ux_spec/SKILL.md +100 -0
- package/plugins/spectre-codex/skills/validate/SKILL.md +352 -0
- package/src/config.test.js +6 -5
- package/src/install.test.js +100 -11
- package/src/lib/config.js +107 -54
- package/src/lib/constants.js +17 -23
- package/src/lib/doctor.js +19 -22
- package/src/lib/install.js +98 -313
- package/src/lib/knowledge.js +7 -37
- package/src/lib/paths.js +0 -12
- package/src/pack.test.js +87 -0
- package/plugins/spectre/commands/learn.md +0 -15
- package/plugins/spectre/commands/recall.md +0 -5
- package/plugins/spectre/hooks/scripts/load-knowledge.cjs +0 -120
- package/plugins/spectre/hooks/scripts/precompact-warning.cjs +0 -19
- package/plugins/spectre/hooks/scripts/register_learning.cjs +0 -144
- package/plugins/spectre/hooks/scripts/test_register-learning.cjs +0 -146
- 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.
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
406
|
-
if (typeof module !== 'undefined') {
|
|
407
|
-
module.exports = { copyPluginReferences, formatContext, buildCheckboxTree };
|
|
408
|
-
}
|
|
400
|
+
export { copyPluginReferences, formatContext, buildCheckboxTree };
|
|
409
401
|
|
|
410
|
-
|
|
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.
|
|
4
|
+
* lib.mjs
|
|
6
5
|
*
|
|
7
6
|
* Shared utilities for spectre hook scripts.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
|
|
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
|
-
|
|
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.
|