@codename_inc/spectre 3.7.0 → 5.0.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/README.md +6 -7
- package/package.json +3 -2
- package/plugins/spectre/.claude-plugin/plugin.json +1 -1
- package/plugins/spectre/bin/spectre-register +5 -0
- package/plugins/spectre/hooks/hooks.json +3 -14
- package/plugins/spectre/hooks/scripts/bootstrap.mjs +98 -0
- package/plugins/spectre/hooks/scripts/handoff-resume.mjs +404 -0
- package/plugins/spectre/hooks/scripts/lib.mjs +82 -0
- package/plugins/spectre/hooks/scripts/load-knowledge.mjs +189 -0
- package/plugins/spectre/hooks/scripts/register_learning.mjs +264 -0
- package/plugins/spectre/hooks/scripts/{test_bootstrap.cjs → test_bootstrap.mjs} +12 -7
- package/plugins/spectre/hooks/scripts/{test_handoff-resume.cjs → test_handoff-resume.mjs} +13 -11
- package/plugins/spectre/hooks/scripts/{test_load-knowledge.cjs → test_load-knowledge.mjs} +103 -22
- package/plugins/spectre/hooks/scripts/test_register-learning.mjs +335 -0
- package/plugins/spectre/skills/apply/SKILL.md +87 -0
- package/plugins/spectre/{commands/architecture_review.md → skills/architecture_review/SKILL.md} +9 -0
- package/plugins/spectre/{commands/clean.md → skills/clean/SKILL.md} +9 -0
- package/plugins/spectre/{commands/code_review.md → skills/code_review/SKILL.md} +9 -0
- package/plugins/spectre/{commands/create_plan.md → skills/create_plan/SKILL.md} +9 -0
- package/plugins/spectre/{commands/create_tasks.md → skills/create_tasks/SKILL.md} +9 -0
- package/plugins/spectre/{commands/create_test_guide.md → skills/create_test_guide/SKILL.md} +9 -0
- package/plugins/spectre/{commands/evaluate.md → skills/evaluate/SKILL.md} +11 -2
- package/plugins/spectre/{commands/execute.md → skills/execute/SKILL.md} +12 -3
- package/plugins/spectre/{commands/fix.md → skills/fix/SKILL.md} +9 -0
- package/plugins/spectre/{commands/forget.md → skills/forget/SKILL.md} +9 -0
- package/plugins/spectre/skills/{spectre-guide → guide}/SKILL.md +6 -5
- package/plugins/spectre/{commands/handoff.md → skills/handoff/SKILL.md} +9 -0
- package/plugins/spectre/{commands/kickoff.md → skills/kickoff/SKILL.md} +9 -0
- package/plugins/spectre/skills/{spectre-learn → learn}/SKILL.md +19 -59
- package/plugins/spectre/skills/learn/references/recall-template.md +34 -0
- package/plugins/spectre/{commands/plan.md → skills/plan/SKILL.md} +66 -25
- package/plugins/spectre/{commands/plan_review.md → skills/plan_review/SKILL.md} +9 -0
- package/plugins/spectre/skills/prototype/SKILL.md +314 -0
- package/plugins/spectre/{commands/quick_dev.md → skills/quick_dev/SKILL.md} +9 -0
- package/plugins/spectre/{commands/rebase.md → skills/rebase/SKILL.md} +9 -0
- package/plugins/spectre/skills/recall/SKILL.md +17 -0
- package/plugins/spectre/{commands/research.md → skills/research/SKILL.md} +9 -0
- package/plugins/spectre/skills/scope/SKILL.md +174 -0
- package/plugins/spectre/{commands/ship.md → skills/ship/SKILL.md} +9 -0
- package/plugins/spectre/{commands/sweep.md → skills/sweep/SKILL.md} +9 -0
- package/plugins/spectre/skills/tdd/SKILL.md +111 -0
- package/plugins/spectre/{commands/test.md → skills/test/SKILL.md} +9 -0
- package/plugins/spectre/skills/ux/SKILL.md +121 -0
- package/plugins/spectre/{commands/validate.md → skills/validate/SKILL.md} +9 -0
- package/plugins/spectre-codex/agents/analyst.toml +117 -0
- package/plugins/spectre-codex/agents/dev.toml +65 -0
- package/plugins/spectre-codex/agents/finder.toml +101 -0
- package/plugins/spectre-codex/agents/patterns.toml +203 -0
- package/plugins/spectre-codex/agents/reviewer.toml +123 -0
- package/plugins/spectre-codex/agents/sync.toml +146 -0
- package/plugins/spectre-codex/agents/tester.toml +205 -0
- package/plugins/spectre-codex/agents/web-research.toml +104 -0
- package/plugins/spectre-codex/hooks/hooks.json +23 -0
- package/plugins/{spectre/hooks/scripts/bootstrap.cjs → spectre-codex/hooks/scripts/bootstrap.mjs} +15 -16
- package/plugins/{spectre/hooks/scripts/handoff-resume.cjs → spectre-codex/hooks/scripts/handoff-resume.mjs} +21 -27
- package/plugins/{spectre/hooks/scripts/lib.cjs → spectre-codex/hooks/scripts/lib.mjs} +3 -4
- package/plugins/spectre-codex/hooks/scripts/load-knowledge.mjs +189 -0
- package/plugins/spectre-codex/hooks/scripts/register_learning.mjs +264 -0
- package/plugins/spectre-codex/skills/apply/SKILL.md +87 -0
- package/plugins/spectre-codex/skills/architecture_review/SKILL.md +129 -0
- package/plugins/spectre-codex/skills/clean/SKILL.md +322 -0
- package/plugins/spectre-codex/skills/code_review/SKILL.md +417 -0
- package/plugins/spectre-codex/skills/create_plan/SKILL.md +126 -0
- package/plugins/spectre-codex/skills/create_tasks/SKILL.md +383 -0
- package/plugins/spectre-codex/skills/create_test_guide/SKILL.md +129 -0
- package/plugins/spectre-codex/skills/evaluate/SKILL.md +59 -0
- package/plugins/spectre-codex/skills/execute/SKILL.md +96 -0
- package/plugins/spectre-codex/skills/fix/SKILL.md +70 -0
- package/plugins/spectre-codex/skills/forget/SKILL.md +67 -0
- package/plugins/spectre-codex/skills/guide/SKILL.md +359 -0
- package/plugins/spectre-codex/skills/handoff/SKILL.md +170 -0
- package/plugins/spectre-codex/skills/kickoff/SKILL.md +124 -0
- package/plugins/spectre-codex/skills/learn/SKILL.md +595 -0
- package/plugins/{spectre/skills/spectre-learn → spectre-codex/skills/learn}/references/recall-template.md +4 -1
- package/plugins/spectre-codex/skills/plan/SKILL.md +211 -0
- package/plugins/spectre-codex/skills/plan_review/SKILL.md +42 -0
- package/plugins/spectre-codex/skills/prototype/SKILL.md +314 -0
- package/plugins/spectre-codex/skills/quick_dev/SKILL.md +110 -0
- package/plugins/spectre-codex/skills/rebase/SKILL.md +82 -0
- package/plugins/spectre-codex/skills/recall/SKILL.md +17 -0
- package/plugins/spectre-codex/skills/research/SKILL.md +168 -0
- package/plugins/spectre-codex/skills/scope/SKILL.md +174 -0
- package/plugins/spectre-codex/skills/ship/SKILL.md +181 -0
- package/plugins/spectre-codex/skills/sweep/SKILL.md +91 -0
- package/plugins/{spectre/skills/spectre-tdd → spectre-codex/skills/tdd}/SKILL.md +1 -1
- package/plugins/spectre-codex/skills/test/SKILL.md +389 -0
- package/plugins/spectre-codex/skills/ux/SKILL.md +121 -0
- package/plugins/spectre-codex/skills/validate/SKILL.md +352 -0
- package/src/config.test.js +6 -5
- package/src/install.test.js +100 -11
- package/src/lib/config.js +107 -54
- package/src/lib/constants.js +17 -23
- package/src/lib/doctor.js +19 -22
- package/src/lib/install.js +98 -313
- package/src/lib/knowledge.js +7 -37
- package/src/lib/paths.js +0 -12
- package/src/pack.test.js +87 -0
- package/plugins/spectre/commands/learn.md +0 -15
- package/plugins/spectre/commands/recall.md +0 -5
- package/plugins/spectre/commands/scope.md +0 -119
- package/plugins/spectre/commands/ux_spec.md +0 -91
- package/plugins/spectre/hooks/scripts/load-knowledge.cjs +0 -120
- package/plugins/spectre/hooks/scripts/precompact-warning.cjs +0 -19
- package/plugins/spectre/hooks/scripts/register_learning.cjs +0 -144
- package/plugins/spectre/hooks/scripts/test_register-learning.cjs +0 -146
- package/plugins/spectre/skills/spectre-apply/SKILL.md +0 -189
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ That's it. You just start with 1 command to build features.
|
|
|
35
35
|
### Within Codex
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
npx spectre install codex
|
|
38
|
+
npx @codename_inc/spectre install codex
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
When prompted, choose `project` to install into the current repo's `.codex`, or `user` to install into `~/.codex`.
|
|
@@ -47,7 +47,7 @@ If you choose `user`, restart or open your normal Codex session.
|
|
|
47
47
|
Then run a Spectre command such as:
|
|
48
48
|
|
|
49
49
|
```plaintext
|
|
50
|
-
|
|
50
|
+
scope
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
Current Codex behavior:
|
|
@@ -65,7 +65,7 @@ Session continuity deep dive: [`docs/codex-sessionstart-memory.md`](./docs/codex
|
|
|
65
65
|
|
|
66
66
|
## 🔁 How It Works
|
|
67
67
|
|
|
68
|
-
- run one of the kickoff prompts in Claude Code - `/spectre:scope` is the main command for building new features, but also `/spectre:kickoff` for high ambiguity new features (includes web research), `/spectre:research` for codebase research "how might we build …” style Qs, or `/spectre:
|
|
68
|
+
- run one of the kickoff prompts in Claude Code - `/spectre:scope` is the main command for building new features, but also `/spectre:kickoff` for high ambiguity new features (includes web research), `/spectre:research` for codebase research "how might we build …” style Qs, or `/spectre:ux` to define user flows, components, and layout for a new feature.
|
|
69
69
|
|
|
70
70
|
- follow the prompts/instructions to create the related canonical document and Claude Code will suggest the next step in the SPECTRE workflow automatically (e.g., going from `scope` to `plan` to `tasks` and so on)
|
|
71
71
|
|
|
@@ -281,7 +281,7 @@ Although I do sometimes use @spectre:web-research for web research. It's like mi
|
|
|
281
281
|
|
|
282
282
|
- start /spectre:scope to get crisp on what's in/out. this is non-negotiable unless the feature is a one line ask.
|
|
283
283
|
|
|
284
|
-
- if the feature's ux/user flow is unclear to me, or I want to make sure to really nail it, i run /spectre:
|
|
284
|
+
- if the feature's ux/user flow is unclear to me, or I want to make sure to really nail it, i run /spectre:ux. Its similar to /spectre:scope but focuses on getting clear on the core user flows.
|
|
285
285
|
|
|
286
286
|
- /spectre:plan to build out a well researched technical design or set of tasks
|
|
287
287
|
|
|
@@ -367,7 +367,7 @@ I use /spectre:fix for pretty much all bugs I run into.
|
|
|
367
367
|
| --- | --- |
|
|
368
368
|
| `/spectre:sweep` | Light cleanup pass — lint, test, descriptive commits |
|
|
369
369
|
| `/spectre:learn` | Capture knowledge for future sessions |
|
|
370
|
-
| `/spectre:
|
|
370
|
+
| `/spectre:ux` | UX specification for UI-heavy features |
|
|
371
371
|
| `/spectre:fix` | Investigate bugs & implement fixes |
|
|
372
372
|
|
|
373
373
|
## 📁 Repository Structure
|
|
@@ -380,10 +380,9 @@ spectre/
|
|
|
380
380
|
│ └── spectre/
|
|
381
381
|
│ ├── .claude-plugin/
|
|
382
382
|
│ │ └── plugin.json # Plugin manifest
|
|
383
|
-
│ ├── commands/ # Slash commands
|
|
384
383
|
│ ├── agents/ # Subagent definitions
|
|
385
384
|
│ ├── hooks/ # Session memory hooks
|
|
386
|
-
│ └── skills/ #
|
|
385
|
+
│ └── skills/ # Slash workflows + knowledge skills
|
|
387
386
|
├── scripts/ # Release & utility scripts
|
|
388
387
|
└── CLAUDE.md
|
|
389
388
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codename_inc/spectre",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"spectre": "./bin/spectre.js"
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "node --test src/**/*.test.js plugins/spectre/hooks/scripts/test_*.cjs",
|
|
16
|
+
"test": "node --test src/**/*.test.js plugins/spectre/hooks/scripts/test_*.mjs scripts/test_sync-codex.cjs",
|
|
17
|
+
"sync-codex": "node scripts/sync-codex.cjs",
|
|
17
18
|
"tokens": "node scripts/count-tokens.js",
|
|
18
19
|
"release": "node scripts/release.js"
|
|
19
20
|
},
|
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
-
"PreCompact": [
|
|
4
|
-
{
|
|
5
|
-
"matcher": "*",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/precompact-warning.cjs"
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
3
|
"SessionStart": [
|
|
15
4
|
{
|
|
16
5
|
"matcher": "startup|clear|compact",
|
|
17
6
|
"hooks": [
|
|
18
7
|
{
|
|
19
8
|
"type": "command",
|
|
20
|
-
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/bootstrap.
|
|
9
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/bootstrap.mjs"
|
|
21
10
|
},
|
|
22
11
|
{
|
|
23
12
|
"type": "command",
|
|
24
|
-
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/handoff-resume.
|
|
13
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/handoff-resume.mjs"
|
|
25
14
|
},
|
|
26
15
|
{
|
|
27
16
|
"type": "command",
|
|
28
|
-
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/load-knowledge.
|
|
17
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/load-knowledge.mjs"
|
|
29
18
|
}
|
|
30
19
|
]
|
|
31
20
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bootstrap.mjs
|
|
5
|
+
*
|
|
6
|
+
* SessionStart hook that removes stale files from older plugin versions.
|
|
7
|
+
*
|
|
8
|
+
* When users update the plugin via marketplace, old files that were deleted
|
|
9
|
+
* from the repo may still linger in their cached copy. This script runs on
|
|
10
|
+
* every session start and cleans them up.
|
|
11
|
+
*
|
|
12
|
+
* To add new files to the cleanup list, append to STALE_PATHS below.
|
|
13
|
+
* Paths are relative to CLAUDE_PLUGIN_ROOT.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { readStdinWithTimeout } from './lib.mjs';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
|
|
24
|
+
// ──────────────────────────────────────────────────────────────────
|
|
25
|
+
// Stale paths to remove (relative to CLAUDE_PLUGIN_ROOT)
|
|
26
|
+
// ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const STALE_PATHS = [
|
|
29
|
+
// Python scripts replaced by JS hook equivalents (v3.x migration)
|
|
30
|
+
'hooks/scripts/capture-todos.py',
|
|
31
|
+
'hooks/scripts/handoff-resume.py',
|
|
32
|
+
'hooks/scripts/load-knowledge.py',
|
|
33
|
+
'hooks/scripts/precompact-warning.py',
|
|
34
|
+
'hooks/scripts/precompact-warning.mjs',
|
|
35
|
+
'hooks/scripts/register_learning.py',
|
|
36
|
+
'hooks/scripts/test_handoff_resume.py',
|
|
37
|
+
'hooks/scripts/test_load_knowledge.py',
|
|
38
|
+
|
|
39
|
+
// Old skill directory replaced by spectre-guide
|
|
40
|
+
'skills/spectre-next-steps',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// ──────────────────────────────────────────────────────────────────
|
|
44
|
+
// Cleanup logic
|
|
45
|
+
// ──────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function cleanupStalePaths(pluginRoot) {
|
|
48
|
+
let removed = 0;
|
|
49
|
+
|
|
50
|
+
for (const relPath of STALE_PATHS) {
|
|
51
|
+
const fullPath = path.join(pluginRoot, relPath);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const stat = fs.statSync(fullPath);
|
|
55
|
+
|
|
56
|
+
if (stat.isDirectory()) {
|
|
57
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
58
|
+
removed++;
|
|
59
|
+
} else {
|
|
60
|
+
fs.unlinkSync(fullPath);
|
|
61
|
+
removed++;
|
|
62
|
+
}
|
|
63
|
+
} catch (_) {
|
|
64
|
+
// File doesn't exist or can't be removed — skip silently
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return removed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ──────────────────────────────────────────────────────────────────
|
|
72
|
+
// Main
|
|
73
|
+
// ──────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
// Drain stdin so the hook system doesn't hang
|
|
77
|
+
await readStdinWithTimeout();
|
|
78
|
+
|
|
79
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..', '..');
|
|
80
|
+
|
|
81
|
+
const removed = cleanupStalePaths(pluginRoot);
|
|
82
|
+
|
|
83
|
+
if (removed > 0) {
|
|
84
|
+
process.stdout.write(JSON.stringify({
|
|
85
|
+
systemMessage: `bootstrap: cleaned ${removed} stale file${removed > 1 ? 's' : ''} from previous plugin version`
|
|
86
|
+
}) + '\n');
|
|
87
|
+
} else {
|
|
88
|
+
process.stdout.write(JSON.stringify({}) + '\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { cleanupStalePaths, STALE_PATHS };
|
|
95
|
+
|
|
96
|
+
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fs.realpathSync(__filename)) {
|
|
97
|
+
main();
|
|
98
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* handoff-resume.mjs
|
|
5
|
+
*
|
|
6
|
+
* SessionStart hook that injects context from the last /spectre:handoff.
|
|
7
|
+
* Consolidates the previous session-resume-hook.sh + format-resume-context.py.
|
|
8
|
+
*
|
|
9
|
+
* Outputs JSON for Claude Code hook system:
|
|
10
|
+
* - systemMessage: User-visible notice
|
|
11
|
+
* - hookSpecificOutput.additionalContext: Full session context in <session-context> tags
|
|
12
|
+
*
|
|
13
|
+
* Background modes:
|
|
14
|
+
* - --bg-copy-refs: Copy plugin references (fork #1)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fork } from 'node:child_process';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { readStdinWithTimeout, getGitBranch } from './lib.mjs';
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = path.dirname(__filename);
|
|
25
|
+
|
|
26
|
+
function getPluginRoot() {
|
|
27
|
+
return process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..', '..');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ──────────────────────────────────────────────────────────────────
|
|
31
|
+
// Plugin reference copying (background fork #1)
|
|
32
|
+
// ──────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function copyPluginReferences() {
|
|
35
|
+
const pluginRoot = getPluginRoot();
|
|
36
|
+
if (!pluginRoot) return;
|
|
37
|
+
|
|
38
|
+
const referencesSrc = path.join(pluginRoot, 'references');
|
|
39
|
+
if (!fs.existsSync(referencesSrc)) return;
|
|
40
|
+
|
|
41
|
+
const referencesDst = path.join('.claude', 'spectre');
|
|
42
|
+
fs.mkdirSync(referencesDst, { recursive: true });
|
|
43
|
+
|
|
44
|
+
const files = fs.readdirSync(referencesSrc).filter(f => f.endsWith('.md'));
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const dst = path.join(referencesDst, file);
|
|
47
|
+
if (!fs.existsSync(dst)) {
|
|
48
|
+
fs.copyFileSync(path.join(referencesSrc, file), dst);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Append to .gitignore if it exists and .claude/ not already ignored
|
|
53
|
+
const gitignorePath = '.gitignore';
|
|
54
|
+
if (fs.existsSync(gitignorePath)) {
|
|
55
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
56
|
+
if (!content.includes('.claude/') && !content.includes('.claude/spectre/')) {
|
|
57
|
+
fs.appendFileSync(gitignorePath, '\n# spectre plugin files\n.claude/spectre/\n');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ──────────────────────────────────────────────────────────────────
|
|
63
|
+
// Session discovery
|
|
64
|
+
// ──────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function findLatestHandoff(sessionDir) {
|
|
67
|
+
if (!fs.existsSync(sessionDir)) return null;
|
|
68
|
+
|
|
69
|
+
// Only look at top-level files, not in archive/
|
|
70
|
+
const files = fs.readdirSync(sessionDir)
|
|
71
|
+
.filter(f => f.endsWith('_handoff.json'))
|
|
72
|
+
.map(f => ({
|
|
73
|
+
name: f,
|
|
74
|
+
full: path.join(sessionDir, f),
|
|
75
|
+
mtime: fs.statSync(path.join(sessionDir, f)).mtimeMs
|
|
76
|
+
}))
|
|
77
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
78
|
+
|
|
79
|
+
return files.length > 0 ? files[0].full : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ──────────────────────────────────────────────────────────────────
|
|
83
|
+
// Formatting helpers
|
|
84
|
+
// ──────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function formatList(items, prefix) {
|
|
87
|
+
prefix = prefix != null ? prefix : '- ';
|
|
88
|
+
if (!items || !items.length) return `${prefix}None`;
|
|
89
|
+
return items.map(item => `${prefix}${item}`).join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildCheckboxTree(tasks) {
|
|
93
|
+
if (!tasks || !tasks.length) return 'No tasks found.';
|
|
94
|
+
|
|
95
|
+
const byParent = {};
|
|
96
|
+
for (const task of tasks) {
|
|
97
|
+
const parent = task.parent || null;
|
|
98
|
+
if (!byParent[parent]) byParent[parent] = [];
|
|
99
|
+
byParent[parent].push(task);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderTask(task, indent) {
|
|
103
|
+
indent = indent || 0;
|
|
104
|
+
const lines = [];
|
|
105
|
+
const prefix = ' '.repeat(indent);
|
|
106
|
+
const checkbox = task.completed ? '[x]' : '[ ]';
|
|
107
|
+
const status = task.status || 'open';
|
|
108
|
+
const title = task.title || 'Untitled';
|
|
109
|
+
const taskId = task.id || 'unknown';
|
|
110
|
+
|
|
111
|
+
if (task.completed) {
|
|
112
|
+
lines.push(`${prefix}- ${checkbox} ${title} (${taskId}) - COMPLETED`);
|
|
113
|
+
} else {
|
|
114
|
+
const cmd = task.resume_command || `bd update ${taskId} --status in_progress`;
|
|
115
|
+
const statusBadge = status !== 'open' ? `[${status}]` : '';
|
|
116
|
+
lines.push(`${prefix}- ${checkbox} ${title} (${taskId}) ${statusBadge} - \`${cmd}\``);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Render children
|
|
120
|
+
const childrenIds = task.children || [];
|
|
121
|
+
if (childrenIds.length) {
|
|
122
|
+
for (const childTask of tasks) {
|
|
123
|
+
if (childrenIds.includes(childTask.id) || childTask.parent === taskId) {
|
|
124
|
+
lines.push(...renderTask(childTask, indent + 1));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return lines;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Start with root tasks (no parent or parent is null)
|
|
133
|
+
const rootTasks = (byParent[null] || []).concat(byParent['null'] || []);
|
|
134
|
+
|
|
135
|
+
// If no root tasks found, just list all tasks flat
|
|
136
|
+
const startTasks = rootTasks.length ? rootTasks : tasks;
|
|
137
|
+
|
|
138
|
+
const lines = [];
|
|
139
|
+
const renderedIds = new Set();
|
|
140
|
+
|
|
141
|
+
for (const task of startTasks) {
|
|
142
|
+
if (!renderedIds.has(task.id)) {
|
|
143
|
+
const taskLines = renderTask(task);
|
|
144
|
+
lines.push(...taskLines);
|
|
145
|
+
renderedIds.add(task.id);
|
|
146
|
+
for (const t of tasks) {
|
|
147
|
+
if (t.parent === task.id) {
|
|
148
|
+
renderedIds.add(t.id);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ──────────────────────────────────────────────────────────────────
|
|
158
|
+
// Main context formatter
|
|
159
|
+
// ──────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function formatContext(data, opts) {
|
|
162
|
+
opts = opts || {};
|
|
163
|
+
const handoffPath = opts.handoffPath;
|
|
164
|
+
|
|
165
|
+
const taskName = data.task_name || 'unknown';
|
|
166
|
+
const branchName = data.branch_name || 'unknown';
|
|
167
|
+
|
|
168
|
+
// Progress update fields (v1.1 schema)
|
|
169
|
+
const progress = data.progress_update || {};
|
|
170
|
+
const summary = progress.summary || 'No summary available.';
|
|
171
|
+
const goal = progress.goal || '';
|
|
172
|
+
const constraints = progress.constraints || [];
|
|
173
|
+
const decisions = progress.decisions || [];
|
|
174
|
+
const accomplished = progress.accomplished || [];
|
|
175
|
+
const now = progress.now || '';
|
|
176
|
+
const nextSteps = progress.next_steps || [];
|
|
177
|
+
const blockers = progress.blockers || [];
|
|
178
|
+
const openQuestions = progress.open_questions || [];
|
|
179
|
+
const confidence = progress.confidence || 'unknown';
|
|
180
|
+
const risks = progress.risks || [];
|
|
181
|
+
|
|
182
|
+
// Working set (v1.1 schema) - fall back to context.key_files for v1.0
|
|
183
|
+
const workingSet = data.working_set || {};
|
|
184
|
+
let keyFiles = workingSet.key_files || [];
|
|
185
|
+
const activeIds = workingSet.active_ids || [];
|
|
186
|
+
const recentCommands = workingSet.recent_commands || [];
|
|
187
|
+
|
|
188
|
+
// Fall back to old context structure if working_set not present
|
|
189
|
+
if (!keyFiles.length) {
|
|
190
|
+
const ctx = data.context || {};
|
|
191
|
+
keyFiles = ctx.key_files || [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Beads tasks
|
|
195
|
+
const beads = data.beads || {};
|
|
196
|
+
const beadsAvailable = beads.available != null ? beads.available : true;
|
|
197
|
+
const tasks = beads.tasks || [];
|
|
198
|
+
|
|
199
|
+
// Context
|
|
200
|
+
const context = data.context || {};
|
|
201
|
+
const lastCommit = context.last_commit || 'unknown';
|
|
202
|
+
const wipState = context.wip_state || 'unknown';
|
|
203
|
+
|
|
204
|
+
// Build checkbox tree for beads tasks
|
|
205
|
+
const checkboxTree = (beadsAvailable && tasks.length) ? buildCheckboxTree(tasks) : '';
|
|
206
|
+
|
|
207
|
+
// User-visible notice
|
|
208
|
+
const noticeLines = ['\ud83d\udc7b SPECTRE'];
|
|
209
|
+
noticeLines.push(`\n\ud83d\udd04 Session Resumed: ${taskName} | Branch: ${branchName}`);
|
|
210
|
+
|
|
211
|
+
if (goal) {
|
|
212
|
+
noticeLines.push(`\n\ud83c\udfaf Goal: ${goal}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
noticeLines.push(`\n\ud83d\udcdd Summary: ${summary}`);
|
|
216
|
+
|
|
217
|
+
if (nextSteps.length) {
|
|
218
|
+
noticeLines.push('\n\u27a1\ufe0f Next Steps:');
|
|
219
|
+
for (const step of nextSteps) {
|
|
220
|
+
noticeLines.push(` - ${step}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (handoffPath) {
|
|
225
|
+
noticeLines.push(`\n\ud83d\udcc1 Full details: ${handoffPath}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
noticeLines.push('\n\ud83d\udca1 Run /spectre:forget to clear session memory and start fresh.');
|
|
229
|
+
|
|
230
|
+
const visibleNotice = noticeLines.join('\n');
|
|
231
|
+
|
|
232
|
+
// Build the hidden context sections
|
|
233
|
+
const sections = [];
|
|
234
|
+
|
|
235
|
+
sections.push(`# Session Context: ${taskName}`);
|
|
236
|
+
|
|
237
|
+
// Last session summary
|
|
238
|
+
sections.push(`\n## Last Session Summary\n${summary}`);
|
|
239
|
+
|
|
240
|
+
// Goal (if available - v1.1)
|
|
241
|
+
if (goal) {
|
|
242
|
+
sections.push(`\n### Goal\n${goal}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Constraints (if available - v1.1)
|
|
246
|
+
if (constraints.length) {
|
|
247
|
+
sections.push(`\n### Constraints\n${formatList(constraints)}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// What we accomplished
|
|
251
|
+
sections.push(`\n### What We Accomplished\n${formatList(accomplished)}`);
|
|
252
|
+
|
|
253
|
+
// What we were working on (critical for resume - v1.1)
|
|
254
|
+
if (now) {
|
|
255
|
+
sections.push(`\n### Active Work (Resume Here)\n**${now}**`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// What's next
|
|
259
|
+
sections.push(`\n### What's Next\n${formatList(nextSteps)}`);
|
|
260
|
+
|
|
261
|
+
// Blockers
|
|
262
|
+
if (blockers.length) {
|
|
263
|
+
sections.push(`\n### Blockers\n${formatList(blockers)}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Open questions (v1.1)
|
|
267
|
+
if (openQuestions.length) {
|
|
268
|
+
sections.push(`\n### Open Questions\n${formatList(openQuestions)}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Decisions
|
|
272
|
+
if (decisions.length) {
|
|
273
|
+
sections.push(`\n### Decisions Made\n${formatList(decisions)}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Confidence and risks
|
|
277
|
+
const risksStr = risks.length ? formatList(risks, '') : 'None identified';
|
|
278
|
+
sections.push(`\n**Confidence**: ${confidence} | **Risks**: ${risksStr}`);
|
|
279
|
+
|
|
280
|
+
// Working set (v1.1)
|
|
281
|
+
const wsLines = [];
|
|
282
|
+
if (keyFiles.length) {
|
|
283
|
+
wsLines.push(`- **Key Files**: ${keyFiles.join(', ')}`);
|
|
284
|
+
}
|
|
285
|
+
if (activeIds.length) {
|
|
286
|
+
wsLines.push(`- **Active IDs**: ${activeIds.join(', ')}`);
|
|
287
|
+
}
|
|
288
|
+
if (recentCommands.length) {
|
|
289
|
+
wsLines.push(`- **Recent Commands**: ${recentCommands.join(', ')}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (wsLines.length) {
|
|
293
|
+
sections.push('\n### Working Set\n' + wsLines.join('\n'));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Context
|
|
297
|
+
sections.push(
|
|
298
|
+
'\n---\n\n' +
|
|
299
|
+
'## Context\n' +
|
|
300
|
+
`- **Branch**: ${branchName}\n` +
|
|
301
|
+
`- **Last Commit**: ${lastCommit}\n` +
|
|
302
|
+
`- **WIP State**: ${wipState}`
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Beads tasks (if available)
|
|
306
|
+
if (beadsAvailable && checkboxTree) {
|
|
307
|
+
sections.push(`\n### Beads Tasks\n${checkboxTree}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const hiddenContext = `<session-context>\n${sections.join('')}\n</session-context>`;
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
systemMessage: visibleNotice,
|
|
314
|
+
hookSpecificOutput: {
|
|
315
|
+
hookEventName: 'SessionStart',
|
|
316
|
+
additionalContext: hiddenContext
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ──────────────────────────────────────────────────────────────────
|
|
322
|
+
// Main entry point
|
|
323
|
+
// ──────────────────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
async function main() {
|
|
326
|
+
// Handle background fork modes
|
|
327
|
+
if (process.argv[2] === '--bg-copy-refs') {
|
|
328
|
+
try { copyPluginReferences(); } catch (_) {}
|
|
329
|
+
process.exit(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Read stdin
|
|
333
|
+
await readStdinWithTimeout();
|
|
334
|
+
|
|
335
|
+
// Fork to copy plugin references in background (non-blocking)
|
|
336
|
+
const copyChild = fork(__filename, ['--bg-copy-refs'], {
|
|
337
|
+
detached: true,
|
|
338
|
+
stdio: 'ignore'
|
|
339
|
+
});
|
|
340
|
+
copyChild.unref();
|
|
341
|
+
|
|
342
|
+
// Get project directory from environment or cwd
|
|
343
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
344
|
+
|
|
345
|
+
// Get branch name
|
|
346
|
+
const branchName = getGitBranch();
|
|
347
|
+
|
|
348
|
+
// Find session logs directory
|
|
349
|
+
const sessionDir = path.join(projectDir, 'docs', 'tasks', branchName, 'session_logs');
|
|
350
|
+
|
|
351
|
+
// Find latest handoff
|
|
352
|
+
const latestHandoff = findLatestHandoff(sessionDir);
|
|
353
|
+
|
|
354
|
+
if (!latestHandoff) {
|
|
355
|
+
// No session to resume - show welcome banner with tips
|
|
356
|
+
const banner = '\ud83d\udc7b SPECTRE';
|
|
357
|
+
const tips = [
|
|
358
|
+
'',
|
|
359
|
+
'Getting Started with SPECTRE:',
|
|
360
|
+
'',
|
|
361
|
+
'\u2699\ufe0f Tip: Turn off auto-compact via /config \u2014 SPECTRE works best with manual context management',
|
|
362
|
+
'\ud83d\udcbe Use /spectre:handoff when context is getting full but you\'re still going \u2014 saves state for the next session',
|
|
363
|
+
'\ud83e\uddf9 Use /spectre:forget to clear session memory and start fresh',
|
|
364
|
+
'\ud83d\ude80 Use /spectre:scope to start building features with the full SPECTRE workflow',
|
|
365
|
+
'\ud83c\udf93 Use /spectre:learn to create a documentation skill that your Agent will auto-load when relevant.'
|
|
366
|
+
].join('\n');
|
|
367
|
+
|
|
368
|
+
const welcome = {
|
|
369
|
+
systemMessage: banner + '\n' + tips
|
|
370
|
+
};
|
|
371
|
+
process.stdout.write(JSON.stringify(welcome) + '\n');
|
|
372
|
+
process.exit(0);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Read and parse handoff JSON
|
|
376
|
+
let data;
|
|
377
|
+
try {
|
|
378
|
+
data = JSON.parse(fs.readFileSync(latestHandoff, 'utf8'));
|
|
379
|
+
} catch (_) {
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Compute relative path to handoff for user reference
|
|
384
|
+
let handoffRelative;
|
|
385
|
+
try {
|
|
386
|
+
handoffRelative = path.relative(projectDir, latestHandoff);
|
|
387
|
+
} catch (_) {
|
|
388
|
+
handoffRelative = latestHandoff;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Format and output context
|
|
392
|
+
const output = formatContext(data, {
|
|
393
|
+
handoffPath: handoffRelative
|
|
394
|
+
});
|
|
395
|
+
process.stdout.write(JSON.stringify(output) + '\n');
|
|
396
|
+
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export { copyPluginReferences, formatContext, buildCheckboxTree };
|
|
401
|
+
|
|
402
|
+
if (process.argv[1] && fs.realpathSync(path.resolve(process.argv[1])) === fs.realpathSync(__filename)) {
|
|
403
|
+
main();
|
|
404
|
+
}
|