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.
- package/.claude-plugin/marketplace.json +2 -2
- package/dist/src/core/doctor/checkers/hooks-checker.d.ts +1 -1
- package/dist/src/core/doctor/checkers/hooks-checker.d.ts.map +1 -1
- package/dist/src/core/doctor/checkers/hooks-checker.js +5 -4
- package/dist/src/core/doctor/checkers/hooks-checker.js.map +1 -1
- 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/package.json +1 -1
- package/plugins/specweave/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave/hooks/hooks.json +5 -9
- package/plugins/specweave/hooks/run-worker.mjs +229 -0
- package/plugins/specweave/hooks/run.mjs +62 -219
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specweave",
|
|
3
|
-
"version": "2.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.
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
84
|
-
problems.push(`${event}:
|
|
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: '
|
|
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":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAEhF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAExC,wEAAwE;AACxE,MAAM,CAAC,MAAM,iBAAiB,GAAkE;IAC9F,eAAe,EAAE,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE;IACvE,cAAc,EAAE;QACd,eAAe,EAAE,YAAY;QAC7B,SAAS,EAAE,MAAM;QACjB,UAAU,EAAE,EAAE,SAAS,EAAE,yDAAyD,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE;KACvH;IACD,MAAM,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE,gBAAgB,EAAE,KAAK,EAAE;IAC5D,aAAa,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE;CAClE,CAAC;AAEF,kGAAkG;AAClG,MAAM,UAAU,WAAW;IACzB,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAsB,CAAC;YACrG,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW;gBAAE,OAAO,GAAG,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,eAAe;QACjB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAClG,CAAC;AAED,MAAM,OAAO,YAAY;IAAzB;QACE,aAAQ,GAAG,OAAO,CAAC;
|
|
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
|
-
|
|
36
|
-
|
|
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;
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
57
|
-
shortSha: '',
|
|
58
|
-
statusPorcelain: '',
|
|
59
|
-
diffStat: '',
|
|
60
|
-
hasUncommittedChanges: false,
|
|
26
|
+
isGitRepo: false, branch: '', shortSha: '', statusPorcelain: '',
|
|
27
|
+
diffStat: '', hasUncommittedChanges: false,
|
|
61
28
|
};
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;
|
|
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.
|
|
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.
|
|
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
|
+
"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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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()));
|
|
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
|
+
});
|