@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
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
4
|
+
* Tests for handoff-resume.mjs
|
|
6
5
|
*
|
|
7
|
-
* Run with: node --test plugins/spectre/hooks/scripts/test_handoff-resume.
|
|
6
|
+
* Run with: node --test plugins/spectre/hooks/scripts/test_handoff-resume.mjs
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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-'));
|