@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,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spectre-tdd
|
|
3
|
+
description: "Load this skill when executing TDD (Test-Driven Development) methodology. Use when implementing features via strict RED-GREEN-REFACTOR cycles, or when a prompt instructs execution via TDD."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TDD: Test-Driven Development Methodology
|
|
7
|
+
|
|
8
|
+
Execute tasks using strict TDD (RED → GREEN → REFACTOR). Outcome: Tasks completed with Happy/Failure tests passing, minimal code shipped.
|
|
9
|
+
|
|
10
|
+
## Iron Law
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Wrote code before the test? **Delete it. Start over.** Don't keep it as "reference." Don't "adapt" it. Delete means delete. Implement fresh from tests.
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
- **2 tests per Test Opportunity (TO)**: 1 Happy path, 1 Failure path — then stop
|
|
21
|
+
- **Scoped execution**: Never run repo-wide tests; use `--testPathPattern`, `--findRelatedTests`, or per-file lint
|
|
22
|
+
- **YAGNI**: No abstractions unless test forces it or ≥2 call sites exist
|
|
23
|
+
- **Anti-flake**: Use fake timers, stubs, seeded RNG
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Step 1 - Generate TDD TODO List
|
|
28
|
+
|
|
29
|
+
- **Action** — ParseTaskList: Extract tasks from ARGUMENTS or thread context
|
|
30
|
+
- **If** no clear tasks → stop and ask for guidance
|
|
31
|
+
- **Action** — IdentifyTestOpportunities: Derive TOs (smallest behavior unit: function, route, bug fix, acceptance criterion)
|
|
32
|
+
- **Action** — TransformToTDD: Convert each TO to cycle using TodoWrite:
|
|
33
|
+
- `RED: Happy — {test}` → `RED: Failure — {test}` → `GREEN: Minimal impl` → `REFACTOR: Tidy` → `COMMIT`
|
|
34
|
+
- **Action** — VerifyScope: Confirm TODO contains ONLY assigned tasks
|
|
35
|
+
|
|
36
|
+
## Step 2 - RED Phase: Write Failing Tests
|
|
37
|
+
|
|
38
|
+
- **Action** — WriteHappyTest: Write first failing test (happy path)
|
|
39
|
+
- Execute only this test/file, not entire suite
|
|
40
|
+
- **Action** — WriteFailureTest: Write second failing test (primary failure mode)
|
|
41
|
+
- **Action** — VerifyRed: **MANDATORY** — Confirm each test:
|
|
42
|
+
- Fails (not errors)
|
|
43
|
+
- Fails for expected reason (feature missing, not typo)
|
|
44
|
+
- **If** passes → you're testing existing behavior; fix test
|
|
45
|
+
|
|
46
|
+
## Step 3 - GREEN Phase: Minimal Implementation
|
|
47
|
+
|
|
48
|
+
- **Action** — ImplementMinimal: Write least code to pass tests
|
|
49
|
+
- No extra branches, params, or dependencies unless test forces them
|
|
50
|
+
- **Action** — VerifyGreen: **MANDATORY** — Run tests (narrowest scope)
|
|
51
|
+
- **If** fail → fix code, not test
|
|
52
|
+
- Remove any speculative code not forced by tests
|
|
53
|
+
|
|
54
|
+
## Step 4 - REFACTOR Phase: Clean Code
|
|
55
|
+
|
|
56
|
+
- **Action** — RefactorSafely: Improve only if duplication ≥3 OR readability materially improves
|
|
57
|
+
- Keep tests green; **If** tests fail → revert
|
|
58
|
+
- **Action** — HandleLintFailures: Apply in order until clear:
|
|
59
|
+
1. Guard clauses, split compound expressions
|
|
60
|
+
2. Extract tiny private helpers (same file)
|
|
61
|
+
3. Hoist literals to file constants
|
|
62
|
+
4. Split into orchestrator + helpers
|
|
63
|
+
5. Only if still failing: same-directory helper module
|
|
64
|
+
|
|
65
|
+
## Step 5 - Loop or Complete
|
|
66
|
+
|
|
67
|
+
- **If** more TOs → return to Step 2
|
|
68
|
+
- **Else** → proceed to Step 6
|
|
69
|
+
|
|
70
|
+
## Step 6 - Commit & Report
|
|
71
|
+
|
|
72
|
+
- **Action** — CommitCode: Conventional format (`feat({task}): description`)
|
|
73
|
+
- **Action** — GenerateReport:
|
|
74
|
+
- **Summary**: Tasks completed, test status (✅ Happy ✅ Failure), files modified
|
|
75
|
+
- **Artifacts**: Test helpers, mocks, fixtures created
|
|
76
|
+
- **API Surface**: New/modified exports with signatures
|
|
77
|
+
- **Patterns**: Code/testing patterns to follow
|
|
78
|
+
- **Deferred**: Coverage gaps for follow-up
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Red Flags — STOP and Restart
|
|
83
|
+
|
|
84
|
+
If any of these occur, delete code and start over with TDD:
|
|
85
|
+
|
|
86
|
+
| Red Flag | Why It's Wrong |
|
|
87
|
+
|----------|----------------|
|
|
88
|
+
| Code written before test | Violates Iron Law |
|
|
89
|
+
| Test passes immediately | Testing existing behavior, not new |
|
|
90
|
+
| Can't explain why test failed | Don't understand what you're testing |
|
|
91
|
+
| "Just this once" thinking | Rationalization — TDD has no exceptions |
|
|
92
|
+
| Keeping code "as reference" | You'll adapt it; that's tests-after |
|
|
93
|
+
|
|
94
|
+
## When Stuck
|
|
95
|
+
|
|
96
|
+
| Problem | Solution |
|
|
97
|
+
|---------|----------|
|
|
98
|
+
| Don't know how to test | Write wished-for API first, then assert on it |
|
|
99
|
+
| Test too complicated | Design too complicated — simplify interface |
|
|
100
|
+
| Must mock everything | Code too coupled — use dependency injection |
|
|
101
|
+
| Test setup huge | Extract helpers; still complex? Simplify design |
|
|
102
|
+
|
|
103
|
+
## Pre-Completion Checklist
|
|
104
|
+
|
|
105
|
+
Before marking complete, verify:
|
|
106
|
+
- [ ] Every new function has a test
|
|
107
|
+
- [ ] Watched each test fail before implementing
|
|
108
|
+
- [ ] Each failure was for expected reason
|
|
109
|
+
- [ ] Wrote minimal code to pass
|
|
110
|
+
- [ ] All tests pass, output clean
|
|
111
|
+
- [ ] Mocks used only when unavoidable
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
function makeProject() {
|
|
9
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'spectre-codex-test-'));
|
|
10
|
+
execFileSync('git', ['init', '-b', 'main'], { cwd: tmp, stdio: 'ignore' });
|
|
11
|
+
fs.mkdirSync(path.join(tmp, 'docs', 'tasks', 'main', 'session_logs'), { recursive: true });
|
|
12
|
+
fs.mkdirSync(path.join(tmp, '.agents', 'skills', 'feature-auth'), { recursive: true });
|
|
13
|
+
fs.mkdirSync(path.join(tmp, '.agents', 'skills', 'spectre-recall', 'references'), { recursive: true });
|
|
14
|
+
fs.writeFileSync(
|
|
15
|
+
path.join(tmp, 'docs', 'tasks', 'main', 'session_logs', '2026-03-09-100000_handoff.json'),
|
|
16
|
+
JSON.stringify({
|
|
17
|
+
version: '1.1',
|
|
18
|
+
timestamp: '2026-03-09-100000',
|
|
19
|
+
branch_name: 'main',
|
|
20
|
+
task_name: 'codex-port-migration',
|
|
21
|
+
progress_update: {
|
|
22
|
+
goal: 'Ship official SessionStart migration',
|
|
23
|
+
summary: 'Switched Codex continuity to a managed AGENTS.override.md block with a SessionStart status line.',
|
|
24
|
+
accomplished: ['Updated hook generation', 'Cleaned up legacy bridge files'],
|
|
25
|
+
now: 'Wiring tests for the new SessionStart payload',
|
|
26
|
+
next_steps: ['Update docs', 'Run the full test suite'],
|
|
27
|
+
confidence: 'high',
|
|
28
|
+
constraints: ['Hook output must stay JSON'],
|
|
29
|
+
decisions: ['Use a managed AGENTS.override.md block as the continuity channel'],
|
|
30
|
+
blockers: [],
|
|
31
|
+
open_questions: ['Whether to show systemMessage in the UI'],
|
|
32
|
+
risks: ['Codex hook schema is still experimental']
|
|
33
|
+
},
|
|
34
|
+
working_set: {
|
|
35
|
+
key_files: ['src/lib/install.js', 'src/lib/project.js'],
|
|
36
|
+
active_ids: ['bd-123'],
|
|
37
|
+
recent_commands: ['npm test']
|
|
38
|
+
},
|
|
39
|
+
context: {
|
|
40
|
+
wip_state: 'uncommitted',
|
|
41
|
+
last_commit: 'abc1234'
|
|
42
|
+
}
|
|
43
|
+
}, null, 2)
|
|
44
|
+
);
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
path.join(tmp, '.agents', 'skills', 'feature-auth', 'SKILL.md'),
|
|
47
|
+
[
|
|
48
|
+
'---',
|
|
49
|
+
'name: feature-auth',
|
|
50
|
+
'description: Use when modifying auth flows.',
|
|
51
|
+
'---',
|
|
52
|
+
'',
|
|
53
|
+
'# Auth Knowledge',
|
|
54
|
+
'',
|
|
55
|
+
'The auth system uses token rotation.'
|
|
56
|
+
].join('\n')
|
|
57
|
+
);
|
|
58
|
+
fs.writeFileSync(
|
|
59
|
+
path.join(tmp, '.agents', 'skills', 'spectre-recall', 'references', 'registry.toon'),
|
|
60
|
+
[
|
|
61
|
+
'# SPECTRE Knowledge Registry',
|
|
62
|
+
'# Format: skill-name|category|triggers|description',
|
|
63
|
+
'',
|
|
64
|
+
'feature-auth|feature|auth, login|Use when modifying auth flows'
|
|
65
|
+
].join('\n')
|
|
66
|
+
);
|
|
67
|
+
return tmp;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test('buildSessionStartOutput returns official SessionStart payload from the latest handoff', async () => {
|
|
71
|
+
const tmp = makeProject();
|
|
72
|
+
const { buildSessionStartOutput } = await import('./lib/project.js');
|
|
73
|
+
const output = buildSessionStartOutput(tmp, { source: 'resume' });
|
|
74
|
+
const overridePath = path.join(tmp, 'AGENTS.override.md');
|
|
75
|
+
|
|
76
|
+
assert.ok(output);
|
|
77
|
+
assert.equal(output.hookSpecificOutput.hookEventName, 'SessionStart');
|
|
78
|
+
assert.equal(output.systemMessage, '🟢 👻 SPECTRE active | injected docs/tasks/main/session_logs/2026-03-09-100000_handoff.json | 👻 spectre: 1 knowledge skills available');
|
|
79
|
+
assert.deepEqual(output.hookSpecificOutput, { hookEventName: 'SessionStart' });
|
|
80
|
+
|
|
81
|
+
assert.ok(fs.existsSync(overridePath));
|
|
82
|
+
const overrideContent = fs.readFileSync(overridePath, 'utf8');
|
|
83
|
+
assert.match(overrideContent, /<!-- spectre-session:start -->/);
|
|
84
|
+
assert.match(overrideContent, /## SPECTRE Session Context/);
|
|
85
|
+
assert.match(overrideContent, /official SessionStart migration/);
|
|
86
|
+
assert.match(overrideContent, /Wiring tests for the new SessionStart payload/);
|
|
87
|
+
assert.match(overrideContent, /### Spectre Notes/);
|
|
88
|
+
assert.match(overrideContent, /\*\*SessionStart Source\*\*: resume/);
|
|
89
|
+
assert.match(overrideContent, /docs\/tasks\/main\/session_logs\/2026-03-09-100000_handoff\.json/);
|
|
90
|
+
assert.match(overrideContent, /<!-- spectre-session:end -->/);
|
|
91
|
+
assert.match(overrideContent, /<!-- spectre-knowledge:start -->/);
|
|
92
|
+
assert.match(overrideContent, /## SPECTRE Knowledge Context/);
|
|
93
|
+
assert.match(overrideContent, /If ANY entry's triggers or description match your current task, you MUST load the skill FIRST/);
|
|
94
|
+
assert.match(overrideContent, /feature-auth\|feature\|auth, login\|Use when modifying auth flows/);
|
|
95
|
+
assert.match(overrideContent, /<!-- spectre-knowledge:end -->/);
|
|
96
|
+
assert.ok(fs.existsSync(path.join(tmp, '.agents', 'skills', 'spectre-recall', 'SKILL.md')));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('buildSessionStartOutput keeps knowledge active when no handoff exists and removes only the session block', async () => {
|
|
100
|
+
const tmp = makeProject();
|
|
101
|
+
fs.rmSync(path.join(tmp, '.agents'), { recursive: true, force: true });
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
path.join(tmp, 'AGENTS.override.md'),
|
|
104
|
+
[
|
|
105
|
+
'User content before.',
|
|
106
|
+
'',
|
|
107
|
+
'<!-- spectre-session:start -->',
|
|
108
|
+
'old spectre content',
|
|
109
|
+
'<!-- spectre-session:end -->',
|
|
110
|
+
'',
|
|
111
|
+
'<!-- spectre-knowledge:start -->',
|
|
112
|
+
'old knowledge content',
|
|
113
|
+
'<!-- spectre-knowledge:end -->',
|
|
114
|
+
'',
|
|
115
|
+
'User content after.'
|
|
116
|
+
].join('\n')
|
|
117
|
+
);
|
|
118
|
+
const activePath = path.join(tmp, 'docs', 'tasks', 'main', 'session_logs', '2026-03-09-100000_handoff.json');
|
|
119
|
+
const archiveDir = path.join(tmp, 'docs', 'tasks', 'main', 'session_logs', 'archive');
|
|
120
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
121
|
+
fs.renameSync(activePath, path.join(archiveDir, '2026-03-09-100000_handoff.json'));
|
|
122
|
+
|
|
123
|
+
const { buildSessionStartOutput } = await import('./lib/project.js');
|
|
124
|
+
const output = buildSessionStartOutput(tmp, { source: 'clear' });
|
|
125
|
+
assert.ok(output);
|
|
126
|
+
assert.equal(output.systemMessage, '🟢 👻 SPECTRE active | 👻 spectre: ready — capture knowledge with spectre-learn');
|
|
127
|
+
const overrideContent = fs.readFileSync(path.join(tmp, 'AGENTS.override.md'), 'utf8');
|
|
128
|
+
assert.match(overrideContent, /User content before\./);
|
|
129
|
+
assert.match(overrideContent, /User content after\./);
|
|
130
|
+
assert.doesNotMatch(overrideContent, /spectre-session:start/);
|
|
131
|
+
assert.match(overrideContent, /spectre-knowledge:start/);
|
|
132
|
+
assert.match(overrideContent, /No knowledge has been captured for this project yet/);
|
|
133
|
+
assert.match(overrideContent, /use `spectre-learn` after completing significant work/);
|
|
134
|
+
});
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
function makeProject() {
|
|
9
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'spectre-codex-install-'));
|
|
10
|
+
execFileSync('git', ['init', '-b', 'main'], { cwd: tmp, stdio: 'ignore' });
|
|
11
|
+
fs.mkdirSync(path.join(tmp, 'docs', 'tasks', 'main', 'session_logs'), { recursive: true });
|
|
12
|
+
fs.writeFileSync(
|
|
13
|
+
path.join(tmp, 'docs', 'tasks', 'main', 'session_logs', '2026-03-09-100000_handoff.json'),
|
|
14
|
+
JSON.stringify({
|
|
15
|
+
branch_name: 'main',
|
|
16
|
+
progress_update: {
|
|
17
|
+
goal: 'Port Spectre to Codex',
|
|
18
|
+
summary: 'Subagents and workflow skills wired.',
|
|
19
|
+
now: 'Verifying installer',
|
|
20
|
+
next_steps: ['Run tests'],
|
|
21
|
+
constraints: ['Hooks hidden injection remains unverified']
|
|
22
|
+
},
|
|
23
|
+
working_set: {
|
|
24
|
+
key_files: ['src/lib/install.js']
|
|
25
|
+
}
|
|
26
|
+
}, null, 2)
|
|
27
|
+
);
|
|
28
|
+
return tmp;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('project install writes workflow skills, agent config, and official SessionStart continuity', { concurrency: false }, async () => {
|
|
32
|
+
const projectDir = makeProject();
|
|
33
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
34
|
+
delete process.env.CODEX_HOME;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { main } = await import('./main.js');
|
|
38
|
+
await main(['install', 'codex', '--scope', 'project', '--project-dir', projectDir]);
|
|
39
|
+
|
|
40
|
+
const codeHome = path.join(projectDir, '.codex');
|
|
41
|
+
const scopeSkillPath = path.join(codeHome, 'skills', 'spectre-scope', 'SKILL.md');
|
|
42
|
+
assert.ok(fs.existsSync(scopeSkillPath));
|
|
43
|
+
assert.match(fs.readFileSync(scopeSkillPath, 'utf8'), /Treat the current user request as the input arguments for this workflow/);
|
|
44
|
+
assert.match(fs.readFileSync(scopeSkillPath, 'utf8'), /This is the Codex skill replacement for the deprecated custom prompt \/spectre:scope/);
|
|
45
|
+
|
|
46
|
+
const applySkillPath = path.join(codeHome, 'skills', 'spectre-apply', 'SKILL.md');
|
|
47
|
+
assert.ok(fs.existsSync(applySkillPath));
|
|
48
|
+
assert.match(fs.readFileSync(applySkillPath, 'utf8'), /If ANY entry's triggers or description match your current task, you MUST load the skill FIRST/);
|
|
49
|
+
|
|
50
|
+
const learnSkillPath = path.join(codeHome, 'skills', 'spectre-learn', 'SKILL.md');
|
|
51
|
+
assert.ok(fs.existsSync(learnSkillPath));
|
|
52
|
+
assert.match(fs.readFileSync(learnSkillPath, 'utf8'), /### 13\. Register the Learning/);
|
|
53
|
+
assert.match(fs.readFileSync(learnSkillPath, 'utf8'), /\.agents\/skills\/spectre-recall\/references\/registry\.toon/);
|
|
54
|
+
|
|
55
|
+
const agentPath = path.join(codeHome, 'spectre', 'agents', 'dev.toml');
|
|
56
|
+
assert.ok(fs.existsSync(agentPath));
|
|
57
|
+
const agentConfig = fs.readFileSync(agentPath, 'utf8');
|
|
58
|
+
assert.match(agentConfig, /name = "dev"/);
|
|
59
|
+
assert.match(agentConfig, /description = /);
|
|
60
|
+
assert.match(agentConfig, /developer_instructions = \"\"\"/);
|
|
61
|
+
assert.doesNotMatch(agentConfig, /base_instructions = /);
|
|
62
|
+
|
|
63
|
+
const config = fs.readFileSync(path.join(codeHome, 'config.toml'), 'utf8');
|
|
64
|
+
assert.match(config, /suppress_unstable_features_warning = true/);
|
|
65
|
+
assert.match(config, /\[agents\.spectre_dev\]/);
|
|
66
|
+
assert.match(config, /codex_hooks = true/);
|
|
67
|
+
assert.match(config, /multi_agent = true/);
|
|
68
|
+
assert.doesNotMatch(config, /session_start = /);
|
|
69
|
+
assert.doesNotMatch(config, /pre_session_start/);
|
|
70
|
+
assert.match(config, /\[\[skills\.config\]\]/);
|
|
71
|
+
assert.match(config, /path = ".*\.agents\/skills\/spectre-recall\/SKILL\.md"/);
|
|
72
|
+
|
|
73
|
+
const hooksConfig = JSON.parse(fs.readFileSync(path.join(codeHome, 'hooks.json'), 'utf8'));
|
|
74
|
+
assert.ok(Array.isArray(hooksConfig.hooks.SessionStart));
|
|
75
|
+
assert.equal(hooksConfig.hooks.SessionStart.length, 1);
|
|
76
|
+
assert.deepEqual(hooksConfig.hooks.SessionStart[0].hooks, [
|
|
77
|
+
{
|
|
78
|
+
type: 'command',
|
|
79
|
+
command: `node '${path.join(codeHome, 'spectre', 'hooks', 'session-start.mjs')}'`,
|
|
80
|
+
statusMessage: 'Spectre: loading session context'
|
|
81
|
+
}
|
|
82
|
+
]);
|
|
83
|
+
assert.ok(fs.existsSync(path.join(codeHome, 'spectre', 'tools', 'sync-session-override.mjs')));
|
|
84
|
+
assert.ok(fs.existsSync(path.join(projectDir, '.agents', 'skills', 'spectre-recall', 'SKILL.md')));
|
|
85
|
+
assert.ok(fs.existsSync(path.join(projectDir, '.agents', 'skills', 'spectre-recall', 'references', 'registry.toon')));
|
|
86
|
+
|
|
87
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(projectDir, '.spectre', 'manifest.json'), 'utf8'));
|
|
88
|
+
assert.equal(manifest.codexIntegration.hiddenContextInjection, 'agents_override_managed_block');
|
|
89
|
+
assert.equal(manifest.codexIntegration.fallback, 'none');
|
|
90
|
+
|
|
91
|
+
assert.ok(!fs.existsSync(path.join(projectDir, 'AGENTS.md')));
|
|
92
|
+
assert.ok(!fs.existsSync(path.join(projectDir, 'AGENTS.override.md')));
|
|
93
|
+
assert.ok(!fs.existsSync(path.join(projectDir, '.agents', 'skills', 'spectre-session')));
|
|
94
|
+
assert.ok(!fs.existsSync(path.join(projectDir, '.spectre', 'bin', 'codex')));
|
|
95
|
+
assert.ok(!fs.existsSync(path.join(codeHome, 'prompts', 'spectre:scope.md')));
|
|
96
|
+
execFileSync('codex', ['--version'], {
|
|
97
|
+
env: {
|
|
98
|
+
...process.env,
|
|
99
|
+
CODEX_HOME: codeHome
|
|
100
|
+
},
|
|
101
|
+
stdio: 'ignore'
|
|
102
|
+
});
|
|
103
|
+
} finally {
|
|
104
|
+
if (previousCodexHome == null) {
|
|
105
|
+
delete process.env.CODEX_HOME;
|
|
106
|
+
} else {
|
|
107
|
+
process.env.CODEX_HOME = previousCodexHome;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('project install removes legacy bridge artifacts while preserving non-managed AGENTS content', { concurrency: false }, async () => {
|
|
113
|
+
const projectDir = makeProject();
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
path.join(projectDir, 'AGENTS.md'),
|
|
116
|
+
[
|
|
117
|
+
'Project-specific instructions.',
|
|
118
|
+
'',
|
|
119
|
+
'<!-- spectre-codex:start -->',
|
|
120
|
+
'Read `AGENTS.override.md` before doing work in this repository.',
|
|
121
|
+
'<!-- spectre-codex:end -->'
|
|
122
|
+
].join('\n')
|
|
123
|
+
);
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
path.join(projectDir, 'AGENTS.override.md'),
|
|
126
|
+
[
|
|
127
|
+
'User-owned override content.',
|
|
128
|
+
'',
|
|
129
|
+
'<!-- spectre-session:start -->',
|
|
130
|
+
'legacy session context',
|
|
131
|
+
'<!-- spectre-session:end -->',
|
|
132
|
+
'',
|
|
133
|
+
'<!-- spectre-knowledge:start -->',
|
|
134
|
+
'legacy knowledge context',
|
|
135
|
+
'<!-- spectre-knowledge:end -->'
|
|
136
|
+
].join('\n')
|
|
137
|
+
);
|
|
138
|
+
fs.mkdirSync(path.join(projectDir, '.agents', 'skills', 'spectre-session'), { recursive: true });
|
|
139
|
+
fs.writeFileSync(path.join(projectDir, '.agents', 'skills', 'spectre-session', 'SKILL.md'), 'legacy session skill\n');
|
|
140
|
+
fs.mkdirSync(path.join(projectDir, '.spectre', 'bin'), { recursive: true });
|
|
141
|
+
fs.writeFileSync(path.join(projectDir, '.spectre', 'bin', 'codex'), '#!/bin/sh\n');
|
|
142
|
+
|
|
143
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
144
|
+
delete process.env.CODEX_HOME;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const { main } = await import('./main.js');
|
|
148
|
+
await main(['install', 'codex', '--scope', 'project', '--project-dir', projectDir]);
|
|
149
|
+
|
|
150
|
+
const agentsContent = fs.readFileSync(path.join(projectDir, 'AGENTS.md'), 'utf8');
|
|
151
|
+
assert.match(agentsContent, /Project-specific instructions\./);
|
|
152
|
+
assert.doesNotMatch(agentsContent, /spectre-codex:start/);
|
|
153
|
+
const overrideContent = fs.readFileSync(path.join(projectDir, 'AGENTS.override.md'), 'utf8');
|
|
154
|
+
assert.match(overrideContent, /User-owned override content\./);
|
|
155
|
+
assert.doesNotMatch(overrideContent, /spectre-session:start/);
|
|
156
|
+
assert.doesNotMatch(overrideContent, /spectre-knowledge:start/);
|
|
157
|
+
assert.ok(!fs.existsSync(path.join(projectDir, '.agents', 'skills', 'spectre-session')));
|
|
158
|
+
assert.ok(!fs.existsSync(path.join(projectDir, '.spectre', 'bin', 'codex')));
|
|
159
|
+
} finally {
|
|
160
|
+
if (previousCodexHome == null) {
|
|
161
|
+
delete process.env.CODEX_HOME;
|
|
162
|
+
} else {
|
|
163
|
+
process.env.CODEX_HOME = previousCodexHome;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('project install preserves unrelated hooks.json handlers while adding Spectre SessionStart', { concurrency: false }, async () => {
|
|
169
|
+
const projectDir = makeProject();
|
|
170
|
+
const codeHome = path.join(projectDir, '.codex');
|
|
171
|
+
fs.mkdirSync(codeHome, { recursive: true });
|
|
172
|
+
fs.writeFileSync(
|
|
173
|
+
path.join(codeHome, 'hooks.json'),
|
|
174
|
+
JSON.stringify({
|
|
175
|
+
hooks: {
|
|
176
|
+
Stop: [
|
|
177
|
+
{
|
|
178
|
+
matcher: '*',
|
|
179
|
+
hooks: [
|
|
180
|
+
{
|
|
181
|
+
type: 'command',
|
|
182
|
+
command: 'echo existing-stop-hook'
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
}, null, 2)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
192
|
+
delete process.env.CODEX_HOME;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const { main } = await import('./main.js');
|
|
196
|
+
await main(['install', 'codex', '--scope', 'project', '--project-dir', projectDir]);
|
|
197
|
+
|
|
198
|
+
const hooksConfig = JSON.parse(fs.readFileSync(path.join(codeHome, 'hooks.json'), 'utf8'));
|
|
199
|
+
assert.deepEqual(hooksConfig.hooks.Stop, [
|
|
200
|
+
{
|
|
201
|
+
matcher: '*',
|
|
202
|
+
hooks: [
|
|
203
|
+
{
|
|
204
|
+
type: 'command',
|
|
205
|
+
command: 'echo existing-stop-hook'
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
]);
|
|
210
|
+
assert.ok(Array.isArray(hooksConfig.hooks.SessionStart));
|
|
211
|
+
assert.ok(hooksConfig.hooks.SessionStart.some(group =>
|
|
212
|
+
Array.isArray(group.hooks) && group.hooks.some(hook => hook.command.includes('spectre/hooks/session-start.mjs'))
|
|
213
|
+
));
|
|
214
|
+
} finally {
|
|
215
|
+
if (previousCodexHome == null) {
|
|
216
|
+
delete process.env.CODEX_HOME;
|
|
217
|
+
} else {
|
|
218
|
+
process.env.CODEX_HOME = previousCodexHome;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('project uninstall removes managed workflow skills, agent config, and project skill registrations', { concurrency: false }, async () => {
|
|
224
|
+
const projectDir = makeProject();
|
|
225
|
+
fs.writeFileSync(
|
|
226
|
+
path.join(projectDir, 'AGENTS.override.md'),
|
|
227
|
+
[
|
|
228
|
+
'User-owned override content.',
|
|
229
|
+
'',
|
|
230
|
+
'<!-- spectre-session:start -->',
|
|
231
|
+
'legacy session context',
|
|
232
|
+
'<!-- spectre-session:end -->',
|
|
233
|
+
'',
|
|
234
|
+
'<!-- spectre-knowledge:start -->',
|
|
235
|
+
'legacy knowledge context',
|
|
236
|
+
'<!-- spectre-knowledge:end -->'
|
|
237
|
+
].join('\n')
|
|
238
|
+
);
|
|
239
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
240
|
+
delete process.env.CODEX_HOME;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const { main } = await import('./main.js');
|
|
244
|
+
await main(['install', 'codex', '--scope', 'project', '--project-dir', projectDir]);
|
|
245
|
+
await main(['uninstall', 'codex', '--scope', 'project', '--project-dir', projectDir]);
|
|
246
|
+
|
|
247
|
+
const codeHome = path.join(projectDir, '.codex');
|
|
248
|
+
|
|
249
|
+
assert.ok(!fs.existsSync(path.join(codeHome, 'skills', 'spectre-scope')));
|
|
250
|
+
assert.ok(!fs.existsSync(path.join(codeHome, 'spectre')));
|
|
251
|
+
|
|
252
|
+
const config = fs.readFileSync(path.join(codeHome, 'config.toml'), 'utf8');
|
|
253
|
+
assert.doesNotMatch(config, /\[agents\.spectre_dev\]/);
|
|
254
|
+
assert.doesNotMatch(config, /\[\[skills\.config\]\][\s\S]*spectre-recall/);
|
|
255
|
+
assert.doesNotMatch(config, /pre_session_start/);
|
|
256
|
+
assert.doesNotMatch(config, /session_start = /);
|
|
257
|
+
|
|
258
|
+
assert.ok(!fs.existsSync(path.join(projectDir, '.spectre', 'manifest.json')));
|
|
259
|
+
const overrideContent = fs.readFileSync(path.join(projectDir, 'AGENTS.override.md'), 'utf8');
|
|
260
|
+
assert.match(overrideContent, /User-owned override content\./);
|
|
261
|
+
assert.doesNotMatch(overrideContent, /spectre-session:start/);
|
|
262
|
+
assert.doesNotMatch(overrideContent, /spectre-knowledge:start/);
|
|
263
|
+
assert.ok(!fs.existsSync(path.join(projectDir, '.spectre', 'bin', 'codex')));
|
|
264
|
+
assert.ok(!fs.existsSync(path.join(codeHome, 'hooks.json')));
|
|
265
|
+
assert.ok(fs.existsSync(path.join(projectDir, '.agents', 'skills', 'spectre-recall', 'SKILL.md')));
|
|
266
|
+
} finally {
|
|
267
|
+
if (previousCodexHome == null) {
|
|
268
|
+
delete process.env.CODEX_HOME;
|
|
269
|
+
} else {
|
|
270
|
+
process.env.CODEX_HOME = previousCodexHome;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|