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.
Files changed (63) 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 +91 -74
  8. package/.claude/hooks/stop-check.sh +88 -36
  9. package/.claude/hooks/telemetry-log.sh +34 -0
  10. package/.claude/rules/code-style.md +37 -37
  11. package/.claude/rules/commit-standards.md +37 -37
  12. package/.claude/rules/dw.md +136 -0
  13. package/.claude/settings.json +120 -99
  14. package/.claude/skills/dw-arch-review/SKILL.md +119 -119
  15. package/.claude/skills/dw-archive/SKILL.md +81 -81
  16. package/.claude/skills/dw-commit/SKILL.md +81 -81
  17. package/.claude/skills/dw-config-init/SKILL.md +91 -91
  18. package/.claude/skills/dw-config-validate/SKILL.md +75 -75
  19. package/.claude/skills/dw-dashboard/SKILL.md +209 -209
  20. package/.claude/skills/dw-debug/SKILL.md +97 -97
  21. package/.claude/skills/dw-decision/SKILL.md +116 -0
  22. package/.claude/skills/dw-docs-update/SKILL.md +125 -125
  23. package/.claude/skills/dw-estimate/SKILL.md +90 -90
  24. package/.claude/skills/dw-execute/SKILL.md +98 -98
  25. package/.claude/skills/dw-flow/SKILL.md +274 -274
  26. package/.claude/skills/dw-handoff/SKILL.md +81 -81
  27. package/.claude/skills/dw-kit-report/SKILL.md +152 -152
  28. package/.claude/skills/dw-log-work/SKILL.md +69 -69
  29. package/.claude/skills/dw-onboard/SKILL.md +201 -201
  30. package/.claude/skills/dw-plan/SKILL.md +125 -125
  31. package/.claude/skills/dw-prompt/SKILL.md +62 -62
  32. package/.claude/skills/dw-requirements/SKILL.md +98 -98
  33. package/.claude/skills/dw-research/SKILL.md +114 -114
  34. package/.claude/skills/dw-retroactive/SKILL.md +311 -311
  35. package/.claude/skills/dw-review/SKILL.md +66 -66
  36. package/.claude/skills/dw-rollback/SKILL.md +90 -90
  37. package/.claude/skills/dw-sprint-review/SKILL.md +99 -99
  38. package/.claude/skills/dw-task-init/SKILL.md +59 -59
  39. package/.claude/skills/dw-test-plan/SKILL.md +113 -113
  40. package/.claude/skills/dw-thinking/SKILL.md +70 -70
  41. package/.claude/skills/dw-upgrade/SKILL.md +72 -72
  42. package/.dw/config/dw.config.yml +82 -82
  43. package/.dw/core/PILLARS.md +122 -0
  44. package/.dw/core/templates/v2/spec.md +68 -0
  45. package/.dw/core/templates/v2/tracking.md +62 -0
  46. package/.dw/core/v14-evaluation-protocol.md +118 -0
  47. package/CLAUDE.md +42 -39
  48. package/MIGRATION-v1.3.md +201 -0
  49. package/README.md +43 -6
  50. package/package.json +86 -84
  51. package/src/cli.mjs +45 -9
  52. package/src/commands/dashboard.mjs +116 -0
  53. package/src/commands/doctor.mjs +165 -149
  54. package/src/commands/init.mjs +339 -332
  55. package/src/commands/metrics.mjs +165 -0
  56. package/src/commands/upgrade.mjs +297 -262
  57. package/src/lib/active-index.mjs +87 -0
  58. package/src/lib/copy.mjs +118 -110
  59. package/src/lib/cut-analysis.mjs +161 -0
  60. package/src/lib/telemetry.mjs +80 -0
  61. package/.claude/rules/dw-core.md +0 -100
  62. package/.claude/rules/dw-skills.md +0 -53
  63. package/.claude/rules/workflow-rules.md +0 -77
@@ -1,262 +1,297 @@
1
- import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { header, ok, warn, err, info, log, dry } from '../lib/ui.mjs';
5
- import { loadConfig, writeConfig, getToolkitVersions } from '../lib/config.mjs';
6
- import { diffDirs, copyDir, copyFile } from '../lib/copy.mjs';
7
-
8
- const TOOLKIT_ROOT = resolve(fileURLToPath(import.meta.url), '..', '..', '..');
9
-
10
- export async function upgradeCommand(opts) {
11
- const projectDir = process.cwd();
12
- const configPath = join(projectDir, '.dw', 'config', 'dw.config.yml');
13
-
14
- if (!existsSync(configPath)) {
15
- err('No .dw/config/dw.config.yml found. Run `dw init` first.');
16
- process.exit(1);
17
- }
18
-
19
- const projectConfig = loadConfig(configPath);
20
- const projectVersions = getToolkitVersions(projectConfig);
21
-
22
- const toolkitPkg = JSON.parse(readFileSync(join(TOOLKIT_ROOT, 'package.json'), 'utf-8'));
23
- const toolkitConfig = loadConfig(join(TOOLKIT_ROOT, '.dw', 'config', 'dw.config.yml'));
24
- const toolkitVersions = getToolkitVersions(toolkitConfig);
25
-
26
- header('dw-kit Upgrade');
27
- log(`Installed (package) : v${toolkitPkg.version}`);
28
- log(`Project core : ${projectVersions.core}`);
29
- log(`Toolkit core : ${toolkitVersions.core}`);
30
- if (opts.dryRun) log('Mode: DRY RUN (no changes)');
31
- console.log();
32
-
33
- if (opts.check) {
34
- if (projectVersions.core === toolkitVersions.core) {
35
- ok('Already up to date.');
36
- } else {
37
- log(`Update available: ${projectVersions.core} → ${toolkitVersions.core}`);
38
- }
39
- return;
40
- }
41
-
42
- const layer = opts.layer || 'all';
43
- let totalChanges = 0;
44
-
45
- if (layer === 'all' || layer === 'core') {
46
- totalChanges += upgradeCore(projectDir, opts);
47
- }
48
-
49
- if (layer === 'all' || layer === 'platform') {
50
- totalChanges += upgradePlatform(projectDir, opts);
51
- }
52
-
53
- if (layer === 'all' || layer === 'capability') {
54
- totalChanges += upgradeCapability(projectDir, opts);
55
- }
56
-
57
- upgradeScripts(projectDir, opts);
58
- upgradeConfigSchema(projectDir, opts);
59
-
60
- if (!opts.dryRun && totalChanges > 0) {
61
- updateVersionTracking(configPath, projectConfig, toolkitVersions);
62
- }
63
-
64
- console.log();
65
- header(opts.dryRun ? 'DRY RUN complete — no changes made' : `Upgrade complete (${totalChanges} files updated)`);
66
- if (opts.dryRun) log('Run without --dry-run to apply.');
67
- console.log();
68
- }
69
-
70
- function upgradeCore(projectDir, opts) {
71
- info('Layer 0: Methodology Core (.dw/core/)');
72
- const src = join(TOOLKIT_ROOT, '.dw', 'core');
73
- const dst = join(projectDir, '.dw', 'core');
74
-
75
- const diff = diffDirs(src, dst);
76
- reportDiff(diff);
77
-
78
- if (diff.added.length === 0 && diff.modified.length === 0) {
79
- ok('Core files are up to date');
80
- return 0;
81
- }
82
-
83
- const filesToUpdate = [...diff.added, ...diff.modified];
84
- for (const file of filesToUpdate) {
85
- if (opts.dryRun) {
86
- dry(`${diff.added.includes(file) ? 'add' : 'update'} .dw/core/${file}`);
87
- } else {
88
- copyFile(join(src, file), join(dst, file));
89
- ok(`.dw/core/${file}`);
90
- }
91
- }
92
- return filesToUpdate.length;
93
- }
94
-
95
- function upgradePlatform(projectDir, opts) {
96
- info('Layer 1: Platform Files (.claude/)');
97
- const src = join(TOOLKIT_ROOT, '.claude');
98
- const dst = join(projectDir, '.claude');
99
-
100
- if (!existsSync(dst)) {
101
- warn('.claude/ not found — skipping platform upgrade (run dw init first)');
102
- return 0;
103
- }
104
-
105
- const overridesDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'overrides');
106
- const diff = diffDirs(src, dst);
107
- reportDiff(diff);
108
-
109
- if (diff.added.length === 0 && diff.modified.length === 0) {
110
- ok('Platform files are up to date');
111
- return 0;
112
- }
113
-
114
- let count = 0;
115
- const filesToUpdate = [...diff.added, ...diff.modified];
116
- for (const file of filesToUpdate) {
117
- const overridePath = join(overridesDir, file);
118
- if (existsSync(overridePath)) {
119
- warn(`${file}: override exists → keeping your version`);
120
- continue;
121
- }
122
-
123
- if (opts.dryRun) {
124
- dry(`${diff.added.includes(file) ? 'add' : 'update'} .claude/${file}`);
125
- } else {
126
- copyFile(join(src, file), join(dst, file));
127
- ok(`.claude/${file}`);
128
- }
129
- count++;
130
- }
131
-
132
- copyExtensions(projectDir, opts);
133
- mergeSettingsJson(projectDir, opts);
134
-
135
- return count;
136
- }
137
-
138
- function copyExtensions(projectDir, opts) {
139
- const extDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'extensions');
140
- const skillsDir = join(projectDir, '.claude', 'skills');
141
-
142
- if (!existsSync(extDir)) return;
143
-
144
- let count = 0;
145
- try {
146
- const entries = readdirSync(extDir, { withFileTypes: true });
147
- for (const entry of entries) {
148
- if (!entry.isDirectory() || entry.name === '.gitkeep') continue;
149
- const src = join(extDir, entry.name);
150
- const dst = join(skillsDir, entry.name);
151
- if (opts.dryRun) {
152
- dry(`install extension: ${entry.name}`);
153
- } else {
154
- copyDir(src, dst, { overwrite: true });
155
- ok(`Extension '${entry.name}' installed`);
156
- }
157
- count++;
158
- }
159
- } catch { /* empty extensions dir */ }
160
- if (count === 0) log('No extensions found');
161
- }
162
-
163
- function mergeSettingsJson(projectDir, opts) {
164
- const toolkitSettings = join(TOOLKIT_ROOT, '.claude', 'settings.json');
165
- const projectSettings = join(projectDir, '.claude', 'settings.json');
166
-
167
- if (!existsSync(toolkitSettings) || !existsSync(projectSettings)) return;
168
-
169
- if (opts.dryRun) {
170
- dry('merge .claude/settings.json');
171
- return;
172
- }
173
-
174
- try {
175
- const template = JSON.parse(readFileSync(toolkitSettings, 'utf-8'));
176
- const current = JSON.parse(readFileSync(projectSettings, 'utf-8'));
177
- const merged = deepMerge(template, current);
178
- writeFileSync(projectSettings, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
179
- ok('settings.json: merged');
180
- } catch (e) {
181
- warn(`settings.json merge failed: ${e.message}`);
182
- }
183
- }
184
-
185
- function deepMerge(base, override) {
186
- const result = { ...base };
187
- for (const [key, val] of Object.entries(override)) {
188
- if (key in result && typeof result[key] === 'object' && !Array.isArray(result[key]) && typeof val === 'object' && !Array.isArray(val)) {
189
- result[key] = deepMerge(result[key], val);
190
- } else {
191
- result[key] = val;
192
- }
193
- }
194
- return result;
195
- }
196
-
197
- function upgradeCapability(projectDir, opts) {
198
- info('Layer 2: Capability Config');
199
- ok('Capability layer is config-driven — no file changes needed');
200
- log('Review claude: section in .dw/config/dw.config.yml for new options');
201
- return 0;
202
- }
203
-
204
- function upgradeScripts(projectDir, opts) {
205
- info('Scripts');
206
- const src = join(TOOLKIT_ROOT, 'scripts');
207
- const dst = join(projectDir, 'scripts');
208
-
209
- if (!existsSync(src)) return;
210
-
211
- const diff = diffDirs(src, dst);
212
- if (diff.added.length === 0 && diff.modified.length === 0) {
213
- ok('Scripts are up to date');
214
- return;
215
- }
216
-
217
- for (const file of [...diff.added, ...diff.modified]) {
218
- if (opts.dryRun) {
219
- dry(`update scripts/${file}`);
220
- } else {
221
- copyFile(join(src, file), join(dst, file));
222
- ok(`scripts/${file}`);
223
- }
224
- }
225
- }
226
-
227
- function upgradeConfigSchema(projectDir, opts) {
228
- const src = join(TOOLKIT_ROOT, '.dw', 'config', 'config.schema.json');
229
- const dst = join(projectDir, '.dw', 'config', 'config.schema.json');
230
-
231
- if (!existsSync(src)) return;
232
- if (existsSync(dst)) {
233
- const srcContent = readFileSync(src, 'utf-8');
234
- const dstContent = readFileSync(dst, 'utf-8');
235
- if (srcContent === dstContent) return;
236
- }
237
-
238
- if (opts.dryRun) {
239
- dry('update .dw/config/config.schema.json');
240
- } else {
241
- copyFile(src, dst);
242
- ok('.dw/config/config.schema.json updated');
243
- }
244
- }
245
-
246
- function updateVersionTracking(configPath, config, toolkitVersions) {
247
- const today = new Date().toISOString().split('T')[0];
248
- if (!config._toolkit) config._toolkit = {};
249
- config._toolkit.core_version = toolkitVersions.core;
250
- config._toolkit.platform_version = toolkitVersions.platform;
251
- config._toolkit.capability_version = toolkitVersions.capability;
252
- config._toolkit.last_upgrade = today;
253
-
254
- writeConfig(configPath, config);
255
- ok(`Version tracking updated: core=${toolkitVersions.core}, date=${today}`);
256
- }
257
-
258
- function reportDiff(diff) {
259
- if (diff.added.length > 0) log(` New files: ${diff.added.length}`);
260
- if (diff.modified.length > 0) log(` Modified: ${diff.modified.length}`);
261
- if (diff.unchanged.length > 0) log(` Unchanged: ${diff.unchanged.length}`);
262
- }
1
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { header, ok, warn, err, info, log, dry } from '../lib/ui.mjs';
5
+ import { loadConfig, writeConfig, getToolkitVersions } from '../lib/config.mjs';
6
+ import { diffDirs, copyDir, copyFile } from '../lib/copy.mjs';
7
+
8
+ const TOOLKIT_ROOT = resolve(fileURLToPath(import.meta.url), '..', '..', '..');
9
+
10
+ export async function upgradeCommand(opts) {
11
+ const projectDir = process.cwd();
12
+ const configPath = join(projectDir, '.dw', 'config', 'dw.config.yml');
13
+
14
+ if (!existsSync(configPath)) {
15
+ err('No .dw/config/dw.config.yml found. Run `dw init` first.');
16
+ process.exit(1);
17
+ }
18
+
19
+ const projectConfig = loadConfig(configPath);
20
+ const projectVersions = getToolkitVersions(projectConfig);
21
+
22
+ const toolkitPkg = JSON.parse(readFileSync(join(TOOLKIT_ROOT, 'package.json'), 'utf-8'));
23
+ const toolkitConfig = loadConfig(join(TOOLKIT_ROOT, '.dw', 'config', 'dw.config.yml'));
24
+ const toolkitVersions = getToolkitVersions(toolkitConfig);
25
+
26
+ header('dw-kit Upgrade');
27
+ log(`Installed (package) : v${toolkitPkg.version}`);
28
+ log(`Project core : ${projectVersions.core}`);
29
+ log(`Toolkit core : ${toolkitVersions.core}`);
30
+ if (opts.dryRun) log('Mode: DRY RUN (no changes)');
31
+ console.log();
32
+
33
+ if (opts.check) {
34
+ if (projectVersions.core === toolkitVersions.core) {
35
+ ok('Already up to date.');
36
+ } else {
37
+ log(`Update available: ${projectVersions.core} → ${toolkitVersions.core}`);
38
+ }
39
+ return;
40
+ }
41
+
42
+ const layer = opts.layer || 'all';
43
+ let totalChanges = 0;
44
+
45
+ if (layer === 'all' || layer === 'core') {
46
+ totalChanges += upgradeCore(projectDir, opts);
47
+ }
48
+
49
+ if (layer === 'all' || layer === 'platform') {
50
+ totalChanges += upgradePlatform(projectDir, opts);
51
+ upgradeGitattributes(projectDir, opts);
52
+ }
53
+
54
+ if (layer === 'all' || layer === 'capability') {
55
+ totalChanges += upgradeCapability(projectDir, opts);
56
+ }
57
+
58
+ upgradeScripts(projectDir, opts);
59
+ upgradeConfigSchema(projectDir, opts);
60
+
61
+ if (!opts.dryRun && totalChanges > 0) {
62
+ updateVersionTracking(configPath, projectConfig, toolkitVersions);
63
+ }
64
+
65
+ console.log();
66
+ header(opts.dryRun ? 'DRY RUN complete no changes made' : `Upgrade complete (${totalChanges} files updated)`);
67
+ if (opts.dryRun) log('Run without --dry-run to apply.');
68
+ console.log();
69
+ }
70
+
71
+ function upgradeCore(projectDir, opts) {
72
+ info('Layer 0: Methodology Core (.dw/core/)');
73
+ const src = join(TOOLKIT_ROOT, '.dw', 'core');
74
+ const dst = join(projectDir, '.dw', 'core');
75
+
76
+ const diff = diffDirs(src, dst);
77
+ reportDiff(diff);
78
+
79
+ if (diff.added.length === 0 && diff.modified.length === 0) {
80
+ ok('Core files are up to date');
81
+ return 0;
82
+ }
83
+
84
+ const filesToUpdate = [...diff.added, ...diff.modified];
85
+ for (const file of filesToUpdate) {
86
+ if (opts.dryRun) {
87
+ dry(`${diff.added.includes(file) ? 'add' : 'update'} .dw/core/${file}`);
88
+ } else {
89
+ copyFile(join(src, file), join(dst, file));
90
+ ok(`.dw/core/${file}`);
91
+ }
92
+ }
93
+ return filesToUpdate.length;
94
+ }
95
+
96
+ function upgradePlatform(projectDir, opts) {
97
+ info('Layer 1: Platform Files (.claude/)');
98
+ const src = join(TOOLKIT_ROOT, '.claude');
99
+ const dst = join(projectDir, '.claude');
100
+
101
+ if (!existsSync(dst)) {
102
+ warn('.claude/ not found — skipping platform upgrade (run dw init first)');
103
+ return 0;
104
+ }
105
+
106
+ const overridesDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'overrides');
107
+ const diff = diffDirs(src, dst);
108
+ reportDiff(diff);
109
+
110
+ if (diff.added.length === 0 && diff.modified.length === 0) {
111
+ ok('Platform files are up to date');
112
+ return 0;
113
+ }
114
+
115
+ let count = 0;
116
+ const filesToUpdate = [...diff.added, ...diff.modified];
117
+ for (const file of filesToUpdate) {
118
+ const overridePath = join(overridesDir, file);
119
+ if (existsSync(overridePath)) {
120
+ warn(`${file}: override exists → keeping your version`);
121
+ continue;
122
+ }
123
+
124
+ if (opts.dryRun) {
125
+ dry(`${diff.added.includes(file) ? 'add' : 'update'} .claude/${file}`);
126
+ } else {
127
+ copyFile(join(src, file), join(dst, file));
128
+ ok(`.claude/${file}`);
129
+ }
130
+ count++;
131
+ }
132
+
133
+ copyExtensions(projectDir, opts);
134
+ mergeSettingsJson(projectDir, opts);
135
+
136
+ return count;
137
+ }
138
+
139
+ function copyExtensions(projectDir, opts) {
140
+ const extDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'extensions');
141
+ const skillsDir = join(projectDir, '.claude', 'skills');
142
+
143
+ if (!existsSync(extDir)) return;
144
+
145
+ let count = 0;
146
+ try {
147
+ const entries = readdirSync(extDir, { withFileTypes: true });
148
+ for (const entry of entries) {
149
+ if (!entry.isDirectory() || entry.name === '.gitkeep') continue;
150
+ const src = join(extDir, entry.name);
151
+ const dst = join(skillsDir, entry.name);
152
+ if (opts.dryRun) {
153
+ dry(`install extension: ${entry.name}`);
154
+ } else {
155
+ copyDir(src, dst, { overwrite: true });
156
+ ok(`Extension '${entry.name}' installed`);
157
+ }
158
+ count++;
159
+ }
160
+ } catch { /* empty extensions dir */ }
161
+ if (count === 0) log('No extensions found');
162
+ }
163
+
164
+ function mergeSettingsJson(projectDir, opts) {
165
+ const toolkitSettings = join(TOOLKIT_ROOT, '.claude', 'settings.json');
166
+ const projectSettings = join(projectDir, '.claude', 'settings.json');
167
+
168
+ if (!existsSync(toolkitSettings) || !existsSync(projectSettings)) return;
169
+
170
+ if (opts.dryRun) {
171
+ dry('merge .claude/settings.json');
172
+ return;
173
+ }
174
+
175
+ try {
176
+ const template = JSON.parse(readFileSync(toolkitSettings, 'utf-8'));
177
+ const current = JSON.parse(readFileSync(projectSettings, 'utf-8'));
178
+ const merged = deepMerge(template, current);
179
+ writeFileSync(projectSettings, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
180
+ ok('settings.json: merged');
181
+ } catch (e) {
182
+ warn(`settings.json merge failed: ${e.message}`);
183
+ }
184
+ }
185
+
186
+ function deepMerge(base, override) {
187
+ const result = { ...base };
188
+ for (const [key, val] of Object.entries(override)) {
189
+ if (key in result && typeof result[key] === 'object' && !Array.isArray(result[key]) && typeof val === 'object' && !Array.isArray(val)) {
190
+ result[key] = deepMerge(result[key], val);
191
+ } else {
192
+ result[key] = val;
193
+ }
194
+ }
195
+ return result;
196
+ }
197
+
198
+ function upgradeGitattributes(projectDir, opts) {
199
+ info('Gitattributes');
200
+ const dst = join(projectDir, '.gitattributes');
201
+
202
+ // Entries dw-kit needs in the user's repo to survive git checkout on Windows
203
+ const requiredEntries = [
204
+ '.claude/hooks/*.sh text eol=lf',
205
+ '.claude/skills/**/*.sh text eol=lf',
206
+ ];
207
+
208
+ const existing = existsSync(dst)
209
+ ? readFileSync(dst, 'utf-8').replace(/\r\n/g, '\n')
210
+ : '';
211
+
212
+ const missing = requiredEntries.filter(e => !existing.includes(e));
213
+
214
+ if (missing.length === 0) {
215
+ ok('.gitattributes: dw-kit entries already present');
216
+ return;
217
+ }
218
+
219
+ if (opts.dryRun) {
220
+ missing.forEach(e => dry(`add to .gitattributes: ${e}`));
221
+ return;
222
+ }
223
+
224
+ // Append block (idempotent: only adds what's missing)
225
+ const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
226
+ const block = `${separator}\n# dw-kit: enforce LF for shell scripts (cross-platform safety)\n${missing.join('\n')}\n`;
227
+ writeFileSync(dst, existing + block, 'utf-8');
228
+ ok(`.gitattributes: added ${missing.length} dw-kit hook entr${missing.length > 1 ? 'ies' : 'y'}`);
229
+ log(' → Tip: run `git add --renormalize .claude/hooks/` to normalize any existing checked-out files');
230
+ }
231
+
232
+ function upgradeCapability(projectDir, opts) {
233
+ info('Layer 2: Capability Config');
234
+ ok('Capability layer is config-driven — no file changes needed');
235
+ log('Review claude: section in .dw/config/dw.config.yml for new options');
236
+ return 0;
237
+ }
238
+
239
+ function upgradeScripts(projectDir, opts) {
240
+ info('Scripts');
241
+ const src = join(TOOLKIT_ROOT, 'scripts');
242
+ const dst = join(projectDir, 'scripts');
243
+
244
+ if (!existsSync(src)) return;
245
+
246
+ const diff = diffDirs(src, dst);
247
+ if (diff.added.length === 0 && diff.modified.length === 0) {
248
+ ok('Scripts are up to date');
249
+ return;
250
+ }
251
+
252
+ for (const file of [...diff.added, ...diff.modified]) {
253
+ if (opts.dryRun) {
254
+ dry(`update scripts/${file}`);
255
+ } else {
256
+ copyFile(join(src, file), join(dst, file));
257
+ ok(`scripts/${file}`);
258
+ }
259
+ }
260
+ }
261
+
262
+ function upgradeConfigSchema(projectDir, opts) {
263
+ const src = join(TOOLKIT_ROOT, '.dw', 'config', 'config.schema.json');
264
+ const dst = join(projectDir, '.dw', 'config', 'config.schema.json');
265
+
266
+ if (!existsSync(src)) return;
267
+ if (existsSync(dst)) {
268
+ const srcContent = readFileSync(src, 'utf-8');
269
+ const dstContent = readFileSync(dst, 'utf-8');
270
+ if (srcContent === dstContent) return;
271
+ }
272
+
273
+ if (opts.dryRun) {
274
+ dry('update .dw/config/config.schema.json');
275
+ } else {
276
+ copyFile(src, dst);
277
+ ok('.dw/config/config.schema.json updated');
278
+ }
279
+ }
280
+
281
+ function updateVersionTracking(configPath, config, toolkitVersions) {
282
+ const today = new Date().toISOString().split('T')[0];
283
+ if (!config._toolkit) config._toolkit = {};
284
+ config._toolkit.core_version = toolkitVersions.core;
285
+ config._toolkit.platform_version = toolkitVersions.platform;
286
+ config._toolkit.capability_version = toolkitVersions.capability;
287
+ config._toolkit.last_upgrade = today;
288
+
289
+ writeConfig(configPath, config);
290
+ ok(`Version tracking updated: core=${toolkitVersions.core}, date=${today}`);
291
+ }
292
+
293
+ function reportDiff(diff) {
294
+ if (diff.added.length > 0) log(` New files: ${diff.added.length}`);
295
+ if (diff.modified.length > 0) log(` Modified: ${diff.modified.length}`);
296
+ if (diff.unchanged.length > 0) log(` Unchanged: ${diff.unchanged.length}`);
297
+ }
@@ -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
+ }