@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,473 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import {
|
|
5
|
+
AGENTS_BRIDGE_END,
|
|
6
|
+
AGENTS_BRIDGE_START,
|
|
7
|
+
KNOWLEDGE_OVERRIDE_END,
|
|
8
|
+
KNOWLEDGE_OVERRIDE_START,
|
|
9
|
+
MANIFEST_VERSION,
|
|
10
|
+
SESSION_OVERRIDE_END,
|
|
11
|
+
SESSION_OVERRIDE_START,
|
|
12
|
+
repoMetadata
|
|
13
|
+
} from './constants.js';
|
|
14
|
+
import {
|
|
15
|
+
buildKnowledgeOverrideBody,
|
|
16
|
+
ensureKnowledgeFiles,
|
|
17
|
+
knowledgeStatusMessage,
|
|
18
|
+
readKnowledgeRegistry
|
|
19
|
+
} from './knowledge.js';
|
|
20
|
+
import { ensureDir, projectPaths } from './paths.js';
|
|
21
|
+
|
|
22
|
+
function gitBranch(projectDir) {
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync('git', ['symbolic-ref', '--short', 'HEAD'], {
|
|
25
|
+
cwd: projectDir,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
28
|
+
}).trim();
|
|
29
|
+
} catch {
|
|
30
|
+
// Fall through to rev-parse for detached HEAD or older setups.
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
35
|
+
cwd: projectDir,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
38
|
+
}).trim();
|
|
39
|
+
} catch {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findLatestHandoff(projectDir, branchName) {
|
|
45
|
+
const sessionDir = path.join(projectDir, 'docs', 'tasks', branchName, 'session_logs');
|
|
46
|
+
if (!fs.existsSync(sessionDir)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const files = fs.readdirSync(sessionDir)
|
|
51
|
+
.filter(name => name.endsWith('_handoff.json'))
|
|
52
|
+
.map(name => {
|
|
53
|
+
const fullPath = path.join(sessionDir, name);
|
|
54
|
+
return {
|
|
55
|
+
fullPath,
|
|
56
|
+
mtimeMs: fs.statSync(fullPath).mtimeMs
|
|
57
|
+
};
|
|
58
|
+
})
|
|
59
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
60
|
+
|
|
61
|
+
return files[0]?.fullPath ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatList(items, fallback) {
|
|
65
|
+
const emptyValue = fallback ?? '- None recorded.';
|
|
66
|
+
if (!items || items.length === 0) {
|
|
67
|
+
return emptyValue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return items.map(item => `- ${item}`).join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildCheckboxTree(tasks) {
|
|
74
|
+
if (!tasks || tasks.length === 0) {
|
|
75
|
+
return 'No tasks found.';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const byParent = {};
|
|
79
|
+
for (const task of tasks) {
|
|
80
|
+
const parent = task.parent || null;
|
|
81
|
+
if (!byParent[parent]) {
|
|
82
|
+
byParent[parent] = [];
|
|
83
|
+
}
|
|
84
|
+
byParent[parent].push(task);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderTask(task, indent = 0) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
const prefix = ' '.repeat(indent);
|
|
90
|
+
const checkbox = task.completed ? '[x]' : '[ ]';
|
|
91
|
+
const status = task.status || 'open';
|
|
92
|
+
const title = task.title || 'Untitled';
|
|
93
|
+
const taskId = task.id || 'unknown';
|
|
94
|
+
|
|
95
|
+
if (task.completed) {
|
|
96
|
+
lines.push(`${prefix}- ${checkbox} ${title} (${taskId}) - COMPLETED`);
|
|
97
|
+
} else {
|
|
98
|
+
const cmd = task.resume_command || `bd update ${taskId} --status in_progress`;
|
|
99
|
+
const statusBadge = status !== 'open' ? `[${status}]` : '';
|
|
100
|
+
lines.push(`${prefix}- ${checkbox} ${title} (${taskId}) ${statusBadge} - \`${cmd}\``);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const childrenIds = task.children || [];
|
|
104
|
+
if (childrenIds.length > 0) {
|
|
105
|
+
for (const childTask of tasks) {
|
|
106
|
+
if (childrenIds.includes(childTask.id) || childTask.parent === taskId) {
|
|
107
|
+
lines.push(...renderTask(childTask, indent + 1));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return lines;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rootTasks = (byParent[null] || []).concat(byParent.null || []);
|
|
116
|
+
const startTasks = rootTasks.length > 0 ? rootTasks : tasks;
|
|
117
|
+
const lines = [];
|
|
118
|
+
const renderedIds = new Set();
|
|
119
|
+
|
|
120
|
+
for (const task of startTasks) {
|
|
121
|
+
if (renderedIds.has(task.id)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const taskLines = renderTask(task);
|
|
126
|
+
lines.push(...taskLines);
|
|
127
|
+
renderedIds.add(task.id);
|
|
128
|
+
|
|
129
|
+
for (const entry of tasks) {
|
|
130
|
+
if (entry.parent === task.id) {
|
|
131
|
+
renderedIds.add(entry.id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildVisibleResumeNotice(options = {}) {
|
|
140
|
+
const parts = ['🟢 👻 SPECTRE active'];
|
|
141
|
+
|
|
142
|
+
if (options.handoffPath) {
|
|
143
|
+
parts.push(`injected ${options.handoffPath}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (options.knowledgeStatus) {
|
|
147
|
+
parts.push(options.knowledgeStatus);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return parts.join(' | ');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildSessionOverrideContent(handoff, options = {}) {
|
|
154
|
+
const branchName = handoff.branch_name || options.branchName || 'unknown';
|
|
155
|
+
const handoffPath = options.handoffPath || 'unknown';
|
|
156
|
+
const source = options.source || 'unknown';
|
|
157
|
+
|
|
158
|
+
const taskName = handoff.task_name || branchName;
|
|
159
|
+
const progress = handoff.progress_update || {};
|
|
160
|
+
const summary = progress.summary || 'No summary available.';
|
|
161
|
+
const goal = progress.goal || '';
|
|
162
|
+
const constraints = progress.constraints || [];
|
|
163
|
+
const decisions = progress.decisions || [];
|
|
164
|
+
const accomplished = progress.accomplished || [];
|
|
165
|
+
const now = progress.now || '';
|
|
166
|
+
const nextSteps = progress.next_steps || [];
|
|
167
|
+
const blockers = progress.blockers || [];
|
|
168
|
+
const openQuestions = progress.open_questions || [];
|
|
169
|
+
const confidence = progress.confidence || 'unknown';
|
|
170
|
+
const risks = progress.risks || [];
|
|
171
|
+
const workingSet = handoff.working_set || {};
|
|
172
|
+
const keyFiles = workingSet.key_files || [];
|
|
173
|
+
const activeIds = workingSet.active_ids || [];
|
|
174
|
+
const recentCommands = workingSet.recent_commands || [];
|
|
175
|
+
const context = handoff.context || {};
|
|
176
|
+
const beads = handoff.beads || {};
|
|
177
|
+
const beadsAvailable = beads.available != null ? beads.available : true;
|
|
178
|
+
const tasks = beads.tasks || [];
|
|
179
|
+
const checkboxTree = beadsAvailable && tasks.length > 0 ? buildCheckboxTree(tasks) : '';
|
|
180
|
+
|
|
181
|
+
const sections = [];
|
|
182
|
+
sections.push(`# Session Context: ${taskName}`);
|
|
183
|
+
sections.push(`\n## Last Session Summary\n${summary}`);
|
|
184
|
+
|
|
185
|
+
if (goal) {
|
|
186
|
+
sections.push(`\n### Goal\n${goal}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (constraints.length > 0) {
|
|
190
|
+
sections.push(`\n### Constraints\n${formatList(constraints, '- None recorded.')}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
sections.push(`\n### What We Accomplished\n${formatList(accomplished, '- None recorded.')}`);
|
|
194
|
+
|
|
195
|
+
if (now) {
|
|
196
|
+
sections.push(`\n### Active Work (Resume Here)\n**${now}**`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sections.push(`\n### What's Next\n${formatList(nextSteps, '- None recorded.')}`);
|
|
200
|
+
|
|
201
|
+
if (blockers.length > 0) {
|
|
202
|
+
sections.push(`\n### Blockers\n${formatList(blockers, '- None recorded.')}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (openQuestions.length > 0) {
|
|
206
|
+
sections.push(`\n### Open Questions\n${formatList(openQuestions, '- None recorded.')}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (decisions.length > 0) {
|
|
210
|
+
sections.push(`\n### Decisions Made\n${formatList(decisions, '- None recorded.')}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const risksSummary = risks.length > 0 ? formatList(risks, '') : 'None identified';
|
|
214
|
+
sections.push(`\n**Confidence**: ${confidence} | **Risks**: ${risksSummary}`);
|
|
215
|
+
|
|
216
|
+
const workingSetLines = [];
|
|
217
|
+
if (keyFiles.length > 0) {
|
|
218
|
+
workingSetLines.push(`- **Key Files**: ${keyFiles.join(', ')}`);
|
|
219
|
+
}
|
|
220
|
+
if (activeIds.length > 0) {
|
|
221
|
+
workingSetLines.push(`- **Active IDs**: ${activeIds.join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
if (recentCommands.length > 0) {
|
|
224
|
+
workingSetLines.push(`- **Recent Commands**: ${recentCommands.join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
if (workingSetLines.length > 0) {
|
|
227
|
+
sections.push(`\n### Working Set\n${workingSetLines.join('\n')}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
sections.push(
|
|
231
|
+
'\n### Spectre Notes\n' +
|
|
232
|
+
'- Preserve Spectre artifact paths under `docs/tasks/{branch}/...`.\n' +
|
|
233
|
+
'- Prefer existing project skills under `.agents/skills/` before rediscovering codebase context.\n' +
|
|
234
|
+
'- Treat `.spectre/manifest.json` as Spectre install metadata.\n' +
|
|
235
|
+
`- **SessionStart Source**: ${source}\n` +
|
|
236
|
+
`- **Snapshot**: ${handoffPath}`
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
sections.push(
|
|
240
|
+
'\n---\n\n' +
|
|
241
|
+
'## Context\n' +
|
|
242
|
+
`- **Branch**: ${branchName}\n` +
|
|
243
|
+
`- **Last Commit**: ${context.last_commit || 'unknown'}\n` +
|
|
244
|
+
`- **WIP State**: ${context.wip_state || 'unknown'}`
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (beadsAvailable && checkboxTree) {
|
|
248
|
+
sections.push(`\n### Beads Tasks\n${checkboxTree}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return sections.join('');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function escapeRegExp(value) {
|
|
255
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function managedOverridePattern(startMarker, endMarker) {
|
|
259
|
+
return new RegExp(`\\n?${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, 'm');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizeOverrideFile(content) {
|
|
263
|
+
return content
|
|
264
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
265
|
+
.trim();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function writeManagedOverride(overridePath, startMarker, endMarker, bodyContent) {
|
|
269
|
+
const current = fs.existsSync(overridePath) ? fs.readFileSync(overridePath, 'utf8') : '';
|
|
270
|
+
const pattern = managedOverridePattern(startMarker, endMarker);
|
|
271
|
+
const blockContent = `${startMarker}\n${bodyContent}\n${endMarker}`;
|
|
272
|
+
let updated;
|
|
273
|
+
|
|
274
|
+
if (pattern.test(current)) {
|
|
275
|
+
updated = current.replace(pattern, `${blockContent}\n`);
|
|
276
|
+
} else if (current.trim()) {
|
|
277
|
+
updated = `${current.trimEnd()}\n\n${blockContent}\n`;
|
|
278
|
+
} else {
|
|
279
|
+
updated = `${blockContent}\n`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const normalized = normalizeOverrideFile(updated);
|
|
283
|
+
ensureDir(path.dirname(overridePath));
|
|
284
|
+
fs.writeFileSync(overridePath, normalized ? `${normalized}\n` : '');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function removeManagedOverride(overridePath, startMarker, endMarker) {
|
|
288
|
+
if (!fs.existsSync(overridePath)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const current = fs.readFileSync(overridePath, 'utf8');
|
|
293
|
+
const updated = normalizeOverrideFile(current.replace(managedOverridePattern(startMarker, endMarker), '\n'));
|
|
294
|
+
|
|
295
|
+
if (!updated) {
|
|
296
|
+
fs.unlinkSync(overridePath);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fs.writeFileSync(overridePath, `${updated}\n`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function clearSessionOverride(projectDir) {
|
|
304
|
+
removeManagedOverride(projectPaths(projectDir).overrideAgentsPath, SESSION_OVERRIDE_START, SESSION_OVERRIDE_END);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function syncSessionOverride(projectDir, payload = {}) {
|
|
308
|
+
const branchName = gitBranch(projectDir);
|
|
309
|
+
const latestHandoff = findLatestHandoff(projectDir, branchName);
|
|
310
|
+
if (!latestHandoff) {
|
|
311
|
+
clearSessionOverride(projectDir);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let handoff;
|
|
316
|
+
try {
|
|
317
|
+
handoff = JSON.parse(fs.readFileSync(latestHandoff, 'utf8'));
|
|
318
|
+
} catch {
|
|
319
|
+
clearSessionOverride(projectDir);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const handoffPath = path.relative(projectDir, latestHandoff) || latestHandoff;
|
|
324
|
+
const source = payload.source || 'unknown';
|
|
325
|
+
const overridePath = projectPaths(projectDir).overrideAgentsPath;
|
|
326
|
+
writeManagedOverride(
|
|
327
|
+
overridePath,
|
|
328
|
+
SESSION_OVERRIDE_START,
|
|
329
|
+
SESSION_OVERRIDE_END,
|
|
330
|
+
[
|
|
331
|
+
'## SPECTRE Session Context',
|
|
332
|
+
'',
|
|
333
|
+
'This block is managed by SPECTRE and replaced automatically on session start.',
|
|
334
|
+
'Use it as prior working context for this repository session.',
|
|
335
|
+
'',
|
|
336
|
+
buildSessionOverrideContent(handoff, {
|
|
337
|
+
branchName,
|
|
338
|
+
handoffPath,
|
|
339
|
+
source
|
|
340
|
+
})
|
|
341
|
+
].join('\n')
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
handoff,
|
|
346
|
+
handoffPath,
|
|
347
|
+
branchName,
|
|
348
|
+
source
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function clearKnowledgeOverride(projectDir) {
|
|
353
|
+
removeManagedOverride(projectPaths(projectDir).overrideAgentsPath, KNOWLEDGE_OVERRIDE_START, KNOWLEDGE_OVERRIDE_END);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function syncKnowledgeOverride(projectDir) {
|
|
357
|
+
ensureKnowledgeFiles(projectDir);
|
|
358
|
+
const { overrideAgentsPath, knowledgeRegistryPath } = projectPaths(projectDir);
|
|
359
|
+
const { entryCount } = readKnowledgeRegistry(projectDir);
|
|
360
|
+
writeManagedOverride(
|
|
361
|
+
overrideAgentsPath,
|
|
362
|
+
KNOWLEDGE_OVERRIDE_START,
|
|
363
|
+
KNOWLEDGE_OVERRIDE_END,
|
|
364
|
+
[
|
|
365
|
+
'## SPECTRE Knowledge Context',
|
|
366
|
+
'',
|
|
367
|
+
'This block is managed by SPECTRE and replaced automatically on session start.',
|
|
368
|
+
'Use it before searching or implementing work in this repository.',
|
|
369
|
+
'',
|
|
370
|
+
buildKnowledgeOverrideBody(projectDir)
|
|
371
|
+
].join('\n')
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
entryCount,
|
|
376
|
+
knowledgeStatus: knowledgeStatusMessage(projectDir),
|
|
377
|
+
registryPath: path.relative(projectDir, knowledgeRegistryPath) || knowledgeRegistryPath
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function buildSessionStartOutput(projectDir, payload = {}) {
|
|
382
|
+
const synced = syncSessionOverride(projectDir, payload);
|
|
383
|
+
const knowledge = syncKnowledgeOverride(projectDir);
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
systemMessage: buildVisibleResumeNotice({
|
|
387
|
+
handoffPath: synced?.handoffPath,
|
|
388
|
+
knowledgeStatus: knowledge.knowledgeStatus
|
|
389
|
+
}),
|
|
390
|
+
hookSpecificOutput: {
|
|
391
|
+
hookEventName: 'SessionStart'
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function removeBridge(rootAgentsPath) {
|
|
397
|
+
if (!fs.existsSync(rootAgentsPath)) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const current = fs.readFileSync(rootAgentsPath, 'utf8');
|
|
402
|
+
const pattern = new RegExp(`\\n?${AGENTS_BRIDGE_START}[\\s\\S]*?${AGENTS_BRIDGE_END}\\n?`, 'm');
|
|
403
|
+
const updated = current.replace(pattern, '\n').trimEnd();
|
|
404
|
+
|
|
405
|
+
if (!updated) {
|
|
406
|
+
fs.unlinkSync(rootAgentsPath);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
fs.writeFileSync(rootAgentsPath, `${updated}\n`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function cleanupLegacyProjectContext(projectDir) {
|
|
414
|
+
const paths = projectPaths(projectDir);
|
|
415
|
+
removeBridge(paths.rootAgentsPath);
|
|
416
|
+
removeManagedOverride(paths.overrideAgentsPath, SESSION_OVERRIDE_START, SESSION_OVERRIDE_END);
|
|
417
|
+
removeManagedOverride(paths.overrideAgentsPath, KNOWLEDGE_OVERRIDE_START, KNOWLEDGE_OVERRIDE_END);
|
|
418
|
+
|
|
419
|
+
if (fs.existsSync(paths.sessionSkillDir)) {
|
|
420
|
+
fs.rmSync(paths.sessionSkillDir, { recursive: true, force: true });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function installProjectFiles(projectDir, scope) {
|
|
425
|
+
const paths = projectPaths(projectDir);
|
|
426
|
+
ensureDir(paths.spectreDir);
|
|
427
|
+
ensureDir(paths.projectSkillsDir);
|
|
428
|
+
|
|
429
|
+
const metadata = repoMetadata();
|
|
430
|
+
const branchName = gitBranch(projectDir);
|
|
431
|
+
const manifest = {
|
|
432
|
+
version: MANIFEST_VERSION,
|
|
433
|
+
scope,
|
|
434
|
+
projectRoot: projectDir,
|
|
435
|
+
branchName,
|
|
436
|
+
spectreVersion: metadata.version,
|
|
437
|
+
codexIntegration: {
|
|
438
|
+
installedAt: new Date().toISOString(),
|
|
439
|
+
hiddenContextInjection: 'agents_override_managed_block',
|
|
440
|
+
fallback: 'none'
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
fs.writeFileSync(paths.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
445
|
+
ensureKnowledgeFiles(projectDir);
|
|
446
|
+
cleanupLegacyProjectContext(projectDir);
|
|
447
|
+
|
|
448
|
+
const legacyLauncherPath = path.join(paths.projectSpectreBinDir, 'codex');
|
|
449
|
+
if (fs.existsSync(legacyLauncherPath)) {
|
|
450
|
+
fs.unlinkSync(legacyLauncherPath);
|
|
451
|
+
}
|
|
452
|
+
if (fs.existsSync(paths.projectSpectreBinDir) && fs.readdirSync(paths.projectSpectreBinDir).length === 0) {
|
|
453
|
+
fs.rmdirSync(paths.projectSpectreBinDir);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function uninstallProjectFiles(projectDir) {
|
|
458
|
+
const paths = projectPaths(projectDir);
|
|
459
|
+
cleanupLegacyProjectContext(projectDir);
|
|
460
|
+
if (fs.existsSync(paths.manifestPath)) {
|
|
461
|
+
fs.unlinkSync(paths.manifestPath);
|
|
462
|
+
}
|
|
463
|
+
const legacyLauncherPath = path.join(paths.projectSpectreBinDir, 'codex');
|
|
464
|
+
if (fs.existsSync(legacyLauncherPath)) {
|
|
465
|
+
fs.unlinkSync(legacyLauncherPath);
|
|
466
|
+
}
|
|
467
|
+
if (fs.existsSync(paths.projectSpectreBinDir) && fs.readdirSync(paths.projectSpectreBinDir).length === 0) {
|
|
468
|
+
fs.rmdirSync(paths.projectSpectreBinDir);
|
|
469
|
+
}
|
|
470
|
+
if (fs.existsSync(paths.spectreDir) && fs.readdirSync(paths.spectreDir).length === 0) {
|
|
471
|
+
fs.rmdirSync(paths.spectreDir);
|
|
472
|
+
}
|
|
473
|
+
}
|
package/src/main.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import readline from 'readline/promises';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { runDoctor } from './lib/doctor.js';
|
|
5
|
+
import { installCodex, uninstallCodex } from './lib/install.js';
|
|
6
|
+
import { projectCodexHome } from './lib/paths.js';
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const positional = [];
|
|
10
|
+
const flags = new Map();
|
|
11
|
+
|
|
12
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
13
|
+
const value = argv[index];
|
|
14
|
+
if (value.startsWith('--')) {
|
|
15
|
+
const next = argv[index + 1];
|
|
16
|
+
if (!next || next.startsWith('--')) {
|
|
17
|
+
flags.set(value, true);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
flags.set(value, next);
|
|
21
|
+
index += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
positional.push(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { positional, flags };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function usage() {
|
|
32
|
+
return `Usage:
|
|
33
|
+
spectre install codex [--scope user|project] [--project-dir <path>]
|
|
34
|
+
spectre uninstall codex [--scope user|project] [--project-dir <path>]
|
|
35
|
+
spectre update codex [--scope user|project] [--project-dir <path>]
|
|
36
|
+
spectre doctor codex [--scope user|project] [--project-dir <path>] [--verify-hooks] [--json]
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveProjectDir(flags) {
|
|
41
|
+
const projectDir = flags.get('--project-dir');
|
|
42
|
+
return path.resolve(projectDir || process.cwd());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function detectInstalledScope(projectDir) {
|
|
46
|
+
const manifestPath = path.join(projectDir, '.spectre', 'manifest.json');
|
|
47
|
+
if (fs.existsSync(manifestPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
50
|
+
if (manifest.scope === 'project') {
|
|
51
|
+
return 'project';
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignore malformed manifests and fall back to global scope.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return 'user';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function promptForScope(command, projectDir) {
|
|
62
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
63
|
+
return command === 'install' ? 'project' : detectInstalledScope(projectDir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rl = readline.createInterface({
|
|
67
|
+
input: process.stdin,
|
|
68
|
+
output: process.stdout
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const prompt = command === 'install'
|
|
72
|
+
? 'Install scope? [p]roject or [u]ser: '
|
|
73
|
+
: 'Target scope? [p]roject or [u]ser: ';
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
while (true) {
|
|
77
|
+
const answer = (await rl.question(prompt)).trim().toLowerCase();
|
|
78
|
+
if (answer === 'p' || answer === 'project') {
|
|
79
|
+
return 'project';
|
|
80
|
+
}
|
|
81
|
+
if (answer === 'u' || answer === 'user') {
|
|
82
|
+
return 'user';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
rl.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function withScopedCodexHome(scope, projectDir, fn) {
|
|
91
|
+
const previous = process.env.CODEX_HOME;
|
|
92
|
+
if (scope === 'project') {
|
|
93
|
+
process.env.CODEX_HOME = projectCodexHome(projectDir);
|
|
94
|
+
} else if (previous == null) {
|
|
95
|
+
delete process.env.CODEX_HOME;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
return fn();
|
|
100
|
+
} finally {
|
|
101
|
+
if (previous == null) {
|
|
102
|
+
delete process.env.CODEX_HOME;
|
|
103
|
+
} else {
|
|
104
|
+
process.env.CODEX_HOME = previous;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function main(argv) {
|
|
110
|
+
const { positional, flags } = parseArgs(argv);
|
|
111
|
+
const [command, target] = positional;
|
|
112
|
+
|
|
113
|
+
if (!command || command === 'help' || command === '--help') {
|
|
114
|
+
process.stdout.write(usage());
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (target !== 'codex') {
|
|
119
|
+
throw new Error('Only the Codex target is currently implemented.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const projectDir = resolveProjectDir(flags);
|
|
123
|
+
const scope = flags.get('--scope') || await promptForScope(command, projectDir);
|
|
124
|
+
|
|
125
|
+
if (command === 'install') {
|
|
126
|
+
withScopedCodexHome(scope, projectDir, () => installCodex({ scope, projectDir }));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (command === 'uninstall') {
|
|
131
|
+
withScopedCodexHome(scope, projectDir, () => uninstallCodex({ scope, projectDir }));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (command === 'update') {
|
|
136
|
+
withScopedCodexHome(scope, projectDir, () => installCodex({ scope, projectDir }));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (command === 'doctor') {
|
|
141
|
+
withScopedCodexHome(scope, projectDir, () => runDoctor({
|
|
142
|
+
verifyHooks: Boolean(flags.get('--verify-hooks')),
|
|
143
|
+
json: Boolean(flags.get('--json')),
|
|
144
|
+
projectDir
|
|
145
|
+
}));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw new Error(`Unknown command "${command}".\n${usage()}`);
|
|
150
|
+
}
|