@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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/bin/spectre.js +8 -0
  4. package/package.json +23 -0
  5. package/plugins/spectre/.claude-plugin/plugin.json +5 -0
  6. package/plugins/spectre/agents/analyst.md +122 -0
  7. package/plugins/spectre/agents/dev.md +70 -0
  8. package/plugins/spectre/agents/finder.md +105 -0
  9. package/plugins/spectre/agents/patterns.md +207 -0
  10. package/plugins/spectre/agents/reviewer.md +128 -0
  11. package/plugins/spectre/agents/sync.md +151 -0
  12. package/plugins/spectre/agents/tester.md +209 -0
  13. package/plugins/spectre/agents/web-research.md +109 -0
  14. package/plugins/spectre/commands/architecture_review.md +120 -0
  15. package/plugins/spectre/commands/clean.md +313 -0
  16. package/plugins/spectre/commands/code_review.md +408 -0
  17. package/plugins/spectre/commands/create_plan.md +117 -0
  18. package/plugins/spectre/commands/create_tasks.md +374 -0
  19. package/plugins/spectre/commands/create_test_guide.md +120 -0
  20. package/plugins/spectre/commands/evaluate.md +50 -0
  21. package/plugins/spectre/commands/execute.md +87 -0
  22. package/plugins/spectre/commands/fix.md +61 -0
  23. package/plugins/spectre/commands/forget.md +58 -0
  24. package/plugins/spectre/commands/handoff.md +161 -0
  25. package/plugins/spectre/commands/kickoff.md +115 -0
  26. package/plugins/spectre/commands/learn.md +15 -0
  27. package/plugins/spectre/commands/plan.md +170 -0
  28. package/plugins/spectre/commands/plan_review.md +33 -0
  29. package/plugins/spectre/commands/quick_dev.md +101 -0
  30. package/plugins/spectre/commands/rebase.md +73 -0
  31. package/plugins/spectre/commands/recall.md +5 -0
  32. package/plugins/spectre/commands/research.md +159 -0
  33. package/plugins/spectre/commands/scope.md +119 -0
  34. package/plugins/spectre/commands/ship.md +172 -0
  35. package/plugins/spectre/commands/sweep.md +82 -0
  36. package/plugins/spectre/commands/test.md +380 -0
  37. package/plugins/spectre/commands/ux_spec.md +91 -0
  38. package/plugins/spectre/commands/validate.md +343 -0
  39. package/plugins/spectre/hooks/hooks.json +34 -0
  40. package/plugins/spectre/hooks/scripts/bootstrap.cjs +99 -0
  41. package/plugins/spectre/hooks/scripts/handoff-resume.cjs +410 -0
  42. package/plugins/spectre/hooks/scripts/lib.cjs +83 -0
  43. package/plugins/spectre/hooks/scripts/load-knowledge.cjs +120 -0
  44. package/plugins/spectre/hooks/scripts/precompact-warning.cjs +19 -0
  45. package/plugins/spectre/hooks/scripts/register_learning.cjs +144 -0
  46. package/plugins/spectre/hooks/scripts/test_bootstrap.cjs +84 -0
  47. package/plugins/spectre/hooks/scripts/test_handoff-resume.cjs +858 -0
  48. package/plugins/spectre/hooks/scripts/test_load-knowledge.cjs +285 -0
  49. package/plugins/spectre/hooks/scripts/test_register-learning.cjs +146 -0
  50. package/plugins/spectre/skills/spectre-apply/SKILL.md +189 -0
  51. package/plugins/spectre/skills/spectre-guide/SKILL.md +358 -0
  52. package/plugins/spectre/skills/spectre-learn/SKILL.md +635 -0
  53. package/plugins/spectre/skills/spectre-learn/references/recall-template.md +31 -0
  54. package/plugins/spectre/skills/spectre-tdd/SKILL.md +111 -0
  55. package/src/config.test.js +134 -0
  56. package/src/install.test.js +273 -0
  57. package/src/lib/config.js +516 -0
  58. package/src/lib/constants.js +60 -0
  59. package/src/lib/doctor.js +168 -0
  60. package/src/lib/install.js +482 -0
  61. package/src/lib/knowledge.js +217 -0
  62. package/src/lib/paths.js +98 -0
  63. package/src/lib/project.js +473 -0
  64. 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
+ });