@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,168 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import {
|
|
5
|
+
codexCommandSkillName,
|
|
6
|
+
listCodexWorkflowCommands,
|
|
7
|
+
listSpectreAgents,
|
|
8
|
+
listSpectreCommands,
|
|
9
|
+
MIN_CODEX_VERSION
|
|
10
|
+
} from './constants.js';
|
|
11
|
+
import {
|
|
12
|
+
codexConfigPath,
|
|
13
|
+
codexHooksConfigPath,
|
|
14
|
+
codexRuntimeRoot,
|
|
15
|
+
codexSkillsDir,
|
|
16
|
+
projectPaths,
|
|
17
|
+
resolveCodexHome
|
|
18
|
+
} from './paths.js';
|
|
19
|
+
|
|
20
|
+
function compareVersions(left, right) {
|
|
21
|
+
const leftParts = left.split('.').map(Number);
|
|
22
|
+
const rightParts = right.split('.').map(Number);
|
|
23
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
24
|
+
for (let index = 0; index < length; index += 1) {
|
|
25
|
+
const leftValue = leftParts[index] ?? 0;
|
|
26
|
+
const rightValue = rightParts[index] ?? 0;
|
|
27
|
+
if (leftValue > rightValue) return 1;
|
|
28
|
+
if (leftValue < rightValue) return -1;
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function codexVersion() {
|
|
34
|
+
const output = execFileSync('codex', ['--version'], {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
37
|
+
}).trim();
|
|
38
|
+
const versionMatch = output.match(/(\d+\.\d+\.\d+)/);
|
|
39
|
+
if (!versionMatch) {
|
|
40
|
+
throw new Error(`Unable to parse Codex version from "${output}"`);
|
|
41
|
+
}
|
|
42
|
+
return versionMatch[1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sessionStartHookConfigured() {
|
|
46
|
+
const hooksPath = codexHooksConfigPath();
|
|
47
|
+
if (!fs.existsSync(hooksPath)) {
|
|
48
|
+
return { configured: false, error: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
|
53
|
+
const groups = Array.isArray(parsed?.hooks?.SessionStart) ? parsed.hooks.SessionStart : [];
|
|
54
|
+
const configured = groups.some(group =>
|
|
55
|
+
Array.isArray(group?.hooks) && group.hooks.some(hook =>
|
|
56
|
+
hook?.type === 'command'
|
|
57
|
+
&& typeof hook.command === 'string'
|
|
58
|
+
&& hook.command.includes('spectre/hooks/session-start.mjs')
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return { configured, error: null };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
configured: false,
|
|
66
|
+
error: error instanceof Error ? error.message : String(error)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function commandSkillPath(projectDir, commandName) {
|
|
72
|
+
const skillName = codexCommandSkillName(commandName);
|
|
73
|
+
if (skillName === 'spectre-recall') {
|
|
74
|
+
return projectPaths(projectDir).recallSkillPath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return path.join(codexSkillsDir(), skillName, 'SKILL.md');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function runDoctor({ verifyHooks = false, json = false, projectDir = process.cwd() } = {}) {
|
|
81
|
+
const home = resolveCodexHome();
|
|
82
|
+
const version = codexVersion();
|
|
83
|
+
const hookConfigStatus = sessionStartHookConfigured();
|
|
84
|
+
const result = {
|
|
85
|
+
codexHome: home,
|
|
86
|
+
codexVersion: version,
|
|
87
|
+
minVersion: MIN_CODEX_VERSION,
|
|
88
|
+
supported: compareVersions(version, MIN_CODEX_VERSION) >= 0,
|
|
89
|
+
paths: {
|
|
90
|
+
config: codexConfigPath(),
|
|
91
|
+
skills: codexSkillsDir(),
|
|
92
|
+
runtime: codexRuntimeRoot()
|
|
93
|
+
},
|
|
94
|
+
installed: {
|
|
95
|
+
config: fs.existsSync(codexConfigPath()),
|
|
96
|
+
runtimeDir: fs.existsSync(codexRuntimeRoot())
|
|
97
|
+
},
|
|
98
|
+
hooks: {
|
|
99
|
+
verifyRequested: verifyHooks,
|
|
100
|
+
sessionStartConfigured: false,
|
|
101
|
+
codexHooksEnabled: false,
|
|
102
|
+
hiddenContextInjection: 'unconfigured',
|
|
103
|
+
hooksConfigPath: codexHooksConfigPath(),
|
|
104
|
+
hooksConfigPresent: fs.existsSync(codexHooksConfigPath())
|
|
105
|
+
},
|
|
106
|
+
capabilities: {
|
|
107
|
+
workflowSkillsInstalled: false,
|
|
108
|
+
exactWorkflowSkillsInstalled: false,
|
|
109
|
+
subagentsInstalled: false,
|
|
110
|
+
multiAgentEnabled: false,
|
|
111
|
+
sharedSkillsInstalled: false
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (fs.existsSync(codexConfigPath())) {
|
|
116
|
+
const config = fs.readFileSync(codexConfigPath(), 'utf8');
|
|
117
|
+
result.hooks.codexHooksEnabled = config.includes('codex_hooks = true');
|
|
118
|
+
result.hooks.sessionStartConfigured = hookConfigStatus.configured;
|
|
119
|
+
if (hookConfigStatus.error) {
|
|
120
|
+
result.hooks.configError = hookConfigStatus.error;
|
|
121
|
+
}
|
|
122
|
+
if (result.hooks.sessionStartConfigured && result.hooks.codexHooksEnabled) {
|
|
123
|
+
result.hooks.hiddenContextInjection = 'agents_override_managed_block';
|
|
124
|
+
} else if (result.hooks.sessionStartConfigured) {
|
|
125
|
+
result.hooks.hiddenContextInjection = 'configured_but_feature_disabled';
|
|
126
|
+
} else if (hookConfigStatus.error) {
|
|
127
|
+
result.hooks.hiddenContextInjection = 'malformed_hooks_json';
|
|
128
|
+
}
|
|
129
|
+
result.capabilities.subagentsInstalled = listSpectreAgents().every(agent => config.includes(`[agents.spectre_${agent.replace(/-/g, '_')}]`));
|
|
130
|
+
result.capabilities.multiAgentEnabled = config.includes('multi_agent = true');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const commandSkillFiles = listSpectreCommands().map(name => commandSkillPath(projectDir, name));
|
|
134
|
+
result.capabilities.workflowSkillsInstalled = listCodexWorkflowCommands()
|
|
135
|
+
.some(name => fs.existsSync(path.join(codexSkillsDir(), codexCommandSkillName(name), 'SKILL.md')));
|
|
136
|
+
result.capabilities.exactWorkflowSkillsInstalled = commandSkillFiles.every(filePath => fs.existsSync(filePath));
|
|
137
|
+
|
|
138
|
+
result.capabilities.sharedSkillsInstalled = ['spectre-apply', 'spectre-guide', 'spectre-learn', 'spectre-tdd']
|
|
139
|
+
.every(skill => fs.existsSync(path.join(codexSkillsDir(), skill, 'SKILL.md')));
|
|
140
|
+
|
|
141
|
+
if (verifyHooks) {
|
|
142
|
+
result.hooks.manualVerification = 'Use an interactive Codex session to verify SessionStart context injection. `codex exec` is not treated as authoritative for this hook lifecycle.';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (json) {
|
|
146
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
process.stdout.write(`Codex version: ${result.codexVersion}\n`);
|
|
151
|
+
process.stdout.write(`Codex home: ${result.codexHome}\n`);
|
|
152
|
+
process.stdout.write(`Supported: ${result.supported ? 'yes' : 'no'} (requires >= ${result.minVersion})\n`);
|
|
153
|
+
process.stdout.write(`Config present: ${result.installed.config ? 'yes' : 'no'}\n`);
|
|
154
|
+
process.stdout.write(`Runtime present: ${result.installed.runtimeDir ? 'yes' : 'no'}\n`);
|
|
155
|
+
process.stdout.write(`session_start hook configured: ${result.hooks.sessionStartConfigured ? 'yes' : 'no'}\n`);
|
|
156
|
+
process.stdout.write(`hooks.json present: ${result.hooks.hooksConfigPresent ? 'yes' : 'no'}\n`);
|
|
157
|
+
process.stdout.write(`Experimental codex_hooks enabled: ${result.hooks.codexHooksEnabled ? 'yes' : 'no'}\n`);
|
|
158
|
+
process.stdout.write(`Hidden context injection: ${result.hooks.hiddenContextInjection}\n`);
|
|
159
|
+
if (result.hooks.configError) {
|
|
160
|
+
process.stdout.write(`Hook config error: ${result.hooks.configError}\n`);
|
|
161
|
+
}
|
|
162
|
+
if (result.hooks.manualVerification) {
|
|
163
|
+
process.stdout.write(`Hook verification: ${result.hooks.manualVerification}\n`);
|
|
164
|
+
}
|
|
165
|
+
process.stdout.write(`Exact Spectre workflow skills installed: ${result.capabilities.exactWorkflowSkillsInstalled ? 'yes' : 'no'}\n`);
|
|
166
|
+
process.stdout.write(`Spectre subagents installed: ${result.capabilities.subagentsInstalled ? 'yes' : 'no'}\n`);
|
|
167
|
+
process.stdout.write(`Multi-agent enabled: ${result.capabilities.multiAgentEnabled ? 'yes' : 'no'}\n`);
|
|
168
|
+
}
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import {
|
|
5
|
+
codexCommandSkillName,
|
|
6
|
+
listCodexWorkflowCommands,
|
|
7
|
+
listSpectreAgents,
|
|
8
|
+
listSpectreCommands,
|
|
9
|
+
SHARED_SKILLS,
|
|
10
|
+
repoMetadata
|
|
11
|
+
} from './constants.js';
|
|
12
|
+
import {
|
|
13
|
+
ensureSpectreHooksConfigured,
|
|
14
|
+
removeProjectSkillsConfigured,
|
|
15
|
+
removeSpectreHooksConfigured,
|
|
16
|
+
syncProjectSkillsConfigured
|
|
17
|
+
} from './config.js';
|
|
18
|
+
import { codexSharedSkillContent } from './knowledge.js';
|
|
19
|
+
import { installProjectFiles, uninstallProjectFiles } from './project.js';
|
|
20
|
+
import {
|
|
21
|
+
codexPromptsDir,
|
|
22
|
+
codexRuntimeRoot,
|
|
23
|
+
codexSkillsDir,
|
|
24
|
+
ensureDir,
|
|
25
|
+
repoRoot,
|
|
26
|
+
runtimeAgentsDir,
|
|
27
|
+
runtimeHooksDir,
|
|
28
|
+
runtimeSourceAgentsDir,
|
|
29
|
+
runtimeSourceCommandsDir,
|
|
30
|
+
runtimeSourceRoot,
|
|
31
|
+
runtimeToolsDir,
|
|
32
|
+
spectrePluginRoot
|
|
33
|
+
} from './paths.js';
|
|
34
|
+
|
|
35
|
+
function writeFile(targetPath, content, mode) {
|
|
36
|
+
ensureDir(path.dirname(targetPath));
|
|
37
|
+
fs.writeFileSync(targetPath, content);
|
|
38
|
+
if (mode != null) {
|
|
39
|
+
fs.chmodSync(targetPath, mode);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readMarkdown(filePath) {
|
|
44
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function escapeTomlMultilineString(value) {
|
|
48
|
+
return value.replaceAll('"""', '\\"""');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function escapeTomlBasicString(value) {
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function escapeYamlDoubleQuotedString(value) {
|
|
56
|
+
return JSON.stringify(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function splitFrontmatter(content) {
|
|
60
|
+
if (!content.startsWith('---\n')) {
|
|
61
|
+
return { frontmatter: '', body: content.trim() };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const end = content.indexOf('\n---\n', 4);
|
|
65
|
+
if (end === -1) {
|
|
66
|
+
return { frontmatter: '', body: content.trim() };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
frontmatter: content.slice(4, end).trim(),
|
|
71
|
+
body: content.slice(end + 5).trim()
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function frontmatterValue(frontmatter, key) {
|
|
76
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
77
|
+
return match ? match[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function commandLabel(commandName) {
|
|
81
|
+
return `spectre-${commandName}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function commandSourceLabel(commandName) {
|
|
85
|
+
return `/spectre:${commandName}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function copySourceAssets() {
|
|
89
|
+
const pluginRoot = spectrePluginRoot();
|
|
90
|
+
ensureDir(runtimeSourceRoot());
|
|
91
|
+
fs.cpSync(path.join(pluginRoot, 'commands'), runtimeSourceCommandsDir(), { recursive: true });
|
|
92
|
+
fs.cpSync(path.join(pluginRoot, 'agents'), runtimeSourceAgentsDir(), { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sharedSkillContent(skillName) {
|
|
96
|
+
const codexSkill = codexSharedSkillContent(skillName);
|
|
97
|
+
if (codexSkill) {
|
|
98
|
+
return codexSkill;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (skillName === 'spectre-guide') {
|
|
102
|
+
return `---
|
|
103
|
+
name: ${escapeYamlDoubleQuotedString('spectre-guide')}
|
|
104
|
+
description: ${escapeYamlDoubleQuotedString('Use when suggesting the next Spectre workflow skill or explaining the Spectre workflow.')}
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
# Spectre Guide
|
|
108
|
+
|
|
109
|
+
Core path: \`spectre-scope\` -> \`spectre-plan\` -> \`spectre-execute\` -> \`spectre-clean\` -> \`spectre-test\` -> \`spectre-rebase\` -> \`spectre-evaluate\`.
|
|
110
|
+
|
|
111
|
+
Use \`spectre-handoff\` to save continuity and \`spectre-forget\` to clear it.
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (skillName === 'spectre-learn') {
|
|
116
|
+
return `---
|
|
117
|
+
name: ${escapeYamlDoubleQuotedString('spectre-learn')}
|
|
118
|
+
description: ${escapeYamlDoubleQuotedString('Use when capturing durable project knowledge into Codex-native skills.')}
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
# Spectre Learn
|
|
122
|
+
|
|
123
|
+
Store learned project knowledge under \`.agents/skills/{category}-{slug}/SKILL.md\`.
|
|
124
|
+
|
|
125
|
+
Each new learning should:
|
|
126
|
+
- include YAML frontmatter with \`name\` and \`description\`
|
|
127
|
+
- focus on actionable project knowledge
|
|
128
|
+
- be updated when behavior changes
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return `---
|
|
133
|
+
name: ${escapeYamlDoubleQuotedString('spectre-tdd')}
|
|
134
|
+
description: ${escapeYamlDoubleQuotedString("Use when following Spectre's TDD-first execution style.")}
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
# Spectre TDD
|
|
138
|
+
|
|
139
|
+
Default cycle:
|
|
140
|
+
1. Write or identify the failing behavior.
|
|
141
|
+
2. Add the smallest test that proves the behavior.
|
|
142
|
+
3. Implement the smallest code change that makes the test pass.
|
|
143
|
+
4. Refactor only after the test passes.
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function installSharedSkills() {
|
|
148
|
+
const skillsRoot = codexSkillsDir();
|
|
149
|
+
ensureDir(skillsRoot);
|
|
150
|
+
for (const skillName of SHARED_SKILLS) {
|
|
151
|
+
const skillPath = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
152
|
+
writeFile(skillPath, sharedSkillContent(skillName));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function workflowPostStep(commandName, runtimeRoot) {
|
|
157
|
+
const refreshCommand = `node "${path.join(runtimeRoot, 'tools', 'refresh-project-context.mjs')}" --project-root "$PWD"`;
|
|
158
|
+
const syncCommand = `node "${path.join(runtimeRoot, 'tools', 'sync-session-override.mjs')}" --project-root "$PWD" --source handoff`;
|
|
159
|
+
const clearCommand = `node "${path.join(runtimeRoot, 'tools', 'sync-session-override.mjs')}" --project-root "$PWD" --clear`;
|
|
160
|
+
|
|
161
|
+
if (commandName === 'learn') {
|
|
162
|
+
return `After creating or updating project skills, run:\n\n\`\`\`bash\n${refreshCommand}\n\`\`\`\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (commandName === 'handoff') {
|
|
166
|
+
return `After saving the handoff, run:\n\n\`\`\`bash\n${syncCommand}\n\`\`\`\n`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (commandName === 'forget') {
|
|
170
|
+
return `After clearing session memory, run:\n\n\`\`\`bash\n${clearCommand}\n\`\`\`\n`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return 'No extra runtime step is required for this command.\n';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function workflowBody(sourceContent) {
|
|
177
|
+
return sourceContent
|
|
178
|
+
.replace(/(?:<|<)ARGUMENTS(?:>|>)\s*\$ARGUMENTS\s*(?:<\/|<\/)ARGUMENTS(?:>|>)/g, 'Treat the current user request as the input for this workflow.')
|
|
179
|
+
.replace(/\$ARGUMENTS/g, 'the current user request')
|
|
180
|
+
.replaceAll('/spectre:', 'spectre-');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function workflowSkill(commandName, runtimeRoot) {
|
|
184
|
+
const commandSource = readMarkdown(path.join(runtimeSourceCommandsDir(), `${commandName}.md`)).trim();
|
|
185
|
+
const { frontmatter, body } = splitFrontmatter(commandSource);
|
|
186
|
+
const description = frontmatterValue(frontmatter, 'description') || `Use when running the Spectre ${commandName} workflow.`;
|
|
187
|
+
|
|
188
|
+
return `---
|
|
189
|
+
name: ${escapeYamlDoubleQuotedString(codexCommandSkillName(commandName))}
|
|
190
|
+
description: ${escapeYamlDoubleQuotedString(description)}
|
|
191
|
+
user-invocable: true
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
# ${commandLabel(commandName)}
|
|
195
|
+
|
|
196
|
+
Use when the user explicitly wants the Spectre \`${commandName}\` workflow, or when another Spectre workflow delegates to it.
|
|
197
|
+
|
|
198
|
+
This is the Codex skill replacement for the deprecated custom prompt ${commandSourceLabel(commandName)}.
|
|
199
|
+
|
|
200
|
+
## Input Handling
|
|
201
|
+
|
|
202
|
+
Treat the current user request as the input arguments for this workflow.
|
|
203
|
+
|
|
204
|
+
## Required setup
|
|
205
|
+
|
|
206
|
+
1. Read \`AGENTS.md\` if present.
|
|
207
|
+
2. Read \`.spectre/manifest.json\` if present.
|
|
208
|
+
3. Read project skills under \`.agents/skills/\` when their descriptions match the task.
|
|
209
|
+
4. Prefer the installed Spectre subagents when the workflow dispatches specialized roles.
|
|
210
|
+
|
|
211
|
+
## Codex translation layer
|
|
212
|
+
|
|
213
|
+
- Treat both \`@spectre:name\` and \`@name\` as the installed Spectre Codex subagent for that role.
|
|
214
|
+
- If multi-agent support is available, spawn the relevant subagent(s). If not, execute the same work sequentially yourself and preserve the same artifacts, checks, and completion reports.
|
|
215
|
+
- Treat \`Skill(name)\` or skill-tool instructions as: load the named Codex skill from \`.agents/skills/{name}/SKILL.md\` or \`$CODEX_HOME/skills/{name}/SKILL.md\` before continuing.
|
|
216
|
+
- Treat nested \`/spectre:other\` references as instructions to execute the installed \`spectre-other\` workflow skill immediately, not as a suggestion to stop and ask the user.
|
|
217
|
+
- Ignore Claude plugin, marketplace, model, and tool declarations. Preserve Spectre artifact paths and handoff JSON shapes.
|
|
218
|
+
|
|
219
|
+
## Canonical workflow
|
|
220
|
+
|
|
221
|
+
${workflowBody(body)}
|
|
222
|
+
|
|
223
|
+
## Command-specific post step
|
|
224
|
+
|
|
225
|
+
${workflowPostStep(commandName, runtimeRoot)}
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function codexAgentBody(agentName, sourceContent) {
|
|
230
|
+
const { frontmatter, body } = splitFrontmatter(sourceContent);
|
|
231
|
+
const description = frontmatterValue(frontmatter, 'description') || `${agentName} specialist`;
|
|
232
|
+
const instructions = `You are the Codex port of Spectre's \`${agentName}\` subagent.
|
|
233
|
+
|
|
234
|
+
## Role
|
|
235
|
+
|
|
236
|
+
${description}
|
|
237
|
+
|
|
238
|
+
## Operating rules
|
|
239
|
+
|
|
240
|
+
- Stay inside this role's scope.
|
|
241
|
+
- Preserve Spectre file locations and document contracts.
|
|
242
|
+
- If the parent command provided task, scope, or handoff docs, read them before acting.
|
|
243
|
+
- Return concrete findings or completion output that the parent workflow can consume directly.
|
|
244
|
+
|
|
245
|
+
## Canonical instructions
|
|
246
|
+
|
|
247
|
+
${body}`;
|
|
248
|
+
return {
|
|
249
|
+
description,
|
|
250
|
+
content: `name = ${escapeTomlBasicString(agentName)}
|
|
251
|
+
description = ${escapeTomlBasicString(description)}
|
|
252
|
+
developer_instructions = """
|
|
253
|
+
${escapeTomlMultilineString(instructions)}
|
|
254
|
+
"""
|
|
255
|
+
`
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function agentNicknames(agentName) {
|
|
260
|
+
return Array.from(new Set([
|
|
261
|
+
agentName,
|
|
262
|
+
`spectre-${agentName}`,
|
|
263
|
+
`spectre ${agentName}`,
|
|
264
|
+
`spectre_${agentName.replace(/-/g, '_')}`
|
|
265
|
+
]));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function installAgentConfigs() {
|
|
269
|
+
const configs = [];
|
|
270
|
+
ensureDir(runtimeAgentsDir());
|
|
271
|
+
for (const entry of fs.readdirSync(runtimeAgentsDir(), { withFileTypes: true })) {
|
|
272
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
273
|
+
fs.unlinkSync(path.join(runtimeAgentsDir(), entry.name));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const agentName of listSpectreAgents()) {
|
|
277
|
+
const sourcePath = path.join(runtimeSourceAgentsDir(), `${agentName}.md`);
|
|
278
|
+
const agent = codexAgentBody(agentName, readMarkdown(sourcePath));
|
|
279
|
+
const configFile = path.join(runtimeAgentsDir(), `${agentName}.toml`);
|
|
280
|
+
writeFile(configFile, agent.content);
|
|
281
|
+
configs.push({
|
|
282
|
+
id: agentName.replace(/-/g, '_'),
|
|
283
|
+
name: agentName,
|
|
284
|
+
description: agent.description,
|
|
285
|
+
configFile,
|
|
286
|
+
nicknames: agentNicknames(agentName)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return configs;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function installWorkflowSkills() {
|
|
293
|
+
const runtimeRoot = codexRuntimeRoot();
|
|
294
|
+
const skillsRoot = codexSkillsDir();
|
|
295
|
+
ensureDir(skillsRoot);
|
|
296
|
+
|
|
297
|
+
for (const commandName of listCodexWorkflowCommands()) {
|
|
298
|
+
writeFile(
|
|
299
|
+
path.join(skillsRoot, codexCommandSkillName(commandName), 'SKILL.md'),
|
|
300
|
+
workflowSkill(commandName, runtimeRoot)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function cleanupLegacyPrompts() {
|
|
306
|
+
if (!fs.existsSync(codexPromptsDir())) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const commandName of listSpectreCommands()) {
|
|
311
|
+
for (const fileName of [`spectre:${commandName}.md`, `spectre-${commandName}.md`]) {
|
|
312
|
+
const filePath = path.join(codexPromptsDir(), fileName);
|
|
313
|
+
if (fs.existsSync(filePath)) {
|
|
314
|
+
fs.unlinkSync(filePath);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function sessionStartHook() {
|
|
321
|
+
const projectModuleUrl = pathToFileURL(path.join(repoRoot(), 'src', 'lib', 'project.js')).href;
|
|
322
|
+
return `#!/usr/bin/env node
|
|
323
|
+
import fs from 'fs';
|
|
324
|
+
import path from 'path';
|
|
325
|
+
|
|
326
|
+
function readStdin() {
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
let input = '';
|
|
329
|
+
process.stdin.setEncoding('utf8');
|
|
330
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
331
|
+
process.stdin.on('end', () => resolve(input));
|
|
332
|
+
process.stdin.on('error', reject);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const input = await readStdin();
|
|
337
|
+
let payload = {};
|
|
338
|
+
if (input) {
|
|
339
|
+
try {
|
|
340
|
+
payload = JSON.parse(input);
|
|
341
|
+
} catch {
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const cwd = payload.cwd || process.cwd();
|
|
346
|
+
const manifestPath = path.join(cwd, '.spectre', 'manifest.json');
|
|
347
|
+
|
|
348
|
+
if (!fs.existsSync(manifestPath)) {
|
|
349
|
+
process.exit(0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const { buildSessionStartOutput } = await import(${JSON.stringify(projectModuleUrl)});
|
|
353
|
+
const output = buildSessionStartOutput(cwd, payload);
|
|
354
|
+
if (!output) {
|
|
355
|
+
process.exit(0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
process.stdout.write(JSON.stringify(output) + '\\n');
|
|
359
|
+
`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function refreshProjectTool() {
|
|
363
|
+
const configModuleUrl = pathToFileURL(path.join(repoRoot(), 'src', 'lib', 'config.js')).href;
|
|
364
|
+
const projectModuleUrl = pathToFileURL(path.join(repoRoot(), 'src', 'lib', 'project.js')).href;
|
|
365
|
+
return `#!/usr/bin/env node
|
|
366
|
+
const projectRootIndex = process.argv.indexOf('--project-root');
|
|
367
|
+
if (projectRootIndex === -1 || !process.argv[projectRootIndex + 1]) {
|
|
368
|
+
throw new Error('Missing required --project-root argument');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const projectRoot = process.argv[projectRootIndex + 1];
|
|
372
|
+
const { syncProjectSkillsConfigured } = await import(${JSON.stringify(configModuleUrl)});
|
|
373
|
+
const { syncKnowledgeOverride } = await import(${JSON.stringify(projectModuleUrl)});
|
|
374
|
+
syncProjectSkillsConfigured(projectRoot);
|
|
375
|
+
syncKnowledgeOverride(projectRoot);
|
|
376
|
+
process.stdout.write('Synced Spectre project skills and knowledge context.\\n');
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function syncSessionOverrideTool() {
|
|
381
|
+
const projectModuleUrl = pathToFileURL(path.join(repoRoot(), 'src', 'lib', 'project.js')).href;
|
|
382
|
+
return `#!/usr/bin/env node
|
|
383
|
+
const projectRootIndex = process.argv.indexOf('--project-root');
|
|
384
|
+
if (projectRootIndex === -1 || !process.argv[projectRootIndex + 1]) {
|
|
385
|
+
throw new Error('Missing required --project-root argument');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const projectRoot = process.argv[projectRootIndex + 1];
|
|
389
|
+
const clear = process.argv.includes('--clear');
|
|
390
|
+
const sourceIndex = process.argv.indexOf('--source');
|
|
391
|
+
const source = sourceIndex === -1 ? 'manual' : (process.argv[sourceIndex + 1] || 'manual');
|
|
392
|
+
|
|
393
|
+
const { clearSessionOverride, syncKnowledgeOverride, syncSessionOverride } = await import(${JSON.stringify(projectModuleUrl)});
|
|
394
|
+
|
|
395
|
+
if (clear) {
|
|
396
|
+
clearSessionOverride(projectRoot);
|
|
397
|
+
const knowledge = syncKnowledgeOverride(projectRoot);
|
|
398
|
+
process.stdout.write(\`Cleared SPECTRE session context. Knowledge status: \${knowledge.knowledgeStatus}.\\n\`);
|
|
399
|
+
} else {
|
|
400
|
+
const session = syncSessionOverride(projectRoot, { source });
|
|
401
|
+
const knowledge = syncKnowledgeOverride(projectRoot);
|
|
402
|
+
if (session) {
|
|
403
|
+
process.stdout.write(\`Synced SPECTRE context from \${session.handoffPath}. Knowledge status: \${knowledge.knowledgeStatus}.\\n\`);
|
|
404
|
+
} else {
|
|
405
|
+
process.stdout.write(\`No active handoff found; refreshed knowledge context. Knowledge status: \${knowledge.knowledgeStatus}.\\n\`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function installRuntimeScripts() {
|
|
412
|
+
const hooksDir = runtimeHooksDir();
|
|
413
|
+
const toolsDir = runtimeToolsDir();
|
|
414
|
+
ensureDir(hooksDir);
|
|
415
|
+
ensureDir(toolsDir);
|
|
416
|
+
|
|
417
|
+
for (const stalePath of [
|
|
418
|
+
path.join(hooksDir, 'pre-session-start.mjs'),
|
|
419
|
+
path.join(toolsDir, 'forget-project-context.mjs')
|
|
420
|
+
]) {
|
|
421
|
+
if (fs.existsSync(stalePath)) {
|
|
422
|
+
fs.unlinkSync(stalePath);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
writeFile(path.join(hooksDir, 'session-start.mjs'), sessionStartHook(), 0o755);
|
|
427
|
+
writeFile(path.join(toolsDir, 'refresh-project-context.mjs'), refreshProjectTool(), 0o755);
|
|
428
|
+
writeFile(path.join(toolsDir, 'sync-session-override.mjs'), syncSessionOverrideTool(), 0o755);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function installCodex({ scope, projectDir }) {
|
|
432
|
+
const runtimeRoot = codexRuntimeRoot();
|
|
433
|
+
ensureDir(runtimeRoot);
|
|
434
|
+
copySourceAssets();
|
|
435
|
+
installRuntimeScripts();
|
|
436
|
+
cleanupLegacyPrompts();
|
|
437
|
+
installSharedSkills();
|
|
438
|
+
installWorkflowSkills();
|
|
439
|
+
const agents = installAgentConfigs();
|
|
440
|
+
ensureSpectreHooksConfigured(runtimeRoot, agents);
|
|
441
|
+
|
|
442
|
+
if (scope === 'project') {
|
|
443
|
+
installProjectFiles(projectDir, scope);
|
|
444
|
+
syncProjectSkillsConfigured(projectDir);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const metadata = repoMetadata();
|
|
448
|
+
process.stdout.write(`Installed Spectre ${metadata.version} for Codex (${scope} scope).\n`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function uninstallCodex({ scope, projectDir }) {
|
|
452
|
+
const agents = listSpectreAgents().map(agentName => ({
|
|
453
|
+
id: agentName.replace(/-/g, '_')
|
|
454
|
+
}));
|
|
455
|
+
removeSpectreHooksConfigured(codexRuntimeRoot(), agents);
|
|
456
|
+
cleanupLegacyPrompts();
|
|
457
|
+
|
|
458
|
+
if (fs.existsSync(codexRuntimeRoot())) {
|
|
459
|
+
fs.rmSync(codexRuntimeRoot(), { recursive: true, force: true });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
for (const skillName of SHARED_SKILLS) {
|
|
463
|
+
const skillDir = path.join(codexSkillsDir(), skillName);
|
|
464
|
+
if (fs.existsSync(skillDir)) {
|
|
465
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
for (const commandName of listCodexWorkflowCommands()) {
|
|
470
|
+
const skillDir = path.join(codexSkillsDir(), codexCommandSkillName(commandName));
|
|
471
|
+
if (fs.existsSync(skillDir)) {
|
|
472
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (scope === 'project') {
|
|
477
|
+
removeProjectSkillsConfigured(projectDir);
|
|
478
|
+
uninstallProjectFiles(projectDir);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
process.stdout.write(`Uninstalled Spectre for Codex (${scope} scope).\n`);
|
|
482
|
+
}
|