specweave 2.0.0 → 2.1.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 (97) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +15 -6
  3. package/dist/dashboard/assets/index-BeKUULf0.css +1 -0
  4. package/dist/dashboard/assets/index-Bf5TF0_l.js +13 -0
  5. package/dist/dashboard/index.html +2 -2
  6. package/dist/src/cli/commands/sync.d.ts.map +1 -1
  7. package/dist/src/cli/commands/sync.js +23 -5
  8. package/dist/src/cli/commands/sync.js.map +1 -1
  9. package/dist/src/core/doctor/checkers/hooks-checker.d.ts +2 -2
  10. package/dist/src/core/doctor/checkers/hooks-checker.d.ts.map +1 -1
  11. package/dist/src/core/doctor/checkers/hooks-checker.js +6 -5
  12. package/dist/src/core/doctor/checkers/hooks-checker.js.map +1 -1
  13. package/dist/src/core/hooks/handlers/session-start.d.ts.map +1 -1
  14. package/dist/src/core/hooks/handlers/session-start.js +20 -0
  15. package/dist/src/core/hooks/handlers/session-start.js.map +1 -1
  16. package/dist/src/core/intent/portable-context.d.ts +4 -0
  17. package/dist/src/core/intent/portable-context.d.ts.map +1 -0
  18. package/dist/src/core/intent/portable-context.js +68 -0
  19. package/dist/src/core/intent/portable-context.js.map +1 -0
  20. package/dist/src/core/intent/types.d.ts +28 -0
  21. package/dist/src/core/intent/types.d.ts.map +1 -0
  22. package/dist/src/core/intent/types.js +3 -0
  23. package/dist/src/core/intent/types.js.map +1 -0
  24. package/dist/src/core/intent/validation.d.ts +5 -0
  25. package/dist/src/core/intent/validation.d.ts.map +1 -0
  26. package/dist/src/core/intent/validation.js +39 -0
  27. package/dist/src/core/intent/validation.js.map +1 -0
  28. package/dist/src/core/session/handoff-git-state.d.ts +3 -14
  29. package/dist/src/core/session/handoff-git-state.d.ts.map +1 -1
  30. package/dist/src/core/session/handoff-git-state.js +68 -124
  31. package/dist/src/core/session/handoff-git-state.js.map +1 -1
  32. package/dist/src/core/session/handoff-pointer.d.ts +4 -0
  33. package/dist/src/core/session/handoff-pointer.d.ts.map +1 -0
  34. package/dist/src/core/session/handoff-pointer.js +60 -0
  35. package/dist/src/core/session/handoff-pointer.js.map +1 -0
  36. package/dist/src/core/session/work-handoff.d.ts.map +1 -1
  37. package/dist/src/core/session/work-handoff.js +10 -3
  38. package/dist/src/core/session/work-handoff.js.map +1 -1
  39. package/dist/src/dashboard/server/dashboard-server.d.ts +1 -0
  40. package/dist/src/dashboard/server/dashboard-server.d.ts.map +1 -1
  41. package/dist/src/dashboard/server/dashboard-server.js +10 -90
  42. package/dist/src/dashboard/server/dashboard-server.js.map +1 -1
  43. package/dist/src/dashboard/server/data/cost-aggregator.d.ts +10 -5
  44. package/dist/src/dashboard/server/data/cost-aggregator.d.ts.map +1 -1
  45. package/dist/src/dashboard/server/data/cost-aggregator.js +31 -33
  46. package/dist/src/dashboard/server/data/cost-aggregator.js.map +1 -1
  47. package/dist/src/dashboard/server/data/dashboard-data-aggregator.d.ts +0 -1
  48. package/dist/src/dashboard/server/data/dashboard-data-aggregator.d.ts.map +1 -1
  49. package/dist/src/dashboard/server/data/dashboard-data-aggregator.js +14 -66
  50. package/dist/src/dashboard/server/data/dashboard-data-aggregator.js.map +1 -1
  51. package/dist/src/dashboard/server/data/dashboard-task-board.d.ts +4 -0
  52. package/dist/src/dashboard/server/data/dashboard-task-board.d.ts.map +1 -0
  53. package/dist/src/dashboard/server/data/dashboard-task-board.js +56 -0
  54. package/dist/src/dashboard/server/data/dashboard-task-board.js.map +1 -0
  55. package/dist/src/dashboard/server/data/intent-store.d.ts +19 -0
  56. package/dist/src/dashboard/server/data/intent-store.d.ts.map +1 -0
  57. package/dist/src/dashboard/server/data/intent-store.js +268 -0
  58. package/dist/src/dashboard/server/data/intent-store.js.map +1 -0
  59. package/dist/src/dashboard/server/data/local-sessions.d.ts +15 -0
  60. package/dist/src/dashboard/server/data/local-sessions.d.ts.map +1 -0
  61. package/dist/src/dashboard/server/data/local-sessions.js +197 -0
  62. package/dist/src/dashboard/server/data/local-sessions.js.map +1 -0
  63. package/dist/src/dashboard/server/data/work-projection.d.ts +8 -0
  64. package/dist/src/dashboard/server/data/work-projection.d.ts.map +1 -0
  65. package/dist/src/dashboard/server/data/work-projection.js +175 -0
  66. package/dist/src/dashboard/server/data/work-projection.js.map +1 -0
  67. package/dist/src/dashboard/server/file-watcher.d.ts.map +1 -1
  68. package/dist/src/dashboard/server/file-watcher.js +2 -1
  69. package/dist/src/dashboard/server/file-watcher.js.map +1 -1
  70. package/dist/src/dashboard/server/local-request.d.ts +4 -0
  71. package/dist/src/dashboard/server/local-request.d.ts.map +1 -0
  72. package/dist/src/dashboard/server/local-request.js +29 -0
  73. package/dist/src/dashboard/server/local-request.js.map +1 -0
  74. package/dist/src/dashboard/server/routes/work-routes.d.ts +5 -0
  75. package/dist/src/dashboard/server/routes/work-routes.d.ts.map +1 -0
  76. package/dist/src/dashboard/server/routes/work-routes.js +37 -0
  77. package/dist/src/dashboard/server/routes/work-routes.js.map +1 -0
  78. package/dist/src/dashboard/types.d.ts +3 -3
  79. package/dist/src/dashboard/types.d.ts.map +1 -1
  80. package/dist/src/dashboard/work-types.d.ts +39 -0
  81. package/dist/src/dashboard/work-types.d.ts.map +1 -0
  82. package/dist/src/dashboard/work-types.js +3 -0
  83. package/dist/src/dashboard/work-types.js.map +1 -0
  84. package/dist/src/sync/external-change-puller.d.ts +19 -0
  85. package/dist/src/sync/external-change-puller.d.ts.map +1 -1
  86. package/dist/src/sync/external-change-puller.js +87 -77
  87. package/dist/src/sync/external-change-puller.js.map +1 -1
  88. package/package.json +3 -3
  89. package/plugins/specweave/.claude-plugin/plugin.json +2 -2
  90. package/plugins/specweave/hooks/hooks.json +3 -30
  91. package/plugins/specweave/hooks/run-worker.mjs +229 -0
  92. package/plugins/specweave/hooks/run.mjs +83 -217
  93. package/plugins/specweave/skills/increment/SKILL.md +7 -5
  94. package/src/templates/AGENTS.md.template +5 -5
  95. package/src/templates/CLAUDE.md.template +5 -5
  96. package/dist/dashboard/assets/index-BiJhGDZV.js +0 -13
  97. package/dist/dashboard/assets/index-heYRtWFf.css +0 -1
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SpecWeave hook launcher (zero-dependency, cross-platform).
4
+ *
5
+ * Invoked by Claude Code in exec form (no shell):
6
+ * node ${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs <event>
7
+ *
8
+ * Contract:
9
+ * - reads the hook JSON from stdin (5 s cap), normalizes backslashes in
10
+ * tool_input.file_path, locates the installed `specweave` CLI, imports its
11
+ * built hook router and prints exactly ONE JSON object;
12
+ * - always exits 0; on ANY failure prints `{}` (pass) — except SessionStart,
13
+ * which prints an additionalContext line telling the user the CLI is missing.
14
+ *
15
+ * CLI root resolution order:
16
+ * 1. $SPECWEAVE_HOME
17
+ * 2. <plugin root>/../.. (plugin shipped inside the npm package / repo checkout)
18
+ * 3. require.resolve('specweave/package.json') from $CLAUDE_PROJECT_DIR / cwd
19
+ * 4. `npm root -g` (result cached in a per-user private file under ~/.specweave)
20
+ *
21
+ * Fast path: PreToolUse for a file outside `.specweave/increments/` prints `{}`
22
+ * without loading the CLI at all (the guards only concern increment files).
23
+ */
24
+ import { createRequire } from 'node:module';
25
+ import { spawnSync } from 'node:child_process';
26
+ import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
27
+ import { homedir } from 'node:os';
28
+ import path from 'node:path';
29
+ import { fileURLToPath, pathToFileURL } from 'node:url';
30
+
31
+ const EVENTS = new Set(['session-start', 'pre-tool-use', 'stop', 'pre-compact']);
32
+ const STDIN_CAP_MS = 5000;
33
+ const ROUTER_REL = path.join('dist', 'src', 'core', 'hooks', 'handlers', 'hook-router.js');
34
+ /**
35
+ * Per-user cache path. NEVER the shared OS temp dir: run.mjs `import()`s the
36
+ * path it reads back, so a world-writable cache is arbitrary code execution on
37
+ * every SessionStart/Stop/PreCompact of every session on the machine.
38
+ */
39
+ const CACHE_DIR = safeHomeDir() ? path.join(safeHomeDir(), '.specweave') : null;
40
+ const CACHE_FILE = CACHE_DIR ? path.join(CACHE_DIR, 'hook-cli-root') : null;
41
+
42
+ function safeHomeDir() {
43
+ try {
44
+ const home = homedir();
45
+ return home && path.isAbsolute(home) ? home : null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /** True only for a regular file (not a symlink) owned by the current user. */
52
+ function isPrivateOwnedFile(file) {
53
+ try {
54
+ const st = lstatSync(file);
55
+ if (!st.isFile()) return false;
56
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
57
+ return uid === null || st.uid === uid;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ const event = process.argv[2] ?? '';
64
+
65
+ function inactiveOutput() {
66
+ if (event === 'session-start') {
67
+ return {
68
+ hookSpecificOutput: {
69
+ hookEventName: 'SessionStart',
70
+ additionalContext:
71
+ 'SpecWeave hooks inactive: specweave CLI not found (npm i -g specweave)',
72
+ },
73
+ };
74
+ }
75
+ return {};
76
+ }
77
+
78
+ let printed = false;
79
+ function emit(obj) {
80
+ if (printed) return;
81
+ printed = true;
82
+ let text = '{}';
83
+ try {
84
+ text = JSON.stringify(obj && typeof obj === 'object' ? obj : {});
85
+ } catch {
86
+ text = '{}';
87
+ }
88
+ process.stdout.write(text + '\n', () => process.exit(0));
89
+ }
90
+
91
+ function readStdin() {
92
+ return new Promise((resolve) => {
93
+ const chunks = [];
94
+ let done = false;
95
+ const finish = () => {
96
+ if (done) return;
97
+ done = true;
98
+ resolve(Buffer.concat(chunks).toString('utf8'));
99
+ };
100
+ const timer = setTimeout(finish, STDIN_CAP_MS);
101
+ if (timer.unref) timer.unref();
102
+ try {
103
+ process.stdin.on('data', (c) => chunks.push(c));
104
+ process.stdin.on('end', finish);
105
+ process.stdin.on('error', finish);
106
+ process.stdin.on('close', finish);
107
+ if (process.stdin.readableEnded) finish();
108
+ } catch {
109
+ finish();
110
+ }
111
+ });
112
+ }
113
+
114
+ /** Returns { raw, skip } — `skip` when the event cannot possibly need the CLI. */
115
+ function normalizeInput(raw) {
116
+ try {
117
+ const input = JSON.parse(raw);
118
+ if (input && typeof input === 'object') {
119
+ const ti = input.tool_input;
120
+ let filePath = '';
121
+ if (ti && typeof ti === 'object' && typeof ti.file_path === 'string') {
122
+ ti.file_path = ti.file_path.replace(/\\/g, '/');
123
+ filePath = ti.file_path;
124
+ }
125
+ if (typeof input.cwd === 'string') input.cwd = input.cwd.replace(/\\/g, '/');
126
+ const skip = event === 'pre-tool-use' && !filePath.includes('.specweave/increments/');
127
+ return { raw: JSON.stringify(input), skip };
128
+ }
129
+ } catch {
130
+ // fall through — the router tolerates a non-JSON payload
131
+ }
132
+ return { raw, skip: false };
133
+ }
134
+
135
+ function isCliRoot(dir) {
136
+ if (!dir) return false;
137
+ try {
138
+ const pkg = path.join(dir, 'package.json');
139
+ if (!existsSync(pkg) || !existsSync(path.join(dir, ROUTER_REL))) return false;
140
+ return JSON.parse(readFileSync(pkg, 'utf8')).name === 'specweave';
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ function pluginRoot() {
147
+ if (process.env.CLAUDE_PLUGIN_ROOT) return path.resolve(process.env.CLAUDE_PLUGIN_ROOT);
148
+ return path.dirname(path.dirname(fileURLToPath(import.meta.url)));
149
+ }
150
+
151
+ function fromCache() {
152
+ if (!CACHE_FILE || !isPrivateOwnedFile(CACHE_FILE)) return null;
153
+ try {
154
+ const cached = readFileSync(CACHE_FILE, 'utf8').trim();
155
+ return isCliRoot(cached) ? cached : null;
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ function writeCache(dir) {
162
+ if (!CACHE_DIR || !CACHE_FILE) return;
163
+ try {
164
+ mkdirSync(CACHE_DIR, { recursive: true, mode: 0o700 });
165
+ // Drop whatever is there (possibly a planted symlink) before writing.
166
+ rmSync(CACHE_FILE, { force: true });
167
+ writeFileSync(CACHE_FILE, dir, { flag: 'wx', mode: 0o600 });
168
+ } catch {
169
+ // cache is best-effort
170
+ }
171
+ }
172
+
173
+ function fromNpmGlobal() {
174
+ try {
175
+ const res = spawnSync('npm root -g', { shell: true, encoding: 'utf8', timeout: 4000 });
176
+ const root = (res.stdout || '').trim().split(/\r?\n/).pop();
177
+ if (!root) return null;
178
+ const dir = path.join(root, 'specweave');
179
+ if (!isCliRoot(dir)) return null;
180
+ writeCache(dir);
181
+ return dir;
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ function resolveCliRoot() {
188
+ const candidates = [
189
+ () => process.env.SPECWEAVE_HOME,
190
+ () => path.resolve(pluginRoot(), '..', '..'),
191
+ ...[process.env.CLAUDE_PROJECT_DIR, process.cwd()].filter(Boolean).flatMap((dir) => [
192
+ () => path.dirname(createRequire(path.join(dir, 'package.json')).resolve('specweave/package.json')),
193
+ () => path.join(dir, 'node_modules', 'specweave'),
194
+ ]),
195
+ fromCache,
196
+ fromNpmGlobal,
197
+ ];
198
+ for (const candidate of candidates) {
199
+ try {
200
+ const dir = candidate();
201
+ if (isCliRoot(dir)) return dir;
202
+ } catch {
203
+ // try the next strategy
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+
209
+ async function main() {
210
+ if (!EVENTS.has(event)) {
211
+ await readStdin();
212
+ return emit({});
213
+ }
214
+ const { raw, skip } = normalizeInput(await readStdin());
215
+ if (skip) return emit({});
216
+ const cliRoot = resolveCliRoot();
217
+ if (!cliRoot) return emit(inactiveOutput());
218
+ try {
219
+ const mod = await import(pathToFileURL(path.join(cliRoot, ROUTER_REL)).href);
220
+ const result = await mod.hookRouter(event, raw);
221
+ return emit(result);
222
+ } catch {
223
+ return emit(inactiveOutput());
224
+ }
225
+ }
226
+
227
+ process.on('uncaughtException', () => emit(inactiveOutput()));
228
+ process.on('unhandledRejection', () => emit(inactiveOutput()));
229
+ main().catch(() => emit(inactiveOutput()));
@@ -1,229 +1,95 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * SpecWeave hook launcher (zero-dependency, cross-platform).
4
- *
5
- * Invoked by Claude Code in exec form (no shell):
6
- * node ${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs <event>
7
- *
8
- * Contract:
9
- * - reads the hook JSON from stdin (5 s cap), normalizes backslashes in
10
- * tool_input.file_path, locates the installed `specweave` CLI, imports its
11
- * built hook router and prints exactly ONE JSON object;
12
- * - always exits 0; on ANY failure prints `{}` (pass) — except SessionStart,
13
- * which prints an additionalContext line telling the user the CLI is missing.
14
- *
15
- * CLI root resolution order:
16
- * 1. $SPECWEAVE_HOME
17
- * 2. <plugin root>/../.. (plugin shipped inside the npm package / repo checkout)
18
- * 3. require.resolve('specweave/package.json') from $CLAUDE_PROJECT_DIR / cwd
19
- * 4. `npm root -g` (result cached in a per-user private file under ~/.specweave)
20
- *
21
- * Fast path: PreToolUse for a file outside `.specweave/increments/` prints `{}`
22
- * without loading the CLI at all (the guards only concern increment files).
23
- */
24
- import { createRequire } from 'node:module';
25
- import { spawnSync } from 'node:child_process';
26
- import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ // SpecWeave hook supervisor: parent deadline remains live while the worker blocks in Git.
3
+ import { spawn, spawnSync } from 'node:child_process';
4
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
27
5
  import { homedir } from 'node:os';
28
6
  import path from 'node:path';
29
- import { fileURLToPath, pathToFileURL } from 'node:url';
30
-
31
- const EVENTS = new Set(['session-start', 'pre-tool-use', 'stop', 'pre-compact']);
32
- const STDIN_CAP_MS = 5000;
33
- const ROUTER_REL = path.join('dist', 'src', 'core', 'hooks', 'handlers', 'hook-router.js');
34
- /**
35
- * Per-user cache path. NEVER the shared OS temp dir: run.mjs `import()`s the
36
- * path it reads back, so a world-writable cache is arbitrary code execution on
37
- * every SessionStart/Stop/PreCompact of every session on the machine.
38
- */
39
- const CACHE_DIR = safeHomeDir() ? path.join(safeHomeDir(), '.specweave') : null;
40
- const CACHE_FILE = CACHE_DIR ? path.join(CACHE_DIR, 'hook-cli-root') : null;
41
-
42
- function safeHomeDir() {
43
- try {
44
- const home = homedir();
45
- return home && path.isAbsolute(home) ? home : null;
46
- } catch {
47
- return null;
48
- }
49
- }
50
-
51
- /** True only for a regular file (not a symlink) owned by the current user. */
52
- function isPrivateOwnedFile(file) {
53
- try {
54
- const st = lstatSync(file);
55
- if (!st.isFile()) return false;
56
- const uid = typeof process.getuid === 'function' ? process.getuid() : null;
57
- return uid === null || st.uid === uid;
58
- } catch {
59
- return false;
60
- }
61
- }
62
-
63
- const event = process.argv[2] ?? '';
64
-
65
- function inactiveOutput() {
66
- if (event === 'session-start') {
67
- return {
68
- hookSpecificOutput: {
69
- hookEventName: 'SessionStart',
70
- additionalContext:
71
- 'SpecWeave hooks inactive: specweave CLI not found (npm i -g specweave)',
72
- },
73
- };
74
- }
75
- return {};
76
- }
77
-
78
- let printed = false;
79
- function emit(obj) {
80
- if (printed) return;
81
- printed = true;
82
- let text = '{}';
83
- try {
84
- text = JSON.stringify(obj && typeof obj === 'object' ? obj : {});
85
- } catch {
86
- text = '{}';
87
- }
88
- process.stdout.write(text + '\n', () => process.exit(0));
89
- }
90
-
91
- function readStdin() {
92
- return new Promise((resolve) => {
93
- const chunks = [];
94
- let done = false;
95
- const finish = () => {
96
- if (done) return;
97
- done = true;
98
- resolve(Buffer.concat(chunks).toString('utf8'));
99
- };
100
- const timer = setTimeout(finish, STDIN_CAP_MS);
101
- if (timer.unref) timer.unref();
102
- try {
103
- process.stdin.on('data', (c) => chunks.push(c));
104
- process.stdin.on('end', finish);
105
- process.stdin.on('error', finish);
106
- process.stdin.on('close', finish);
107
- if (process.stdin.readableEnded) finish();
108
- } catch {
109
- finish();
110
- }
111
- });
112
- }
113
-
114
- /** Returns { raw, skip } — `skip` when the event cannot possibly need the CLI. */
115
- function normalizeInput(raw) {
116
- try {
117
- const input = JSON.parse(raw);
118
- if (input && typeof input === 'object') {
119
- const ti = input.tool_input;
120
- let filePath = '';
121
- if (ti && typeof ti === 'object' && typeof ti.file_path === 'string') {
122
- ti.file_path = ti.file_path.replace(/\\/g, '/');
123
- filePath = ti.file_path;
124
- }
125
- if (typeof input.cwd === 'string') input.cwd = input.cwd.replace(/\\/g, '/');
126
- const skip = event === 'pre-tool-use' && !filePath.includes('.specweave/increments/');
127
- return { raw: JSON.stringify(input), skip };
7
+ import { fileURLToPath } from 'node:url';
8
+ const event = process.argv[2];
9
+ const worker = path.join(path.dirname(fileURLToPath(import.meta.url)), 'run-worker.mjs');
10
+ const budget = event === 'stop' ? 25000 : 7500;
11
+ let child, finished = false, input = '', output = '', size = 0;
12
+ const started = Date.now();
13
+ // Ordinary turns should not resolve npm, import the CLI, or start a second Node
14
+ // process. Auto mode is explicit and its small local marker is authoritative.
15
+ function hasActiveAutoSession(data) {
16
+ let current = path.resolve(typeof data.cwd === 'string' ? data.cwd : process.cwd());
17
+ for (let depth = 0; depth < 100; depth++) {
18
+ const state = path.join(current, '.specweave', 'state');
19
+ if (existsSync(path.join(current, '.specweave', 'config.json'))) {
20
+ const sessionId = process.env.CLAUDE_SESSION_ID;
21
+ // Match session-state-manager's path-safe IDs, including dots and spaces.
22
+ const scoped = sessionId && !/[/\\]|\.\.|\0/.test(sessionId)
23
+ ? path.join(state, 'sessions', sessionId, 'auto-mode.json') : null;
24
+ const marker = scoped && existsSync(scoped) ? scoped : path.join(state, 'auto-mode.json');
25
+ try { return JSON.parse(readFileSync(marker, 'utf8')).active === true; }
26
+ catch { return false; }
128
27
  }
129
- } catch {
130
- // fall through — the router tolerates a non-JSON payload
28
+ const parent = path.dirname(current);
29
+ if (parent === current) break;
30
+ current = parent;
131
31
  }
132
- return { raw, skip: false };
32
+ return false;
133
33
  }
134
-
135
- function isCliRoot(dir) {
136
- if (!dir) return false;
34
+ function record(reason) {
137
35
  try {
138
- const pkg = path.join(dir, 'package.json');
139
- if (!existsSync(pkg) || !existsSync(path.join(dir, ROUTER_REL))) return false;
140
- return JSON.parse(readFileSync(pkg, 'utf8')).name === 'specweave';
141
- } catch {
142
- return false;
143
- }
36
+ const dir = path.join(homedir(), '.specweave', 'logs');
37
+ mkdirSync(dir, {recursive:true, mode:0o700});
38
+ appendFileSync(path.join(dir, 'hook-health.jsonl'), JSON.stringify({at:new Date().toISOString(),event,reason,elapsedMs:Date.now()-started})+'\n', {mode:0o600});
39
+ } catch {}
144
40
  }
145
-
146
- function pluginRoot() {
147
- if (process.env.CLAUDE_PLUGIN_ROOT) return path.resolve(process.env.CLAUDE_PLUGIN_ROOT);
148
- return path.dirname(path.dirname(fileURLToPath(import.meta.url)));
149
- }
150
-
151
- function fromCache() {
152
- if (!CACHE_FILE || !isPrivateOwnedFile(CACHE_FILE)) return null;
153
- try {
154
- const cached = readFileSync(CACHE_FILE, 'utf8').trim();
155
- return isCliRoot(cached) ? cached : null;
156
- } catch {
157
- return null;
158
- }
159
- }
160
-
161
- function writeCache(dir) {
162
- if (!CACHE_DIR || !CACHE_FILE) return;
163
- try {
164
- mkdirSync(CACHE_DIR, { recursive: true, mode: 0o700 });
165
- // Drop whatever is there (possibly a planted symlink) before writing.
166
- rmSync(CACHE_FILE, { force: true });
167
- writeFileSync(CACHE_FILE, dir, { flag: 'wx', mode: 0o600 });
168
- } catch {
169
- // cache is best-effort
41
+ function killWorker() {
42
+ if (!child?.pid) return;
43
+ if (process.platform === 'win32') {
44
+ spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], {timeout:1000, windowsHide:true, stdio:'ignore'});
45
+ } else {
46
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { try {child.kill('SIGKILL');} catch {} }
170
47
  }
171
48
  }
172
-
173
- function fromNpmGlobal() {
174
- try {
175
- const res = spawnSync('npm root -g', { shell: true, encoding: 'utf8', timeout: 4000 });
176
- const root = (res.stdout || '').trim().split(/\r?\n/).pop();
177
- if (!root) return null;
178
- const dir = path.join(root, 'specweave');
179
- if (!isCliRoot(dir)) return null;
180
- writeCache(dir);
181
- return dir;
182
- } catch {
183
- return null;
184
- }
49
+ function finish(result = {}, reason) {
50
+ if (finished) return;
51
+ finished = true;
52
+ clearTimeout(deadline);
53
+ killWorker();
54
+ if (reason) record(reason);
55
+ process.stdout.write(JSON.stringify(result)+'\n',()=>process.exit(0));
185
56
  }
186
-
187
- function resolveCliRoot() {
188
- const candidates = [
189
- () => process.env.SPECWEAVE_HOME,
190
- () => path.resolve(pluginRoot(), '..', '..'),
191
- ...[process.env.CLAUDE_PROJECT_DIR, process.cwd()].filter(Boolean).flatMap((dir) => [
192
- () => path.dirname(createRequire(path.join(dir, 'package.json')).resolve('specweave/package.json')),
193
- () => path.join(dir, 'node_modules', 'specweave'),
194
- ]),
195
- fromCache,
196
- fromNpmGlobal,
197
- ];
198
- for (const candidate of candidates) {
57
+ const deadline = setTimeout(()=>finish({},'deadline_exceeded'),budget);
58
+ process.on('SIGTERM',()=>finish({},'terminated'));
59
+ process.on('SIGINT',()=>finish({},'interrupted'));
60
+ process.on('uncaughtException',()=>finish({},'adapter_error'));
61
+ process.stdin.setEncoding('utf8');
62
+ process.stdin.on('data',chunk=>{
63
+ size += Buffer.byteLength(chunk, 'utf8');
64
+ if(size>1024*1024) return finish({},'input_limit');
65
+ input+=chunk;
66
+ });
67
+ process.stdin.on('error',()=>finish({},'stdin_error'));
68
+ process.stdin.on('end',()=>{
69
+ let data;
70
+ try {data=JSON.parse(input);if(!data || typeof data!=='object'||Array.isArray(data))throw Error();}
71
+ catch {return finish({},'invalid_input');}
72
+ if(!['session-start','pre-tool-use','pre-compact','stop'].includes(event))return finish({});
73
+ if(process.env.SPECWEAVE_DISABLE_HOOKS === '1')return finish({});
74
+ if(event === 'stop' && !hasActiveAutoSession(data))return finish({});
75
+ const env={...process.env};
76
+ child=spawn(process.execPath,[worker,event],{env,detached:process.platform !== 'win32',stdio:['pipe','pipe','pipe']});
77
+ child.stdout.setEncoding('utf8');
78
+ child.stdout.on('data',chunk=>{
79
+ output+=chunk;
80
+ if(output.length>1024*1024)finish({},'output_limit');
81
+ });
82
+ child.stderr.resume();
83
+ child.stdin.on('error',()=>{});
84
+ child.on('error',()=>finish({},'worker_spawn_failed'));
85
+ child.on('close',code=>{
86
+ if(finished)return;
87
+ if(code!==0)return finish({},'worker_failed');
199
88
  try {
200
- const dir = candidate();
201
- if (isCliRoot(dir)) return dir;
202
- } catch {
203
- // try the next strategy
204
- }
205
- }
206
- return null;
207
- }
208
-
209
- async function main() {
210
- if (!EVENTS.has(event)) {
211
- await readStdin();
212
- return emit({});
213
- }
214
- const { raw, skip } = normalizeInput(await readStdin());
215
- if (skip) return emit({});
216
- const cliRoot = resolveCliRoot();
217
- if (!cliRoot) return emit(inactiveOutput());
218
- try {
219
- const mod = await import(pathToFileURL(path.join(cliRoot, ROUTER_REL)).href);
220
- const result = await mod.hookRouter(event, raw);
221
- return emit(result);
222
- } catch {
223
- return emit(inactiveOutput());
224
- }
225
- }
226
-
227
- process.on('uncaughtException', () => emit(inactiveOutput()));
228
- process.on('unhandledRejection', () => emit(inactiveOutput()));
229
- main().catch(() => emit(inactiveOutput()));
89
+ const result=JSON.parse(output);
90
+ if(!result||typeof result!=='object'||Array.isArray(result))throw Error();
91
+ finish(result);
92
+ } catch {finish({},'invalid_worker_output');}
93
+ });
94
+ child.stdin.end(input);
95
+ });
@@ -11,11 +11,13 @@ An increment is one folder — `.specweave/increments/NNNN-slug/` — holding
11
11
  `ledger.jsonl` (state, written by the CLI) and `metadata.json`. It is the unit of
12
12
  work and the tracker; every commit for it carries its id in the subject.
13
13
 
14
- ## Plan mode first (blocking)
14
+ ## Scope before implementation
15
15
 
16
- Planning produces a document the user approves before any code is written. If your
17
- tool has a plan mode, enter it now (Claude Code: `EnterPlanMode`) and wait for
18
- confirmation. Do not create files, do not implement, until the plan is approved.
16
+ Write a concise plan for work that needs shared acceptance criteria. Ask only about
17
+ material ambiguity or actions beyond the user's authorization. When the user has
18
+ asked for autonomous execution, make routine decisions and continue through
19
+ implementation and verification. A small self-contained fix can remain a
20
+ lightweight intent without this workflow.
19
21
 
20
22
  ## Steps
21
23
 
@@ -79,7 +81,7 @@ confirmation. Do not create files, do not implement, until the plan is approved.
79
81
  least one task; every task needs a `Test:` that a machine can run (or an explicit
80
82
  "manual: <what to check>"). Shared contracts (types, schema, migrations) go in an early
81
83
  task everything else depends on.
82
- 6. **Present the plan** (problem, ACs, task list, risks) and get approval. Then leave plan mode.
84
+ 6. **Share the plan** (problem, ACs, tasks, material risks). Resolve any remaining decision that needs the user; otherwise continue within existing authorization.
83
85
  7. **Hand off to execution**: `sw:do <id>` for one agent; `sw:team <id>` when the work has
84
86
  3+ disjoint lanes or 15+ tasks.
85
87
 
@@ -1,7 +1,7 @@
1
1
  <!-- SECTION:header required -->
2
2
  # {PROJECT_NAME}
3
3
 
4
- SpecWeave project. Work happens in increments under `.specweave/increments/NNNN-slug/`. `spec.md` (Problem, ACs, Approach) is the source of truth; the ledger records who did what. This file is for Codex, OpenCode, Cursor, Gemini, Aider and humans; Claude Code reads CLAUDE.md (same content). Keep it under one page; project-specific notes go in **Project notes** at the bottom.
4
+ SpecWeave project. Small requests live in the intent board; scoped work lives in increments under `.specweave/increments/NNNN-slug/`. `spec.md` (Problem, ACs, Approach) is the source of truth; the ledger records who did what. This file is for Codex, OpenCode, Cursor, Gemini, Aider and humans; Claude Code reads CLAUDE.md (same content). Keep it under one page; project-specific notes go in **Project notes** at the bottom.
5
5
  <!-- /SECTION -->
6
6
 
7
7
  <!-- SECTION:structure -->
@@ -9,6 +9,7 @@ SpecWeave project. Work happens in increments under `.specweave/increments/NNNN-
9
9
 
10
10
  ```
11
11
  .specweave/
12
+ intents/board.jsonl append-only intent history (latest revision per id; planning state, not verification)
12
13
  increments/NNNN-slug/ spec.md (Problem · ACs · Approach) · tasks.md (tasks + rendered board) · ledger.jsonl (claims/done, CLI-written) · handoff.md · reports/ (evidence; binaries in reports/artifacts/, gitignored)
13
14
  docs/ ADRs and hand-written docs (read before changing architecture)
14
15
  config.json
@@ -23,7 +24,7 @@ Only those files (plus optional `plan.md` when Approach outgrows a page, and `sc
23
24
 
24
25
  | Step | Command (each step is also a standalone skill you can install into any tool with `npx vskill install anton-abyzov/specweave/sw-do` — same for `sw-increment`, `sw-review`, `sw-handoff`, `sw-task`) |
25
26
  |---|---|
26
- | Plan a feature: write `spec.md` (Problem, Scope, `AC-01…` checkboxes, Approach: files that change, order of work, risks, decisions), get approval before any code | `specweave create-increment "title"` |
27
+ | Plan a feature: write `spec.md` (Problem, Scope, `AC-01…` checkboxes, Approach: files that change, order of work, risks, decisions), resolve material scope questions before implementation | `specweave create-increment "title"` |
27
28
  | Work the tasks | `specweave task next` → `specweave task claim T-NN` → implement → commit (`NNNN: …`) → `specweave task done T-NN --run "<test cmd>"` (the command runs through your OS shell: cmd.exe on Windows, sh elsewhere) |
28
29
  | Verify the whole increment | `specweave verify` |
29
30
  | Review before closing (recommended for anything that ships) | in a fresh session: read spec.md, list findings by severity citing path:line, re-verify each before writing `reports/review.md`; the authoring session never approves its own work |
@@ -31,7 +32,7 @@ Only those files (plus optional `plan.md` when Approach outgrows a page, and `sc
31
32
  | Stop for any reason | `specweave handoff` |
32
33
 
33
34
  Without the `specweave` CLI: create the folder by hand (id = highest existing 4-digit prefix + 1, zero-padded); append one JSON line per event to `ledger.jsonl` as UTF-8 without BOM, LF-terminated, e.g. `{"t":"T-01","e":"claim","by":"<tool>@<host>","at":"<ISO>"}` (bash: `>>`; PowerShell: `[IO.File]::AppendAllText('ledger.jsonl', $line + "`n")`, never `>>`, it writes UTF-16); mark done with the same line using `"e":"done","evidence":"<commit sha>"` after the Test command exits 0; write `reports/verify.md` with the real test output and say the increment is ready to close (closing itself needs the CLI).
34
- Start every feature with an increment, even a small one, unless the user says "no increment". Fix a bug by writing the failing test first, then fixing the code without touching the test.
35
+ Use an increment for work that needs shared scope, acceptance criteria, or a handoff. Small self-contained fixes can remain a lightweight intent. For bugs, reproduce the failure and add a regression test when it protects behavior. User authorization carries through routine implementation and verification.
35
36
  <!-- /SECTION -->
36
37
 
37
38
  <!-- SECTION:verify required -->
@@ -48,8 +49,7 @@ Run the project's build/test/lint (see **Commands**) before reporting any task c
48
49
  3. Never edit ledger lines or another task's status; append only. On a git conflict in `ledger.jsonl` keep every line from both sides.
49
50
  4. `done` needs the task's **Test** command exit 0 and a commit sha. A task that turns out unnecessary is closed with `specweave task skip T-NN --reason "..."` (no CLI: append a `skip` ledger line with a reason), never ticked.
50
51
  5. When you stop: `specweave task release --all-mine` then `specweave handoff`.
51
-
52
- Resuming: find the active increment with `specweave status` (or the `NNNN-*` folder whose `metadata.json` has `"status": "active"`), read `spec.md`, then the latest `handoff.md`, then `specweave task next`.
52
+ Resuming: check `.specweave/intents/board.jsonl` (latest revision per id) or `specweave dashboard` for unfinished intents, including work without an increment. Then find the active increment with `specweave status` (or the `NNNN-*` folder whose `metadata.json` has `"status": "active"`), read `spec.md`, then the latest `handoff.md`, then `specweave task next`.
53
53
  <!-- /SECTION -->
54
54
 
55
55
  <!-- SECTION:conventions required -->
@@ -1,7 +1,7 @@
1
1
  <!-- SECTION:header required -->
2
2
  # {PROJECT_NAME}
3
3
 
4
- SpecWeave project. Work happens in increments under `.specweave/increments/NNNN-slug/`. `spec.md` (Problem, ACs, Approach) is the source of truth; the ledger records who did what. Keep this file under one page; project-specific notes go in **Project notes** at the bottom.
4
+ SpecWeave project. Small requests live in the intent board; scoped work lives in increments under `.specweave/increments/NNNN-slug/`. `spec.md` (Problem, ACs, Approach) is the source of truth; the ledger records who did what. Keep this file under one page; project-specific notes go in **Project notes** at the bottom.
5
5
  <!-- /SECTION -->
6
6
 
7
7
  <!-- SECTION:structure -->
@@ -9,6 +9,7 @@ SpecWeave project. Work happens in increments under `.specweave/increments/NNNN-
9
9
 
10
10
  ```
11
11
  .specweave/
12
+ intents/board.jsonl append-only intent history (latest revision per id; planning state, not verification)
12
13
  increments/NNNN-slug/ spec.md (Problem · ACs · Approach) · tasks.md (tasks + rendered board) · ledger.jsonl (claims/done, CLI-written) · handoff.md · reports/ (evidence; binaries in reports/artifacts/, gitignored)
13
14
  docs/ ADRs and hand-written docs (read before changing architecture)
14
15
  config.json
@@ -23,14 +24,14 @@ Only those files (plus optional `plan.md` when Approach outgrows a page, and `sc
23
24
 
24
25
  | Step | Command |
25
26
  |---|---|
26
- | Plan a feature (spec.md with ACs + Approach, in plan mode; approved before any code) | `/sw:increment "title"` |
27
+ | Plan a feature (spec.md with ACs + Approach; resolve material scope questions) | `/sw:increment "title"` |
27
28
  | Work the tasks (claim → implement → commit → done with evidence) | `/sw:do` (loops `specweave task next` / `claim` / `done --run`) |
28
29
  | Verify the whole increment | `specweave verify` |
29
30
  | Review before closing (recommended for anything that ships: fresh context, findings cite path:line and are re-verified) | `/sw:review` |
30
31
  | Close (needs a green verify or `--reason`) | `/sw:done` |
31
32
  | Stop for any reason (context, tokens, switching tools) | `/sw:handoff` |
32
33
 
33
- Start every feature with the increment, even a small one, unless the user says "no increment". Fix a bug by writing the failing test first, then fixing the code without touching the test.
34
+ Use an increment for work that needs shared scope, acceptance criteria, or a handoff. Small self-contained fixes can remain a lightweight intent. For bugs, reproduce the failure and add a regression test when it protects behavior. User authorization carries through routine implementation and verification.
34
35
  <!-- /SECTION -->
35
36
 
36
37
  <!-- SECTION:verify required -->
@@ -47,8 +48,7 @@ Run the project's build/test/lint (see **Commands**) before reporting any task c
47
48
  3. Never edit ledger lines or another task's status; append only. On a git conflict in `ledger.jsonl` keep every line from both sides.
48
49
  4. `done` needs the task's **Test** command exit 0 and a commit sha. A task that turns out unnecessary is closed with `specweave task skip T-NN --reason "..."`, never ticked.
49
50
  5. When you stop: `specweave task release --all-mine` then `/sw:handoff`.
50
-
51
- Resuming: find the active increment with `specweave status` (or the `NNNN-*` folder whose `metadata.json` has `"status": "active"`), read `spec.md`, then the latest `handoff.md`, then `/sw:do`.
51
+ Resuming: check `.specweave/intents/board.jsonl` (latest revision per id) or `specweave dashboard` for unfinished intents, including work without an increment. Then find the active increment with `specweave status` (or the `NNNN-*` folder whose `metadata.json` has `"status": "active"`), read `spec.md`, then the latest `handoff.md`, then `/sw:do`.
52
52
  <!-- /SECTION -->
53
53
 
54
54
  <!-- SECTION:conventions required -->