kld-sdd 2.4.7 → 2.4.9
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/lib/init.js +201 -12
- package/package.json +1 -1
- package/skywalk-sdd/index.js +2241 -127
- package/templates/ci/github-actions-sdd.yml +67 -0
- package/templates/ci/gitlab-ci-sdd.yml +44 -0
- package/templates/git-hooks/pre-commit-sdd-check.js +155 -0
- package/templates/git-hooks/pre-push-sdd-check.js +41 -0
- package/templates/hooks/claude/hooks/sdd-post-tool.js +120 -0
- package/templates/hooks/claude/hooks/sdd-pre-tool.js +38 -0
- package/templates/hooks/claude/hooks/sdd-prompt.js +66 -0
- package/templates/hooks/claude/hooks/sdd-stop.js +82 -0
- package/templates/hooks/claude/settings.json +46 -0
- package/templates/opsx-commands/apply.md +75 -7
- package/templates/opsx-commands/archive.md +116 -55
- package/templates/opsx-commands/check.md +123 -4
- package/templates/opsx-commands/design.md +14 -4
- package/templates/opsx-commands/explore.md +14 -4
- package/templates/opsx-commands/propose.md +10 -4
- package/templates/opsx-commands/spec.md +14 -4
- package/templates/opsx-commands/task.md +15 -5
- package/templates/opsx-commands/test.md +41 -4
- package/templates/skills/opsx-apply/SKILL.md +63 -5
- package/templates/skills/opsx-archive/SKILL.md +94 -47
- package/templates/skills/opsx-check/SKILL.md +47 -3
- package/templates/skills/opsx-design/SKILL.md +8 -3
- package/templates/skills/opsx-explore/SKILL.md +8 -3
- package/templates/skills/opsx-propose/SKILL.md +8 -3
- package/templates/skills/opsx-spec/SKILL.md +8 -3
- package/templates/skills/opsx-task/SKILL.md +8 -3
- package/templates/skills/opsx-test/SKILL.md +8 -3
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: SDD Quality Gate
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
- master
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
sdd-quality-gate:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
env:
|
|
14
|
+
SDD_PROJECT: ${{ github.workspace }}
|
|
15
|
+
SDD_CHANGE: ${{ github.head_ref || github.ref_name }}
|
|
16
|
+
SDD_AGENT: github-actions
|
|
17
|
+
SDD_SESSION: github-${{ github.run_id }}
|
|
18
|
+
SDD_BUILD_COMMAND: npm run build --if-present
|
|
19
|
+
SDD_TEST_COMMAND: npm test --if-present
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: 20
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: npm install
|
|
29
|
+
|
|
30
|
+
- name: SDD doctor
|
|
31
|
+
run: node skywalk-sdd/log.js doctor --project="$SDD_PROJECT" --change="$SDD_CHANGE"
|
|
32
|
+
|
|
33
|
+
- name: Build and record SDD result
|
|
34
|
+
shell: bash
|
|
35
|
+
run: |
|
|
36
|
+
set +e
|
|
37
|
+
start_ms=$(date +%s%3N)
|
|
38
|
+
bash -lc "$SDD_BUILD_COMMAND"
|
|
39
|
+
status=$?
|
|
40
|
+
end_ms=$(date +%s%3N)
|
|
41
|
+
duration=$((end_ms - start_ms))
|
|
42
|
+
result="success"
|
|
43
|
+
success="true"
|
|
44
|
+
if [ "$status" -ne 0 ]; then
|
|
45
|
+
result="failure"
|
|
46
|
+
success="false"
|
|
47
|
+
fi
|
|
48
|
+
node skywalk-sdd/log.js record --type=build_result --command=ci --project="$SDD_PROJECT" --change="$SDD_CHANGE" --agent="$SDD_AGENT" --source=ci --session-id="$SDD_SESSION" --result="$result" --summary="CI build $result" --details-json="{\"build_results\":{\"command\":\"$SDD_BUILD_COMMAND\",\"success\":$success,\"duration_ms\":$duration,\"error_count\":0}}"
|
|
49
|
+
exit "$status"
|
|
50
|
+
|
|
51
|
+
- name: Test and record SDD result
|
|
52
|
+
shell: bash
|
|
53
|
+
run: |
|
|
54
|
+
set +e
|
|
55
|
+
start_ms=$(date +%s%3N)
|
|
56
|
+
bash -lc "$SDD_TEST_COMMAND"
|
|
57
|
+
status=$?
|
|
58
|
+
end_ms=$(date +%s%3N)
|
|
59
|
+
duration=$((end_ms - start_ms))
|
|
60
|
+
result="success"
|
|
61
|
+
failed=0
|
|
62
|
+
if [ "$status" -ne 0 ]; then
|
|
63
|
+
result="failure"
|
|
64
|
+
failed=1
|
|
65
|
+
fi
|
|
66
|
+
node skywalk-sdd/log.js record --type=test_result --command=ci --project="$SDD_PROJECT" --change="$SDD_CHANGE" --agent="$SDD_AGENT" --source=ci --session-id="$SDD_SESSION" --result="$result" --summary="CI test $result" --details-json="{\"test_results\":{\"command\":\"$SDD_TEST_COMMAND\",\"passed\":0,\"failed\":$failed,\"skipped\":0,\"coverage\":null,\"duration_ms\":$duration}}"
|
|
67
|
+
exit "$status"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
sdd_quality_gate:
|
|
2
|
+
image: node:20
|
|
3
|
+
stage: test
|
|
4
|
+
variables:
|
|
5
|
+
SDD_PROJECT: "$CI_PROJECT_DIR"
|
|
6
|
+
SDD_CHANGE: "$CI_COMMIT_REF_NAME"
|
|
7
|
+
SDD_AGENT: "gitlab-ci"
|
|
8
|
+
SDD_SESSION: "gitlab-$CI_PIPELINE_ID"
|
|
9
|
+
SDD_BUILD_COMMAND: "npm run build --if-present"
|
|
10
|
+
SDD_TEST_COMMAND: "npm test --if-present"
|
|
11
|
+
before_script:
|
|
12
|
+
- npm install
|
|
13
|
+
script:
|
|
14
|
+
- node skywalk-sdd/log.js doctor --project="$SDD_PROJECT" --change="$SDD_CHANGE"
|
|
15
|
+
- |
|
|
16
|
+
set +e
|
|
17
|
+
start_ms=$(date +%s%3N)
|
|
18
|
+
sh -lc "$SDD_BUILD_COMMAND"
|
|
19
|
+
status=$?
|
|
20
|
+
end_ms=$(date +%s%3N)
|
|
21
|
+
duration=$((end_ms - start_ms))
|
|
22
|
+
result="success"
|
|
23
|
+
success="true"
|
|
24
|
+
if [ "$status" -ne 0 ]; then
|
|
25
|
+
result="failure"
|
|
26
|
+
success="false"
|
|
27
|
+
fi
|
|
28
|
+
node skywalk-sdd/log.js record --type=build_result --command=ci --project="$SDD_PROJECT" --change="$SDD_CHANGE" --agent="$SDD_AGENT" --source=ci --session-id="$SDD_SESSION" --result="$result" --summary="CI build $result" --details-json="{\"build_results\":{\"command\":\"$SDD_BUILD_COMMAND\",\"success\":$success,\"duration_ms\":$duration,\"error_count\":0}}"
|
|
29
|
+
if [ "$status" -ne 0 ]; then exit "$status"; fi
|
|
30
|
+
- |
|
|
31
|
+
set +e
|
|
32
|
+
start_ms=$(date +%s%3N)
|
|
33
|
+
sh -lc "$SDD_TEST_COMMAND"
|
|
34
|
+
status=$?
|
|
35
|
+
end_ms=$(date +%s%3N)
|
|
36
|
+
duration=$((end_ms - start_ms))
|
|
37
|
+
result="success"
|
|
38
|
+
failed=0
|
|
39
|
+
if [ "$status" -ne 0 ]; then
|
|
40
|
+
result="failure"
|
|
41
|
+
failed=1
|
|
42
|
+
fi
|
|
43
|
+
node skywalk-sdd/log.js record --type=test_result --command=ci --project="$SDD_PROJECT" --change="$SDD_CHANGE" --agent="$SDD_AGENT" --source=ci --session-id="$SDD_SESSION" --result="$result" --summary="CI test $result" --details-json="{\"test_results\":{\"command\":\"$SDD_TEST_COMMAND\",\"passed\":0,\"failed\":$failed,\"skipped\":0,\"coverage\":null,\"duration_ms\":$duration}}"
|
|
44
|
+
if [ "$status" -ne 0 ]; then exit "$status"; fi
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function readArg(name) {
|
|
8
|
+
const prefix = `--${name}=`;
|
|
9
|
+
const found = process.argv.find(arg => arg.startsWith(prefix));
|
|
10
|
+
return found ? found.slice(prefix.length) : '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasFlag(name) {
|
|
14
|
+
return process.argv.includes(`--${name}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getStagedFiles(projectRoot) {
|
|
18
|
+
try {
|
|
19
|
+
return execFileSync('git', ['diff', '--cached', '--name-only'], {
|
|
20
|
+
cwd: projectRoot,
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
23
|
+
}).split(/\r?\n/).filter(Boolean);
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePath(filePath) {
|
|
30
|
+
return filePath.replace(/\\/g, '/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getChangeNameFromPath(filePath) {
|
|
34
|
+
const normalized = normalizePath(filePath);
|
|
35
|
+
const match = normalized.match(/^openspec\/changes\/([^/]+)\//);
|
|
36
|
+
return match ? match[1] : '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasArchiveIntent(files) {
|
|
40
|
+
return files.some(file => {
|
|
41
|
+
const normalized = normalizePath(file);
|
|
42
|
+
return /^openspec\/changes\/[^/]+\/archive/i.test(normalized) ||
|
|
43
|
+
/^openspec\/archive\//i.test(normalized);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function discoverChanges(projectRoot, explicitChange, archiveMode) {
|
|
48
|
+
if (explicitChange) {
|
|
49
|
+
return [explicitChange];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const stagedFiles = getStagedFiles(projectRoot);
|
|
53
|
+
if (!archiveMode && !hasArchiveIntent(stagedFiles)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const changes = new Set();
|
|
58
|
+
for (const file of stagedFiles) {
|
|
59
|
+
const changeName = getChangeNameFromPath(file);
|
|
60
|
+
if (changeName) {
|
|
61
|
+
changes.add(changeName);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (changes.size > 0) {
|
|
66
|
+
return Array.from(changes);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const changesDir = path.join(projectRoot, 'openspec', 'changes');
|
|
70
|
+
if (!fs.existsSync(changesDir)) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return fs.readdirSync(changesDir)
|
|
75
|
+
.filter(name => fs.statSync(path.join(changesDir, name)).isDirectory());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function walkFiles(dirPath, predicate, files = []) {
|
|
79
|
+
if (!fs.existsSync(dirPath)) {
|
|
80
|
+
return files;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
84
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
walkFiles(fullPath, predicate, files);
|
|
87
|
+
} else if (!predicate || predicate(fullPath)) {
|
|
88
|
+
files.push(fullPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return files;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findIncompleteTasks(projectRoot, changeName) {
|
|
96
|
+
const changePath = path.join(projectRoot, 'openspec', 'changes', changeName);
|
|
97
|
+
const taskFiles = walkFiles(changePath, filePath => {
|
|
98
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
99
|
+
return fileName === 'tasks.md' || fileName === 'task.md';
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const incomplete = [];
|
|
103
|
+
for (const tasksPath of taskFiles) {
|
|
104
|
+
const relPath = path.relative(projectRoot, tasksPath).replace(/\\/g, '/');
|
|
105
|
+
const items = fs.readFileSync(tasksPath, 'utf8')
|
|
106
|
+
.split(/\r?\n/)
|
|
107
|
+
.map((line, index) => ({ file: relPath, line, lineNumber: index + 1 }))
|
|
108
|
+
.filter(item => /\[\s\]/.test(item.line));
|
|
109
|
+
incomplete.push(...items);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return incomplete;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main() {
|
|
116
|
+
const projectRoot = path.resolve(readArg('project') || process.env.SDD_PROJECT || process.cwd());
|
|
117
|
+
const explicitChange = readArg('change') || process.env.SDD_CHANGE || process.env.OPENSPEC_CHANGE || '';
|
|
118
|
+
const archiveMode = hasFlag('archive') || process.env.SDD_ARCHIVE_CHECK === '1';
|
|
119
|
+
const changes = discoverChanges(projectRoot, explicitChange, archiveMode);
|
|
120
|
+
|
|
121
|
+
if (changes.length === 0) {
|
|
122
|
+
console.log('SDD pre-commit: no archive intent detected; tasks gate skipped.');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const failures = [];
|
|
127
|
+
for (const changeName of changes) {
|
|
128
|
+
const incomplete = findIncompleteTasks(projectRoot, changeName);
|
|
129
|
+
if (incomplete.length > 0) {
|
|
130
|
+
failures.push({ changeName, incomplete });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (failures.length === 0) {
|
|
135
|
+
console.log(`SDD pre-commit: tasks gate passed for ${changes.length} change(s).`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.warn('SDD pre-commit: tasks.md still has incomplete items. Archive is allowed, but these items must be shown as unfinished/unchecked in telemetry and reports.');
|
|
140
|
+
for (const failure of failures) {
|
|
141
|
+
console.warn(`- ${failure.changeName}`);
|
|
142
|
+
for (const item of failure.incomplete.slice(0, 10)) {
|
|
143
|
+
console.warn(` ${item.file}:${item.lineNumber}: ${item.line.trim()}`);
|
|
144
|
+
}
|
|
145
|
+
if (failure.incomplete.length > 10) {
|
|
146
|
+
console.warn(` ... ${failure.incomplete.length - 10} more`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (hasFlag('strict-tasks') || process.env.SDD_STRICT_TASKS === '1') {
|
|
150
|
+
console.error('SDD pre-commit: strict task mode enabled; blocking archive commit.');
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
main();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function readArg(name) {
|
|
8
|
+
const prefix = `--${name}=`;
|
|
9
|
+
const found = process.argv.find(arg => arg.startsWith(prefix));
|
|
10
|
+
return found ? found.slice(prefix.length) : '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function main() {
|
|
14
|
+
const projectRoot = path.resolve(readArg('project') || process.env.SDD_PROJECT || process.cwd());
|
|
15
|
+
const changeName = readArg('change') || process.env.SDD_CHANGE || process.env.OPENSPEC_CHANGE || '';
|
|
16
|
+
const logCli = path.join(projectRoot, 'skywalk-sdd', 'log.js');
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(logCli)) {
|
|
19
|
+
console.log('SDD pre-push: skywalk-sdd/log.js not found; doctor gate skipped.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const args = [logCli, 'doctor', `--project=${projectRoot}`];
|
|
24
|
+
if (changeName) {
|
|
25
|
+
args.push(`--change=${changeName}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = spawnSync(process.execPath, args, {
|
|
29
|
+
cwd: projectRoot,
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (result.error) {
|
|
34
|
+
console.error(`SDD pre-push: failed to run doctor: ${result.error.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
process.exit(result.status || 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
main();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function readStdin() {
|
|
7
|
+
try {
|
|
8
|
+
return fs.readFileSync(0, 'utf8');
|
|
9
|
+
} catch {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseInput(raw) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(raw || '{}');
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findProjectRoot(input) {
|
|
23
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
24
|
+
const candidates = [
|
|
25
|
+
toolInput.cwd,
|
|
26
|
+
input.cwd,
|
|
27
|
+
input.project_root,
|
|
28
|
+
process.env.PWD,
|
|
29
|
+
process.cwd(),
|
|
30
|
+
].filter(Boolean);
|
|
31
|
+
return candidates.find(dir => fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.js'))) || process.cwd();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function latestActiveStage(projectRoot) {
|
|
35
|
+
const stateDir = path.join(projectRoot, 'skywalk-sdd', 'state');
|
|
36
|
+
if (!fs.existsSync(stateDir)) return null;
|
|
37
|
+
return fs.readdirSync(stateDir)
|
|
38
|
+
.filter(file => file.endsWith('.json'))
|
|
39
|
+
.map(file => {
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(fs.readFileSync(path.join(stateDir, file), 'utf8'));
|
|
42
|
+
return data.event || null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0] || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inferResult(input) {
|
|
52
|
+
const response = input.tool_response || input.toolResponse || {};
|
|
53
|
+
if (typeof response.exit_code === 'number') return response.exit_code === 0 ? 'success' : 'failure';
|
|
54
|
+
if (typeof response.exitCode === 'number') return response.exitCode === 0 ? 'success' : 'failure';
|
|
55
|
+
if (typeof response.success === 'boolean') return response.success ? 'success' : 'failure';
|
|
56
|
+
return 'partial';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function inferRecord(command) {
|
|
60
|
+
if (/\b(npm|pnpm|yarn)\s+(test|run\s+test)\b|\bpytest\b|\bmvn\b.*\btest\b|\bgo\s+test\b|\bcargo\s+test\b/i.test(command)) {
|
|
61
|
+
return {
|
|
62
|
+
type: 'test_result',
|
|
63
|
+
detailsKey: 'test_results',
|
|
64
|
+
details: { command, passed: 0, failed: 0, skipped: 0, coverage: null, duration_ms: null },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (/\b(npm|pnpm|yarn)\s+(run\s+build|build)\b|\btsc\b|\bmvn\b.*\bcompile\b|\bgradle\b.*\bcompile|\bgo\s+build\b|\bcargo\s+check\b/i.test(command)) {
|
|
68
|
+
return {
|
|
69
|
+
type: 'build_result',
|
|
70
|
+
detailsKey: 'build_results',
|
|
71
|
+
details: { command, success: null, duration_ms: null, error_count: null },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function recordTelemetry(projectRoot, activeStage, record, result) {
|
|
78
|
+
const logPath = path.join(projectRoot, 'skywalk-sdd', 'log.js');
|
|
79
|
+
const details = { [record.detailsKey]: { ...record.details } };
|
|
80
|
+
if (record.type === 'build_result') {
|
|
81
|
+
details.build_results.success = result === 'success';
|
|
82
|
+
details.build_results.error_count = result === 'success' ? 0 : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const args = [
|
|
86
|
+
logPath,
|
|
87
|
+
'record',
|
|
88
|
+
`--type=${record.type}`,
|
|
89
|
+
`--command=${activeStage.command || activeStage.stage || 'unknown'}`,
|
|
90
|
+
`--project=${projectRoot}`,
|
|
91
|
+
`--change=${activeStage.change || 'general'}`,
|
|
92
|
+
`--agent=${activeStage.agent_type || 'claude-code'}`,
|
|
93
|
+
'--source=claude-hook',
|
|
94
|
+
`--result=${result}`,
|
|
95
|
+
`--summary=Claude hook captured ${record.type}`,
|
|
96
|
+
`--details-json=${JSON.stringify(details)}`,
|
|
97
|
+
];
|
|
98
|
+
if (activeStage.capability) args.push(`--capability=${activeStage.capability}`);
|
|
99
|
+
if (activeStage.task_id) args.push(`--task-id=${activeStage.task_id}`);
|
|
100
|
+
if (activeStage.session_id) args.push(`--session-id=${activeStage.session_id}`);
|
|
101
|
+
execFileSync('node', args, { cwd: projectRoot, stdio: 'ignore' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const input = parseInput(readStdin());
|
|
105
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
106
|
+
const command = String(toolInput.command || input.command || '');
|
|
107
|
+
const record = inferRecord(command);
|
|
108
|
+
if (!record) process.exit(0);
|
|
109
|
+
|
|
110
|
+
const projectRoot = findProjectRoot(input);
|
|
111
|
+
const activeStage = latestActiveStage(projectRoot);
|
|
112
|
+
if (!activeStage || !activeStage.change || activeStage.change === 'general') {
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
recordTelemetry(projectRoot, activeStage, record, inferResult(input));
|
|
118
|
+
} catch {
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
function readStdin() {
|
|
5
|
+
try {
|
|
6
|
+
return fs.readFileSync(0, 'utf8');
|
|
7
|
+
} catch {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseInput(raw) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(raw || '{}');
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const input = parseInput(readStdin());
|
|
21
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
22
|
+
const command = String(toolInput.command || input.command || '');
|
|
23
|
+
|
|
24
|
+
const dangerousPatterns = [
|
|
25
|
+
/\brm\s+-rf\b/i,
|
|
26
|
+
/\brmdir\s+\/s\b/i,
|
|
27
|
+
/\bdel\s+\/[fsq]/i,
|
|
28
|
+
/\bgit\s+reset\s+--hard\b/i,
|
|
29
|
+
/\bgit\s+clean\s+-fdx\b/i,
|
|
30
|
+
/\bRemove-Item\b.*\b-Recurse\b.*\b-Force\b/i,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
if (dangerousPatterns.some(pattern => pattern.test(command))) {
|
|
34
|
+
console.log(JSON.stringify({
|
|
35
|
+
decision: 'block',
|
|
36
|
+
reason: 'Blocked by SDD hook: destructive command requires explicit user approval.',
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function readStdin() {
|
|
6
|
+
try {
|
|
7
|
+
return fs.readFileSync(0, 'utf8');
|
|
8
|
+
} catch {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseInput(raw) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(raw || '{}');
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findProjectRoot(input) {
|
|
22
|
+
const candidates = [
|
|
23
|
+
input.cwd,
|
|
24
|
+
input.project_root,
|
|
25
|
+
process.env.PWD,
|
|
26
|
+
process.cwd(),
|
|
27
|
+
].filter(Boolean);
|
|
28
|
+
return candidates.find(dir => fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.js'))) || process.cwd();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readActiveStages(projectRoot) {
|
|
32
|
+
const stateDir = path.join(projectRoot, 'skywalk-sdd', 'state');
|
|
33
|
+
if (!fs.existsSync(stateDir)) return [];
|
|
34
|
+
return fs.readdirSync(stateDir)
|
|
35
|
+
.filter(file => file.endsWith('.json'))
|
|
36
|
+
.map(file => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fs.readFileSync(path.join(stateDir, file), 'utf8')).event;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const input = parseInput(readStdin());
|
|
47
|
+
const prompt = input.prompt || input.message || input.user_prompt || '';
|
|
48
|
+
if (!/\/opsx:/.test(prompt)) process.exit(0);
|
|
49
|
+
|
|
50
|
+
const projectRoot = findProjectRoot(input);
|
|
51
|
+
const activeStages = readActiveStages(projectRoot)
|
|
52
|
+
.map(event => `${event.change || 'general'}:${event.command || event.stage || 'unknown'}`)
|
|
53
|
+
.join(', ');
|
|
54
|
+
|
|
55
|
+
const lines = [
|
|
56
|
+
'SDD Telemetry reminder:',
|
|
57
|
+
'- Run skywalk-sdd/log.js start before the OPSX stage work begins.',
|
|
58
|
+
'- Run skywalk-sdd/log.js end before stopping the stage.',
|
|
59
|
+
'- Hooks are only an enhancement; OPSX command instructions remain authoritative.',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
if (activeStages) {
|
|
63
|
+
lines.push(`- Active stage state: ${activeStages}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(lines.join('\n'));
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function readStdin() {
|
|
7
|
+
try {
|
|
8
|
+
return fs.readFileSync(0, 'utf8');
|
|
9
|
+
} catch {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseInput(raw) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(raw || '{}');
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findProjectRoot(input) {
|
|
23
|
+
const candidates = [
|
|
24
|
+
input.cwd,
|
|
25
|
+
input.project_root,
|
|
26
|
+
process.env.PWD,
|
|
27
|
+
process.cwd(),
|
|
28
|
+
].filter(Boolean);
|
|
29
|
+
return candidates.find(dir => fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.js'))) || process.cwd();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readActiveStages(projectRoot) {
|
|
33
|
+
const stateDir = path.join(projectRoot, 'skywalk-sdd', 'state');
|
|
34
|
+
if (!fs.existsSync(stateDir)) return [];
|
|
35
|
+
return fs.readdirSync(stateDir)
|
|
36
|
+
.filter(file => file.endsWith('.json'))
|
|
37
|
+
.map(file => {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(path.join(stateDir, file), 'utf8')).event;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeWarning(projectRoot, event) {
|
|
48
|
+
const logPath = path.join(projectRoot, 'skywalk-sdd', 'log.js');
|
|
49
|
+
const args = [
|
|
50
|
+
logPath,
|
|
51
|
+
'record',
|
|
52
|
+
'--type=telemetry_warning',
|
|
53
|
+
`--command=${event.command || event.stage || 'unknown'}`,
|
|
54
|
+
`--project=${projectRoot}`,
|
|
55
|
+
`--change=${event.change || 'general'}`,
|
|
56
|
+
`--agent=${event.agent_type || 'claude-code'}`,
|
|
57
|
+
'--source=claude-hook',
|
|
58
|
+
'--result=partial',
|
|
59
|
+
'--summary=Claude hook detected an open SDD stage at stop',
|
|
60
|
+
`--details-json=${JSON.stringify({ warning: 'open_stage_at_stop', event_id: event.event_id })}`,
|
|
61
|
+
];
|
|
62
|
+
if (event.capability) args.push(`--capability=${event.capability}`);
|
|
63
|
+
if (event.session_id) args.push(`--session-id=${event.session_id}`);
|
|
64
|
+
execFileSync('node', args, { cwd: projectRoot, stdio: 'ignore' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const input = parseInput(readStdin());
|
|
68
|
+
const projectRoot = findProjectRoot(input);
|
|
69
|
+
const activeStages = readActiveStages(projectRoot);
|
|
70
|
+
if (activeStages.length === 0) process.exit(0);
|
|
71
|
+
|
|
72
|
+
for (const event of activeStages) {
|
|
73
|
+
try {
|
|
74
|
+
writeWarning(projectRoot, event);
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log([
|
|
79
|
+
'SDD Telemetry warning: open stage(s) detected.',
|
|
80
|
+
...activeStages.map(event => `- ${event.change || 'general'}:${event.command || event.stage || 'unknown'} event_id=${event.event_id}`),
|
|
81
|
+
'Run skywalk-sdd/log.js end before closing the OPSX stage, or explicitly mark it partial/failure.',
|
|
82
|
+
].join('\n'));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node .claude/hooks/sdd-prompt.js"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"PreToolUse": [
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Bash",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "node .claude/hooks/sdd-pre-tool.js"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"PostToolUse": [
|
|
25
|
+
{
|
|
26
|
+
"matcher": "Bash",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "node .claude/hooks/sdd-post-tool.js"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"Stop": [
|
|
36
|
+
{
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "node .claude/hooks/sdd-stop.js"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|