dw-kit 1.2.1 → 1.3.4
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 +6 -0
- package/.claude/hooks/stop-check.sh +88 -36
- package/.claude/hooks/telemetry-log.sh +34 -0
- package/.claude/rules/dw.md +138 -0
- package/.claude/settings.json +28 -1
- 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 +121 -98
- package/.claude/skills/dw-flow/SKILL.md +274 -274
- package/.claude/skills/dw-handoff/SKILL.md +92 -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 +222 -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 +128 -114
- package/.claude/skills/dw-retroactive/SKILL.md +195 -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 +71 -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/core/PILLARS.md +122 -0
- package/.dw/core/ROLES.md +257 -257
- 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 +202 -0
- package/README.md +35 -6
- package/package.json +4 -2
- package/src/cli.mjs +29 -1
- 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 +185 -0
- package/src/lib/active-index.mjs +87 -0
- package/src/lib/cut-analysis.mjs +240 -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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { readEvents, summarize } from '../lib/telemetry.mjs';
|
|
2
|
+
import { analyze, analyzeTaskDocs, THRESHOLDS, TASK_DOC_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
|
+
|
|
166
|
+
// Task doc health — invalidation trigger for 3→2 file consolidation (ADR-0001)
|
|
167
|
+
const td = analyzeTaskDocs(process.cwd());
|
|
168
|
+
if (td && td.totalTasks > 0) {
|
|
169
|
+
log('');
|
|
170
|
+
log(chalk.bold('Task Doc Health (ADR-0001 invalidation signal for 3→2 consolidation)'));
|
|
171
|
+
log(` Tasks total: ${td.totalTasks} (v2=${td.v2Count}, v1=${td.v1Count})`);
|
|
172
|
+
log(` tracking.md lines: avg=${td.avgTrackingLines} max=${td.maxTrackingLines}`);
|
|
173
|
+
log(` Tasks with ≥3 md files: ${td.extraFilesCount} (${td.extraFilesPct}%)`);
|
|
174
|
+
if (td.triggers.length === 0) {
|
|
175
|
+
ok('No task-doc invalidation triggers fired — 3→2 consolidation holding.');
|
|
176
|
+
} else {
|
|
177
|
+
for (const t of td.triggers) {
|
|
178
|
+
err(t);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
log('');
|
|
182
|
+
info('Task doc thresholds:');
|
|
183
|
+
log(` avg_tracking_lines > ${TASK_DOC_THRESHOLDS.trackingLinesWarn} OR pct_tasks_with_3plus_files > ${TASK_DOC_THRESHOLDS.extraFilesPctWarn}%`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -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,240 @@
|
|
|
1
|
+
// Cut Criteria Matrix — implements ADR-0001 v1.4 decision gate.
|
|
2
|
+
// Consumes telemetry events, emits cut candidates with evidence.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
// Task doc thresholds — invalidation triggers for 3→2 file consolidation (ADR-0001)
|
|
10
|
+
export const TASK_DOC_THRESHOLDS = {
|
|
11
|
+
trackingLinesWarn: 400, // avg tracking.md lines → reopen 3→2 decision
|
|
12
|
+
extraFilesPctWarn: 30, // % tasks with ≥3 md files → tasks re-growing structure
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Never-cut: skills that are the workflow's load-bearing verbs.
|
|
16
|
+
export const CRITICAL_SKILLS = new Set([
|
|
17
|
+
'dw:flow', 'dw:task-init', 'dw:commit', 'dw:handoff',
|
|
18
|
+
'dw:execute', 'dw:plan', 'dw:research', 'dw:thinking',
|
|
19
|
+
'dw:review', 'dw:debug', 'dw:decision',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// Per-project skills — evaluated separately (low cadence is expected).
|
|
23
|
+
export const PER_PROJECT_SKILLS = new Set([
|
|
24
|
+
'dw:onboard', 'dw:retroactive', 'dw:config-init', 'dw:upgrade', 'dw:rollback',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// Cut thresholds (ADR-0001 Cut Criteria Matrix)
|
|
28
|
+
export const THRESHOLDS = {
|
|
29
|
+
skill: {
|
|
30
|
+
minUsesPerWeekPerDev: 5,
|
|
31
|
+
minDevs: 5,
|
|
32
|
+
minCoverageDays: 21,
|
|
33
|
+
},
|
|
34
|
+
hook: {
|
|
35
|
+
maxAvgLatencyMs: 500,
|
|
36
|
+
maxFiresPerSession: 10,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function coverageDays(events) {
|
|
41
|
+
if (events.length === 0) return 0;
|
|
42
|
+
const first = new Date(events[0].ts).getTime();
|
|
43
|
+
const last = new Date(events[events.length - 1].ts).getTime();
|
|
44
|
+
return Math.max(1, Math.round((last - first) / MS_PER_DAY));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function uniqueSessions(events) {
|
|
48
|
+
return new Set(events.map((e) => e.session).filter(Boolean)).size;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Group events by (event type, name)
|
|
52
|
+
function groupByNameAndType(events) {
|
|
53
|
+
const groups = { skill: {}, hook: {} };
|
|
54
|
+
for (const e of events) {
|
|
55
|
+
if (e.event !== 'skill' && e.event !== 'hook') continue;
|
|
56
|
+
const bucket = groups[e.event];
|
|
57
|
+
if (!bucket[e.name]) bucket[e.name] = [];
|
|
58
|
+
bucket[e.name].push(e);
|
|
59
|
+
}
|
|
60
|
+
return groups;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function skillStats(events, totalSessions, totalDays) {
|
|
64
|
+
const count = events.length;
|
|
65
|
+
const weeks = Math.max(1, totalDays / 7);
|
|
66
|
+
const devs = Math.max(1, totalSessions); // session-hash as dev proxy (undercounts real devs)
|
|
67
|
+
const usesPerWeekPerDev = count / weeks / devs;
|
|
68
|
+
return { count, usesPerWeekPerDev, weeks, devs };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hookStats(events, totalSessions) {
|
|
72
|
+
const count = events.length;
|
|
73
|
+
const sessions = Math.max(1, totalSessions);
|
|
74
|
+
const firesPerSession = count / sessions;
|
|
75
|
+
const latencies = events.map((e) => e.latency_ms).filter((n) => typeof n === 'number');
|
|
76
|
+
const avgLatency =
|
|
77
|
+
latencies.length > 0 ? latencies.reduce((a, b) => a + b, 0) / latencies.length : null;
|
|
78
|
+
return { count, firesPerSession, avgLatency };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function evaluateSkill(name, events, totalSessions, totalDays) {
|
|
82
|
+
const s = skillStats(events, totalSessions, totalDays);
|
|
83
|
+
const reasons = [];
|
|
84
|
+
let critical = false;
|
|
85
|
+
let perProject = false;
|
|
86
|
+
|
|
87
|
+
if (CRITICAL_SKILLS.has(name)) {
|
|
88
|
+
critical = true;
|
|
89
|
+
reasons.push('critical path — protected');
|
|
90
|
+
}
|
|
91
|
+
if (PER_PROJECT_SKILLS.has(name)) {
|
|
92
|
+
perProject = true;
|
|
93
|
+
reasons.push('per-project cadence — evaluated separately');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const usesBelow = s.usesPerWeekPerDev < THRESHOLDS.skill.minUsesPerWeekPerDev;
|
|
97
|
+
const devsOk = totalSessions >= THRESHOLDS.skill.minDevs;
|
|
98
|
+
const coverageOk = totalDays >= THRESHOLDS.skill.minCoverageDays;
|
|
99
|
+
|
|
100
|
+
let qualify = false;
|
|
101
|
+
if (!critical && !perProject) {
|
|
102
|
+
if (usesBelow && devsOk && coverageOk) {
|
|
103
|
+
qualify = true;
|
|
104
|
+
reasons.push(
|
|
105
|
+
`uses/week/dev=${s.usesPerWeekPerDev.toFixed(2)} < ${THRESHOLDS.skill.minUsesPerWeekPerDev}`
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
if (!usesBelow) reasons.push(`uses/week/dev=${s.usesPerWeekPerDev.toFixed(2)} (above threshold)`);
|
|
109
|
+
if (!devsOk) reasons.push(`devs=${totalSessions} < ${THRESHOLDS.skill.minDevs}`);
|
|
110
|
+
if (!coverageOk) reasons.push(`coverage=${totalDays}d < ${THRESHOLDS.skill.minCoverageDays}d`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { name, type: 'skill', qualify, stats: s, reasons, critical, perProject };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function evaluateHook(name, events, totalSessions) {
|
|
118
|
+
const s = hookStats(events, totalSessions);
|
|
119
|
+
const reasons = [];
|
|
120
|
+
let qualify = false;
|
|
121
|
+
|
|
122
|
+
const latencyExceed =
|
|
123
|
+
s.avgLatency !== null && s.avgLatency > THRESHOLDS.hook.maxAvgLatencyMs;
|
|
124
|
+
const firesExceed = s.firesPerSession > THRESHOLDS.hook.maxFiresPerSession;
|
|
125
|
+
|
|
126
|
+
if (latencyExceed) {
|
|
127
|
+
qualify = true;
|
|
128
|
+
reasons.push(`avg_latency=${s.avgLatency.toFixed(0)}ms > ${THRESHOLDS.hook.maxAvgLatencyMs}ms`);
|
|
129
|
+
}
|
|
130
|
+
if (firesExceed) {
|
|
131
|
+
qualify = true;
|
|
132
|
+
reasons.push(
|
|
133
|
+
`fires/session=${s.firesPerSession.toFixed(1)} > ${THRESHOLDS.hook.maxFiresPerSession}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (!latencyExceed && !firesExceed) {
|
|
137
|
+
reasons.push(
|
|
138
|
+
`fires/session=${s.firesPerSession.toFixed(1)}, avg_latency=${s.avgLatency !== null ? s.avgLatency.toFixed(0) + 'ms' : 'n/a'} (within limits)`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { name, type: 'hook', qualify, stats: s, reasons };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function analyzeTaskDocs(rootDir = process.cwd()) {
|
|
146
|
+
const tasksDir = join(rootDir, '.dw', 'tasks');
|
|
147
|
+
if (!existsSync(tasksDir)) return null;
|
|
148
|
+
|
|
149
|
+
const entries = readdirSync(tasksDir).filter((name) => {
|
|
150
|
+
if (name === 'archive' || name === 'ACTIVE.md') return false;
|
|
151
|
+
try {
|
|
152
|
+
return statSync(join(tasksDir, name)).isDirectory();
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const tasks = [];
|
|
159
|
+
for (const name of entries) {
|
|
160
|
+
const dir = join(tasksDir, name);
|
|
161
|
+
let mdFiles;
|
|
162
|
+
try {
|
|
163
|
+
mdFiles = readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
164
|
+
} catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const hasSpec = mdFiles.includes('spec.md');
|
|
168
|
+
const hasTracking = mdFiles.includes('tracking.md');
|
|
169
|
+
const format = hasSpec && hasTracking ? 'v2' : mdFiles.some((f) => f.endsWith('-progress.md')) ? 'v1' : 'unknown';
|
|
170
|
+
|
|
171
|
+
let trackingLines = 0;
|
|
172
|
+
if (hasTracking) {
|
|
173
|
+
try {
|
|
174
|
+
trackingLines = readFileSync(join(dir, 'tracking.md'), 'utf8').split('\n').length;
|
|
175
|
+
} catch {
|
|
176
|
+
/* skip */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
tasks.push({ name, format, mdFileCount: mdFiles.length, trackingLines });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const v2 = tasks.filter((t) => t.format === 'v2');
|
|
184
|
+
const avgTrackingLines =
|
|
185
|
+
v2.length > 0 ? Math.round(v2.reduce((s, t) => s + t.trackingLines, 0) / v2.length) : 0;
|
|
186
|
+
const maxTrackingLines = v2.length > 0 ? Math.max(...v2.map((t) => t.trackingLines)) : 0;
|
|
187
|
+
const extraFiles = tasks.filter((t) => t.mdFileCount >= 3);
|
|
188
|
+
const extraFilesPct = tasks.length > 0 ? Math.round((extraFiles.length / tasks.length) * 100) : 0;
|
|
189
|
+
|
|
190
|
+
const triggers = [];
|
|
191
|
+
if (avgTrackingLines > TASK_DOC_THRESHOLDS.trackingLinesWarn) {
|
|
192
|
+
triggers.push(
|
|
193
|
+
`avg_tracking_lines=${avgTrackingLines} > ${TASK_DOC_THRESHOLDS.trackingLinesWarn} — tracking.md phình → reopen 3→2 decision (ADR-0001)`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (extraFilesPct > TASK_DOC_THRESHOLDS.extraFilesPctWarn) {
|
|
197
|
+
triggers.push(
|
|
198
|
+
`${extraFilesPct}% tasks have ≥3 md files > ${TASK_DOC_THRESHOLDS.extraFilesPctWarn}% — structure re-growing → reopen 3→2 decision (ADR-0001)`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
totalTasks: tasks.length,
|
|
204
|
+
v2Count: v2.length,
|
|
205
|
+
v1Count: tasks.filter((t) => t.format === 'v1').length,
|
|
206
|
+
avgTrackingLines,
|
|
207
|
+
maxTrackingLines,
|
|
208
|
+
extraFilesCount: extraFiles.length,
|
|
209
|
+
extraFilesPct,
|
|
210
|
+
triggers,
|
|
211
|
+
tasks,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function analyze(events) {
|
|
216
|
+
const totalSessions = uniqueSessions(events);
|
|
217
|
+
const totalDays = coverageDays(events);
|
|
218
|
+
const coverageOk = totalDays >= THRESHOLDS.skill.minCoverageDays;
|
|
219
|
+
const devsOk = totalSessions >= THRESHOLDS.skill.minDevs;
|
|
220
|
+
|
|
221
|
+
const groups = groupByNameAndType(events);
|
|
222
|
+
|
|
223
|
+
const skillResults = Object.entries(groups.skill)
|
|
224
|
+
.map(([name, evts]) => evaluateSkill(name, evts, totalSessions, totalDays))
|
|
225
|
+
.sort((a, b) => a.stats.usesPerWeekPerDev - b.stats.usesPerWeekPerDev);
|
|
226
|
+
|
|
227
|
+
const hookResults = Object.entries(groups.hook)
|
|
228
|
+
.map(([name, evts]) => evaluateHook(name, evts, totalSessions))
|
|
229
|
+
.sort((a, b) => b.stats.firesPerSession - a.stats.firesPerSession);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
coverage: { days: totalDays, sessions: totalSessions, coverageOk, devsOk },
|
|
233
|
+
skills: skillResults,
|
|
234
|
+
hooks: hookResults,
|
|
235
|
+
candidates: {
|
|
236
|
+
skills: skillResults.filter((r) => r.qualify),
|
|
237
|
+
hooks: hookResults.filter((r) => r.qualify),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -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
|
-
```
|