@codename_inc/spectre 3.7.0 → 4.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.
Files changed (102) hide show
  1. package/README.md +3 -4
  2. package/package.json +3 -2
  3. package/plugins/spectre/.claude-plugin/plugin.json +1 -1
  4. package/plugins/spectre/bin/spectre-register +5 -0
  5. package/plugins/spectre/hooks/hooks.json +3 -14
  6. package/plugins/spectre/hooks/scripts/bootstrap.mjs +98 -0
  7. package/plugins/spectre/hooks/scripts/handoff-resume.mjs +404 -0
  8. package/plugins/spectre/hooks/scripts/lib.mjs +82 -0
  9. package/plugins/spectre/hooks/scripts/load-knowledge.mjs +189 -0
  10. package/plugins/spectre/hooks/scripts/register_learning.mjs +264 -0
  11. package/plugins/spectre/hooks/scripts/{test_bootstrap.cjs → test_bootstrap.mjs} +12 -7
  12. package/plugins/spectre/hooks/scripts/{test_handoff-resume.cjs → test_handoff-resume.mjs} +13 -11
  13. package/plugins/spectre/hooks/scripts/{test_load-knowledge.cjs → test_load-knowledge.mjs} +103 -22
  14. package/plugins/spectre/hooks/scripts/test_register-learning.mjs +335 -0
  15. package/plugins/spectre/skills/apply/SKILL.md +87 -0
  16. package/plugins/spectre/{commands/architecture_review.md → skills/architecture_review/SKILL.md} +9 -0
  17. package/plugins/spectre/{commands/clean.md → skills/clean/SKILL.md} +9 -0
  18. package/plugins/spectre/{commands/code_review.md → skills/code_review/SKILL.md} +9 -0
  19. package/plugins/spectre/{commands/create_plan.md → skills/create_plan/SKILL.md} +9 -0
  20. package/plugins/spectre/{commands/create_tasks.md → skills/create_tasks/SKILL.md} +9 -0
  21. package/plugins/spectre/{commands/create_test_guide.md → skills/create_test_guide/SKILL.md} +9 -0
  22. package/plugins/spectre/{commands/evaluate.md → skills/evaluate/SKILL.md} +11 -2
  23. package/plugins/spectre/{commands/execute.md → skills/execute/SKILL.md} +12 -3
  24. package/plugins/spectre/{commands/fix.md → skills/fix/SKILL.md} +9 -0
  25. package/plugins/spectre/{commands/forget.md → skills/forget/SKILL.md} +9 -0
  26. package/plugins/spectre/skills/{spectre-guide → guide}/SKILL.md +2 -1
  27. package/plugins/spectre/{commands/handoff.md → skills/handoff/SKILL.md} +9 -0
  28. package/plugins/spectre/{commands/kickoff.md → skills/kickoff/SKILL.md} +9 -0
  29. package/plugins/spectre/skills/{spectre-learn → learn}/SKILL.md +19 -59
  30. package/plugins/spectre/skills/learn/references/recall-template.md +34 -0
  31. package/plugins/spectre/{commands/plan.md → skills/plan/SKILL.md} +66 -25
  32. package/plugins/spectre/{commands/plan_review.md → skills/plan_review/SKILL.md} +9 -0
  33. package/plugins/spectre/{commands/quick_dev.md → skills/quick_dev/SKILL.md} +9 -0
  34. package/plugins/spectre/{commands/rebase.md → skills/rebase/SKILL.md} +9 -0
  35. package/plugins/spectre/skills/recall/SKILL.md +17 -0
  36. package/plugins/spectre/{commands/research.md → skills/research/SKILL.md} +9 -0
  37. package/plugins/spectre/{commands/scope.md → skills/scope/SKILL.md} +9 -0
  38. package/plugins/spectre/{commands/ship.md → skills/ship/SKILL.md} +9 -0
  39. package/plugins/spectre/{commands/sweep.md → skills/sweep/SKILL.md} +9 -0
  40. package/plugins/spectre/skills/tdd/SKILL.md +111 -0
  41. package/plugins/spectre/{commands/test.md → skills/test/SKILL.md} +9 -0
  42. package/plugins/spectre/{commands/ux_spec.md → skills/ux_spec/SKILL.md} +9 -0
  43. package/plugins/spectre/{commands/validate.md → skills/validate/SKILL.md} +9 -0
  44. package/plugins/spectre-codex/agents/analyst.toml +117 -0
  45. package/plugins/spectre-codex/agents/dev.toml +65 -0
  46. package/plugins/spectre-codex/agents/finder.toml +101 -0
  47. package/plugins/spectre-codex/agents/patterns.toml +203 -0
  48. package/plugins/spectre-codex/agents/reviewer.toml +123 -0
  49. package/plugins/spectre-codex/agents/sync.toml +146 -0
  50. package/plugins/spectre-codex/agents/tester.toml +205 -0
  51. package/plugins/spectre-codex/agents/web-research.toml +104 -0
  52. package/plugins/spectre-codex/hooks/hooks.json +23 -0
  53. package/plugins/{spectre/hooks/scripts/bootstrap.cjs → spectre-codex/hooks/scripts/bootstrap.mjs} +15 -16
  54. package/plugins/{spectre/hooks/scripts/handoff-resume.cjs → spectre-codex/hooks/scripts/handoff-resume.mjs} +21 -27
  55. package/plugins/{spectre/hooks/scripts/lib.cjs → spectre-codex/hooks/scripts/lib.mjs} +3 -4
  56. package/plugins/spectre-codex/hooks/scripts/load-knowledge.mjs +189 -0
  57. package/plugins/spectre-codex/hooks/scripts/register_learning.mjs +264 -0
  58. package/plugins/spectre-codex/skills/apply/SKILL.md +87 -0
  59. package/plugins/spectre-codex/skills/architecture_review/SKILL.md +129 -0
  60. package/plugins/spectre-codex/skills/clean/SKILL.md +322 -0
  61. package/plugins/spectre-codex/skills/code_review/SKILL.md +417 -0
  62. package/plugins/spectre-codex/skills/create_plan/SKILL.md +126 -0
  63. package/plugins/spectre-codex/skills/create_tasks/SKILL.md +383 -0
  64. package/plugins/spectre-codex/skills/create_test_guide/SKILL.md +129 -0
  65. package/plugins/spectre-codex/skills/evaluate/SKILL.md +59 -0
  66. package/plugins/spectre-codex/skills/execute/SKILL.md +96 -0
  67. package/plugins/spectre-codex/skills/fix/SKILL.md +70 -0
  68. package/plugins/spectre-codex/skills/forget/SKILL.md +67 -0
  69. package/plugins/spectre-codex/skills/guide/SKILL.md +359 -0
  70. package/plugins/spectre-codex/skills/handoff/SKILL.md +170 -0
  71. package/plugins/spectre-codex/skills/kickoff/SKILL.md +124 -0
  72. package/plugins/spectre-codex/skills/learn/SKILL.md +595 -0
  73. package/plugins/{spectre/skills/spectre-learn → spectre-codex/skills/learn}/references/recall-template.md +4 -1
  74. package/plugins/spectre-codex/skills/plan/SKILL.md +211 -0
  75. package/plugins/spectre-codex/skills/plan_review/SKILL.md +42 -0
  76. package/plugins/spectre-codex/skills/quick_dev/SKILL.md +110 -0
  77. package/plugins/spectre-codex/skills/rebase/SKILL.md +82 -0
  78. package/plugins/spectre-codex/skills/recall/SKILL.md +17 -0
  79. package/plugins/spectre-codex/skills/research/SKILL.md +168 -0
  80. package/plugins/spectre-codex/skills/scope/SKILL.md +128 -0
  81. package/plugins/spectre-codex/skills/ship/SKILL.md +181 -0
  82. package/plugins/spectre-codex/skills/sweep/SKILL.md +91 -0
  83. package/plugins/{spectre/skills/spectre-tdd → spectre-codex/skills/tdd}/SKILL.md +1 -1
  84. package/plugins/spectre-codex/skills/test/SKILL.md +389 -0
  85. package/plugins/spectre-codex/skills/ux_spec/SKILL.md +100 -0
  86. package/plugins/spectre-codex/skills/validate/SKILL.md +352 -0
  87. package/src/config.test.js +6 -5
  88. package/src/install.test.js +100 -11
  89. package/src/lib/config.js +107 -54
  90. package/src/lib/constants.js +17 -23
  91. package/src/lib/doctor.js +19 -22
  92. package/src/lib/install.js +98 -313
  93. package/src/lib/knowledge.js +7 -37
  94. package/src/lib/paths.js +0 -12
  95. package/src/pack.test.js +87 -0
  96. package/plugins/spectre/commands/learn.md +0 -15
  97. package/plugins/spectre/commands/recall.md +0 -5
  98. package/plugins/spectre/hooks/scripts/load-knowledge.cjs +0 -120
  99. package/plugins/spectre/hooks/scripts/precompact-warning.cjs +0 -19
  100. package/plugins/spectre/hooks/scripts/register_learning.cjs +0 -144
  101. package/plugins/spectre/hooks/scripts/test_register-learning.cjs +0 -146
  102. 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
- spectre-scope
50
+ scope
51
51
  ```
52
52
 
53
53
  Current Codex behavior:
@@ -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/ # 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.7.0",
3
+ "version": "4.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,5 +1,5 @@
1
1
  {
2
2
  "name": "spectre",
3
- "version": "3.7.0",
3
+ "version": "4.0.0",
4
4
  "description": "Agentic coding workflow with session memory. spectre guides you through Scope, Plan, Execute, Clean, Test, Rebase, and Extract phases."
5
5
  }
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ # Thin wrapper so skills can call `spectre-register` from Bash.
3
+ # Delegates to the hook registration runtime.
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ exec node "$SCRIPT_DIR/../hooks/scripts/register_learning.mjs" "$@"
@@ -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.cjs"
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.cjs"
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.cjs"
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
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * lib.mjs
5
+ *
6
+ * Shared utilities for spectre hook scripts.
7
+ */
8
+
9
+ import { execSync } from 'node:child_process';
10
+
11
+ const STDIN_TIMEOUT = 2000; // milliseconds
12
+
13
+ /**
14
+ * Read stdin with a timeout to avoid blocking indefinitely.
15
+ * @param {number} [timeout=2000] - Timeout in milliseconds
16
+ * @returns {Promise<string|null>} - stdin content or null on timeout
17
+ */
18
+ function readStdinWithTimeout(timeout) {
19
+ timeout = timeout != null ? timeout : STDIN_TIMEOUT;
20
+
21
+ return new Promise((resolve) => {
22
+ // If stdin is a TTY (no piped data), resolve immediately
23
+ if (process.stdin.isTTY) {
24
+ resolve(null);
25
+ return;
26
+ }
27
+
28
+ let data = '';
29
+ let settled = false;
30
+
31
+ const timer = setTimeout(() => {
32
+ if (!settled) {
33
+ settled = true;
34
+ process.stdin.removeAllListeners('data');
35
+ process.stdin.removeAllListeners('end');
36
+ process.stdin.removeAllListeners('error');
37
+ try { process.stdin.pause(); } catch (_) {}
38
+ resolve(data || null);
39
+ }
40
+ }, timeout);
41
+
42
+ process.stdin.setEncoding('utf8');
43
+ process.stdin.on('data', (chunk) => {
44
+ data += chunk;
45
+ });
46
+ process.stdin.on('end', () => {
47
+ if (!settled) {
48
+ settled = true;
49
+ clearTimeout(timer);
50
+ resolve(data || null);
51
+ }
52
+ });
53
+ process.stdin.on('error', () => {
54
+ if (!settled) {
55
+ settled = true;
56
+ clearTimeout(timer);
57
+ resolve(null);
58
+ }
59
+ });
60
+ process.stdin.resume();
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Get current git branch name.
66
+ * @param {string} [cwd] - Working directory for git command
67
+ * @returns {string} Branch name or 'unknown'
68
+ */
69
+ function getGitBranch(cwd) {
70
+ try {
71
+ return execSync('git rev-parse --abbrev-ref HEAD', {
72
+ timeout: 5000,
73
+ encoding: 'utf8',
74
+ cwd: cwd || undefined,
75
+ stdio: ['pipe', 'pipe', 'pipe']
76
+ }).trim();
77
+ } catch (_) {
78
+ return 'unknown';
79
+ }
80
+ }
81
+
82
+ export { readStdinWithTimeout, getGitBranch, STDIN_TIMEOUT };