dw-kit 1.2.1 → 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.
Files changed (58) hide show
  1. package/.claude/hooks/post-write.sh +64 -58
  2. package/.claude/hooks/pre-commit-gate.sh +96 -90
  3. package/.claude/hooks/privacy-block.sh +99 -94
  4. package/.claude/hooks/progress-ping.sh +53 -47
  5. package/.claude/hooks/safety-guard.sh +60 -54
  6. package/.claude/hooks/scout-block.sh +88 -82
  7. package/.claude/hooks/session-init.sh +6 -0
  8. package/.claude/hooks/stop-check.sh +88 -36
  9. package/.claude/hooks/telemetry-log.sh +34 -0
  10. package/.claude/rules/dw.md +136 -0
  11. package/.claude/settings.json +20 -1
  12. package/.claude/skills/dw-arch-review/SKILL.md +119 -119
  13. package/.claude/skills/dw-archive/SKILL.md +81 -81
  14. package/.claude/skills/dw-commit/SKILL.md +81 -81
  15. package/.claude/skills/dw-config-init/SKILL.md +91 -91
  16. package/.claude/skills/dw-config-validate/SKILL.md +75 -75
  17. package/.claude/skills/dw-dashboard/SKILL.md +209 -209
  18. package/.claude/skills/dw-debug/SKILL.md +97 -97
  19. package/.claude/skills/dw-decision/SKILL.md +116 -0
  20. package/.claude/skills/dw-docs-update/SKILL.md +125 -125
  21. package/.claude/skills/dw-estimate/SKILL.md +90 -90
  22. package/.claude/skills/dw-execute/SKILL.md +98 -98
  23. package/.claude/skills/dw-flow/SKILL.md +274 -274
  24. package/.claude/skills/dw-handoff/SKILL.md +81 -81
  25. package/.claude/skills/dw-kit-report/SKILL.md +152 -152
  26. package/.claude/skills/dw-log-work/SKILL.md +69 -69
  27. package/.claude/skills/dw-onboard/SKILL.md +201 -201
  28. package/.claude/skills/dw-plan/SKILL.md +125 -125
  29. package/.claude/skills/dw-prompt/SKILL.md +62 -62
  30. package/.claude/skills/dw-requirements/SKILL.md +98 -98
  31. package/.claude/skills/dw-research/SKILL.md +114 -114
  32. package/.claude/skills/dw-retroactive/SKILL.md +311 -311
  33. package/.claude/skills/dw-review/SKILL.md +66 -66
  34. package/.claude/skills/dw-rollback/SKILL.md +90 -90
  35. package/.claude/skills/dw-sprint-review/SKILL.md +99 -99
  36. package/.claude/skills/dw-task-init/SKILL.md +59 -59
  37. package/.claude/skills/dw-test-plan/SKILL.md +113 -113
  38. package/.claude/skills/dw-thinking/SKILL.md +70 -70
  39. package/.claude/skills/dw-upgrade/SKILL.md +72 -72
  40. package/.dw/core/PILLARS.md +122 -0
  41. package/.dw/core/templates/v2/spec.md +68 -0
  42. package/.dw/core/templates/v2/tracking.md +62 -0
  43. package/.dw/core/v14-evaluation-protocol.md +118 -0
  44. package/CLAUDE.md +42 -39
  45. package/MIGRATION-v1.3.md +201 -0
  46. package/README.md +35 -6
  47. package/package.json +4 -2
  48. package/src/cli.mjs +29 -1
  49. package/src/commands/dashboard.mjs +116 -0
  50. package/src/commands/doctor.mjs +165 -149
  51. package/src/commands/init.mjs +339 -332
  52. package/src/commands/metrics.mjs +165 -0
  53. package/src/lib/active-index.mjs +87 -0
  54. package/src/lib/cut-analysis.mjs +161 -0
  55. package/src/lib/telemetry.mjs +80 -0
  56. package/.claude/rules/dw-core.md +0 -100
  57. package/.claude/rules/dw-skills.md +0 -53
  58. package/.claude/rules/workflow-rules.md +0 -77
@@ -0,0 +1,165 @@
1
+ import { readEvents, summarize } from '../lib/telemetry.mjs';
2
+ import { analyze, THRESHOLDS } from '../lib/cut-analysis.mjs';
3
+ import { banner, log, info, warn, ok, err } from '../lib/ui.mjs';
4
+ import chalk from 'chalk';
5
+
6
+ export async function metricsCommand(opts) {
7
+ const sub = opts.sub || 'show';
8
+
9
+ if (sub === 'show') {
10
+ return showMetrics(opts);
11
+ }
12
+ if (sub === 'cut-analysis') {
13
+ return cutAnalysisReport(opts);
14
+ }
15
+ if (sub === 'clear') {
16
+ warn('Use `rm .dw/metrics/events.jsonl` to clear manually. Telemetry is append-only.');
17
+ return;
18
+ }
19
+ warn(`Unknown subcommand: ${sub}. Available: show | cut-analysis | clear`);
20
+ }
21
+
22
+ function showMetrics(opts) {
23
+ banner('dw-kit Telemetry — Local Events');
24
+
25
+ if (process.env.DW_NO_TELEMETRY === '1' || process.env.DW_NO_TELEMETRY === 'true') {
26
+ warn('Telemetry is disabled (DW_NO_TELEMETRY=1).');
27
+ log('No events are being collected.');
28
+ return;
29
+ }
30
+
31
+ const events = readEvents(process.cwd(), {
32
+ since: opts.since,
33
+ filterName: opts.skill,
34
+ });
35
+
36
+ if (events.length === 0) {
37
+ info('No telemetry events recorded yet.');
38
+ log('Events will be logged as you use dw-kit skills and hooks.');
39
+ log('Storage: .dw/metrics/events.jsonl (local, append-only, zero network)');
40
+ return;
41
+ }
42
+
43
+ const s = summarize(events);
44
+ log('');
45
+ log(chalk.bold(`Total events: ${s.totalEvents}`));
46
+ if (s.dateRange) {
47
+ log(`Range: ${s.dateRange.from.slice(0, 10)} → ${s.dateRange.to.slice(0, 10)}`);
48
+ }
49
+ log('');
50
+
51
+ if (Object.keys(s.bySkill).length > 0) {
52
+ log(chalk.bold('Skills invoked:'));
53
+ const skillsSorted = Object.entries(s.bySkill).sort((a, b) => b[1] - a[1]);
54
+ for (const [name, count] of skillsSorted) {
55
+ log(` ${count.toString().padStart(4)}× /${name}`);
56
+ }
57
+ log('');
58
+ }
59
+
60
+ if (Object.keys(s.byHook).length > 0) {
61
+ log(chalk.bold('Hooks fired:'));
62
+ const hooksSorted = Object.entries(s.byHook).sort((a, b) => b[1] - a[1]);
63
+ for (const [name, count] of hooksSorted) {
64
+ log(` ${count.toString().padStart(4)}× ${name}`);
65
+ }
66
+ log('');
67
+ }
68
+
69
+ if (Object.keys(s.byTask).length > 0) {
70
+ log(chalk.bold('Task actions:'));
71
+ for (const [action, count] of Object.entries(s.byTask)) {
72
+ log(` ${count.toString().padStart(4)}× ${action}`);
73
+ }
74
+ log('');
75
+ }
76
+
77
+ info('Privacy: all data is local. Set DW_NO_TELEMETRY=1 to disable.');
78
+ }
79
+
80
+ function cutAnalysisReport(opts) {
81
+ banner('dw-kit Cut-Analysis — ADR-0001 Cut Criteria Matrix');
82
+
83
+ if (process.env.DW_NO_TELEMETRY === '1' || process.env.DW_NO_TELEMETRY === 'true') {
84
+ warn('Telemetry disabled — no data to analyze.');
85
+ return;
86
+ }
87
+
88
+ const events = readEvents(process.cwd(), { since: opts.since });
89
+ if (events.length === 0) {
90
+ info('No telemetry events recorded — nothing to analyze.');
91
+ return;
92
+ }
93
+
94
+ const result = analyze(events);
95
+ const { coverage, skills, hooks, candidates } = result;
96
+
97
+ log('');
98
+ log(chalk.bold('Coverage'));
99
+ const covMsg = ` days=${coverage.days} unique_sessions=${coverage.sessions}`;
100
+ log(covMsg);
101
+ if (!coverage.coverageOk) {
102
+ warn(`coverage_days=${coverage.days} < ${THRESHOLDS.skill.minCoverageDays} — skill cuts NOT recommended yet`);
103
+ } else {
104
+ ok(`coverage_days ≥ ${THRESHOLDS.skill.minCoverageDays} — eligible for skill evaluation`);
105
+ }
106
+ if (!coverage.devsOk) {
107
+ warn(`devs=${coverage.sessions} < ${THRESHOLDS.skill.minDevs} — skill cuts NOT recommended yet (session-hash is a proxy; may undercount)`);
108
+ }
109
+
110
+ log('');
111
+ log(chalk.bold('Skills (sorted by uses/week/dev, ascending)'));
112
+ if (skills.length === 0) {
113
+ log(' (no skill events)');
114
+ } else {
115
+ for (const r of skills) {
116
+ const rate = r.stats.usesPerWeekPerDev.toFixed(2);
117
+ const tag = r.qualify
118
+ ? chalk.red('CUT CANDIDATE')
119
+ : r.critical
120
+ ? chalk.cyan('protected')
121
+ : r.perProject
122
+ ? chalk.dim('per-project')
123
+ : chalk.green('keep');
124
+ log(` ${tag.padEnd(24)} /${r.name.padEnd(24)} uses/wk/dev=${rate} total=${r.stats.count}`);
125
+ for (const reason of r.reasons) log(chalk.dim(` └─ ${reason}`));
126
+ }
127
+ }
128
+
129
+ log('');
130
+ log(chalk.bold('Hooks (sorted by fires/session, descending)'));
131
+ if (hooks.length === 0) {
132
+ log(' (no hook events)');
133
+ } else {
134
+ for (const r of hooks) {
135
+ const fires = r.stats.firesPerSession.toFixed(1);
136
+ const lat = r.stats.avgLatency !== null ? `${r.stats.avgLatency.toFixed(0)}ms` : 'n/a';
137
+ const tag = r.qualify ? chalk.red('CUT CANDIDATE') : chalk.green('keep');
138
+ log(` ${tag.padEnd(24)} ${r.name.padEnd(24)} fires/session=${fires} avg_latency=${lat}`);
139
+ for (const reason of r.reasons) log(chalk.dim(` └─ ${reason}`));
140
+ }
141
+ }
142
+
143
+ log('');
144
+ log(chalk.bold('Summary'));
145
+ const skillCount = candidates.skills.length;
146
+ const hookCount = candidates.hooks.length;
147
+ if (skillCount === 0 && hookCount === 0) {
148
+ ok('No cut candidates — keep current surface.');
149
+ } else {
150
+ log(` Skills flagged: ${skillCount}`);
151
+ log(` Hooks flagged: ${hookCount}`);
152
+ log('');
153
+ info('Next steps:');
154
+ log(' 1. Run team survey for qualitative check on flagged items (avoid false-positives).');
155
+ log(' 2. For each confirmed cut → write ADR (`/dw:decision "Remove <name>"`).');
156
+ log(' 3. Remove from .claude/hooks/ or .claude/skills/ + update .claude/settings.json.');
157
+ log(' 4. Document in MIGRATION-v1.4.md with rollback instructions.');
158
+ }
159
+ log('');
160
+ info('Thresholds (from ADR-0001 Cut Criteria Matrix):');
161
+ log(` Skill: uses/wk/dev < ${THRESHOLDS.skill.minUsesPerWeekPerDev} AND devs ≥ ${THRESHOLDS.skill.minDevs} AND coverage ≥ ${THRESHOLDS.skill.minCoverageDays}d`);
162
+ log(` Hook: avg_latency > ${THRESHOLDS.hook.maxAvgLatencyMs}ms OR fires/session > ${THRESHOLDS.hook.maxFiresPerSession}`);
163
+ log('');
164
+ info('Caveat: "devs" proxied by unique session hashes — undercounts real headcount.');
165
+ }
@@ -0,0 +1,87 @@
1
+ import { readdirSync, readFileSync, existsSync, writeFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const TASKS_DIR = '.dw/tasks';
5
+ const ACTIVE_FILE = join(TASKS_DIR, 'ACTIVE.md');
6
+ const EXCLUDE = new Set(['archive', 'ACTIVE.md']);
7
+
8
+ function parseFrontmatter(content) {
9
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
10
+ if (!match) return {};
11
+ const lines = match[1].split('\n');
12
+ const fm = {};
13
+ for (const line of lines) {
14
+ const kv = line.match(/^(\w+):\s*(.+)$/);
15
+ if (kv) fm[kv[1]] = kv[2].trim();
16
+ }
17
+ return fm;
18
+ }
19
+
20
+ function readTaskStatus(taskDir) {
21
+ const fullPath = join(TASKS_DIR, taskDir);
22
+ if (!statSync(fullPath).isDirectory()) return null;
23
+
24
+ const trackingV2 = join(fullPath, 'tracking.md');
25
+ if (existsSync(trackingV2)) {
26
+ const fm = parseFrontmatter(readFileSync(trackingV2, 'utf8'));
27
+ return {
28
+ name: taskDir,
29
+ status: fm.status || 'unknown',
30
+ lastUpdated: fm.last_updated || fm.started || '—',
31
+ blockers: fm.blockers || 'none',
32
+ format: 'v2',
33
+ };
34
+ }
35
+
36
+ const progressV1 = join(fullPath, `${taskDir}-progress.md`);
37
+ if (existsSync(progressV1)) {
38
+ const content = readFileSync(progressV1, 'utf8');
39
+ const statusMatch = content.match(/Trạng thái:\s*([^\n]+)/);
40
+ return {
41
+ name: taskDir,
42
+ status: statusMatch ? statusMatch[1].trim() : 'unknown',
43
+ lastUpdated: '—',
44
+ blockers: 'none',
45
+ format: 'v1',
46
+ };
47
+ }
48
+
49
+ return { name: taskDir, status: 'no-tracking', lastUpdated: '—', blockers: '—', format: 'unknown' };
50
+ }
51
+
52
+ export function generateActiveIndex(rootDir = process.cwd()) {
53
+ const tasksPath = join(rootDir, TASKS_DIR);
54
+ if (!existsSync(tasksPath)) return '';
55
+
56
+ const entries = readdirSync(tasksPath).filter((e) => !EXCLUDE.has(e));
57
+ const tasks = entries.map(readTaskStatus).filter(Boolean);
58
+
59
+ const today = new Date().toISOString().slice(0, 10);
60
+ const lines = [
61
+ '# ACTIVE Tasks',
62
+ '',
63
+ `Auto-generated ${today}. Run \`dw active\` to refresh.`,
64
+ '',
65
+ 'Format: `{task-name} · {status} · {last-updated} · {blockers}`',
66
+ '',
67
+ '## Current',
68
+ '',
69
+ ...tasks.map(
70
+ (t) => `- \`${t.name}\` · ${t.status} · ${t.lastUpdated} · ${t.blockers}`
71
+ ),
72
+ '',
73
+ '## Archive',
74
+ '',
75
+ `Completed tasks: \`.dw/tasks/archive/\``,
76
+ '',
77
+ ];
78
+
79
+ return lines.join('\n');
80
+ }
81
+
82
+ export function writeActiveIndex(rootDir = process.cwd()) {
83
+ const content = generateActiveIndex(rootDir);
84
+ const target = join(rootDir, ACTIVE_FILE);
85
+ writeFileSync(target, content, 'utf8');
86
+ return target;
87
+ }
@@ -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
+ }
@@ -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]`