@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,516 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { MANAGED_CONFIG_MARKER } from './constants.js';
|
|
4
|
+
import { codexConfigPath, codexHooksConfigPath, ensureDir, projectPaths } from './paths.js';
|
|
5
|
+
|
|
6
|
+
function escapeTomlString(value) {
|
|
7
|
+
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function escapeRegExp(value) {
|
|
11
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function tablePattern(tableName) {
|
|
15
|
+
return new RegExp(`(^\\[${escapeRegExp(tableName)}\\]\\n)([\\s\\S]*?)(?=^\\[|(?![\\s\\S]))`, 'm');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readConfig() {
|
|
19
|
+
const configPath = codexConfigPath();
|
|
20
|
+
ensureDir(path.dirname(configPath));
|
|
21
|
+
if (!fs.existsSync(configPath)) {
|
|
22
|
+
fs.writeFileSync(configPath, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let content = fs.readFileSync(configPath, 'utf8');
|
|
26
|
+
if (!content.includes(`# ${MANAGED_CONFIG_MARKER}`)) {
|
|
27
|
+
content = `${content.trimEnd()}\n\n# ${MANAGED_CONFIG_MARKER}\n`.trimStart();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { configPath, content };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeConfig(configPath, content) {
|
|
34
|
+
const normalized = content
|
|
35
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
36
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
37
|
+
.trimEnd();
|
|
38
|
+
|
|
39
|
+
fs.writeFileSync(configPath, `${normalized}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureTable(content, tableName) {
|
|
43
|
+
if (tablePattern(tableName).test(content)) {
|
|
44
|
+
return content;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return `${content.trimEnd()}\n\n[${tableName}]\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function replaceTable(content, tableName, body) {
|
|
51
|
+
const nextContent = ensureTable(content, tableName);
|
|
52
|
+
const pattern = tablePattern(tableName);
|
|
53
|
+
if (!pattern.test(nextContent)) {
|
|
54
|
+
return `${nextContent.trimEnd()}\n\n[${tableName}]\n${body.trimEnd()}\n`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return nextContent.replace(pattern, `[${tableName}]\n${body.trimEnd()}\n`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function removeTable(content, tableName) {
|
|
61
|
+
return content.replace(new RegExp(`\\n*\\[${escapeRegExp(tableName)}\\]\\n[\\s\\S]*?(?=^\\[|(?![\\s\\S]))`, 'm'), '\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function removeEmptyTable(content, tableName) {
|
|
65
|
+
const pattern = new RegExp(`\\n*\\[${escapeRegExp(tableName)}\\]\\n([\\s\\S]*?)(?=^\\[|(?![\\s\\S]))`, 'gm');
|
|
66
|
+
return content.replace(pattern, (match, body) => {
|
|
67
|
+
const stripped = body
|
|
68
|
+
.split('\n')
|
|
69
|
+
.map(line => line.replace(/#.*/, '').trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
|
|
72
|
+
return stripped.length === 0 ? '\n' : match;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function removeScalarKey(content, tableName, key) {
|
|
77
|
+
const pattern = tablePattern(tableName);
|
|
78
|
+
const match = content.match(pattern);
|
|
79
|
+
if (!match) {
|
|
80
|
+
return content;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const body = match[2];
|
|
84
|
+
const keyPattern = new RegExp(`^${escapeRegExp(key)}\\s*=\\s*.*\\n?`, 'm');
|
|
85
|
+
if (!keyPattern.test(body)) {
|
|
86
|
+
return content;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const nextBody = body.replace(keyPattern, '');
|
|
90
|
+
return content.replace(pattern, `${match[1]}${nextBody}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function removeRootScalarKey(content, key) {
|
|
94
|
+
return content.replace(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*.*\\n?`, 'm'), '');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function upsertRootScalarKey(content, key, valueExpression) {
|
|
98
|
+
const pattern = new RegExp(`^${escapeRegExp(key)}\\s*=\\s*.*$`, 'm');
|
|
99
|
+
const line = `${key} = ${valueExpression}`;
|
|
100
|
+
|
|
101
|
+
if (pattern.test(content)) {
|
|
102
|
+
return content.replace(pattern, line);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const markerPattern = new RegExp(`^# ${escapeRegExp(MANAGED_CONFIG_MARKER)}$`, 'm');
|
|
106
|
+
if (markerPattern.test(content)) {
|
|
107
|
+
return content.replace(markerPattern, `# ${MANAGED_CONFIG_MARKER}\n${line}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `${line}\n${content.trimStart()}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function upsertScalarKey(content, tableName, key, valueExpression) {
|
|
114
|
+
const nextContent = ensureTable(content, tableName);
|
|
115
|
+
const pattern = tablePattern(tableName);
|
|
116
|
+
const match = nextContent.match(pattern);
|
|
117
|
+
if (!match) {
|
|
118
|
+
return `${nextContent.trimEnd()}\n\n[${tableName}]\n${key} = ${valueExpression}\n`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const body = match[2];
|
|
122
|
+
const keyPattern = new RegExp(`^${escapeRegExp(key)}\\s*=\\s*.*$`, 'm');
|
|
123
|
+
const line = `${key} = ${valueExpression}`;
|
|
124
|
+
const nextBody = keyPattern.test(body)
|
|
125
|
+
? body.replace(keyPattern, line)
|
|
126
|
+
: `${body}${body.endsWith('\n') || body.length === 0 ? '' : '\n'}${line}\n`;
|
|
127
|
+
|
|
128
|
+
return nextContent.replace(pattern, `${match[1]}${nextBody}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function removeEntryFromArrayLine(content, tableName, key, entry) {
|
|
132
|
+
const pattern = tablePattern(tableName);
|
|
133
|
+
const match = content.match(pattern);
|
|
134
|
+
if (!match) {
|
|
135
|
+
return content;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const body = match[2];
|
|
139
|
+
const keyPattern = new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\\[(.*)\\]\\s*$`, 'm');
|
|
140
|
+
const keyMatch = body.match(keyPattern);
|
|
141
|
+
if (!keyMatch) {
|
|
142
|
+
return content;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const rawEntries = keyMatch[1].trim();
|
|
146
|
+
const tokens = [];
|
|
147
|
+
let depth = 0;
|
|
148
|
+
let current = '';
|
|
149
|
+
|
|
150
|
+
for (const char of rawEntries) {
|
|
151
|
+
if (char === '[' || char === '{') {
|
|
152
|
+
depth += 1;
|
|
153
|
+
} else if (char === ']' || char === '}') {
|
|
154
|
+
depth -= 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (char === ',' && depth === 0) {
|
|
158
|
+
tokens.push(current.trim());
|
|
159
|
+
current = '';
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
current += char;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (current.trim()) {
|
|
167
|
+
tokens.push(current.trim());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalize = value => value.replace(/\s+/g, ' ').trim();
|
|
171
|
+
const nextTokens = tokens.filter(token => normalize(token) !== normalize(entry));
|
|
172
|
+
const nextBody = nextTokens.length
|
|
173
|
+
? body.replace(keyPattern, `${key} = [${nextTokens.join(', ')}]`)
|
|
174
|
+
: body.replace(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\\[.*\\]\\s*\\n?`, 'm'), '');
|
|
175
|
+
|
|
176
|
+
return content.replace(pattern, `${match[1]}${nextBody}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseArrayTableEntries(content, tableName) {
|
|
180
|
+
const escaped = escapeRegExp(tableName);
|
|
181
|
+
const pattern = new RegExp(`(^\\[\\[${escaped}\\]\\]\\n)([\\s\\S]*?)(?=^\\[\\[${escaped}\\]\\]\\n|^\\[(?!\\[)|(?![\\s\\S]))`, 'gm');
|
|
182
|
+
const entries = [];
|
|
183
|
+
|
|
184
|
+
for (const match of content.matchAll(pattern)) {
|
|
185
|
+
const body = match[2].trimEnd();
|
|
186
|
+
const pathMatch = body.match(/^path\s*=\s*"([^"]+)"/m);
|
|
187
|
+
const enabledMatch = body.match(/^enabled\s*=\s*(true|false)/m);
|
|
188
|
+
entries.push({
|
|
189
|
+
raw: match[0],
|
|
190
|
+
body,
|
|
191
|
+
path: pathMatch ? pathMatch[1] : null,
|
|
192
|
+
enabled: enabledMatch ? enabledMatch[1] === 'true' : null
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return entries;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function removeArrayTableEntries(content, tableName) {
|
|
200
|
+
const escaped = escapeRegExp(tableName);
|
|
201
|
+
return content.replace(new RegExp(`\\n*\\[\\[${escaped}\\]\\]\\n[\\s\\S]*?(?=^\\[\\[${escaped}\\]\\]\\n|^\\[(?!\\[)|(?![\\s\\S]))`, 'gm'), '\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderSkillsConfigEntries(entries) {
|
|
205
|
+
if (entries.length === 0) {
|
|
206
|
+
return '';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return `${entries.map(entry => [
|
|
210
|
+
'[[skills.config]]',
|
|
211
|
+
`path = "${escapeTomlString(entry.path)}"`,
|
|
212
|
+
`enabled = ${entry.enabled ? 'true' : 'false'}`
|
|
213
|
+
].join('\n')).join('\n\n')}\n`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function removeLegacyProjectSkillTables(content) {
|
|
217
|
+
return content.replace(/\n*\[skills\.config\.spectre_[^\]]+\]\n[\s\S]*?(?=^\[\[skills\.config\]\]\n|^\[(?!\[)|(?![\s\S]))/gm, '\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function shellQuote(value) {
|
|
221
|
+
return `'${value.replaceAll('\'', `'\\''`)}'`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readHooksConfig() {
|
|
225
|
+
const hooksPath = codexHooksConfigPath();
|
|
226
|
+
ensureDir(path.dirname(hooksPath));
|
|
227
|
+
|
|
228
|
+
if (!fs.existsSync(hooksPath)) {
|
|
229
|
+
return {
|
|
230
|
+
hooksPath,
|
|
231
|
+
config: { hooks: {} }
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const raw = fs.readFileSync(hooksPath, 'utf8').trim();
|
|
236
|
+
if (!raw) {
|
|
237
|
+
return {
|
|
238
|
+
hooksPath,
|
|
239
|
+
config: { hooks: {} }
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const parsed = JSON.parse(raw);
|
|
244
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
245
|
+
throw new Error(`Malformed Codex hooks config at ${hooksPath}: expected a JSON object.`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const hooks = parsed.hooks;
|
|
249
|
+
if (hooks != null && (typeof hooks !== 'object' || Array.isArray(hooks))) {
|
|
250
|
+
throw new Error(`Malformed Codex hooks config at ${hooksPath}: expected "hooks" to be an object.`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
hooksPath,
|
|
255
|
+
config: {
|
|
256
|
+
...parsed,
|
|
257
|
+
hooks: hooks ?? {}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function writeHooksConfig(hooksPath, config) {
|
|
263
|
+
const normalizedHooks = Object.fromEntries(
|
|
264
|
+
Object.entries(config.hooks ?? {}).filter(([, groups]) => Array.isArray(groups) && groups.length > 0)
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (Object.keys(normalizedHooks).length === 0) {
|
|
268
|
+
if (fs.existsSync(hooksPath)) {
|
|
269
|
+
fs.unlinkSync(hooksPath);
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const nextConfig = {
|
|
275
|
+
...config,
|
|
276
|
+
hooks: normalizedHooks
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
fs.writeFileSync(hooksPath, `${JSON.stringify(nextConfig, null, 2)}\n`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function spectreSessionStartCommand(runtimeRoot) {
|
|
283
|
+
return `node ${shellQuote(path.join(runtimeRoot, 'hooks', 'session-start.mjs'))}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isSpectreSessionStartHook(hook) {
|
|
287
|
+
return hook
|
|
288
|
+
&& typeof hook === 'object'
|
|
289
|
+
&& hook.type === 'command'
|
|
290
|
+
&& typeof hook.command === 'string'
|
|
291
|
+
&& hook.command.includes('spectre/hooks/session-start.mjs');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function upsertSpectreSessionStartHook(runtimeRoot) {
|
|
295
|
+
const { hooksPath, config } = readHooksConfig();
|
|
296
|
+
const hooks = { ...(config.hooks ?? {}) };
|
|
297
|
+
const groups = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
|
|
298
|
+
const nextGroups = [];
|
|
299
|
+
|
|
300
|
+
for (const group of groups) {
|
|
301
|
+
if (!group || typeof group !== 'object' || Array.isArray(group)) {
|
|
302
|
+
nextGroups.push(group);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const hookList = Array.isArray(group.hooks) ? group.hooks.filter(hook => !isSpectreSessionStartHook(hook)) : [];
|
|
307
|
+
if (hookList.length > 0) {
|
|
308
|
+
nextGroups.push({
|
|
309
|
+
...group,
|
|
310
|
+
hooks: hookList
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
nextGroups.push({
|
|
316
|
+
hooks: [
|
|
317
|
+
{
|
|
318
|
+
type: 'command',
|
|
319
|
+
command: spectreSessionStartCommand(runtimeRoot),
|
|
320
|
+
statusMessage: 'Spectre: loading session context'
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
hooks.SessionStart = nextGroups;
|
|
326
|
+
writeHooksConfig(hooksPath, {
|
|
327
|
+
...config,
|
|
328
|
+
hooks
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function removeSpectreSessionStartHook() {
|
|
333
|
+
const hooksPath = codexHooksConfigPath();
|
|
334
|
+
if (!fs.existsSync(hooksPath)) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const { config } = readHooksConfig();
|
|
339
|
+
const hooks = { ...(config.hooks ?? {}) };
|
|
340
|
+
const groups = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
|
|
341
|
+
let removed = false;
|
|
342
|
+
const nextGroups = [];
|
|
343
|
+
|
|
344
|
+
for (const group of groups) {
|
|
345
|
+
if (!group || typeof group !== 'object' || Array.isArray(group)) {
|
|
346
|
+
nextGroups.push(group);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const originalHooks = Array.isArray(group.hooks) ? group.hooks : [];
|
|
351
|
+
const filteredHooks = originalHooks.filter(hook => {
|
|
352
|
+
const shouldRemove = isSpectreSessionStartHook(hook);
|
|
353
|
+
removed ||= shouldRemove;
|
|
354
|
+
return !shouldRemove;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (filteredHooks.length > 0) {
|
|
358
|
+
nextGroups.push({
|
|
359
|
+
...group,
|
|
360
|
+
hooks: filteredHooks
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (nextGroups.length > 0) {
|
|
366
|
+
hooks.SessionStart = nextGroups;
|
|
367
|
+
} else {
|
|
368
|
+
delete hooks.SessionStart;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
writeHooksConfig(hooksPath, {
|
|
372
|
+
...config,
|
|
373
|
+
hooks
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
return removed;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function hasRemainingHookDefinitions() {
|
|
380
|
+
const hooksPath = codexHooksConfigPath();
|
|
381
|
+
if (!fs.existsSync(hooksPath)) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const { config } = readHooksConfig();
|
|
386
|
+
return Object.values(config.hooks ?? {}).some(groups => Array.isArray(groups) && groups.length > 0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function hasRemainingAgentTables(content) {
|
|
390
|
+
return /^\[agents\.[^\]]+\]$/m.test(content);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function syncProjectSkillsConfigured(projectDir) {
|
|
394
|
+
const { configPath, content: initialContent } = readConfig();
|
|
395
|
+
const paths = projectPaths(projectDir);
|
|
396
|
+
const projectSkillsRoot = path.resolve(paths.projectSkillsDir);
|
|
397
|
+
const existingEntries = parseArrayTableEntries(initialContent, 'skills.config');
|
|
398
|
+
const unrelatedEntries = existingEntries.filter(entry =>
|
|
399
|
+
!(entry.path && path.resolve(entry.path).startsWith(`${projectSkillsRoot}${path.sep}`))
|
|
400
|
+
);
|
|
401
|
+
const projectEntries = [];
|
|
402
|
+
|
|
403
|
+
if (fs.existsSync(paths.projectSkillsDir)) {
|
|
404
|
+
for (const entry of fs.readdirSync(paths.projectSkillsDir, { withFileTypes: true })) {
|
|
405
|
+
if (!entry.isDirectory()) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const skillPath = path.join(paths.projectSkillsDir, entry.name, 'SKILL.md');
|
|
410
|
+
if (!fs.existsSync(skillPath)) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
projectEntries.push({
|
|
415
|
+
path: skillPath,
|
|
416
|
+
enabled: true
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let content = removeLegacyProjectSkillTables(removeArrayTableEntries(initialContent, 'skills.config')).trimEnd();
|
|
422
|
+
const nextEntries = unrelatedEntries.concat(projectEntries);
|
|
423
|
+
const renderedEntries = renderSkillsConfigEntries(nextEntries).trimEnd();
|
|
424
|
+
if (renderedEntries) {
|
|
425
|
+
content = `${content}\n\n${renderedEntries}`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
writeConfig(configPath, content);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function removeProjectSkillsConfigured(projectDir) {
|
|
432
|
+
const configPath = codexConfigPath();
|
|
433
|
+
if (!fs.existsSync(configPath)) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const projectSkillsRoot = path.resolve(projectPaths(projectDir).projectSkillsDir);
|
|
438
|
+
const initialContent = fs.readFileSync(configPath, 'utf8');
|
|
439
|
+
const remainingEntries = parseArrayTableEntries(initialContent, 'skills.config').filter(entry =>
|
|
440
|
+
!(entry.path && path.resolve(entry.path).startsWith(`${projectSkillsRoot}${path.sep}`))
|
|
441
|
+
);
|
|
442
|
+
let content = removeLegacyProjectSkillTables(removeArrayTableEntries(initialContent, 'skills.config')).trimEnd();
|
|
443
|
+
const renderedEntries = renderSkillsConfigEntries(remainingEntries).trimEnd();
|
|
444
|
+
if (renderedEntries) {
|
|
445
|
+
content = `${content}\n\n${renderedEntries}`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
writeConfig(configPath, content);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function ensureSpectreHooksConfigured(runtimeRoot, agents) {
|
|
452
|
+
const { configPath, content: initialContent } = readConfig();
|
|
453
|
+
let content = initialContent;
|
|
454
|
+
|
|
455
|
+
const preSessionEntry = `{ command = ["node", "${escapeTomlString(path.join(runtimeRoot, 'hooks', 'pre-session-start.mjs'))}"] }`;
|
|
456
|
+
|
|
457
|
+
content = removeScalarKey(content, 'hooks', 'session_start');
|
|
458
|
+
content = removeEntryFromArrayLine(content, 'hooks.blocking', 'pre_session_start', preSessionEntry);
|
|
459
|
+
content = upsertRootScalarKey(content, 'suppress_unstable_features_warning', 'true');
|
|
460
|
+
content = upsertScalarKey(content, 'features', 'codex_hooks', 'true');
|
|
461
|
+
content = upsertScalarKey(content, 'features', 'skills', 'true');
|
|
462
|
+
content = upsertScalarKey(content, 'features', 'multi_agent', 'true');
|
|
463
|
+
content = removeEmptyTable(content, 'hooks');
|
|
464
|
+
content = removeEmptyTable(content, 'hooks.blocking');
|
|
465
|
+
|
|
466
|
+
for (const agent of agents) {
|
|
467
|
+
const tableName = `agents.spectre_${agent.id}`;
|
|
468
|
+
const nicknames = agent.nicknames.map(name => `"${escapeTomlString(name)}"`).join(', ');
|
|
469
|
+
content = removeTable(content, tableName);
|
|
470
|
+
content = replaceTable(
|
|
471
|
+
content,
|
|
472
|
+
tableName,
|
|
473
|
+
[
|
|
474
|
+
`description = "${escapeTomlString(agent.description)}"`,
|
|
475
|
+
`config_file = "${escapeTomlString(agent.configFile)}"`,
|
|
476
|
+
`nickname_candidates = [${nicknames}]`
|
|
477
|
+
].join('\n')
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
writeConfig(configPath, content);
|
|
482
|
+
upsertSpectreSessionStartHook(runtimeRoot);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function removeSpectreHooksConfigured(runtimeRoot, agents) {
|
|
486
|
+
const configPath = codexConfigPath();
|
|
487
|
+
if (!fs.existsSync(configPath)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let content = fs.readFileSync(configPath, 'utf8');
|
|
492
|
+
const preSessionEntry = `{ command = ["node", "${escapeTomlString(path.join(runtimeRoot, 'hooks', 'pre-session-start.mjs'))}"] }`;
|
|
493
|
+
|
|
494
|
+
const removedSessionStartHook = removeSpectreSessionStartHook();
|
|
495
|
+
|
|
496
|
+
content = removeScalarKey(content, 'hooks', 'session_start');
|
|
497
|
+
content = removeEntryFromArrayLine(content, 'hooks.blocking', 'pre_session_start', preSessionEntry);
|
|
498
|
+
content = removeEmptyTable(content, 'hooks');
|
|
499
|
+
content = removeEmptyTable(content, 'hooks.blocking');
|
|
500
|
+
|
|
501
|
+
for (const agent of agents) {
|
|
502
|
+
content = removeTable(content, `agents.spectre_${agent.id}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (removedSessionStartHook && !hasRemainingHookDefinitions()) {
|
|
506
|
+
content = removeScalarKey(content, 'features', 'codex_hooks');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!hasRemainingAgentTables(content)) {
|
|
510
|
+
content = removeScalarKey(content, 'features', 'multi_agent');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
content = removeEmptyTable(content, 'features');
|
|
514
|
+
|
|
515
|
+
writeConfig(configPath, content);
|
|
516
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { repoRoot, spectrePluginRoot } from './paths.js';
|
|
4
|
+
|
|
5
|
+
export const MANIFEST_VERSION = 1;
|
|
6
|
+
export const MIN_CODEX_VERSION = '0.110.0';
|
|
7
|
+
export const MANAGED_CONFIG_MARKER = 'spectre-codex-managed';
|
|
8
|
+
export const AGENTS_BRIDGE_START = '<!-- spectre-codex:start -->';
|
|
9
|
+
export const AGENTS_BRIDGE_END = '<!-- spectre-codex:end -->';
|
|
10
|
+
export const SESSION_OVERRIDE_START = '<!-- spectre-session:start -->';
|
|
11
|
+
export const SESSION_OVERRIDE_END = '<!-- spectre-session:end -->';
|
|
12
|
+
export const KNOWLEDGE_OVERRIDE_START = '<!-- spectre-knowledge:start -->';
|
|
13
|
+
export const KNOWLEDGE_OVERRIDE_END = '<!-- spectre-knowledge:end -->';
|
|
14
|
+
|
|
15
|
+
export function listSpectreCommands() {
|
|
16
|
+
const commandsDir = path.join(spectrePluginRoot(), 'commands');
|
|
17
|
+
return fs.readdirSync(commandsDir)
|
|
18
|
+
.filter(name => name.endsWith('.md'))
|
|
19
|
+
.map(name => path.basename(name, '.md'))
|
|
20
|
+
.sort();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function codexCommandSkillName(commandName) {
|
|
24
|
+
if (commandName === 'learn') {
|
|
25
|
+
return 'spectre-learn';
|
|
26
|
+
}
|
|
27
|
+
if (commandName === 'recall') {
|
|
28
|
+
return 'spectre-recall';
|
|
29
|
+
}
|
|
30
|
+
return `spectre-${commandName}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function listCodexWorkflowCommands() {
|
|
34
|
+
return listSpectreCommands().filter(commandName => !['learn', 'recall'].includes(commandName));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function listSpectreAgents() {
|
|
38
|
+
const agentsDir = path.join(spectrePluginRoot(), 'agents');
|
|
39
|
+
return fs.readdirSync(agentsDir)
|
|
40
|
+
.filter(name => name.endsWith('.md'))
|
|
41
|
+
.map(name => path.basename(name, '.md'))
|
|
42
|
+
.sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const SHARED_SKILLS = [
|
|
46
|
+
'spectre-apply',
|
|
47
|
+
'spectre-guide',
|
|
48
|
+
'spectre-learn',
|
|
49
|
+
'spectre-tdd'
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export function repoMetadata() {
|
|
53
|
+
const packageJson = JSON.parse(
|
|
54
|
+
fs.readFileSync(path.join(repoRoot(), 'package.json'), 'utf8')
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
name: packageJson.name,
|
|
58
|
+
version: packageJson.version
|
|
59
|
+
};
|
|
60
|
+
}
|