@ulysses-ai/create-workspace 0.13.0-beta.2 → 0.14.0-beta.3
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/README.md +15 -3
- package/package.json +1 -1
- package/template/.claude/hooks/_bash-output-advisory.test.mjs +88 -0
- package/template/.claude/hooks/bash-output-advisory.mjs +77 -0
- package/template/.claude/hooks/version-freshness-check.mjs +30 -0
- package/template/.claude/lib/freshness.mjs +75 -0
- package/template/.claude/lib/freshness.test.mjs +175 -0
- package/template/.claude/lib/registry-check.mjs +106 -0
- package/template/.claude/lib/registry-check.test.mjs +130 -0
- package/template/.claude/rules/memory-guidance.md +47 -0
- package/template/.claude/rules/task-list-mirroring.md +69 -0
- package/template/.claude/rules/token-economics.md.skip +23 -8
- package/template/.claude/rules/workspace-structure.md +2 -0
- package/template/.claude/scripts/build-shared-context-index.mjs +212 -0
- package/template/.claude/scripts/build-shared-context-index.test.mjs +318 -0
- package/template/.claude/scripts/migrate-claude-md-freshness-include.mjs +30 -0
- package/template/.claude/scripts/migrate-claude-md-freshness-include.test.mjs +54 -0
- package/template/.claude/scripts/sync-tasks.mjs +234 -0
- package/template/.claude/scripts/sync-tasks.test.mjs +350 -0
- package/template/.claude/settings.json +20 -9
- package/template/.claude/skills/braindump/SKILL.md +15 -0
- package/template/.claude/skills/build-docs-site/SKILL.md +1 -1
- package/template/.claude/skills/build-docs-site/checklists/pitfalls.md +4 -4
- package/template/.claude/skills/complete-work/SKILL.md +47 -55
- package/template/.claude/skills/handoff/SKILL.md +15 -0
- package/template/.claude/skills/maintenance/SKILL.md +49 -7
- package/template/.claude/skills/pause-work/SKILL.md +25 -4
- package/template/.claude/skills/release/SKILL.md +59 -43
- package/template/.claude/skills/start-work/SKILL.md +34 -2
- package/template/.claude/skills/workspace-update/SKILL.md +16 -0
- package/template/CLAUDE.md.tmpl +1 -0
- package/template/_gitignore +2 -3
- package/template/workspace.json.tmpl +1 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Unit tests for sync-tasks.mjs
|
|
3
|
+
// Run: node template/.claude/scripts/sync-tasks.test.mjs
|
|
4
|
+
import { toActiveForm } from './sync-tasks.mjs';
|
|
5
|
+
|
|
6
|
+
let failed = 0;
|
|
7
|
+
let passed = 0;
|
|
8
|
+
|
|
9
|
+
function assert(cond, msg) {
|
|
10
|
+
if (cond) { passed++; } else { failed++; console.error(` FAIL: ${msg}`); }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function assertEq(actual, expected, msg) {
|
|
14
|
+
const a = JSON.stringify(actual);
|
|
15
|
+
const e = JSON.stringify(expected);
|
|
16
|
+
if (a === e) { passed++; } else {
|
|
17
|
+
failed++;
|
|
18
|
+
console.error(` FAIL: ${msg}\n expected: ${e}\n actual: ${a}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log('# toActiveForm');
|
|
23
|
+
assertEq(toActiveForm('Start work'), 'Starting work', 'simple verb');
|
|
24
|
+
assertEq(toActiveForm('Write fix and test'), 'Writing fix and test', 'e-drop');
|
|
25
|
+
assertEq(toActiveForm('Run the tests'), 'Running the tests', 'double consonant');
|
|
26
|
+
assertEq(toActiveForm('Identify race condition'), 'Identifying race condition', 'y-keep');
|
|
27
|
+
assertEq(toActiveForm('Complete work'), 'Completing work', 'e-drop on Complete');
|
|
28
|
+
assertEq(toActiveForm('Reproduce on iOS Safari'), 'Reproducing on iOS Safari', 'e-drop multi-word');
|
|
29
|
+
assertEq(toActiveForm('Fix bug'), 'Fixing bug', 'simple');
|
|
30
|
+
|
|
31
|
+
import { parseTasksSection } from './sync-tasks.mjs';
|
|
32
|
+
|
|
33
|
+
const SAMPLE_WITH_TASKS = `---
|
|
34
|
+
type: session-tracker
|
|
35
|
+
name: demo
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
# Work Session
|
|
39
|
+
|
|
40
|
+
## Tasks
|
|
41
|
+
|
|
42
|
+
> Linked: gh:42 — Auth timeout on mobile
|
|
43
|
+
|
|
44
|
+
- [x] Start work
|
|
45
|
+
- [x] Reproduce on iOS Safari
|
|
46
|
+
- [ ] Identify race condition
|
|
47
|
+
- [ ] Complete work
|
|
48
|
+
|
|
49
|
+
## Progress
|
|
50
|
+
|
|
51
|
+
(stuff)
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const SAMPLE_NO_TASKS = `---
|
|
55
|
+
type: session-tracker
|
|
56
|
+
name: demo
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
# Work Session
|
|
60
|
+
|
|
61
|
+
## Progress
|
|
62
|
+
|
|
63
|
+
(stuff)
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const SAMPLE_NO_LINK = `---
|
|
67
|
+
type: session-tracker
|
|
68
|
+
name: demo
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Tasks
|
|
72
|
+
|
|
73
|
+
- [x] Start work
|
|
74
|
+
- [ ] Complete work
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
console.log('\n# parseTasksSection');
|
|
78
|
+
assertEq(
|
|
79
|
+
parseTasksSection(SAMPLE_WITH_TASKS).linked,
|
|
80
|
+
{ id: 'gh:42', title: 'Auth timeout on mobile' },
|
|
81
|
+
'parses linked blockquote'
|
|
82
|
+
);
|
|
83
|
+
assertEq(
|
|
84
|
+
parseTasksSection(SAMPLE_WITH_TASKS).todos.length,
|
|
85
|
+
4,
|
|
86
|
+
'parses 4 todos'
|
|
87
|
+
);
|
|
88
|
+
assertEq(
|
|
89
|
+
parseTasksSection(SAMPLE_WITH_TASKS).todos[0],
|
|
90
|
+
{ content: 'Start work', activeForm: 'Starting work', status: 'completed' },
|
|
91
|
+
'first todo completed'
|
|
92
|
+
);
|
|
93
|
+
assertEq(
|
|
94
|
+
parseTasksSection(SAMPLE_WITH_TASKS).todos[2],
|
|
95
|
+
{ content: 'Identify race condition', activeForm: 'Identifying race condition', status: 'pending' },
|
|
96
|
+
'pending todo'
|
|
97
|
+
);
|
|
98
|
+
assertEq(
|
|
99
|
+
parseTasksSection(SAMPLE_NO_TASKS),
|
|
100
|
+
{ linked: null, todos: [] },
|
|
101
|
+
'missing section returns empty'
|
|
102
|
+
);
|
|
103
|
+
assertEq(
|
|
104
|
+
parseTasksSection(SAMPLE_NO_LINK).linked,
|
|
105
|
+
null,
|
|
106
|
+
'no blockquote → linked: null'
|
|
107
|
+
);
|
|
108
|
+
assertEq(
|
|
109
|
+
parseTasksSection(SAMPLE_NO_LINK).todos.length,
|
|
110
|
+
2,
|
|
111
|
+
'no blockquote → still parses todos'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
import { renderTasksSection, enforceBookends } from './sync-tasks.mjs';
|
|
115
|
+
|
|
116
|
+
console.log('\n# enforceBookends');
|
|
117
|
+
assertEq(
|
|
118
|
+
enforceBookends([]).map(t => t.content),
|
|
119
|
+
['Start work', 'Complete work'],
|
|
120
|
+
'empty list → bookends inserted'
|
|
121
|
+
);
|
|
122
|
+
assertEq(
|
|
123
|
+
enforceBookends([
|
|
124
|
+
{ content: 'Do thing', activeForm: 'Doing thing', status: 'pending' },
|
|
125
|
+
]).map(t => t.content),
|
|
126
|
+
['Start work', 'Do thing', 'Complete work'],
|
|
127
|
+
'middle task gets wrapped in bookends'
|
|
128
|
+
);
|
|
129
|
+
assertEq(
|
|
130
|
+
enforceBookends([
|
|
131
|
+
{ content: 'Complete work', activeForm: 'Completing work', status: 'pending' },
|
|
132
|
+
{ content: 'Do thing', activeForm: 'Doing thing', status: 'pending' },
|
|
133
|
+
{ content: 'Start work', activeForm: 'Starting work', status: 'completed' },
|
|
134
|
+
]).map(t => t.content),
|
|
135
|
+
['Start work', 'Do thing', 'Complete work'],
|
|
136
|
+
'misplaced bookends moved to ends'
|
|
137
|
+
);
|
|
138
|
+
assertEq(
|
|
139
|
+
enforceBookends([{ content: 'Start work', activeForm: 'Starting work', status: 'completed' }])[0].status,
|
|
140
|
+
'completed',
|
|
141
|
+
'preserves Start work status when present'
|
|
142
|
+
);
|
|
143
|
+
assertEq(
|
|
144
|
+
enforceBookends([])[0].status,
|
|
145
|
+
'completed',
|
|
146
|
+
'inserted Start work defaults to completed'
|
|
147
|
+
);
|
|
148
|
+
assertEq(
|
|
149
|
+
enforceBookends([])[1].status,
|
|
150
|
+
'pending',
|
|
151
|
+
'inserted Complete work defaults to pending'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
console.log('\n# renderTasksSection');
|
|
155
|
+
assertEq(
|
|
156
|
+
renderTasksSection({
|
|
157
|
+
linked: null,
|
|
158
|
+
todos: [
|
|
159
|
+
{ content: 'Start work', activeForm: 'Starting work', status: 'completed' },
|
|
160
|
+
{ content: 'Do thing', activeForm: 'Doing thing', status: 'pending' },
|
|
161
|
+
{ content: 'Complete work', activeForm: 'Completing work', status: 'pending' },
|
|
162
|
+
],
|
|
163
|
+
}),
|
|
164
|
+
'## Tasks\n\n- [x] Start work\n- [ ] Do thing\n- [ ] Complete work\n',
|
|
165
|
+
'no link → no blockquote'
|
|
166
|
+
);
|
|
167
|
+
assertEq(
|
|
168
|
+
renderTasksSection({
|
|
169
|
+
linked: { id: 'gh:42', title: 'Auth timeout on mobile' },
|
|
170
|
+
todos: [
|
|
171
|
+
{ content: 'Start work', activeForm: 'Starting work', status: 'completed' },
|
|
172
|
+
{ content: 'Complete work', activeForm: 'Completing work', status: 'pending' },
|
|
173
|
+
],
|
|
174
|
+
}),
|
|
175
|
+
'## Tasks\n\n> Linked: gh:42 — Auth timeout on mobile\n\n- [x] Start work\n- [ ] Complete work\n',
|
|
176
|
+
'with link → blockquote rendered'
|
|
177
|
+
);
|
|
178
|
+
assertEq(
|
|
179
|
+
renderTasksSection({
|
|
180
|
+
linked: { id: 'gh:42', title: null },
|
|
181
|
+
todos: [
|
|
182
|
+
{ content: 'Start work', activeForm: 'Starting work', status: 'completed' },
|
|
183
|
+
{ content: 'Complete work', activeForm: 'Completing work', status: 'pending' },
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
'## Tasks\n\n> Linked: gh:42\n\n- [x] Start work\n- [ ] Complete work\n',
|
|
187
|
+
'link with null title → bare ID'
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
import { writeTasksToSession } from './sync-tasks.mjs';
|
|
191
|
+
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs';
|
|
192
|
+
import { tmpdir } from 'os';
|
|
193
|
+
import { join } from 'path';
|
|
194
|
+
|
|
195
|
+
console.log('\n# writeTasksToSession');
|
|
196
|
+
|
|
197
|
+
function withTempSession(initialContent, fn) {
|
|
198
|
+
const dir = mkdtempSync(join(tmpdir(), 'sync-tasks-'));
|
|
199
|
+
const file = join(dir, 'session.md');
|
|
200
|
+
writeFileSync(file, initialContent);
|
|
201
|
+
try { fn(file); } finally { rmSync(dir, { recursive: true, force: true }); }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const FRESH_SESSION = `---
|
|
205
|
+
type: session-tracker
|
|
206
|
+
name: demo
|
|
207
|
+
workItem: gh:42
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Work Session: demo
|
|
212
|
+
|
|
213
|
+
description here.
|
|
214
|
+
|
|
215
|
+
## Progress
|
|
216
|
+
|
|
217
|
+
(Updated as the session progresses)
|
|
218
|
+
`;
|
|
219
|
+
|
|
220
|
+
withTempSession(FRESH_SESSION, (file) => {
|
|
221
|
+
writeTasksToSession(file, {
|
|
222
|
+
linked: { id: 'gh:42', title: 'Auth timeout on mobile' },
|
|
223
|
+
todos: [],
|
|
224
|
+
});
|
|
225
|
+
const after = readFileSync(file, 'utf-8');
|
|
226
|
+
assert(after.includes('## Tasks'), 'inserts ## Tasks section');
|
|
227
|
+
assert(after.includes('> Linked: gh:42 — Auth timeout on mobile'), 'inserts blockquote');
|
|
228
|
+
assert(after.includes('- [x] Start work'), 'inserts Start work bookend');
|
|
229
|
+
assert(after.includes('- [ ] Complete work'), 'inserts Complete work bookend');
|
|
230
|
+
assert(after.includes('## Progress'), 'preserves Progress heading');
|
|
231
|
+
assert(after.includes('(Updated as the session progresses)'), 'preserves Progress body');
|
|
232
|
+
assert(after.startsWith('---\ntype: session-tracker'), 'preserves frontmatter');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const SESSION_WITH_TASKS = `---
|
|
236
|
+
type: session-tracker
|
|
237
|
+
name: demo
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# Work Session: demo
|
|
242
|
+
|
|
243
|
+
## Tasks
|
|
244
|
+
|
|
245
|
+
- [x] Start work
|
|
246
|
+
- [ ] Old task
|
|
247
|
+
- [ ] Complete work
|
|
248
|
+
|
|
249
|
+
## Progress
|
|
250
|
+
|
|
251
|
+
original progress text
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
withTempSession(SESSION_WITH_TASKS, (file) => {
|
|
255
|
+
writeTasksToSession(file, {
|
|
256
|
+
linked: null,
|
|
257
|
+
todos: [
|
|
258
|
+
{ content: 'Start work', activeForm: 'Starting work', status: 'completed' },
|
|
259
|
+
{ content: 'New task', activeForm: 'Doing new task', status: 'in_progress' },
|
|
260
|
+
{ content: 'Complete work', activeForm: 'Completing work', status: 'pending' },
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
const after = readFileSync(file, 'utf-8');
|
|
264
|
+
assert(after.includes('- [-] New task'), 'replaced with new task (in_progress → [-])');
|
|
265
|
+
assert(!after.includes('- [ ] Old task'), 'old task removed');
|
|
266
|
+
assert(after.includes('original progress text'), 'preserves Progress body');
|
|
267
|
+
const taskHeadingCount = (after.match(/^## Tasks$/gm) || []).length;
|
|
268
|
+
assertEq(taskHeadingCount, 1, 'exactly one ## Tasks section after rewrite');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
withTempSession(FRESH_SESSION, (file) => {
|
|
272
|
+
const input = {
|
|
273
|
+
linked: { id: 'gh:42', title: 'Auth timeout on mobile' },
|
|
274
|
+
todos: [
|
|
275
|
+
{ content: 'Reproduce', activeForm: 'Reproducing', status: 'completed' },
|
|
276
|
+
{ content: 'Fix it', activeForm: 'Fixing it', status: 'pending' },
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
writeTasksToSession(file, input);
|
|
280
|
+
const round = parseTasksSection(readFileSync(file, 'utf-8'));
|
|
281
|
+
assertEq(round.linked, input.linked, 'round-trip linked');
|
|
282
|
+
assertEq(round.todos.length, 4, 'round-trip todos length (with bookends)');
|
|
283
|
+
assertEq(round.todos[1].content, 'Reproduce', 'round-trip middle task content');
|
|
284
|
+
assertEq(round.todos[2].status, 'pending', 'round-trip middle task status');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
console.log('\n# in_progress round-trip');
|
|
288
|
+
|
|
289
|
+
withTempSession(FRESH_SESSION, (file) => {
|
|
290
|
+
const input = {
|
|
291
|
+
linked: null,
|
|
292
|
+
todos: [
|
|
293
|
+
{ content: 'Doing thing', activeForm: 'Doing thing', status: 'in_progress' },
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
writeTasksToSession(file, input);
|
|
297
|
+
const after = readFileSync(file, 'utf-8');
|
|
298
|
+
assert(after.includes('- [-] Doing thing'), 'in_progress renders as [-]');
|
|
299
|
+
const round = parseTasksSection(after);
|
|
300
|
+
const middle = round.todos.find(t => t.content === 'Doing thing');
|
|
301
|
+
assertEq(middle.status, 'in_progress', 'in_progress round-trips losslessly');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
import { execFileSync } from 'child_process';
|
|
305
|
+
import { fileURLToPath } from 'url';
|
|
306
|
+
import { dirname } from 'path';
|
|
307
|
+
|
|
308
|
+
const SCRIPT_PATH = join(dirname(fileURLToPath(import.meta.url)), 'sync-tasks.mjs');
|
|
309
|
+
|
|
310
|
+
console.log('\n# CLI');
|
|
311
|
+
|
|
312
|
+
withTempSession(FRESH_SESSION, (file) => {
|
|
313
|
+
const input = JSON.stringify({
|
|
314
|
+
todos: [
|
|
315
|
+
{ content: 'Start work', activeForm: 'Starting work', status: 'completed' },
|
|
316
|
+
{ content: 'Test thing', activeForm: 'Testing thing', status: 'in_progress' },
|
|
317
|
+
{ content: 'Complete work', activeForm: 'Completing work', status: 'pending' },
|
|
318
|
+
],
|
|
319
|
+
});
|
|
320
|
+
execFileSync('node', [SCRIPT_PATH, '--write', file], {
|
|
321
|
+
input,
|
|
322
|
+
encoding: 'utf-8',
|
|
323
|
+
});
|
|
324
|
+
const written = readFileSync(file, 'utf-8');
|
|
325
|
+
assert(written.includes('- [-] Test thing'), 'CLI --write rendered task (in_progress → [-])');
|
|
326
|
+
|
|
327
|
+
const out = execFileSync('node', [SCRIPT_PATH, '--read', file], {
|
|
328
|
+
encoding: 'utf-8',
|
|
329
|
+
});
|
|
330
|
+
const parsed = JSON.parse(out);
|
|
331
|
+
assertEq(parsed.todos.length, 3, 'CLI --read returns 3 todos');
|
|
332
|
+
assertEq(parsed.todos[1].content, 'Test thing', 'CLI --read content');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
withTempSession('not a session file\n', (file) => {
|
|
336
|
+
let threw = false;
|
|
337
|
+
try {
|
|
338
|
+
execFileSync('node', [SCRIPT_PATH, '--read', file], {
|
|
339
|
+
encoding: 'utf-8',
|
|
340
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
341
|
+
});
|
|
342
|
+
} catch (e) {
|
|
343
|
+
threw = true;
|
|
344
|
+
assert(e.stderr.includes('Not a session') || e.stderr.includes('frontmatter'), 'errors on non-session file');
|
|
345
|
+
}
|
|
346
|
+
assert(threw, 'CLI throws on non-session file');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
350
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -6,15 +6,21 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/workspace-update-check.mjs",
|
|
9
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/workspace-update-check.mjs",
|
|
10
10
|
"timeout": 5000,
|
|
11
11
|
"statusMessage": "Checking for workspace updates..."
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"type": "command",
|
|
15
|
-
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/session-start.mjs",
|
|
15
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/session-start.mjs",
|
|
16
16
|
"timeout": 30000,
|
|
17
17
|
"statusMessage": "Syncing workspace..."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/version-freshness-check.mjs",
|
|
22
|
+
"timeout": 5000,
|
|
23
|
+
"statusMessage": "Checking template freshness..."
|
|
18
24
|
}
|
|
19
25
|
]
|
|
20
26
|
}
|
|
@@ -24,7 +30,7 @@
|
|
|
24
30
|
"hooks": [
|
|
25
31
|
{
|
|
26
32
|
"type": "command",
|
|
27
|
-
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/subagent-start.mjs",
|
|
33
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/subagent-start.mjs",
|
|
28
34
|
"timeout": 5000,
|
|
29
35
|
"statusMessage": "Loading shared context..."
|
|
30
36
|
}
|
|
@@ -36,7 +42,7 @@
|
|
|
36
42
|
"hooks": [
|
|
37
43
|
{
|
|
38
44
|
"type": "command",
|
|
39
|
-
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/pre-compact.mjs",
|
|
45
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/pre-compact.mjs",
|
|
40
46
|
"timeout": 5000,
|
|
41
47
|
"statusMessage": "Checking for uncaptured context..."
|
|
42
48
|
}
|
|
@@ -48,7 +54,7 @@
|
|
|
48
54
|
"hooks": [
|
|
49
55
|
{
|
|
50
56
|
"type": "command",
|
|
51
|
-
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/post-compact.mjs",
|
|
57
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/post-compact.mjs",
|
|
52
58
|
"timeout": 5000
|
|
53
59
|
}
|
|
54
60
|
]
|
|
@@ -59,12 +65,17 @@
|
|
|
59
65
|
"hooks": [
|
|
60
66
|
{
|
|
61
67
|
"type": "command",
|
|
62
|
-
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/workspace-update-check.mjs",
|
|
68
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/workspace-update-check.mjs",
|
|
69
|
+
"timeout": 5000
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"type": "command",
|
|
73
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/repo-write-detection.mjs",
|
|
63
74
|
"timeout": 5000
|
|
64
75
|
},
|
|
65
76
|
{
|
|
66
77
|
"type": "command",
|
|
67
|
-
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/
|
|
78
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/bash-output-advisory.mjs",
|
|
68
79
|
"timeout": 5000
|
|
69
80
|
}
|
|
70
81
|
]
|
|
@@ -75,7 +86,7 @@
|
|
|
75
86
|
"hooks": [
|
|
76
87
|
{
|
|
77
88
|
"type": "command",
|
|
78
|
-
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/session-end.mjs",
|
|
89
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/session-end.mjs",
|
|
79
90
|
"timeout": 15000,
|
|
80
91
|
"statusMessage": "Saving session state..."
|
|
81
92
|
}
|
|
@@ -87,7 +98,7 @@
|
|
|
87
98
|
"hooks": [
|
|
88
99
|
{
|
|
89
100
|
"type": "command",
|
|
90
|
-
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"$CLAUDE_PROJECT_DIR\"; else echo \"$CLAUDE_PROJECT_DIR\"; fi)\"/.claude/hooks/worktree-create.mjs",
|
|
101
|
+
"command": "node \"$(if command -v cygpath >/dev/null 2>&1; then cygpath -u \"${CLAUDE_PROJECT_DIR:-$PWD}\"; else echo \"${CLAUDE_PROJECT_DIR:-$PWD}\"; fi)\"/.claude/hooks/worktree-create.mjs",
|
|
91
102
|
"timeout": 5000,
|
|
92
103
|
"statusMessage": "Checking for stale worktrees..."
|
|
93
104
|
}
|
|
@@ -72,6 +72,21 @@ updated: {YYYY-MM-DD}
|
|
|
72
72
|
3. If multiple topics: "I see discussion about {topic-1} and {topic-2}. Split into separate braindumps?"
|
|
73
73
|
4. Proceed with named flow for each
|
|
74
74
|
|
|
75
|
+
## Include task snapshot
|
|
76
|
+
|
|
77
|
+
If an active session exists (detected via `.claude/.active-session.json`), include a `## Tasks at capture time` section in the braindump artifact with a snapshot of the current `TodoWrite` state:
|
|
78
|
+
|
|
79
|
+
```markdown
|
|
80
|
+
## Tasks at capture time
|
|
81
|
+
|
|
82
|
+
- [x] Start work
|
|
83
|
+
- [x] Reproduce on iOS Safari
|
|
84
|
+
- [ ] Identify race condition
|
|
85
|
+
- [ ] Complete work
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Use the same GFM checkbox format as `session.md`'s `## Tasks` section (just `content` and `status` per task — no `activeForm` field, no blockquote line) and render it inline in the braindump. Do NOT call `sync-tasks.mjs --write` — braindumps are snapshots, not the canonical store.
|
|
89
|
+
|
|
75
90
|
## Updating Existing Braindumps
|
|
76
91
|
|
|
77
92
|
When updating, rewrite as a fresh snapshot (coherent-revisions rule). The updated braindump should read as if written in one pass.
|
|
@@ -298,7 +298,7 @@ Reuse aggressively — if the same concept appears in multiple chapters, import
|
|
|
298
298
|
|
|
299
299
|
- **CSS variables in SVG fill don't paint reliably** — always use `className={cls.fill.X}`, never `fill={...}`. The primitives already do this.
|
|
300
300
|
- **Bulk migration regressions** — if you wrote chapter components with `fill={colors.X}` and need to migrate, run `python3 .claude/skills/build-docs-site/scripts/bulk-fill-migration.py {chapters-dir}`. The script handles duplicate `className=`, missing imports, and reports variable-bound fills for manual review.
|
|
301
|
-
- **
|
|
301
|
+
- **Automated viewport screenshots can lie** — if a screenshot from the browser automation tool comes back blank, inspect the DOM via the tool's evaluate hook before assuming the diagram is broken.
|
|
302
302
|
- **Arrowhead markers don't theme-switch** — acceptable cosmetic mismatch, or render two markers with media query switching.
|
|
303
303
|
- **SSR for browser-only diagrams** — wrap any diagram that uses browser APIs in `<BrowserOnly>` with a function child. The primitives don't need this; it only matters if a chapter component uses `window` or `document`.
|
|
304
304
|
|
|
@@ -121,15 +121,15 @@ If the build fails on `cls is not defined`, manually add `cls` to the import lin
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
-
## 3.
|
|
124
|
+
## 3. Automated viewport screenshots can lie
|
|
125
125
|
|
|
126
126
|
### Symptom
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
Viewport-cropped screenshots of diagram regions come back blank from the browser automation tool, even though the diagrams render correctly when you load the page in a real browser.
|
|
129
129
|
|
|
130
130
|
### Cause
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
Scroll position and viewport sizing interact with the automated screenshot path such that the diagram region falls outside the captured frame even when the visible browser shows it. The exact interaction varies by tool and rarely rewards deep diagnosis — the workaround below is always sufficient.
|
|
133
133
|
|
|
134
134
|
### Fix
|
|
135
135
|
|
|
@@ -140,7 +140,7 @@ Best guess: scroll position and viewport sizing interact in Playwright's screens
|
|
|
140
140
|
await page.screenshot({ fullPage: true });
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
2. **DOM inspection via
|
|
143
|
+
2. **DOM inspection via the automation tool's evaluate hook** (fastest, most authoritative):
|
|
144
144
|
```js
|
|
145
145
|
const rect = document.querySelector('.dx-fill-primary');
|
|
146
146
|
console.log({
|