dw-kit 1.4.0 โ†’ 1.7.0-rc.1

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 (65) hide show
  1. package/.claude/agents/executor.md +80 -80
  2. package/.claude/hooks/pre-commit-gate.sh +59 -0
  3. package/.claude/hooks/stop-check.sh +111 -31
  4. package/.claude/rules/commit-standards.md +48 -37
  5. package/.claude/rules/dw.md +47 -11
  6. package/.claude/skills/dw-commit/SKILL.md +7 -4
  7. package/.claude/skills/dw-decision/SKILL.md +5 -4
  8. package/.claude/skills/dw-execute/SKILL.md +18 -5
  9. package/.claude/skills/dw-handoff/SKILL.md +8 -3
  10. package/.claude/skills/dw-plan/SKILL.md +15 -2
  11. package/.claude/skills/dw-research/SKILL.md +7 -5
  12. package/.claude/skills/dw-retroactive/SKILL.md +75 -63
  13. package/.claude/skills/dw-task-init/SKILL.md +40 -35
  14. package/.dw/adapters/generic/AGENT.md +171 -169
  15. package/.dw/core/WORKFLOW.md +450 -450
  16. package/.dw/core/schemas/agent-claim.schema.json +127 -0
  17. package/.dw/core/schemas/agent-report.schema.json +72 -0
  18. package/.dw/core/schemas/goal-frontmatter.schema.json +84 -0
  19. package/.dw/core/schemas/task-frontmatter.schema.json +97 -0
  20. package/.dw/core/templates/v3/goal.md +146 -0
  21. package/.dw/core/templates/v3/task.md +188 -0
  22. package/CLAUDE.md +2 -2
  23. package/MIGRATION-v1.5.md +330 -0
  24. package/README.md +17 -0
  25. package/package.json +3 -2
  26. package/src/cli.mjs +312 -0
  27. package/src/commands/agent-claim.mjs +235 -0
  28. package/src/commands/agent-inspect.mjs +123 -0
  29. package/src/commands/doctor.mjs +64 -0
  30. package/src/commands/goal-bump.mjs +50 -0
  31. package/src/commands/goal-delete.mjs +120 -0
  32. package/src/commands/goal-link.mjs +126 -0
  33. package/src/commands/goal-lint.mjs +152 -0
  34. package/src/commands/goal-new.mjs +86 -0
  35. package/src/commands/goal-portfolio.mjs +84 -0
  36. package/src/commands/goal-render.mjs +49 -0
  37. package/src/commands/goal-set.mjs +62 -0
  38. package/src/commands/goal-show.mjs +94 -0
  39. package/src/commands/goal-stubs.mjs +21 -0
  40. package/src/commands/goal-suggest-krs.mjs +139 -0
  41. package/src/commands/goal-summary.mjs +67 -0
  42. package/src/commands/goal-view.mjs +196 -0
  43. package/src/commands/lint-task.mjs +112 -0
  44. package/src/commands/task-migrate.mjs +471 -0
  45. package/src/commands/task-new.mjs +90 -0
  46. package/src/commands/task-render.mjs +235 -0
  47. package/src/commands/task-rotate.mjs +168 -0
  48. package/src/commands/task-show.mjs +137 -0
  49. package/src/commands/task-summary.mjs +68 -0
  50. package/src/commands/task-view.mjs +386 -0
  51. package/src/commands/task-watch.mjs +868 -0
  52. package/src/lib/active-index.mjs +19 -1
  53. package/src/lib/agent-claim.mjs +173 -0
  54. package/src/lib/agent-conflict.mjs +137 -0
  55. package/src/lib/agent-events.mjs +43 -0
  56. package/src/lib/agent-report.mjs +96 -0
  57. package/src/lib/frontmatter.mjs +72 -0
  58. package/src/lib/goal-events.mjs +79 -0
  59. package/src/lib/goal-store.mjs +202 -0
  60. package/src/lib/goal-svg.mjs +293 -0
  61. package/src/lib/goal-watch.mjs +133 -0
  62. package/src/lib/lint-rules.mjs +149 -0
  63. package/src/lib/sse-broker.mjs +91 -0
  64. package/src/lib/timeline-parser.mjs +80 -0
  65. package/src/lib/watch-auth.mjs +64 -0
@@ -0,0 +1,386 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import chalk from 'chalk';
5
+ import { parseFrontmatter } from '../lib/frontmatter.mjs';
6
+ import { parseTimeline, parseSubtaskTracker, splitChangelogEntries } from '../lib/timeline-parser.mjs';
7
+ import { logEvent } from '../lib/telemetry.mjs';
8
+
9
+ const TASKS_DIR = '.dw/tasks';
10
+ const CACHE_DIR = '.dw/cache/preview';
11
+
12
+ const STATUS_COLORS = {
13
+ Draft: '#6b7280',
14
+ Approved: '#0891b2',
15
+ 'In Progress': '#f59e0b',
16
+ Blocked: '#dc2626',
17
+ Paused: '#a855f7',
18
+ Done: '#10b981',
19
+ };
20
+
21
+ const TRACKER_GLYPH_META = {
22
+ 'โฌœ': { color: '#6b7280', label: 'Pending', pct: 0 },
23
+ '๐ŸŸก': { color: '#f59e0b', label: 'In Progress', pct: 50 },
24
+ 'โœ…': { color: '#10b981', label: 'Done', pct: 100 },
25
+ '๐Ÿ”ด': { color: '#dc2626', label: 'Blocked', pct: 25 },
26
+ 'โธ': { color: '#a855f7', label: 'Paused', pct: 30 },
27
+ };
28
+
29
+ function findV3Tasks(rootDir) {
30
+ const tasksRoot = join(rootDir, TASKS_DIR);
31
+ if (!existsSync(tasksRoot)) return [];
32
+ return readdirSync(tasksRoot)
33
+ .filter((e) => e !== 'archive' && e !== 'ACTIVE.md')
34
+ .map((e) => ({ name: e, path: join(tasksRoot, e) }))
35
+ .filter((e) => {
36
+ try { return statSync(e.path).isDirectory() && existsSync(join(e.path, 'task.md')); }
37
+ catch { return false; }
38
+ });
39
+ }
40
+
41
+ function escapeHtml(s) {
42
+ return String(s)
43
+ .replace(/&/g, '&')
44
+ .replace(/</g, '&lt;')
45
+ .replace(/>/g, '&gt;')
46
+ .replace(/"/g, '&quot;')
47
+ .replace(/'/g, '&#39;');
48
+ }
49
+
50
+ function getGlyph(status) {
51
+ if (!status) return 'โฌœ';
52
+ const m = status.trim().match(/^(โฌœ|๐ŸŸก|โœ…|๐Ÿ”ด|โธ)/);
53
+ return m ? m[1] : 'โฌœ';
54
+ }
55
+
56
+ function buildSnapshotCard(fm, parsed, taskName) {
57
+ const status = fm.status || 'Draft';
58
+ const color = STATUS_COLORS[status] || STATUS_COLORS.Draft;
59
+ const blockers = (fm.blockers && fm.blockers !== 'none') ? fm.blockers : null;
60
+ const subtaskRows = parseSubtaskTracker(parsed.sections[3]?.text) || [];
61
+ const total = subtaskRows.length;
62
+ const done = subtaskRows.filter((r) => getGlyph(r.status) === 'โœ…').length;
63
+ const inProgress = subtaskRows.filter((r) => getGlyph(r.status) === '๐ŸŸก').length;
64
+ const blocked = subtaskRows.filter((r) => getGlyph(r.status) === '๐Ÿ”ด').length;
65
+ const pending = total - done - inProgress - blocked;
66
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
67
+
68
+ return `
69
+ <div class="card snapshot">
70
+ <div class="snap-head">
71
+ <span class="badge" style="background:${color};">${escapeHtml(status)}</span>
72
+ <h1>${escapeHtml(parsed.title || taskName)}</h1>
73
+ </div>
74
+ <div class="snap-meta">
75
+ ${fm.phase ? `<span>${escapeHtml(fm.phase)}</span>` : ''}
76
+ ${fm.owner ? `<span>ยท ${escapeHtml(fm.owner)}</span>` : ''}
77
+ ${fm.related_adr && fm.related_adr !== 'none' ? `<span>ยท <strong>${escapeHtml(fm.related_adr)}</strong></span>` : ''}
78
+ ${fm.last_updated ? `<span class="muted">ยท updated ${escapeHtml(fm.last_updated)}</span>` : ''}
79
+ </div>
80
+ ${blockers ? `<div class="blocker"><strong>โš  BLOCKERS</strong> ${escapeHtml(blockers)}</div>` : ''}
81
+ <div class="progress">
82
+ <div class="bar"><div class="fill" style="width:${pct}%; background:${color};"></div></div>
83
+ <div class="counts">
84
+ <span class="ct done">โœ… ${done}</span>
85
+ <span class="ct prog">๐ŸŸก ${inProgress}</span>
86
+ <span class="ct blk">๐Ÿ”ด ${blocked}</span>
87
+ <span class="ct pend">โฌœ ${pending}</span>
88
+ <span class="ct tot">${pct}% (${done}/${total})</span>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ `;
93
+ }
94
+
95
+ function buildTrackerCard(parsed) {
96
+ const rows = parseSubtaskTracker(parsed.sections[3]?.text) || [];
97
+ if (rows.length === 0) {
98
+ return `<div class="card empty">No subtasks yet โ€” populate Section 3 in <code>task.md</code>.</div>`;
99
+ }
100
+
101
+ const items = rows.map((r) => {
102
+ const glyph = getGlyph(r.status);
103
+ const meta = TRACKER_GLYPH_META[glyph] || TRACKER_GLYPH_META['โฌœ'];
104
+ return `
105
+ <div class="track-row">
106
+ <span class="dot" style="background:${meta.color};" title="${escapeHtml(meta.label)}">${glyph}</span>
107
+ <span class="tid">${escapeHtml(r.id || '')}</span>
108
+ <span class="tname">${escapeHtml((r.name || '').slice(0, 80))}</span>
109
+ ${r.date && r.date !== 'โ€”' ? `<span class="tdate">${escapeHtml(r.date)}</span>` : ''}
110
+ </div>
111
+ `;
112
+ }).join('');
113
+
114
+ return `
115
+ <div class="card">
116
+ <h2>Subtasks <span class="muted">(${rows.length})</span></h2>
117
+ <div class="tracker">${items}</div>
118
+ </div>
119
+ `;
120
+ }
121
+
122
+ function buildRecentActivityCard(parsed) {
123
+ const entries = splitChangelogEntries(parsed.sections[4]?.text || '');
124
+ if (entries.length === 0) {
125
+ return '';
126
+ }
127
+ const recent = entries.slice(0, 3).map((e) => {
128
+ const heading = e.heading || 'Untitled';
129
+ const bodyText = e.lines.join(' ').replace(/\s+/g, ' ').replace(/[*_`]/g, '').trim();
130
+ const preview = bodyText.slice(0, 140);
131
+ return `
132
+ <div class="activity-row">
133
+ <span class="aheading">${escapeHtml(heading.slice(0, 100))}</span>
134
+ ${preview ? `<span class="apreview">${escapeHtml(preview)}${bodyText.length > 140 ? 'โ€ฆ' : ''}</span>` : ''}
135
+ </div>
136
+ `;
137
+ }).join('');
138
+
139
+ const moreCount = Math.max(0, entries.length - 3);
140
+ const moreNote = moreCount > 0 ? `<div class="more">+ ${moreCount} earlier session${moreCount === 1 ? '' : 's'} โ€” see <code>task.md</code> Section 4</div>` : '';
141
+
142
+ return `
143
+ <div class="card">
144
+ <h2>Recent activity <span class="muted">(${entries.length} session${entries.length === 1 ? '' : 's'})</span></h2>
145
+ <div class="activity">${recent}</div>
146
+ ${moreNote}
147
+ </div>
148
+ `;
149
+ }
150
+
151
+ function buildHandoffCard(parsed) {
152
+ const text = parsed.sections[5]?.text;
153
+ if (!text) return '';
154
+
155
+ const bullets = [];
156
+ const re = /\*\*(Read first|Current state|Don't do|Watch out|For next session)[^:*]*:\*\*\s*([^\n]+)/gi;
157
+ let m;
158
+ while ((m = re.exec(text)) !== null) {
159
+ bullets.push({ label: m[1].trim(), value: m[2].trim() });
160
+ }
161
+
162
+ if (bullets.length === 0) {
163
+ const lines = text.split('\n').filter((l) => l.trim().startsWith('-')).slice(0, 4);
164
+ if (lines.length === 0) return '';
165
+ const items = lines.map((l) => `<li>${escapeHtml(l.replace(/^-\s*/, '').slice(0, 140))}</li>`).join('');
166
+ return `
167
+ <div class="card">
168
+ <h2>Handoff</h2>
169
+ <ul class="handoff">${items}</ul>
170
+ </div>
171
+ `;
172
+ }
173
+
174
+ const items = bullets.slice(0, 5).map((b) => `
175
+ <div class="hand-row">
176
+ <span class="hlabel">${escapeHtml(b.label)}</span>
177
+ <span class="hval">${escapeHtml(b.value.slice(0, 200))}</span>
178
+ </div>
179
+ `).join('');
180
+
181
+ return `
182
+ <div class="card">
183
+ <h2>Handoff</h2>
184
+ <div class="handoff">${items}</div>
185
+ </div>
186
+ `;
187
+ }
188
+
189
+ function buildOpenLinks(taskDir, taskName, hasSvg) {
190
+ const md = join(taskDir, 'task.md').replace(/\\/g, '/');
191
+ const svg = hasSvg ? join(taskDir, 'timeline.svg').replace(/\\/g, '/') : null;
192
+ return `
193
+ <div class="links">
194
+ <a href="file://${md}" class="link primary">๐Ÿ“„ Open <code>task.md</code> for full details</a>
195
+ ${svg ? `<a href="file://${svg}" class="link">๐Ÿ–ผ๏ธ SVG sidecar</a>` : ''}
196
+ </div>
197
+ `;
198
+ }
199
+
200
+ const PAGE_CSS = `
201
+ :root {
202
+ --bg: #0d1117;
203
+ --panel: #161b22;
204
+ --border: #30363d;
205
+ --text: #c9d1d9;
206
+ --muted: #6b7280;
207
+ --accent: #58a6ff;
208
+ }
209
+ @media (prefers-color-scheme: light) {
210
+ :root {
211
+ --bg: #ffffff;
212
+ --panel: #f6f8fa;
213
+ --border: #d0d7de;
214
+ --text: #1f2328;
215
+ --muted: #57606a;
216
+ --accent: #0969da;
217
+ }
218
+ }
219
+ * { box-sizing: border-box; }
220
+ body {
221
+ margin: 0;
222
+ padding: 24px;
223
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
224
+ background: var(--bg);
225
+ color: var(--text);
226
+ max-width: 880px;
227
+ margin: 0 auto;
228
+ }
229
+ h1, h2 { margin: 0; }
230
+ .muted { color: var(--muted); font-weight: normal; }
231
+ .card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px 18px; margin-bottom: 12px; }
232
+ .card h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--muted); margin-bottom: 10px; font-weight: 600; }
233
+ .card.empty { color: var(--muted); font-size: 13px; padding: 12px 18px; }
234
+ .card code { background: var(--bg); padding: 1px 6px; border-radius: 3px; font-size: 12px; border: 1px solid var(--border); }
235
+ .snapshot { padding: 18px 22px; }
236
+ .snap-head { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
237
+ .badge { padding: 4px 10px; border-radius: 4px; color: #fff; font-size: 11px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; }
238
+ .snap-head h1 { font-size: 18px; flex: 1; }
239
+ .snap-meta { font-size: 12px; color: var(--muted); }
240
+ .snap-meta strong { color: var(--text); }
241
+ .blocker { margin-top: 10px; padding: 8px 12px; background: rgba(220, 38, 38, 0.1); border: 1px solid rgba(220, 38, 38, 0.4); border-radius: 6px; color: #ff7b72; font-size: 13px; }
242
+ .blocker strong { color: #f87171; margin-right: 6px; }
243
+ .progress { margin-top: 12px; }
244
+ .bar { height: 8px; background: rgba(110, 118, 129, 0.2); border-radius: 4px; overflow: hidden; margin-bottom: 6px; }
245
+ .fill { height: 100%; }
246
+ .counts { display: flex; gap: 14px; font-size: 12px; align-items: center; }
247
+ .ct.done { color: #10b981; }
248
+ .ct.prog { color: #f59e0b; }
249
+ .ct.blk { color: #dc2626; }
250
+ .ct.pend { color: var(--muted); }
251
+ .ct.tot { margin-left: auto; font-weight: 600; color: var(--text); }
252
+ .tracker { display: flex; flex-direction: column; gap: 4px; }
253
+ .track-row { display: flex; align-items: center; gap: 10px; font-size: 13px; padding: 2px 0; }
254
+ .dot { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 4px; font-size: 12px; color: #fff; flex-shrink: 0; }
255
+ .tid { font-weight: 600; color: var(--muted); width: 60px; flex-shrink: 0; font-size: 12px; }
256
+ .tname { flex: 1; }
257
+ .tdate { color: var(--muted); font-size: 11px; }
258
+ .activity { display: flex; flex-direction: column; gap: 8px; }
259
+ .activity-row { display: flex; flex-direction: column; gap: 2px; }
260
+ .aheading { font-size: 13px; font-weight: 600; }
261
+ .apreview { font-size: 12px; color: var(--muted); }
262
+ .more { font-size: 12px; color: var(--muted); margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
263
+ .handoff { display: flex; flex-direction: column; gap: 6px; }
264
+ .handoff.ul { padding-left: 20px; }
265
+ .hand-row { display: flex; gap: 8px; font-size: 13px; }
266
+ .hlabel { font-weight: 600; color: var(--muted); min-width: 100px; flex-shrink: 0; font-size: 12px; }
267
+ .hval { flex: 1; }
268
+ ul.handoff li { font-size: 13px; margin-bottom: 4px; }
269
+ .links { display: flex; gap: 12px; padding: 12px 0 4px; flex-wrap: wrap; }
270
+ .link { color: var(--accent); text-decoration: none; font-size: 13px; padding: 8px 14px; background: var(--panel); border: 1px solid var(--border); border-radius: 6px; }
271
+ .link:hover { border-color: var(--accent); }
272
+ .link.primary { font-weight: 600; }
273
+ .link code { background: transparent; border: none; padding: 0; font-size: 12px; }
274
+ .footer { color: var(--muted); font-size: 11px; padding: 12px 0; text-align: center; }
275
+ `;
276
+
277
+ function buildHtml(taskName, taskDir, fm, parsed) {
278
+ const hasSvg = existsSync(join(taskDir, 'timeline.svg'));
279
+ return `<!doctype html>
280
+ <html lang="en">
281
+ <head>
282
+ <meta charset="utf-8">
283
+ <title>${escapeHtml(parsed.title || taskName)} โ€” dw task</title>
284
+ <meta name="viewport" content="width=device-width, initial-scale=1">
285
+ <style>${PAGE_CSS}</style>
286
+ </head>
287
+ <body>
288
+ ${buildSnapshotCard(fm, parsed, taskName)}
289
+ ${buildTrackerCard(parsed)}
290
+ ${buildRecentActivityCard(parsed)}
291
+ ${buildHandoffCard(parsed)}
292
+ ${buildOpenLinks(taskDir, taskName, hasSvg)}
293
+ <div class="footer">
294
+ <code>dw task view ${escapeHtml(taskName)}</code> โ€” ADR-0008 ยท lean preview, full details in <code>task.md</code>
295
+ </div>
296
+ </body>
297
+ </html>`;
298
+ }
299
+
300
+ function openInBrowser(filePath) {
301
+ const platform = process.platform;
302
+ try {
303
+ if (platform === 'win32') {
304
+ execSync(`start "" "${filePath}"`, { stdio: 'ignore', shell: true });
305
+ } else if (platform === 'darwin') {
306
+ execSync(`open "${filePath}"`, { stdio: 'ignore' });
307
+ } else {
308
+ execSync(`xdg-open "${filePath}"`, { stdio: 'ignore' });
309
+ }
310
+ return true;
311
+ } catch {
312
+ return false;
313
+ }
314
+ }
315
+
316
+ async function viewOne(taskName, taskDir, opts) {
317
+ const rootDir = process.cwd();
318
+ const timelineFile = join(taskDir, 'task.md');
319
+ if (!existsSync(timelineFile)) {
320
+ return { ok: false, reason: 'no-timeline' };
321
+ }
322
+
323
+ const content = readFileSync(timelineFile, 'utf8');
324
+ const fm = parseFrontmatter(content);
325
+ const parsed = parseTimeline(content);
326
+ const html = buildHtml(taskName, taskDir, fm, parsed);
327
+
328
+ const cacheDir = join(rootDir, CACHE_DIR);
329
+ if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
330
+ const outFile = join(cacheDir, `${taskName}.html`);
331
+ writeFileSync(outFile, html, 'utf8');
332
+
333
+ const shouldOpen = opts.open !== false && !opts.noOpen;
334
+ logEvent({
335
+ event: 'task',
336
+ action: 'view.invoke',
337
+ name: taskName,
338
+ opened: shouldOpen,
339
+ }, rootDir);
340
+
341
+ let opened = false;
342
+ if (shouldOpen) {
343
+ opened = openInBrowser(outFile);
344
+ }
345
+
346
+ return { ok: true, outFile, opened };
347
+ }
348
+
349
+ export async function taskViewCommand(taskName, opts = {}) {
350
+ const rootDir = process.cwd();
351
+ let targets;
352
+ if (taskName) {
353
+ targets = [{ name: taskName, path: join(rootDir, TASKS_DIR, taskName) }];
354
+ } else {
355
+ const all = findV3Tasks(rootDir);
356
+ if (all.length === 0) {
357
+ console.log(chalk.dim(' No v3 tasks to view.'));
358
+ return;
359
+ }
360
+ if (all.length === 1) {
361
+ targets = all;
362
+ } else {
363
+ all.sort((a, b) => statSync(b.path).mtimeMs - statSync(a.path).mtimeMs);
364
+ targets = [all[0]];
365
+ console.log(chalk.dim(` No task specified โ€” picking most recent: ${all[0].name}`));
366
+ }
367
+ }
368
+
369
+ console.log();
370
+ for (const t of targets) {
371
+ const r = await viewOne(t.name, t.path, opts);
372
+ if (!r.ok) {
373
+ console.log(chalk.red(` โœ— ${t.name} โ€” ${r.reason}`));
374
+ continue;
375
+ }
376
+ console.log(chalk.green(` โœ“ ${t.name}`));
377
+ console.log(chalk.dim(` HTML: ${r.outFile}`));
378
+ const shouldOpen = opts.open !== false && !opts.noOpen;
379
+ if (r.opened) {
380
+ console.log(chalk.dim(` Browser opened`));
381
+ } else if (shouldOpen) {
382
+ console.log(chalk.yellow(` Could not auto-open browser โ€” open the file manually`));
383
+ }
384
+ }
385
+ console.log();
386
+ }