devlog-tracker 0.29.2 → 0.31.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 (37) hide show
  1. package/README.md +23 -7
  2. package/README.zh-TW.md +23 -7
  3. package/bin/devlog-tracker.js +14 -1
  4. package/claude/hooks.json +21 -0
  5. package/cli/agents-md.js +3 -0
  6. package/cli/agents-md.test.js +32 -0
  7. package/cli/core-script.js +25 -0
  8. package/cli/core-script.test.js +62 -0
  9. package/cli/platforms/claude.js +3 -0
  10. package/commands/overview.md +1 -1
  11. package/commands/pr.md +61 -0
  12. package/commands/promote.md +54 -0
  13. package/commands/report.md +23 -0
  14. package/commands/timeline.md +18 -0
  15. package/core/scripts/json-field.sh +16 -0
  16. package/core/scripts/lessons-subagent-done.sh +36 -0
  17. package/core/scripts/lessons-subagent-start.sh +28 -0
  18. package/core/scripts/pr-context.sh +64 -0
  19. package/core/scripts/promote-sources.sh +40 -0
  20. package/core/scripts/promote-target.sh +37 -0
  21. package/core/scripts/promote-write.sh +74 -0
  22. package/core/scripts/report-devlog.sh +140 -0
  23. package/core/scripts/report-scan.awk +74 -0
  24. package/core/scripts/round-start.sh +15 -1
  25. package/core/scripts/tests/test-json-field.sh +5 -0
  26. package/core/scripts/tests/test-lessons-subagent-hooks.sh +95 -0
  27. package/core/scripts/tests/test-pr-context.sh +107 -0
  28. package/core/scripts/tests/test-promote.sh +158 -0
  29. package/core/scripts/tests/test-report.sh +197 -0
  30. package/core/scripts/tests/test-round-start.sh +65 -0
  31. package/core/scripts/tests/test-timeline.sh +65 -0
  32. package/core/scripts/timeline-devlog.sh +41 -0
  33. package/core/scripts/timeline-render.js +247 -0
  34. package/core/scripts/timeline-render.test.js +101 -0
  35. package/package.json +2 -2
  36. package/skills/devlog-tracker/SKILL.md +5 -3
  37. package/skills/devlog-tracker/references/lessons-mode.md +10 -4
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // report-devlog.sh --json --rounds 的輸出 → 一份自足的 HTML 時間軸
4
+ // (docs/design/read-side-and-promote.md C)。零相依;所有 devlog 文字先跳脫
5
+ // 再轉 Markdown 子集,不輸出任何來自 devlog 的原始 HTML。
6
+
7
+ const STATUSES = ['DONE', 'IN_PROGRESS', 'BLOCKED', 'INTERRUPTED'];
8
+
9
+ function escapeHtml(s) {
10
+ return String(s)
11
+ .replace(/&/g, '&')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&#39;');
16
+ }
17
+
18
+ // http(s) 與相對路徑可以當連結;其他 scheme(javascript:、data: …)與
19
+ // protocol-relative(//host)一律不給連結。
20
+ function safeHref(url) {
21
+ const u = String(url).trim();
22
+ // A leading/embedded C0 control char (e.g. \u0001) hides the real scheme
23
+ // from the tests below, but browsers strip it and still run it — reject.
24
+ if (/[\u0000-\u001F\u007F]/.test(u)) return null;
25
+ if (/^https?:\/\//i.test(u)) return u;
26
+ if (u.startsWith('//')) return null;
27
+ if (/^[a-z][a-z0-9+.-]*:/i.test(u)) return null;
28
+ return u;
29
+ }
30
+
31
+ function emphasis(raw) {
32
+ return escapeHtml(raw).replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
33
+ }
34
+
35
+ function inline(text) {
36
+ return String(text)
37
+ .split(/(`[^`]+`)/)
38
+ .map((part) => {
39
+ if (/^`[^`]+`$/.test(part)) return `<code>${escapeHtml(part.slice(1, -1))}</code>`;
40
+ const re = /\[([^\]]+)\]\(([^)\s]+)\)/g;
41
+ let out = '';
42
+ let last = 0;
43
+ let m;
44
+ while ((m = re.exec(part))) {
45
+ out += emphasis(part.slice(last, m.index));
46
+ const href = safeHref(m[2]);
47
+ out += href === null ? emphasis(m[0]) : `<a href="${escapeHtml(href)}">${emphasis(m[1])}</a>`;
48
+ last = re.lastIndex;
49
+ }
50
+ return out + emphasis(part.slice(last));
51
+ })
52
+ .join('');
53
+ }
54
+
55
+ const FENCE = /^\s*```/;
56
+ const HEADING = /^(#{1,4})\s+(.*)$/;
57
+ const LIST_ITEM = /^(\s*)([-*]|\d+\.)\s+(.*)$/;
58
+ const TABLE_ROW = /^\s*\|/;
59
+ const TABLE_SEP = /^\s*\|?\s*:?-{3,}/;
60
+
61
+ function tableCells(line) {
62
+ return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
63
+ }
64
+
65
+ function renderMarkdown(md) {
66
+ const lines = String(md || '').split('\n');
67
+ const out = [];
68
+ let i = 0;
69
+ while (i < lines.length) {
70
+ const line = lines[i];
71
+ if (FENCE.test(line)) {
72
+ const buf = [];
73
+ i++;
74
+ while (i < lines.length && !FENCE.test(lines[i])) buf.push(lines[i++]);
75
+ i++;
76
+ out.push(`<pre><code>${escapeHtml(buf.join('\n'))}</code></pre>`);
77
+ continue;
78
+ }
79
+ const h = HEADING.exec(line);
80
+ if (h) {
81
+ const level = Math.min(h[1].length + 2, 6);
82
+ out.push(`<h${level}>${inline(h[2])}</h${level}>`);
83
+ i++;
84
+ continue;
85
+ }
86
+ if (TABLE_ROW.test(line) && i + 1 < lines.length && TABLE_SEP.test(lines[i + 1])) {
87
+ const head = tableCells(line);
88
+ i += 2;
89
+ const rows = [];
90
+ while (i < lines.length && TABLE_ROW.test(lines[i])) rows.push(tableCells(lines[i++]));
91
+ const th = head.map((c) => `<th>${inline(c)}</th>`).join('');
92
+ const tb = rows.map((r) => `<tr>${r.map((c) => `<td>${inline(c)}</td>`).join('')}</tr>`).join('');
93
+ out.push(`<table><thead><tr>${th}</tr></thead><tbody>${tb}</tbody></table>`);
94
+ continue;
95
+ }
96
+ if (LIST_ITEM.test(line)) {
97
+ const m = LIST_ITEM.exec(line);
98
+ const ordered = /\d/.test(m[2]);
99
+ const items = [];
100
+ while (i < lines.length) {
101
+ const m = LIST_ITEM.exec(lines[i]);
102
+ if (m) {
103
+ const isOrdered = /\d/.test(m[2]);
104
+ if (isOrdered !== ordered) break; // Different list type, stop here
105
+ items.push({ nested: m[1].length >= 2, text: m[3] });
106
+ i++;
107
+ } else if (items.length && /^\s{2,}\S/.test(lines[i])) {
108
+ items[items.length - 1].text += ' ' + lines[i].trim();
109
+ i++;
110
+ } else break;
111
+ }
112
+ const tag = ordered ? 'ol' : 'ul';
113
+ const lis = items.map((it) => `<li${it.nested ? ' class="nested"' : ''}>${inline(it.text)}</li>`).join('');
114
+ out.push(`<${tag}>${lis}</${tag}>`);
115
+ continue;
116
+ }
117
+ if (/^\s*$/.test(line)) {
118
+ i++;
119
+ continue;
120
+ }
121
+ // 至少吃掉目前這一行,避免「像表格但不是表格」的行卡住迴圈。
122
+ const para = [line];
123
+ i++;
124
+ while (
125
+ i < lines.length &&
126
+ !/^\s*$/.test(lines[i]) &&
127
+ !FENCE.test(lines[i]) &&
128
+ !HEADING.test(lines[i]) &&
129
+ !LIST_ITEM.test(lines[i]) &&
130
+ !TABLE_ROW.test(lines[i])
131
+ ) {
132
+ para.push(lines[i++]);
133
+ }
134
+ out.push(`<p>${para.map(inline).join('<br>')}</p>`);
135
+ }
136
+ return out.join('\n');
137
+ }
138
+
139
+ function statusOf(s) {
140
+ return STATUSES.includes(s) ? s : 'NONE';
141
+ }
142
+
143
+ function roundCard(r) {
144
+ const st = statusOf(r.status);
145
+ const parts = [];
146
+ if (r.input) parts.push(`<h4>User Input</h4><pre><code>${escapeHtml(r.input)}</code></pre>`);
147
+ if (r.reply) parts.push(`<h4>Reply</h4>${renderMarkdown(r.reply)}`);
148
+ if (r.handoff) parts.push(`<h4>Handoff</h4>${renderMarkdown(r.handoff)}`);
149
+ for (const seg of r.segments || []) parts.push(`<div class="seg">${renderMarkdown('#### ' + seg)}</div>`);
150
+ const details = parts.length ? `<details><summary>詳細</summary>${parts.join('\n')}</details>` : '';
151
+ return `<article class="card round" data-status="${st}" data-branch="${escapeHtml(r.branch)}">
152
+ <header><span class="badge b-${st}">${st === 'NONE' ? '—' : st}</span><strong>Round ${Number(r.n)}</strong><time>${escapeHtml(r.at || '')}</time><span class="branch">${escapeHtml(r.branch)}</span></header>
153
+ <div class="summary">${renderMarkdown(r.summary)}</div>
154
+ ${details}
155
+ </article>`;
156
+ }
157
+
158
+ function checkpointCard(c) {
159
+ return `<article class="card checkpoint" data-status="CHECKPOINT" data-branch="${escapeHtml(c.branch)}">
160
+ <header><span class="badge b-CHECKPOINT">CHECKPOINT</span><strong>${escapeHtml(c.heading)}</strong><span class="branch">${escapeHtml(c.branch)}</span></header>
161
+ <details open><summary>內容</summary>${renderMarkdown(c.body)}</details>
162
+ </article>`;
163
+ }
164
+
165
+ // Round 依時間排序;Checkpoint 沒有時間,排在同一檔案中位於它之前的最後一個
166
+ // Round 後面。
167
+ function timelineItems(data) {
168
+ const rounds = data.rounds || [];
169
+ const items = rounds.map((r, idx) => ({ key: r.at || '', order: 0, idx, html: roundCard(r) }));
170
+ (data.checkpoint_blocks || []).forEach((c, idx) => {
171
+ let prev = null;
172
+ for (const r of rounds) {
173
+ if (r.file === c.file && r.line < c.line && (!prev || r.line > prev.line)) prev = r;
174
+ }
175
+ items.push({ key: prev ? prev.at || '' : '', order: 1, idx, html: checkpointCard(c) });
176
+ });
177
+ items.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : a.order - b.order || a.idx - b.idx));
178
+ return items.map((it) => it.html);
179
+ }
180
+
181
+ const CSS = `:root{--bg:#f7f7f5;--fg:#1f2328;--muted:#656d76;--card:#fff;--line:#d0d7de;--code:#eff1f3;
182
+ --done:#1a7f37;--progress:#0969da;--blocked:#cf222e;--interrupted:#9a6700;--checkpoint:#8250df;--none:#6e7781}
183
+ @media (prefers-color-scheme:dark){:root{--bg:#0d1117;--fg:#e6edf3;--muted:#8d96a0;--card:#161b22;--line:#30363d;--code:#1f242c;
184
+ --done:#3fb950;--progress:#4493f8;--blocked:#f85149;--interrupted:#d29922;--checkpoint:#ab7df8;--none:#8d96a0}}
185
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.6 system-ui,-apple-system,"Segoe UI","Noto Sans TC",sans-serif}
186
+ main{max-width:920px;margin:0 auto;padding:24px 16px 64px}h1{font-size:22px;margin:0 0 4px}.meta{color:var(--muted);font-size:13px;margin-bottom:16px}
187
+ .stats{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}.stat{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:6px 12px;font-size:13px}
188
+ .stat b{font-size:17px;margin-right:4px}.filters{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px}
189
+ .filters input,.filters select{font:inherit;padding:6px 10px;border:1px solid var(--line);border-radius:6px;background:var(--card);color:var(--fg)}
190
+ .filters input{flex:1;min-width:160px}.card{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--none);border-radius:8px;padding:12px 16px;margin-bottom:12px;overflow-wrap:anywhere}
191
+ .card[data-status=DONE]{border-left-color:var(--done)}.card[data-status=IN_PROGRESS]{border-left-color:var(--progress)}
192
+ .card[data-status=BLOCKED]{border-left-color:var(--blocked)}.card[data-status=INTERRUPTED]{border-left-color:var(--interrupted)}
193
+ .card[data-status=CHECKPOINT]{border-left-color:var(--checkpoint);background:color-mix(in srgb,var(--checkpoint) 6%,var(--card))}
194
+ .card header{display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-size:14px}.card time,.card .branch{color:var(--muted);font-size:12px}
195
+ .badge{font-size:11px;font-weight:600;padding:1px 8px;border-radius:999px;color:#fff;background:var(--none)}
196
+ .b-DONE{background:var(--done)}.b-IN_PROGRESS{background:var(--progress)}.b-BLOCKED{background:var(--blocked)}.b-INTERRUPTED{background:var(--interrupted)}.b-CHECKPOINT{background:var(--checkpoint)}
197
+ details summary{cursor:pointer;color:var(--muted);font-size:13px;margin-top:6px}code{background:var(--code);padding:1px 4px;border-radius:4px;font-size:.9em}
198
+ pre{background:var(--code);padding:10px;border-radius:6px;overflow-x:auto}pre code{padding:0;background:none}
199
+ table{border-collapse:collapse;display:block;overflow-x:auto}th,td{border:1px solid var(--line);padding:4px 8px;text-align:left}
200
+ li.nested{margin-left:1.5em}h3,h4,h5,h6{margin:12px 0 4px}.seg{border-top:1px dashed var(--line);margin-top:8px}.empty{color:var(--muted)}`;
201
+
202
+ const SCRIPT = `(function(){var q=document.getElementById('q'),s=document.getElementById('fs'),b=document.getElementById('fb');
203
+ function f(){var t=q.value.toLowerCase(),sv=s.value,bv=b.value;document.querySelectorAll('.card').forEach(function(c){
204
+ c.hidden=!((!sv||c.dataset.status===sv)&&(!bv||c.dataset.branch===bv)&&(!t||c.textContent.toLowerCase().indexOf(t)>=0));});}
205
+ [q,s,b].forEach(function(e){e.addEventListener('input',f);});})();`;
206
+
207
+ function render(data) {
208
+ const d = data || {};
209
+ const branches = [...new Set([...(d.rounds || []), ...(d.checkpoint_blocks || [])].map((x) => x.branch))].sort();
210
+ const stat = (label, n) => `<span class="stat"><b>${Number(n) || 0}</b>${label}</span>`;
211
+ const range = d.first_round_at ? `${escapeHtml(d.first_round_at)} → ${escapeHtml(d.last_round_at || '')}` : '沒有 Round';
212
+ const items = timelineItems(d);
213
+ const opt = (v, label) => `<option value="${escapeHtml(v)}">${escapeHtml(label)}</option>`;
214
+ return `<!doctype html>
215
+ <html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
216
+ <title>Devlog Timeline</title><style>${CSS}</style></head><body><main>
217
+ <h1>Devlog Timeline</h1>
218
+ <div class="meta">branch ${escapeHtml(d.branch || '')} · ${range}</div>
219
+ <div class="stats">${stat('Rounds', d.rounds_total)}${stat('DONE', d.status_done)}${stat('IN_PROGRESS', d.status_in_progress)}${stat('BLOCKED', d.status_blocked)}${stat('INTERRUPTED', d.status_interrupted)}${stat('Checkpoints', d.checkpoints)}</div>
220
+ <div class="filters"><input id="q" type="search" placeholder="關鍵字篩選">
221
+ <select id="fs">${opt('', '全部 Status')}${[...STATUSES, 'CHECKPOINT'].map((s) => opt(s, s)).join('')}</select>
222
+ <select id="fb">${opt('', '全部 branch')}${branches.map((b) => opt(b, b)).join('')}</select></div>
223
+ ${items.length ? items.join('\n') : '<p class="empty">沒有 Round 可顯示。</p>'}
224
+ </main><script>${SCRIPT}</script></body></html>
225
+ `;
226
+ }
227
+
228
+ if (require.main === module) {
229
+ let input = '';
230
+ process.stdin.setEncoding('utf8');
231
+ process.stdin.on('data', (chunk) => {
232
+ input += chunk;
233
+ });
234
+ process.stdin.on('end', () => {
235
+ let data;
236
+ try {
237
+ data = JSON.parse(input);
238
+ } catch (e) {
239
+ process.stderr.write('INVALID_JSON\n');
240
+ process.exitCode = 1;
241
+ return;
242
+ }
243
+ process.stdout.write(render(data));
244
+ });
245
+ }
246
+
247
+ module.exports = { render, renderMarkdown, inline, escapeHtml, safeHref, timelineItems };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const { render, renderMarkdown, safeHref, escapeHtml } = require('./timeline-render');
7
+
8
+ function round(extra) {
9
+ return { branch: 'main', file: 'devlog.md', line: 1, n: 1, at: '2026-09-01T10:00:00+0800',
10
+ status: 'DONE', summary: 's', reply: '', handoff: '', segments: [], ...extra };
11
+ }
12
+
13
+ test('escapeHtml escapes the five HTML metacharacters', () => {
14
+ assert.equal(escapeHtml(`<a href="x">'&'</a>`), '&lt;a href=&quot;x&quot;&gt;&#39;&amp;&#39;&lt;/a&gt;');
15
+ });
16
+
17
+ test('safeHref allows http(s) and relative, rejects other schemes', () => {
18
+ assert.equal(safeHref('https://example.com'), 'https://example.com');
19
+ assert.equal(safeHref('docs/design/x.md'), 'docs/design/x.md');
20
+ assert.equal(safeHref('javascript:alert(1)'), null);
21
+ assert.equal(safeHref('JaVaScRiPt:alert(1)'), null);
22
+ assert.equal(safeHref('data:text/html,x'), null);
23
+ assert.equal(safeHref('//evil.example'), null);
24
+ });
25
+
26
+ test('safeHref rejects a leading C0 control char that browsers strip', () => {
27
+ assert.equal(safeHref('\u0001javascript:alert(1)'), null);
28
+ const html = renderMarkdown('[x](\u0001javascript:alert(1))');
29
+ assert.ok(!/href=/i.test(html));
30
+ });
31
+
32
+ test('renderMarkdown never emits raw HTML from content', () => {
33
+ const html = renderMarkdown('<script>alert(1)</script>\n<img src=x onerror=alert(1)>');
34
+ assert.ok(!html.includes('<script>'));
35
+ assert.ok(!html.includes('<img'));
36
+ assert.ok(html.includes('&lt;script&gt;'));
37
+ });
38
+
39
+ test('renderMarkdown neutralises javascript: links', () => {
40
+ const html = renderMarkdown('[x](javascript:alert(1)) [ok](https://a.example)');
41
+ assert.ok(!/href="javascript/i.test(html));
42
+ assert.ok(html.includes('<a href="https://a.example">ok</a>'));
43
+ });
44
+
45
+ test('renderMarkdown subset: headings, lists, fences, inline code, bold, tables', () => {
46
+ const html = renderMarkdown('#### 現況\n- a\n - b\n1. one\n\n```\n<b>\n```\nuse `x<y` and **bold**\n\n| h | i |\n|---|---|\n| 1 | 2 |');
47
+ assert.ok(html.includes('<h6>現況</h6>'));
48
+ assert.ok(html.includes('<ul><li>a</li><li class="nested">b</li></ul>'));
49
+ assert.ok(html.includes('<ol><li>one</li></ol>'));
50
+ assert.ok(html.includes('<pre><code>&lt;b&gt;</code></pre>'));
51
+ assert.ok(html.includes('<code>x&lt;y</code>'));
52
+ assert.ok(html.includes('<strong>bold</strong>'));
53
+ assert.ok(html.includes('<th>h</th>') && html.includes('<td>2</td>'));
54
+ });
55
+
56
+ test('a pipe line that is not a table does not hang', () => {
57
+ assert.equal(renderMarkdown('| not a table'), '<p>| not a table</p>');
58
+ });
59
+
60
+ test('render: status badges, CJK, checkpoint after its preceding round', () => {
61
+ const html = render({
62
+ started: true, branch: 'main', rounds_total: 2, status_done: 1, status_blocked: 1,
63
+ first_round_at: '2026-09-01T10:00:00+0800', last_round_at: '2026-09-02T10:00:00+0800',
64
+ rounds: [
65
+ round({ n: 2, line: 20, at: '2026-09-02T10:00:00+0800', status: 'BLOCKED', summary: '卡住了' }),
66
+ round({ n: 1, line: 1, summary: '中文摘要', segments: ['段落 探索\nseg body'] }),
67
+ ],
68
+ checkpoint_blocks: [{ branch: 'main', file: 'devlog.md', line: 10, heading: 'Checkpoint(Round 1-1)', body: '### 決策\n- d' }],
69
+ });
70
+ assert.ok(html.startsWith('<!doctype html>'));
71
+ assert.ok(html.includes('中文摘要') && html.includes('卡住了'));
72
+ assert.ok(html.includes('data-status="BLOCKED"') && html.includes('b-DONE'));
73
+ const r1 = html.indexOf('Round 1</strong>');
74
+ const cp = html.indexOf('Checkpoint(Round 1-1)');
75
+ const r2 = html.indexOf('Round 2</strong>');
76
+ assert.ok(r1 < cp && cp < r2, 'order should be Round 1, Checkpoint, Round 2');
77
+ assert.ok(html.includes('<h6>段落 探索</h6>'));
78
+ assert.ok(!/<(script|link)[^>]+(src|href)=/i.test(html), 'no external resources');
79
+ });
80
+
81
+ test('render: unknown status renders as NONE; no input section by default', () => {
82
+ const html = render({ rounds: [round({ status: '' })], checkpoint_blocks: [] });
83
+ assert.ok(html.includes('data-status="NONE"'));
84
+ assert.ok(!html.includes('User Input'));
85
+ const withInput = render({ rounds: [round({ input: '<secret>' })], checkpoint_blocks: [] });
86
+ assert.ok(withInput.includes('User Input') && withInput.includes('&lt;secret&gt;'));
87
+ });
88
+
89
+ test('render: empty data shows an empty-state message', () => {
90
+ assert.ok(render({ started: true, rounds: [], checkpoint_blocks: [] }).includes('沒有 Round 可顯示'));
91
+ });
92
+
93
+ test('CLI: stdin JSON -> stdout HTML; invalid JSON -> exit 1', () => {
94
+ const script = path.join(__dirname, 'timeline-render.js');
95
+ const ok = spawnSync(process.execPath, [script], { input: JSON.stringify({ rounds: [round({})] }), encoding: 'utf8' });
96
+ assert.equal(ok.status, 0);
97
+ assert.ok(ok.stdout.includes('Round 1'));
98
+ const bad = spawnSync(process.execPath, [script], { input: 'not json', encoding: 'utf8' });
99
+ assert.equal(bad.status, 1);
100
+ assert.equal(bad.stderr, 'INVALID_JSON\n');
101
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlog-tracker",
3
- "version": "0.29.2",
3
+ "version": "0.31.0",
4
4
  "description": "npx installer for devlog-tracker's Claude Code, Cursor and Codex adapters.",
5
5
  "bin": {
6
6
  "devlog-tracker": "bin/devlog-tracker.js"
@@ -21,7 +21,7 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "scripts": {
24
- "test": "node --test cli/*.test.js scripts/*.test.js",
24
+ "test": "node --test cli/*.test.js cli/platforms/*.test.js scripts/*.test.js core/scripts/*.test.js",
25
25
  "version": "node scripts/sync-version.js && git add .claude-plugin/plugin.json .claude-plugin/marketplace.json README.md README.zh-TW.md"
26
26
  },
27
27
  "license": "Apache-2.0",
@@ -315,7 +315,7 @@ Stop hook 會檢查最後一個 Round 是否同時有 `### Summary`、`### Reply
315
315
 
316
316
  這一輪如果是使用者直接呼叫 devlog-tracker 自己的純管理指令——`/devlog-tracker:checkpoint`、
317
317
  `clean`、`compact`、`keep`、`lessons`、`lessons-drift`、`lessons-off`、`lessons-on`、
318
- `overview`、`pause`、`search`、`segment-watch`、`span`、`start`、`status`——`round-start.sh`
318
+ `overview`、`pause`、`report`、`search`、`segment-watch`、`span`、`start`、`status`、`timeline`——`round-start.sh`
319
319
  會整輪直接放行,不開 Round、不動任何計數器,等於這個 tick 沒發生過;不用、也不會被
320
320
  Stop hook 要求補寫 Summary/Reply/Handoff。這些指令本身就是在操作 devlog 系統,不是開發
321
321
  工作,記錄下來對接續開發沒有幫助。
@@ -467,8 +467,10 @@ Handoff。核對用 `commands/continue.md` 步驟 5.1–5.2(不要跟著做 5.
467
467
  若這輪任務是透過 Agent 工具派 sub agent,或用 Workflow 工具跑多階段 pipeline,一樣可能
468
468
  踩到值得記的坑,只是沒有 Round/Status 可比對訊號;讀完 sub agent/workflow 的最終回報後
469
469
  自我判斷(例如 verify 推翻了它先前的 fix、它自陳繞了一圈、多個 agent 重複卡在同一種問題、
470
- 或成果被打回票要求重做),值得的話一樣呼叫 `lessons-append.sh`——sub agent/workflow 本身
471
- 不會、也不需要知道這個機制存在。
470
+ 或成果被打回票要求重做),值得的話一樣呼叫 `lessons-append.sh`。Claude Code 上 hook 會自動
471
+ 觸發:sub agent/Workflow agent 開始時被注入 `lessons-append.sh` 的絕對路徑用法、可以自己記;
472
+ 它回來或背景任務通知到達時,你會看到一句 `[Lessons Mode 提示]`(`failed`/`killed` 另算進
473
+ 共用計數器)。sub agent 已記過的不用重複記。
472
474
 
473
475
  寫法、per-topic 存檔規則、索引重建、機制性訊號細節,見
474
476
  `${CLAUDE_PLUGIN_ROOT}/skills/devlog-tracker/references/lessons-mode.md`(完整
@@ -49,8 +49,14 @@ verify 階段推翻了 sub agent 先前的 fix/claim、sub agent 自陳繞了
49
49
  裡多個 agent 各自卡在類似問題(彙整成一筆更有代表性的)、sub agent 的成果被使用者或
50
50
  reviewer 打回票要求重做。
51
51
 
52
- sub agent 不需要知道 Lessons Mode 存在;不新增回報格式,你覺得這次任務可能踩雷時,自己
53
- 決定要不要在 dispatch prompt 裡順口提一句「回報時順便說一下有沒有繞路」。永遠是你(主
54
- session)讀完回報後自己判斷主題、呼叫 `lessons-append.sh`——sub agent/workflow 本身不
55
- 直接呼叫,避免 worktree isolation 下環境變數指錯專案目錄。詳見
52
+ Claude Code 上 hook 會自動觸發(只在 Lessons Mode 開著時):
53
+ - sub agent/Workflow agent 開始時(`SubagentStart`),hook 會把 `lessons-append.sh` 的用法
54
+ 連同**主專案絕對路徑**注入它的 context,它覺得值得就自己記一筆,並在最終回報裡說一聲。
55
+ worktree isolation 下也照那條絕對路徑指令跑,不會寫錯地方。
56
+ - 前景 sub agent 回來時(`PostToolUse`),或背景 sub agent/Workflow 的 task-notification
57
+ 到達時(`round-start.sh`),你會看到一句 `[Lessons Mode 提示]`,提醒你檢查上面四種訊號;
58
+ 背景任務 `status` 是 `failed`/`killed` 時還會算進共用計數器。
59
+
60
+ 看到提示後:sub agent 已說明記過的主題不要重複記;多個 agent 卡在同一種問題時,由你彙整成
61
+ 一筆更有代表性的。一樣全部非強制。Codex/Cursor 沒有對應 hook,仍是你自己判斷。詳見
56
62
  `docs/design/lessons-mode.md`「sub agent/workflow 情境的自我判斷訊號」。