specweave 2.0.0 → 2.0.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specweave",
3
- "version": "2.0.0",
3
+ "version": "2.0.3",
4
4
  "description": "SpecWeave - Spec-Driven Development Framework. PM-led planning, intelligent model selection, living documentation, multi-tool support.",
5
5
  "owner": {
6
6
  "name": "Anton Abyzov",
@@ -12,7 +12,7 @@
12
12
  "description": "SpecWeave unified plugin — increment lifecycle (plan, work the ledger, verify, review, complete), cross-tool handoff, GitHub/Jira/ADO sync",
13
13
  "source": "./plugins/specweave",
14
14
  "category": "development",
15
- "version": "2.0.0",
15
+ "version": "2.0.3",
16
16
  "author": {
17
17
  "name": "Anton Abyzov",
18
18
  "email": "anton.abyzov@gmail.com"
@@ -14,7 +14,7 @@ export declare function packageRoot(): string;
14
14
  export declare class HooksChecker implements HealthChecker {
15
15
  category: string;
16
16
  check(projectRoot: string, options: DoctorOptions): Promise<CategoryResult>;
17
- /** hooks.json must be exec-form (command + args), node-based, with sane timeouts. */
17
+ /** hooks.json uses a quoted node command string (Codex ignores separate args). */
18
18
  private checkHooksJson;
19
19
  private dryRun;
20
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-checker.d.ts","sourceRoot":"","sources":["../../../../../src/core/doctor/checkers/hooks-checker.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAe,aAAa,EAAE,MAAM,aAAa,CAAC;AAE7F,OAAO,EAAE,WAAW,EAAsB,MAAM,+BAA+B,CAAC;AAEhF,+EAA+E;AAC/E,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAS3F,CAAC;AAEF,kGAAkG;AAClG,wBAAgB,WAAW,IAAI,MAAM,CAcpC;AAED,qBAAa,YAAa,YAAW,aAAa;IAChD,QAAQ,SAAW;IAEb,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IA+BjF,qFAAqF;IACrF,OAAO,CAAC,cAAc;YAsBR,MAAM;CAoDrB"}
1
+ {"version":3,"file":"hooks-checker.d.ts","sourceRoot":"","sources":["../../../../../src/core/doctor/checkers/hooks-checker.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAe,aAAa,EAAE,MAAM,aAAa,CAAC;AAE7F,OAAO,EAAE,WAAW,EAAsB,MAAM,+BAA+B,CAAC;AAEhF,+EAA+E;AAC/E,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAS3F,CAAC;AAEF,kGAAkG;AAClG,wBAAgB,WAAW,IAAI,MAAM,CAcpC;AAED,qBAAa,YAAa,YAAW,aAAa;IAChD,QAAQ,SAAW;IAEb,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IA+BjF,kFAAkF;IAClF,OAAO,CAAC,cAAc;YAwBR,MAAM;CAoDrB"}
@@ -72,7 +72,7 @@ export class HooksChecker {
72
72
  }
73
73
  return { category: this.category, status: calculateOverallStatus(checks), checks };
74
74
  }
75
- /** hooks.json must be exec-form (command + args), node-based, with sane timeouts. */
75
+ /** hooks.json uses a quoted node command string (Codex ignores separate args). */
76
76
  checkHooksJson(hooksJson) {
77
77
  try {
78
78
  const data = JSON.parse(fs.readFileSync(hooksJson, 'utf8'));
@@ -80,15 +80,16 @@ export class HooksChecker {
80
80
  for (const [event, groups] of Object.entries(data.hooks ?? {})) {
81
81
  for (const group of groups) {
82
82
  for (const h of group.hooks ?? []) {
83
- if (h.command !== 'node' || !Array.isArray(h.args))
84
- problems.push(`${event}: not exec-form node`);
83
+ if (!/^node \"\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\/run\.mjs\" [a-z-]+$/.test(h.command ?? '') || h.args !== undefined) {
84
+ problems.push(`${event}: expected quoted node hook command without separate args`);
85
+ }
85
86
  if (typeof h.timeout === 'number' && h.timeout > 60)
86
87
  problems.push(`${event}: timeout ${h.timeout}s > 60s`);
87
88
  }
88
89
  }
89
90
  }
90
91
  return problems.length === 0
91
- ? { name: 'hooks.json', status: 'pass', message: 'exec-form node launcher, timeouts <= 60s' }
92
+ ? { name: 'hooks.json', status: 'pass', message: 'portable node launcher, timeouts <= 60s' }
92
93
  : { name: 'hooks.json', status: 'fail', message: problems.join('; ') };
93
94
  }
94
95
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-checker.js","sourceRoot":"","sources":["../../../../../src/core/doctor/checkers/hooks-checker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hooks-checker.js","sourceRoot":"","sources":["../../../../../src/core/doctor/checkers/hooks-checker.ts"],"names":[],"mappings":"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"}
@@ -31,19 +31,8 @@ export interface GitState {
31
31
  /** True when there are working-tree or staged changes. */
32
32
  hasUncommittedChanges: boolean;
33
33
  }
34
- /**
35
- * Capture git state for `repoRoot` and dump the full uncommitted diff to
36
- * `diffOutputPath`.
37
- *
38
- * The diff file always contains `git diff HEAD` (working-tree vs last commit)
39
- * concatenated with `git diff --cached` (staged vs HEAD). When the repo is
40
- * clean — or before the first commit — the file is written empty so callers can
41
- * unconditionally link to it.
42
- *
43
- * @param repoRoot - Absolute path to the candidate repository root.
44
- * @param diffOutputPath - Absolute path where the full diff is written.
45
- * @returns The captured {@link GitState}; all-empty + `isGitRepo:false` when
46
- * `repoRoot` is not a git work tree (no throw).
47
- */
34
+ /** Total Git budget leaves headroom below PreCompact's five-second budget. */
35
+ export declare const GIT_CAPTURE_BUDGET_MS = 3500;
36
+ /** Capture through a private index; an interrupted hook never stages user files. */
48
37
  export declare function captureGitState(repoRoot: string, diffOutputPath: string): GitState;
49
38
  //# sourceMappingURL=handoff-git-state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handoff-git-state.d.ts","sourceRoot":"","sources":["../../../../src/core/session/handoff-git-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAqBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,QAAQ,CA6DlF"}
1
+ {"version":3,"file":"handoff-git-state.d.ts","sourceRoot":"","sources":["../../../../src/core/session/handoff-git-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAED,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,QAAQ,CAyDlF"}
@@ -17,136 +17,80 @@
17
17
  import { execFileSync } from 'child_process';
18
18
  import * as fs from 'fs';
19
19
  import * as path from 'path';
20
- /**
21
- * Run a git command, returning trimmed stdout or `null` on any failure.
22
- *
23
- * Uses execFileSync with an argument array (no shell) so paths and refs are
24
- * never re-interpreted by a shell.
25
- */
26
- function git(repoRoot, args) {
27
- try {
28
- return execFileSync('git', args, {
29
- cwd: repoRoot,
30
- encoding: 'utf-8',
31
- stdio: ['ignore', 'pipe', 'ignore'],
32
- maxBuffer: 64 * 1024 * 1024, // diffs can be large
33
- }).trim();
34
- }
35
- catch {
36
- return null;
37
- }
38
- }
39
- /**
40
- * Capture git state for `repoRoot` and dump the full uncommitted diff to
41
- * `diffOutputPath`.
42
- *
43
- * The diff file always contains `git diff HEAD` (working-tree vs last commit)
44
- * concatenated with `git diff --cached` (staged vs HEAD). When the repo is
45
- * clean — or before the first commit — the file is written empty so callers can
46
- * unconditionally link to it.
47
- *
48
- * @param repoRoot - Absolute path to the candidate repository root.
49
- * @param diffOutputPath - Absolute path where the full diff is written.
50
- * @returns The captured {@link GitState}; all-empty + `isGitRepo:false` when
51
- * `repoRoot` is not a git work tree (no throw).
52
- */
20
+ import * as os from 'os';
21
+ /** Total Git budget leaves headroom below PreCompact's five-second budget. */
22
+ export const GIT_CAPTURE_BUDGET_MS = 3500;
23
+ /** Capture through a private index; an interrupted hook never stages user files. */
53
24
  export function captureGitState(repoRoot, diffOutputPath) {
54
25
  const empty = {
55
- isGitRepo: false,
56
- branch: '',
57
- shortSha: '',
58
- statusPorcelain: '',
59
- diffStat: '',
60
- hasUncommittedChanges: false,
26
+ isGitRepo: false, branch: '', shortSha: '', statusPorcelain: '',
27
+ diffStat: '', hasUncommittedChanges: false,
61
28
  };
62
- const insideWorkTree = git(repoRoot, ['rev-parse', '--is-inside-work-tree']);
63
- if (insideWorkTree !== 'true') {
64
- // Still write an empty diff file so the doc's link target exists.
29
+ const deadline = Date.now() + GIT_CAPTURE_BUDGET_MS;
30
+ const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
31
+ function git(args, trim = true) {
32
+ const remaining = deadline - Date.now();
33
+ if (remaining <= 0)
34
+ return null;
35
+ try {
36
+ const result = execFileSync('git', args, {
37
+ cwd: repoRoot, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
38
+ maxBuffer: 64 * 1024 * 1024, timeout: remaining, killSignal: 'SIGKILL',
39
+ });
40
+ return trim ? result.trim() : result;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ let temporary;
47
+ try {
48
+ if (git(['rev-parse', '--is-inside-work-tree']) !== 'true') {
49
+ safeWriteDiff(diffOutputPath, '');
50
+ return empty;
51
+ }
52
+ const indexPath = git(['rev-parse', '--path-format=absolute', '--git-path', 'index']);
53
+ if (!indexPath) {
54
+ safeWriteDiff(diffOutputPath, '');
55
+ return empty;
56
+ }
57
+ temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'specweave-handoff-index-'));
58
+ const privateIndex = path.join(temporary, 'index');
59
+ if (fs.existsSync(indexPath))
60
+ fs.copyFileSync(indexPath, privateIndex);
61
+ Object.assign(env, { GIT_INDEX_FILE: privateIndex });
62
+ const branch = git(['rev-parse', '--abbrev-ref', 'HEAD']) ?? '';
63
+ const shortSha = git(['rev-parse', '--short', 'HEAD']) ?? '';
64
+ const statusPorcelain = git(['status', '--porcelain']) ?? '';
65
+ // NUL-delimited paths handle spaces, quotes, Unicode and embedded newlines.
66
+ const untracked = (git(['ls-files', '--others', '--exclude-standard', '-z'], false) ?? '')
67
+ .split('\0').filter(Boolean);
68
+ if (untracked.length)
69
+ git(['add', '-N', '--', ...untracked]);
70
+ const hasHead = shortSha !== '';
71
+ const working = ['diff', '--no-ext-diff', '--no-textconv', ...(hasHead ? ['HEAD'] : [])];
72
+ const staged = ['diff', '--no-ext-diff', '--no-textconv', '--cached'];
73
+ const workingDiff = git(working) ?? '';
74
+ const stagedDiff = (hasHead ? git(staged) : '') ?? '';
75
+ const workingStat = git([...working, '--stat']) ?? '';
76
+ const stagedStat = (hasHead ? git([...staged, '--stat']) : '') ?? '';
77
+ safeWriteDiff(diffOutputPath, [workingDiff, stagedDiff].filter(Boolean).join('\n'));
78
+ return {
79
+ isGitRepo: true, branch, shortSha, statusPorcelain,
80
+ diffStat: [workingStat, stagedStat].filter(Boolean).join('\n'),
81
+ hasUncommittedChanges: statusPorcelain.length > 0,
82
+ };
83
+ }
84
+ catch {
65
85
  safeWriteDiff(diffOutputPath, '');
66
86
  return empty;
67
87
  }
68
- const branch = git(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']) ?? '';
69
- const shortSha = git(repoRoot, ['rev-parse', '--short', 'HEAD']) ?? '';
70
- const statusPorcelain = git(repoRoot, ['status', '--porcelain']) ?? '';
71
- // Intent-to-add every untracked file so `git diff` includes its FULL body,
72
- // not just a `??` porcelain line. Without this, brand-new uncommitted files —
73
- // the most common in-flight-work case — would be captured as a filename only,
74
- // breaking the spec's "exact uncommitted edits" promise (AC-US4-01/02).
75
- // `add -N` records intent only (no content staged) but it DOES add an index
76
- // entry, so we MUST revert it afterward to leave the user's real index exactly
77
- // as we found it — a handoff is a read-only capture and must not silently
78
- // change `git status`, `git stash`, or `git commit -a` behavior.
79
- const intentAdded = stageIntentToAddUntracked(repoRoot, statusPorcelain);
80
- // `git diff HEAD` covers working-tree vs HEAD; if there is no HEAD yet
81
- // (no commits), fall back to plain `git diff` so brand-new repos still work.
82
- const hasHead = shortSha !== '';
83
- const workingDiff = (hasHead ? git(repoRoot, ['diff', 'HEAD']) : git(repoRoot, ['diff'])) ?? '';
84
- const stagedDiff = (hasHead ? git(repoRoot, ['diff', '--cached']) : '') ?? '';
85
- const workingStat = (hasHead ? git(repoRoot, ['diff', '--stat', 'HEAD']) : git(repoRoot, ['diff', '--stat'])) ?? '';
86
- const stagedStat = (hasHead ? git(repoRoot, ['diff', '--cached', '--stat']) : '') ?? '';
87
- // Restore the index: drop the intent-to-add entries we created so the user's
88
- // staging area is byte-identical to its pre-handoff state.
89
- unstageIntentToAdd(repoRoot, hasHead, intentAdded);
90
- const diffStat = [workingStat, stagedStat].filter(Boolean).join('\n');
91
- const fullDiff = [workingDiff, stagedDiff].filter(Boolean).join('\n');
92
- safeWriteDiff(diffOutputPath, fullDiff);
93
- const hasUncommittedChanges = statusPorcelain.length > 0;
94
- return {
95
- isGitRepo: true,
96
- branch,
97
- shortSha,
98
- statusPorcelain,
99
- diffStat,
100
- hasUncommittedChanges,
101
- };
102
- }
103
- /**
104
- * Intent-to-add every untracked file from a porcelain status so its content
105
- * shows up in `git diff`. Parses `??`-prefixed porcelain lines and runs a
106
- * single `git add -N -- <paths...>`. Never throws — a failure here only means
107
- * untracked bodies are omitted, which must not abort the handoff.
108
- *
109
- * `add -N` (intent-to-add) records that a path will be tracked but stages NO
110
- * content. It DOES create an index entry, so the caller MUST pass the returned
111
- * paths to {@link unstageIntentToAdd} once the diff has been captured to leave
112
- * the user's real index untouched.
113
- *
114
- * @returns The untracked paths that were intent-to-added (empty if none).
115
- */
116
- function stageIntentToAddUntracked(repoRoot, statusPorcelain) {
117
- if (!statusPorcelain)
118
- return [];
119
- const untracked = statusPorcelain
120
- .split('\n')
121
- .filter((line) => line.startsWith('??'))
122
- // Porcelain format is `XY <path>`; for untracked the path starts at col 3.
123
- .map((line) => line.slice(3).trim())
124
- .filter(Boolean);
125
- if (untracked.length === 0)
126
- return [];
127
- // `--` guards against any path that looks like a flag.
128
- git(repoRoot, ['add', '-N', '--', ...untracked]);
129
- return untracked;
130
- }
131
- /**
132
- * Revert the intent-to-add entries created by {@link stageIntentToAddUntracked}
133
- * so the user's index is restored to its pre-handoff state. After this the
134
- * affected files are untracked (`??`) again, exactly as before the capture.
135
- *
136
- * `git reset -- <paths>` clears the index entries when a HEAD exists. Before the
137
- * first commit there is no HEAD, so `git reset` would fail; `git rm --cached
138
- * --force -- <paths>` removes the intent entries in that case. Never throws — a
139
- * failed revert must not abort the handoff (worst case: the index keeps the
140
- * intent entries, which is the pre-fix behavior).
141
- */
142
- function unstageIntentToAdd(repoRoot, hasHead, paths) {
143
- if (paths.length === 0)
144
- return;
145
- if (hasHead) {
146
- git(repoRoot, ['reset', '--quiet', '--', ...paths]);
147
- }
148
- else {
149
- git(repoRoot, ['rm', '--cached', '--force', '--quiet', '--', ...paths]);
88
+ finally {
89
+ if (temporary)
90
+ try {
91
+ fs.rmSync(temporary, { recursive: true, force: true });
92
+ }
93
+ catch { /* best effort */ }
150
94
  }
151
95
  }
152
96
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"handoff-git-state.js","sourceRoot":"","sources":["../../../../src/core/session/handoff-git-state.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handoff-git-state.js","sourceRoot":"","sources":["../../../../src/core/session/handoff-git-state.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specweave",
3
- "version": "2.0.0",
3
+ "version": "2.0.3",
4
4
  "description": "100+ domain-expert AI skills — PM, Architect, Frontend, QA, Security and more. Skills learn your team's patterns permanently. Spec-first planning, autonomous execution, multi-agent teams, synced to GitHub/JIRA. Claude Code, Cursor, Copilot & more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sw",
3
3
  "description": "SpecWeave: increment lifecycle for any AI tool. 10 skills (increment, do, done, review, team, handoff, sync, auto, brainstorm, qa), 4 exec-form hooks, and the specweave CLI for everything deterministic.",
4
- "version": "2.0.0",
4
+ "version": "2.0.3",
5
5
  "author": {
6
6
  "name": "SpecWeave Team",
7
7
  "url": "https://spec-weave.com"
@@ -1,13 +1,12 @@
1
1
  {
2
- "description": "SpecWeave 2.0 hooks: session context, status-completion guard, auto-loop driver, compaction handoff. Exec-form node launcher (no shell), Windows-safe.",
2
+ "description": "SpecWeave hooks: portable command strings and bounded worker execution.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "node",
10
- "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "session-start"],
9
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs\" session-start",
11
10
  "timeout": 10
12
11
  }
13
12
  ]
@@ -19,8 +18,7 @@
19
18
  "hooks": [
20
19
  {
21
20
  "type": "command",
22
- "command": "node",
23
- "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "pre-tool-use"],
21
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs\" pre-tool-use",
24
22
  "timeout": 10
25
23
  }
26
24
  ]
@@ -31,8 +29,7 @@
31
29
  "hooks": [
32
30
  {
33
31
  "type": "command",
34
- "command": "node",
35
- "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "stop"],
32
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs\" stop",
36
33
  "timeout": 30
37
34
  }
38
35
  ]
@@ -43,8 +40,7 @@
43
40
  "hooks": [
44
41
  {
45
42
  "type": "command",
46
- "command": "node",
47
- "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "pre-compact"],
43
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs\" pre-compact",
48
44
  "timeout": 10
49
45
  }
50
46
  ]
@@ -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,72 @@
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, mkdirSync } 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() {
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
+ function record(reason) {
43
14
  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
- }
15
+ const dir = path.join(homedir(), '.specweave', 'logs');
16
+ mkdirSync(dir, {recursive:true, mode:0o700});
17
+ appendFileSync(path.join(dir, 'hook-health.jsonl'), JSON.stringify({at:new Date().toISOString(),event,reason,elapsedMs:Date.now()-started})+'\n', {mode:0o600});
18
+ } catch {}
61
19
  }
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
- };
20
+ function killWorker() {
21
+ if (!child?.pid) return;
22
+ if (process.platform === 'win32') {
23
+ spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], {timeout:1000, windowsHide:true, stdio:'ignore'});
24
+ } else {
25
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { try {child.kill('SIGKILL');} catch {} }
74
26
  }
75
- return {};
76
27
  }
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));
28
+ function finish(result = {}, reason) {
29
+ if (finished) return;
30
+ finished = true;
31
+ clearTimeout(deadline);
32
+ killWorker();
33
+ if (reason) record(reason);
34
+ process.stdout.write(JSON.stringify(result)+'\n',()=>process.exit(0));
89
35
  }
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
- }
36
+ const deadline = setTimeout(()=>finish({},'deadline_exceeded'),budget);
37
+ process.on('SIGTERM',()=>finish({},'terminated'));
38
+ process.on('SIGINT',()=>finish({},'interrupted'));
39
+ process.on('uncaughtException',()=>finish({},'adapter_error'));
40
+ process.stdin.setEncoding('utf8');
41
+ process.stdin.on('data',chunk=>{
42
+ size += Buffer.byteLength(chunk, 'utf8');
43
+ if(size>1024*1024) return finish({},'input_limit');
44
+ input+=chunk;
45
+ });
46
+ process.stdin.on('error',()=>finish({},'stdin_error'));
47
+ process.stdin.on('end',()=>{
48
+ let data;
49
+ try {data=JSON.parse(input);if(!data || typeof data!=='object'||Array.isArray(data))throw Error();}
50
+ catch {return finish({},'invalid_input');}
51
+ if(!['session-start','pre-tool-use','pre-compact','stop'].includes(event))return finish({});
52
+ const env={...process.env};
53
+ child=spawn(process.execPath,[worker,event],{env,detached:process.platform !== 'win32',stdio:['pipe','pipe','pipe']});
54
+ child.stdout.setEncoding('utf8');
55
+ child.stdout.on('data',chunk=>{
56
+ output+=chunk;
57
+ if(output.length>1024*1024)finish({},'output_limit');
111
58
  });
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) {
59
+ child.stderr.resume();
60
+ child.stdin.on('error',()=>{});
61
+ child.on('error',()=>finish({},'worker_spawn_failed'));
62
+ child.on('close',code=>{
63
+ if(finished)return;
64
+ if(code!==0)return finish({},'worker_failed');
199
65
  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()));
66
+ const result=JSON.parse(output);
67
+ if(!result||typeof result!=='object'||Array.isArray(result))throw Error();
68
+ finish(result);
69
+ } catch {finish({},'invalid_worker_output');}
70
+ });
71
+ child.stdin.end(input);
72
+ });