dw-kit 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/post-write.sh +64 -58
- package/.claude/hooks/pre-commit-gate.sh +96 -90
- package/.claude/hooks/privacy-block.sh +99 -94
- package/.claude/hooks/progress-ping.sh +53 -47
- package/.claude/hooks/safety-guard.sh +60 -54
- package/.claude/hooks/scout-block.sh +88 -82
- package/.claude/hooks/session-init.sh +91 -74
- package/.claude/hooks/stop-check.sh +88 -36
- package/.claude/hooks/telemetry-log.sh +34 -0
- package/.claude/rules/code-style.md +37 -37
- package/.claude/rules/commit-standards.md +37 -37
- package/.claude/rules/dw.md +136 -0
- package/.claude/settings.json +120 -99
- package/.claude/skills/dw-arch-review/SKILL.md +119 -119
- package/.claude/skills/dw-archive/SKILL.md +81 -81
- package/.claude/skills/dw-commit/SKILL.md +81 -81
- package/.claude/skills/dw-config-init/SKILL.md +91 -91
- package/.claude/skills/dw-config-validate/SKILL.md +75 -75
- package/.claude/skills/dw-dashboard/SKILL.md +209 -209
- package/.claude/skills/dw-debug/SKILL.md +97 -97
- package/.claude/skills/dw-decision/SKILL.md +116 -0
- package/.claude/skills/dw-docs-update/SKILL.md +125 -125
- package/.claude/skills/dw-estimate/SKILL.md +90 -90
- package/.claude/skills/dw-execute/SKILL.md +98 -98
- package/.claude/skills/dw-flow/SKILL.md +274 -274
- package/.claude/skills/dw-handoff/SKILL.md +81 -81
- package/.claude/skills/dw-kit-report/SKILL.md +152 -152
- package/.claude/skills/dw-log-work/SKILL.md +69 -69
- package/.claude/skills/dw-onboard/SKILL.md +201 -201
- package/.claude/skills/dw-plan/SKILL.md +125 -125
- package/.claude/skills/dw-prompt/SKILL.md +62 -62
- package/.claude/skills/dw-requirements/SKILL.md +98 -98
- package/.claude/skills/dw-research/SKILL.md +114 -114
- package/.claude/skills/dw-retroactive/SKILL.md +311 -311
- package/.claude/skills/dw-review/SKILL.md +66 -66
- package/.claude/skills/dw-rollback/SKILL.md +90 -90
- package/.claude/skills/dw-sprint-review/SKILL.md +99 -99
- package/.claude/skills/dw-task-init/SKILL.md +59 -59
- package/.claude/skills/dw-test-plan/SKILL.md +113 -113
- package/.claude/skills/dw-thinking/SKILL.md +70 -70
- package/.claude/skills/dw-upgrade/SKILL.md +72 -72
- package/.dw/config/dw.config.yml +82 -82
- package/.dw/core/PILLARS.md +122 -0
- package/.dw/core/templates/v2/spec.md +68 -0
- package/.dw/core/templates/v2/tracking.md +62 -0
- package/.dw/core/v14-evaluation-protocol.md +118 -0
- package/CLAUDE.md +42 -39
- package/MIGRATION-v1.3.md +201 -0
- package/README.md +43 -6
- package/package.json +86 -84
- package/src/cli.mjs +45 -9
- package/src/commands/dashboard.mjs +116 -0
- package/src/commands/doctor.mjs +165 -149
- package/src/commands/init.mjs +339 -332
- package/src/commands/metrics.mjs +165 -0
- package/src/commands/upgrade.mjs +297 -262
- package/src/lib/active-index.mjs +87 -0
- package/src/lib/copy.mjs +118 -110
- package/src/lib/cut-analysis.mjs +161 -0
- package/src/lib/telemetry.mjs +80 -0
- package/.claude/rules/dw-core.md +0 -100
- package/.claude/rules/dw-skills.md +0 -53
- package/.claude/rules/workflow-rules.md +0 -77
package/src/lib/copy.mjs
CHANGED
|
@@ -1,110 +1,118 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync,
|
|
3
|
-
} from 'node:fs';
|
|
4
|
-
import { join, dirname } from 'node:path';
|
|
5
|
-
|
|
6
|
-
export function ensureDir(dir) {
|
|
7
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function copyFile(src, dst, { dryRun = false } = {}) {
|
|
11
|
-
if (dryRun) return { action: 'copy', src, dst, applied: false };
|
|
12
|
-
ensureDir(dirname(dst));
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync, writeFileSync,
|
|
3
|
+
} from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export function ensureDir(dir) {
|
|
7
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function copyFile(src, dst, { dryRun = false } = {}) {
|
|
11
|
+
if (dryRun) return { action: 'copy', src, dst, applied: false };
|
|
12
|
+
ensureDir(dirname(dst));
|
|
13
|
+
if (src.endsWith('.sh')) {
|
|
14
|
+
// Always write shell scripts with LF endings.
|
|
15
|
+
// Prevents CRLF contamination when git later checks out these files on Windows
|
|
16
|
+
// (core.autocrlf=true overrides .gitattributes in the *user's* repo, which dw-kit cannot control).
|
|
17
|
+
const content = readFileSync(src, 'utf-8').replace(/\r\n/g, '\n');
|
|
18
|
+
writeFileSync(dst, content, 'utf-8');
|
|
19
|
+
} else {
|
|
20
|
+
copyFileSync(src, dst);
|
|
21
|
+
}
|
|
22
|
+
return { action: 'copy', src, dst, applied: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function copyDir(srcDir, dstDir, { dryRun = false, overwrite = false } = {}) {
|
|
26
|
+
const results = [];
|
|
27
|
+
if (!existsSync(srcDir)) return results;
|
|
28
|
+
|
|
29
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const srcPath = join(srcDir, entry.name);
|
|
32
|
+
const dstPath = join(dstDir, entry.name);
|
|
33
|
+
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
results.push(...copyDir(srcPath, dstPath, { dryRun, overwrite }));
|
|
36
|
+
} else if (entry.isFile()) {
|
|
37
|
+
if (!overwrite && existsSync(dstPath)) {
|
|
38
|
+
results.push({ action: 'skip', src: srcPath, dst: dstPath, reason: 'exists' });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
results.push(copyFile(srcPath, dstPath, { dryRun }));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Copy files from srcDir to dstDir, respecting an overrides directory.
|
|
49
|
+
* Files present in overridesDir take precedence over srcDir.
|
|
50
|
+
*/
|
|
51
|
+
export function copyWithOverrides(srcDir, dstDir, overridesDir, { dryRun = false } = {}) {
|
|
52
|
+
const results = [];
|
|
53
|
+
if (!existsSync(srcDir)) return results;
|
|
54
|
+
|
|
55
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const srcPath = join(srcDir, entry.name);
|
|
58
|
+
const dstPath = join(dstDir, entry.name);
|
|
59
|
+
const overridePath = overridesDir ? join(overridesDir, entry.name) : null;
|
|
60
|
+
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
const subOverride = overridePath && existsSync(overridePath) ? overridePath : null;
|
|
63
|
+
results.push(...copyWithOverrides(srcPath, dstPath, subOverride, { dryRun }));
|
|
64
|
+
} else if (entry.isFile()) {
|
|
65
|
+
if (overridePath && existsSync(overridePath)) {
|
|
66
|
+
results.push({
|
|
67
|
+
action: 'override',
|
|
68
|
+
src: overridePath,
|
|
69
|
+
dst: dstPath,
|
|
70
|
+
...(dryRun ? { applied: false } : (() => { ensureDir(dirname(dstPath)); copyFileSync(overridePath, dstPath); return { applied: true }; })()),
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
results.push(copyFile(srcPath, dstPath, { dryRun }));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Compute file differences between two directories.
|
|
82
|
+
* Returns { added, modified, unchanged } arrays of relative paths.
|
|
83
|
+
*/
|
|
84
|
+
export function diffDirs(sourceDir, targetDir) {
|
|
85
|
+
const added = [];
|
|
86
|
+
const modified = [];
|
|
87
|
+
const unchanged = [];
|
|
88
|
+
|
|
89
|
+
if (!existsSync(sourceDir)) return { added, modified, unchanged };
|
|
90
|
+
|
|
91
|
+
function walk(dir, base) {
|
|
92
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const srcPath = join(dir, entry.name);
|
|
95
|
+
const relPath = base ? join(base, entry.name) : entry.name;
|
|
96
|
+
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
walk(srcPath, relPath);
|
|
99
|
+
} else if (entry.isFile()) {
|
|
100
|
+
const targetPath = join(targetDir, relPath);
|
|
101
|
+
if (!existsSync(targetPath)) {
|
|
102
|
+
added.push(relPath);
|
|
103
|
+
} else {
|
|
104
|
+
const srcContent = readFileSync(srcPath);
|
|
105
|
+
const tgtContent = readFileSync(targetPath);
|
|
106
|
+
if (srcContent.equals(tgtContent)) {
|
|
107
|
+
unchanged.push(relPath);
|
|
108
|
+
} else {
|
|
109
|
+
modified.push(relPath);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
walk(sourceDir, '');
|
|
117
|
+
return { added, modified, unchanged };
|
|
118
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Cut Criteria Matrix — implements ADR-0001 v1.4 decision gate.
|
|
2
|
+
// Consumes telemetry events, emits cut candidates with evidence.
|
|
3
|
+
|
|
4
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
// Never-cut: skills that are the workflow's load-bearing verbs.
|
|
7
|
+
export const CRITICAL_SKILLS = new Set([
|
|
8
|
+
'dw:flow', 'dw:task-init', 'dw:commit', 'dw:handoff',
|
|
9
|
+
'dw:execute', 'dw:plan', 'dw:research', 'dw:thinking',
|
|
10
|
+
'dw:review', 'dw:debug', 'dw:decision',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
// Per-project skills — evaluated separately (low cadence is expected).
|
|
14
|
+
export const PER_PROJECT_SKILLS = new Set([
|
|
15
|
+
'dw:onboard', 'dw:retroactive', 'dw:config-init', 'dw:upgrade', 'dw:rollback',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
// Cut thresholds (ADR-0001 Cut Criteria Matrix)
|
|
19
|
+
export const THRESHOLDS = {
|
|
20
|
+
skill: {
|
|
21
|
+
minUsesPerWeekPerDev: 5,
|
|
22
|
+
minDevs: 5,
|
|
23
|
+
minCoverageDays: 21,
|
|
24
|
+
},
|
|
25
|
+
hook: {
|
|
26
|
+
maxAvgLatencyMs: 500,
|
|
27
|
+
maxFiresPerSession: 10,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function coverageDays(events) {
|
|
32
|
+
if (events.length === 0) return 0;
|
|
33
|
+
const first = new Date(events[0].ts).getTime();
|
|
34
|
+
const last = new Date(events[events.length - 1].ts).getTime();
|
|
35
|
+
return Math.max(1, Math.round((last - first) / MS_PER_DAY));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function uniqueSessions(events) {
|
|
39
|
+
return new Set(events.map((e) => e.session).filter(Boolean)).size;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Group events by (event type, name)
|
|
43
|
+
function groupByNameAndType(events) {
|
|
44
|
+
const groups = { skill: {}, hook: {} };
|
|
45
|
+
for (const e of events) {
|
|
46
|
+
if (e.event !== 'skill' && e.event !== 'hook') continue;
|
|
47
|
+
const bucket = groups[e.event];
|
|
48
|
+
if (!bucket[e.name]) bucket[e.name] = [];
|
|
49
|
+
bucket[e.name].push(e);
|
|
50
|
+
}
|
|
51
|
+
return groups;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function skillStats(events, totalSessions, totalDays) {
|
|
55
|
+
const count = events.length;
|
|
56
|
+
const weeks = Math.max(1, totalDays / 7);
|
|
57
|
+
const devs = Math.max(1, totalSessions); // session-hash as dev proxy (undercounts real devs)
|
|
58
|
+
const usesPerWeekPerDev = count / weeks / devs;
|
|
59
|
+
return { count, usesPerWeekPerDev, weeks, devs };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hookStats(events, totalSessions) {
|
|
63
|
+
const count = events.length;
|
|
64
|
+
const sessions = Math.max(1, totalSessions);
|
|
65
|
+
const firesPerSession = count / sessions;
|
|
66
|
+
const latencies = events.map((e) => e.latency_ms).filter((n) => typeof n === 'number');
|
|
67
|
+
const avgLatency =
|
|
68
|
+
latencies.length > 0 ? latencies.reduce((a, b) => a + b, 0) / latencies.length : null;
|
|
69
|
+
return { count, firesPerSession, avgLatency };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function evaluateSkill(name, events, totalSessions, totalDays) {
|
|
73
|
+
const s = skillStats(events, totalSessions, totalDays);
|
|
74
|
+
const reasons = [];
|
|
75
|
+
let critical = false;
|
|
76
|
+
let perProject = false;
|
|
77
|
+
|
|
78
|
+
if (CRITICAL_SKILLS.has(name)) {
|
|
79
|
+
critical = true;
|
|
80
|
+
reasons.push('critical path — protected');
|
|
81
|
+
}
|
|
82
|
+
if (PER_PROJECT_SKILLS.has(name)) {
|
|
83
|
+
perProject = true;
|
|
84
|
+
reasons.push('per-project cadence — evaluated separately');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const usesBelow = s.usesPerWeekPerDev < THRESHOLDS.skill.minUsesPerWeekPerDev;
|
|
88
|
+
const devsOk = totalSessions >= THRESHOLDS.skill.minDevs;
|
|
89
|
+
const coverageOk = totalDays >= THRESHOLDS.skill.minCoverageDays;
|
|
90
|
+
|
|
91
|
+
let qualify = false;
|
|
92
|
+
if (!critical && !perProject) {
|
|
93
|
+
if (usesBelow && devsOk && coverageOk) {
|
|
94
|
+
qualify = true;
|
|
95
|
+
reasons.push(
|
|
96
|
+
`uses/week/dev=${s.usesPerWeekPerDev.toFixed(2)} < ${THRESHOLDS.skill.minUsesPerWeekPerDev}`
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
if (!usesBelow) reasons.push(`uses/week/dev=${s.usesPerWeekPerDev.toFixed(2)} (above threshold)`);
|
|
100
|
+
if (!devsOk) reasons.push(`devs=${totalSessions} < ${THRESHOLDS.skill.minDevs}`);
|
|
101
|
+
if (!coverageOk) reasons.push(`coverage=${totalDays}d < ${THRESHOLDS.skill.minCoverageDays}d`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { name, type: 'skill', qualify, stats: s, reasons, critical, perProject };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function evaluateHook(name, events, totalSessions) {
|
|
109
|
+
const s = hookStats(events, totalSessions);
|
|
110
|
+
const reasons = [];
|
|
111
|
+
let qualify = false;
|
|
112
|
+
|
|
113
|
+
const latencyExceed =
|
|
114
|
+
s.avgLatency !== null && s.avgLatency > THRESHOLDS.hook.maxAvgLatencyMs;
|
|
115
|
+
const firesExceed = s.firesPerSession > THRESHOLDS.hook.maxFiresPerSession;
|
|
116
|
+
|
|
117
|
+
if (latencyExceed) {
|
|
118
|
+
qualify = true;
|
|
119
|
+
reasons.push(`avg_latency=${s.avgLatency.toFixed(0)}ms > ${THRESHOLDS.hook.maxAvgLatencyMs}ms`);
|
|
120
|
+
}
|
|
121
|
+
if (firesExceed) {
|
|
122
|
+
qualify = true;
|
|
123
|
+
reasons.push(
|
|
124
|
+
`fires/session=${s.firesPerSession.toFixed(1)} > ${THRESHOLDS.hook.maxFiresPerSession}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (!latencyExceed && !firesExceed) {
|
|
128
|
+
reasons.push(
|
|
129
|
+
`fires/session=${s.firesPerSession.toFixed(1)}, avg_latency=${s.avgLatency !== null ? s.avgLatency.toFixed(0) + 'ms' : 'n/a'} (within limits)`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { name, type: 'hook', qualify, stats: s, reasons };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function analyze(events) {
|
|
137
|
+
const totalSessions = uniqueSessions(events);
|
|
138
|
+
const totalDays = coverageDays(events);
|
|
139
|
+
const coverageOk = totalDays >= THRESHOLDS.skill.minCoverageDays;
|
|
140
|
+
const devsOk = totalSessions >= THRESHOLDS.skill.minDevs;
|
|
141
|
+
|
|
142
|
+
const groups = groupByNameAndType(events);
|
|
143
|
+
|
|
144
|
+
const skillResults = Object.entries(groups.skill)
|
|
145
|
+
.map(([name, evts]) => evaluateSkill(name, evts, totalSessions, totalDays))
|
|
146
|
+
.sort((a, b) => a.stats.usesPerWeekPerDev - b.stats.usesPerWeekPerDev);
|
|
147
|
+
|
|
148
|
+
const hookResults = Object.entries(groups.hook)
|
|
149
|
+
.map(([name, evts]) => evaluateHook(name, evts, totalSessions))
|
|
150
|
+
.sort((a, b) => b.stats.firesPerSession - a.stats.firesPerSession);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
coverage: { days: totalDays, sessions: totalSessions, coverageOk, devsOk },
|
|
154
|
+
skills: skillResults,
|
|
155
|
+
hooks: hookResults,
|
|
156
|
+
candidates: {
|
|
157
|
+
skills: skillResults.filter((r) => r.qualify),
|
|
158
|
+
hooks: hookResults.filter((r) => r.qualify),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const METRICS_DIR = '.dw/metrics';
|
|
6
|
+
const EVENTS_FILE = 'events.jsonl';
|
|
7
|
+
|
|
8
|
+
function isDisabled() {
|
|
9
|
+
return process.env.DW_NO_TELEMETRY === '1' || process.env.DW_NO_TELEMETRY === 'true';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sessionHash() {
|
|
13
|
+
const pid = process.pid;
|
|
14
|
+
const start = process.env.DW_SESSION_START || Date.now();
|
|
15
|
+
return createHash('sha256').update(`${pid}:${start}`).digest('hex').slice(0, 8);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureMetricsDir(rootDir) {
|
|
19
|
+
const dir = join(rootDir, METRICS_DIR);
|
|
20
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
21
|
+
return join(dir, EVENTS_FILE);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function logEvent(event, rootDir = process.cwd()) {
|
|
25
|
+
if (isDisabled()) return false;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const enriched = {
|
|
29
|
+
ts: new Date().toISOString(),
|
|
30
|
+
session: sessionHash(),
|
|
31
|
+
...event,
|
|
32
|
+
};
|
|
33
|
+
const target = ensureMetricsDir(rootDir);
|
|
34
|
+
appendFileSync(target, JSON.stringify(enriched) + '\n', 'utf8');
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readEvents(rootDir = process.cwd(), { since, filterName, eventType } = {}) {
|
|
42
|
+
const target = join(rootDir, METRICS_DIR, EVENTS_FILE);
|
|
43
|
+
if (!existsSync(target)) return [];
|
|
44
|
+
|
|
45
|
+
const lines = readFileSync(target, 'utf8').split('\n').filter(Boolean);
|
|
46
|
+
const events = [];
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
try {
|
|
49
|
+
const evt = JSON.parse(line);
|
|
50
|
+
if (since && evt.ts < since) continue;
|
|
51
|
+
if (filterName && evt.name !== filterName) continue;
|
|
52
|
+
if (eventType && evt.event !== eventType) continue;
|
|
53
|
+
events.push(evt);
|
|
54
|
+
} catch {
|
|
55
|
+
// skip malformed
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return events;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function summarize(events) {
|
|
62
|
+
const bySkill = {};
|
|
63
|
+
const byHook = {};
|
|
64
|
+
const byTask = {};
|
|
65
|
+
|
|
66
|
+
for (const e of events) {
|
|
67
|
+
if (e.event === 'skill') bySkill[e.name] = (bySkill[e.name] || 0) + 1;
|
|
68
|
+
else if (e.event === 'hook') byHook[e.name] = (byHook[e.name] || 0) + 1;
|
|
69
|
+
else if (e.event === 'task') byTask[e.action || 'unknown'] = (byTask[e.action || 'unknown'] || 0) + 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
totalEvents: events.length,
|
|
74
|
+
bySkill,
|
|
75
|
+
byHook,
|
|
76
|
+
byTask,
|
|
77
|
+
dateRange:
|
|
78
|
+
events.length > 0 ? { from: events[0].ts, to: events[events.length - 1].ts } : null,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/.claude/rules/dw-core.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
<!-- dw-kit | core: 1.2 -->
|
|
2
|
-
# dw Workflow
|
|
3
|
-
|
|
4
|
-
Config: `.dw/config/dw.config.yml`
|
|
5
|
-
Full methodology: `.dw/core/WORKFLOW.md` · `.dw/core/THINKING.md` · `.dw/core/QUALITY.md` · `.dw/core/ROLES.md`
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Override
|
|
10
|
-
|
|
11
|
-
If the prompt contains `--no-dw`: ignore all dw instructions, work as plain Claude Code. Applies only to that request.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Depth Routing
|
|
16
|
-
|
|
17
|
-
Read `workflow.default_depth` from config. Assess per task based on facts (file count, API changes, git blame) — not assumptions:
|
|
18
|
-
|
|
19
|
-
| Scope | Depth | Approach |
|
|
20
|
-
|-------|-------|----------|
|
|
21
|
-
| ≤2 files, hotfix, familiar module | quick | Understand → Execute → Close |
|
|
22
|
-
| 3–5 files, new module | standard | Full 6-phase |
|
|
23
|
-
| 6+ files, API/DB/security changes | thorough | Full + arch-review + test-plan |
|
|
24
|
-
|
|
25
|
-
When unsure → default to `standard`.
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Session Start
|
|
30
|
-
|
|
31
|
-
1. Read `.dw/config/dw.config.yml` — depth, roles, quality commands
|
|
32
|
-
2. Check `.dw/tasks/` for active tasks; if one is in progress, resume from its `[task]-progress.md`
|
|
33
|
-
|
|
34
|
-
> `session-init` hook auto-injects active task context — no manual action needed.
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## Task Docs
|
|
39
|
-
|
|
40
|
-
For tasks spanning 3+ files, research → plan → approve → execute gives better outcomes.
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
.dw/tasks/[task-name]/
|
|
44
|
-
├── [name]-context.md # Research findings
|
|
45
|
-
├── [name]-plan.md # Implementation plan
|
|
46
|
-
└── [name]-progress.md # Progress + handoff notes
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## Commit Format
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
<type>(<scope>): <description ≤72 chars>
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Types: `feat` `fix` `refactor` `test` `docs` `chore` `style` `perf`
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Hooks (v1.2)
|
|
62
|
-
|
|
63
|
-
Auto-run — no user action needed:
|
|
64
|
-
|
|
65
|
-
| Hook | Trigger | Effect |
|
|
66
|
-
|------|---------|--------|
|
|
67
|
-
| `session-init` | Session start | Inject active task context |
|
|
68
|
-
| `scout-block` | Read/Glob | Block node_modules/, dist/, .git/ etc. |
|
|
69
|
-
| `privacy-block` | Read | Block .env*, *.pem, credentials* |
|
|
70
|
-
| `pre-commit-gate` | Bash (git commit) | Quality check + sensitive data scan |
|
|
71
|
-
| `safety-guard` | Bash | Block destructive commands |
|
|
72
|
-
| `post-write` | Write/Edit | Lint reminder |
|
|
73
|
-
| `stop-check` | Stop | Warn on uncommitted changes + in-progress tasks |
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Agent Reports (v1.2)
|
|
78
|
-
|
|
79
|
-
For multi-phase tasks (standard/thorough), create reports for audit trail:
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
.dw/tasks/[task-name]/reports/[YYMMDD-HHMM]-from-[role]-to-[role]-[desc].md
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Status: `DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT`
|
|
86
|
-
Template: `.claude/templates/agent-report.md` · Guide: `.dw/core/AGENTS.md`
|
|
87
|
-
|
|
88
|
-
---
|
|
89
|
-
|
|
90
|
-
## Config Local Override (v1.2)
|
|
91
|
-
|
|
92
|
-
`.dw/config/dw.config.local.yml` (gitignored) for machine-specific settings:
|
|
93
|
-
|
|
94
|
-
```yaml
|
|
95
|
-
claude:
|
|
96
|
-
models:
|
|
97
|
-
plan: "claude-opus-4-6"
|
|
98
|
-
quality:
|
|
99
|
-
test_command: "npm test"
|
|
100
|
-
```
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
<!-- dw-kit | core: 1.2 -->
|
|
2
|
-
# dw Skills
|
|
3
|
-
|
|
4
|
-
Invoke via Claude Code slash commands. Availability governed by config flags and `workflow.default_depth`.
|
|
5
|
-
|
|
6
|
-
## Core Workflow
|
|
7
|
-
|
|
8
|
-
| Skill | Description | Depth |
|
|
9
|
-
|-------|-------------|-------|
|
|
10
|
-
| `/dw-flow [task]` | Full workflow shortcut | all |
|
|
11
|
-
| `/dw-task-init [name]` | Init task docs | all |
|
|
12
|
-
| `/dw-research [name]` | Survey codebase | all |
|
|
13
|
-
| `/dw-plan [name]` | Design plan (waits for approval) | standard+ |
|
|
14
|
-
| `/dw-execute [name]` | Implement per plan (TDD) | all |
|
|
15
|
-
| `/dw-commit [msg]` | Smart commit + quality gates | all |
|
|
16
|
-
| `/dw-handoff` | Session handoff | all |
|
|
17
|
-
|
|
18
|
-
## Dev
|
|
19
|
-
|
|
20
|
-
| Skill | Description |
|
|
21
|
-
|-------|-------------|
|
|
22
|
-
| `/dw-debug [issue]` | Investigate → diagnose → fix |
|
|
23
|
-
| `/dw-review` | Code review with checklist |
|
|
24
|
-
| `/dw-thinking [question]` | Apply thinking framework |
|
|
25
|
-
| `/dw-prompt [desc]` | Build structured prompt |
|
|
26
|
-
| `/dw-docs-update` | Update living docs |
|
|
27
|
-
|
|
28
|
-
## Role-Specific
|
|
29
|
-
|
|
30
|
-
| Skill | Role | Description |
|
|
31
|
-
|-------|------|-------------|
|
|
32
|
-
| `/dw-requirements` | BA | Requirements + user stories |
|
|
33
|
-
| `/dw-test-plan` | QC | Test plan + regression |
|
|
34
|
-
| `/dw-arch-review` | TechLead | Architecture review |
|
|
35
|
-
| `/dw-dashboard` | PM | Metrics report |
|
|
36
|
-
| `/dw-sprint-review` | All | Retrospective |
|
|
37
|
-
| `/dw-estimate [name]` | if enabled | Effort estimation |
|
|
38
|
-
| `/dw-log-work [name]` | if enabled | Log actual effort |
|
|
39
|
-
|
|
40
|
-
## Setup & Maintenance
|
|
41
|
-
|
|
42
|
-
| Skill | Description |
|
|
43
|
-
|-------|-------------|
|
|
44
|
-
| `/dw-onboard` | Onboard dw to existing project (breadth-first scan) |
|
|
45
|
-
| `/dw-retroactive [name]` | Document existing feature (depth-first) |
|
|
46
|
-
| `/dw-config-init` | Initialize new config |
|
|
47
|
-
| `/dw-config-validate` | Validate config file |
|
|
48
|
-
| `/dw-upgrade` | Upgrade toolkit |
|
|
49
|
-
| `/dw-rollback [name]` | Rollback task docs |
|
|
50
|
-
| `/dw-archive [name]` | Archive completed task |
|
|
51
|
-
| `/dw-kit-report [desc]` | Submit feedback/bug to GitHub |
|
|
52
|
-
|
|
53
|
-
> **Maintainer-only** (TechLead, dw-kit repo): `/dw-kit-evolve [issue#]` · `/dw-kit-audit [days]`
|