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.
- package/.claude-plugin/marketplace.json +3 -3
- package/README.md +15 -6
- package/dist/dashboard/assets/index-BeKUULf0.css +1 -0
- package/dist/dashboard/assets/index-Bf5TF0_l.js +13 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/src/cli/commands/sync.d.ts.map +1 -1
- package/dist/src/cli/commands/sync.js +23 -5
- package/dist/src/cli/commands/sync.js.map +1 -1
- package/dist/src/core/doctor/checkers/hooks-checker.d.ts +2 -2
- package/dist/src/core/doctor/checkers/hooks-checker.d.ts.map +1 -1
- package/dist/src/core/doctor/checkers/hooks-checker.js +6 -5
- package/dist/src/core/doctor/checkers/hooks-checker.js.map +1 -1
- package/dist/src/core/hooks/handlers/session-start.d.ts.map +1 -1
- package/dist/src/core/hooks/handlers/session-start.js +20 -0
- package/dist/src/core/hooks/handlers/session-start.js.map +1 -1
- package/dist/src/core/intent/portable-context.d.ts +4 -0
- package/dist/src/core/intent/portable-context.d.ts.map +1 -0
- package/dist/src/core/intent/portable-context.js +68 -0
- package/dist/src/core/intent/portable-context.js.map +1 -0
- package/dist/src/core/intent/types.d.ts +28 -0
- package/dist/src/core/intent/types.d.ts.map +1 -0
- package/dist/src/core/intent/types.js +3 -0
- package/dist/src/core/intent/types.js.map +1 -0
- package/dist/src/core/intent/validation.d.ts +5 -0
- package/dist/src/core/intent/validation.d.ts.map +1 -0
- package/dist/src/core/intent/validation.js +39 -0
- package/dist/src/core/intent/validation.js.map +1 -0
- package/dist/src/core/session/handoff-git-state.d.ts +3 -14
- package/dist/src/core/session/handoff-git-state.d.ts.map +1 -1
- package/dist/src/core/session/handoff-git-state.js +68 -124
- package/dist/src/core/session/handoff-git-state.js.map +1 -1
- package/dist/src/core/session/handoff-pointer.d.ts +4 -0
- package/dist/src/core/session/handoff-pointer.d.ts.map +1 -0
- package/dist/src/core/session/handoff-pointer.js +60 -0
- package/dist/src/core/session/handoff-pointer.js.map +1 -0
- package/dist/src/core/session/work-handoff.d.ts.map +1 -1
- package/dist/src/core/session/work-handoff.js +10 -3
- package/dist/src/core/session/work-handoff.js.map +1 -1
- package/dist/src/dashboard/server/dashboard-server.d.ts +1 -0
- package/dist/src/dashboard/server/dashboard-server.d.ts.map +1 -1
- package/dist/src/dashboard/server/dashboard-server.js +10 -90
- package/dist/src/dashboard/server/dashboard-server.js.map +1 -1
- package/dist/src/dashboard/server/data/cost-aggregator.d.ts +10 -5
- package/dist/src/dashboard/server/data/cost-aggregator.d.ts.map +1 -1
- package/dist/src/dashboard/server/data/cost-aggregator.js +31 -33
- package/dist/src/dashboard/server/data/cost-aggregator.js.map +1 -1
- package/dist/src/dashboard/server/data/dashboard-data-aggregator.d.ts +0 -1
- package/dist/src/dashboard/server/data/dashboard-data-aggregator.d.ts.map +1 -1
- package/dist/src/dashboard/server/data/dashboard-data-aggregator.js +14 -66
- package/dist/src/dashboard/server/data/dashboard-data-aggregator.js.map +1 -1
- package/dist/src/dashboard/server/data/dashboard-task-board.d.ts +4 -0
- package/dist/src/dashboard/server/data/dashboard-task-board.d.ts.map +1 -0
- package/dist/src/dashboard/server/data/dashboard-task-board.js +56 -0
- package/dist/src/dashboard/server/data/dashboard-task-board.js.map +1 -0
- package/dist/src/dashboard/server/data/intent-store.d.ts +19 -0
- package/dist/src/dashboard/server/data/intent-store.d.ts.map +1 -0
- package/dist/src/dashboard/server/data/intent-store.js +268 -0
- package/dist/src/dashboard/server/data/intent-store.js.map +1 -0
- package/dist/src/dashboard/server/data/local-sessions.d.ts +15 -0
- package/dist/src/dashboard/server/data/local-sessions.d.ts.map +1 -0
- package/dist/src/dashboard/server/data/local-sessions.js +197 -0
- package/dist/src/dashboard/server/data/local-sessions.js.map +1 -0
- package/dist/src/dashboard/server/data/work-projection.d.ts +8 -0
- package/dist/src/dashboard/server/data/work-projection.d.ts.map +1 -0
- package/dist/src/dashboard/server/data/work-projection.js +175 -0
- package/dist/src/dashboard/server/data/work-projection.js.map +1 -0
- package/dist/src/dashboard/server/file-watcher.d.ts.map +1 -1
- package/dist/src/dashboard/server/file-watcher.js +2 -1
- package/dist/src/dashboard/server/file-watcher.js.map +1 -1
- package/dist/src/dashboard/server/local-request.d.ts +4 -0
- package/dist/src/dashboard/server/local-request.d.ts.map +1 -0
- package/dist/src/dashboard/server/local-request.js +29 -0
- package/dist/src/dashboard/server/local-request.js.map +1 -0
- package/dist/src/dashboard/server/routes/work-routes.d.ts +5 -0
- package/dist/src/dashboard/server/routes/work-routes.d.ts.map +1 -0
- package/dist/src/dashboard/server/routes/work-routes.js +37 -0
- package/dist/src/dashboard/server/routes/work-routes.js.map +1 -0
- package/dist/src/dashboard/types.d.ts +3 -3
- package/dist/src/dashboard/types.d.ts.map +1 -1
- package/dist/src/dashboard/work-types.d.ts +39 -0
- package/dist/src/dashboard/work-types.d.ts.map +1 -0
- package/dist/src/dashboard/work-types.js +3 -0
- package/dist/src/dashboard/work-types.js.map +1 -0
- package/dist/src/sync/external-change-puller.d.ts +19 -0
- package/dist/src/sync/external-change-puller.d.ts.map +1 -1
- package/dist/src/sync/external-change-puller.js +87 -77
- package/dist/src/sync/external-change-puller.js.map +1 -1
- package/package.json +3 -3
- package/plugins/specweave/.claude-plugin/plugin.json +2 -2
- package/plugins/specweave/hooks/hooks.json +3 -30
- package/plugins/specweave/hooks/run-worker.mjs +229 -0
- package/plugins/specweave/hooks/run.mjs +83 -217
- package/plugins/specweave/skills/increment/SKILL.md +7 -5
- package/src/templates/AGENTS.md.template +5 -5
- package/src/templates/CLAUDE.md.template +5 -5
- package/dist/dashboard/assets/index-BiJhGDZV.js +0 -13
- 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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
130
|
-
|
|
28
|
+
const parent = path.dirname(current);
|
|
29
|
+
if (parent === current) break;
|
|
30
|
+
current = parent;
|
|
131
31
|
}
|
|
132
|
-
return
|
|
32
|
+
return false;
|
|
133
33
|
}
|
|
134
|
-
|
|
135
|
-
function isCliRoot(dir) {
|
|
136
|
-
if (!dir) return false;
|
|
34
|
+
function record(reason) {
|
|
137
35
|
try {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
if (process.
|
|
148
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
##
|
|
14
|
+
## Scope before implementation
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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. **
|
|
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.
|
|
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),
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 -->
|