@ulysses-ai/create-workspace 0.14.0-beta.1 → 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.
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ // Generate shared-context/index.md by walking shared-context/ and reading frontmatter.
3
+ //
4
+ // Source of truth is the filesystem. Hand edits to index.md are overwritten.
5
+ //
6
+ // Usage:
7
+ // node build-shared-context-index.mjs --write [--root <workspace-root>]
8
+ // node build-shared-context-index.mjs --check [--root <workspace-root>]
9
+ //
10
+ // --write regenerates shared-context/index.md.
11
+ // --check exits 0 if the on-disk index matches what would be generated, 1 otherwise.
12
+
13
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
14
+ import { join, relative, sep } from 'node:path';
15
+ import { parseSessionContent } from '../lib/session-frontmatter.mjs';
16
+
17
+ const INDEX_FILENAME = 'index.md';
18
+ const LOCKED_DIR = 'locked';
19
+ const IGNORE_FILENAME = '.indexignore';
20
+
21
+ function parseArgs(argv) {
22
+ const args = { mode: null, root: process.cwd() };
23
+ for (let i = 2; i < argv.length; i++) {
24
+ const a = argv[i];
25
+ if (a === '--write') args.mode = 'write';
26
+ else if (a === '--check') args.mode = 'check';
27
+ else if (a === '--root') args.root = argv[++i];
28
+ }
29
+ if (!args.mode) {
30
+ throw new Error('Specify --write or --check');
31
+ }
32
+ return args;
33
+ }
34
+
35
+ function walkMarkdown(dir) {
36
+ const out = [];
37
+ for (const name of readdirSync(dir)) {
38
+ const full = join(dir, name);
39
+ const st = statSync(full);
40
+ if (st.isDirectory()) {
41
+ out.push(...walkMarkdown(full));
42
+ } else if (st.isFile() && name.endsWith('.md')) {
43
+ out.push(full);
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ function readDescription(filePath) {
50
+ let frontmatter = {};
51
+ let body = '';
52
+ try {
53
+ const parsed = parseSessionContent(readFileSync(filePath, 'utf-8'));
54
+ frontmatter = parsed.fields || {};
55
+ body = parsed.body || '';
56
+ } catch {
57
+ body = readFileSync(filePath, 'utf-8');
58
+ }
59
+
60
+ if (typeof frontmatter.description === 'string' && frontmatter.description.trim()) {
61
+ return frontmatter.description.trim();
62
+ }
63
+
64
+ const stripped = body.replace(/^#.*$/m, '').trim();
65
+ const firstParagraph = stripped.split(/\n\s*\n/, 1)[0] || '';
66
+ const firstSentence = firstParagraph.replace(/\n/g, ' ').match(/[^.!?]+[.!?]/);
67
+ if (firstSentence) {
68
+ const candidate = firstSentence[0].trim();
69
+ if (candidate.length > 0 && candidate.length <= 200) return candidate;
70
+ }
71
+
72
+ const filename = filePath.split(sep).pop() || '';
73
+ return filename.replace(/\.md$/, '').replace(/-/g, ' ');
74
+ }
75
+
76
+ function classify(relativePath) {
77
+ const parts = relativePath.split(sep);
78
+ if (parts.length === 1) return { group: '__root__', sortKey: parts[0] };
79
+ if (parts[0] === LOCKED_DIR) return { group: LOCKED_DIR, sortKey: parts.slice(1).join('/') };
80
+ return { group: parts[0], sortKey: parts.slice(1).join('/') };
81
+ }
82
+
83
+ function readIgnorePrefixes(sharedContextDir) {
84
+ const ignorePath = join(sharedContextDir, IGNORE_FILENAME);
85
+ if (!existsSync(ignorePath)) return [];
86
+ return readFileSync(ignorePath, 'utf-8')
87
+ .split('\n')
88
+ .map((line) => line.replace(/#.*/, '').trim())
89
+ .filter((line) => line.length > 0);
90
+ }
91
+
92
+ function isIgnored(relativePath, prefixes) {
93
+ for (const prefix of prefixes) {
94
+ if (relativePath === prefix) return true;
95
+ if (relativePath.startsWith(prefix.endsWith('/') ? prefix : prefix + '/')) return true;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ function buildEntries(workspaceRoot) {
101
+ const sharedContextDir = join(workspaceRoot, 'shared-context');
102
+ if (!existsSync(sharedContextDir)) return [];
103
+
104
+ const ignorePrefixes = readIgnorePrefixes(sharedContextDir);
105
+
106
+ const files = walkMarkdown(sharedContextDir).filter((f) => {
107
+ const rel = relative(sharedContextDir, f).split(sep).join('/');
108
+ if (rel === INDEX_FILENAME) return false;
109
+ if (isIgnored(rel, ignorePrefixes)) return false;
110
+ return true;
111
+ });
112
+
113
+ return files
114
+ .map((file) => {
115
+ const rel = relative(sharedContextDir, file);
116
+ const { group, sortKey } = classify(rel);
117
+ return {
118
+ group,
119
+ sortKey,
120
+ relativePath: rel.split(sep).join('/'),
121
+ description: readDescription(file),
122
+ };
123
+ })
124
+ .sort((a, b) => {
125
+ const groupOrder = (g) =>
126
+ g === LOCKED_DIR ? 0 : g === '__root__' ? 1 : 2;
127
+ const ga = groupOrder(a.group);
128
+ const gb = groupOrder(b.group);
129
+ if (ga !== gb) return ga - gb;
130
+ if (a.group !== b.group) return a.group.localeCompare(b.group);
131
+ return a.sortKey.localeCompare(b.sortKey);
132
+ });
133
+ }
134
+
135
+ function groupHeading(group) {
136
+ if (group === LOCKED_DIR) return 'Locked (team truths, always loaded)';
137
+ if (group === '__root__') return 'Team-shared (root)';
138
+ return group;
139
+ }
140
+
141
+ function renderIndex(entries, generatedAt) {
142
+ const lines = [
143
+ '---',
144
+ 'type: index',
145
+ `generated: ${generatedAt}`,
146
+ '---',
147
+ '',
148
+ '# shared-context — index',
149
+ '',
150
+ '> Auto-generated by `.claude/scripts/build-shared-context-index.mjs`. Hand edits will be overwritten — update the source files instead.',
151
+ '',
152
+ ];
153
+
154
+ let currentGroup = null;
155
+ for (const entry of entries) {
156
+ if (entry.group !== currentGroup) {
157
+ if (currentGroup !== null) lines.push('');
158
+ lines.push(`## ${groupHeading(entry.group)}`, '');
159
+ currentGroup = entry.group;
160
+ }
161
+ lines.push(`- [${entry.relativePath}](${entry.relativePath}) — ${entry.description}`);
162
+ }
163
+
164
+ if (entries.length === 0) {
165
+ lines.push('_(no shared-context files yet)_');
166
+ }
167
+
168
+ return lines.join('\n') + '\n';
169
+ }
170
+
171
+ function fingerprint(content) {
172
+ return content
173
+ .split('\n')
174
+ .filter((line) => !line.startsWith('generated:'))
175
+ .join('\n');
176
+ }
177
+
178
+ function main() {
179
+ const args = parseArgs(process.argv);
180
+ const indexPath = join(args.root, 'shared-context', INDEX_FILENAME);
181
+ const entries = buildEntries(args.root);
182
+ const generatedAt = new Date().toISOString();
183
+ const fresh = renderIndex(entries, generatedAt);
184
+
185
+ if (args.mode === 'check') {
186
+ if (!existsSync(indexPath)) {
187
+ process.stdout.write(JSON.stringify({ status: 'missing' }) + '\n');
188
+ process.exit(1);
189
+ }
190
+ const onDisk = readFileSync(indexPath, 'utf-8');
191
+ if (fingerprint(onDisk) === fingerprint(fresh)) {
192
+ process.stdout.write(JSON.stringify({ status: 'current', entries: entries.length }) + '\n');
193
+ process.exit(0);
194
+ }
195
+ process.stdout.write(JSON.stringify({ status: 'stale', entries: entries.length }) + '\n');
196
+ process.exit(1);
197
+ }
198
+
199
+ if (args.mode === 'write') {
200
+ writeFileSync(indexPath, fresh);
201
+ process.stdout.write(
202
+ JSON.stringify({ status: 'written', entries: entries.length, path: indexPath }) + '\n',
203
+ );
204
+ process.exit(0);
205
+ }
206
+ }
207
+
208
+ if (import.meta.url === `file://${process.argv[1]}`) {
209
+ main();
210
+ }
211
+
212
+ export { buildEntries, renderIndex, fingerprint, readDescription };
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+ // Unit tests for build-shared-context-index.mjs
3
+ // Run: node template/.claude/scripts/build-shared-context-index.test.mjs
4
+
5
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { buildEntries, renderIndex, fingerprint, readDescription } from './build-shared-context-index.mjs';
9
+
10
+ let failed = 0;
11
+ let passed = 0;
12
+
13
+ function assert(cond, msg) {
14
+ if (cond) { passed++; } else { failed++; console.error(` FAIL: ${msg}`); }
15
+ }
16
+
17
+ function assertEq(actual, expected, msg) {
18
+ const a = JSON.stringify(actual);
19
+ const e = JSON.stringify(expected);
20
+ if (a === e) { passed++; } else {
21
+ failed++;
22
+ console.error(` FAIL: ${msg}\n expected: ${e}\n actual: ${a}`);
23
+ }
24
+ }
25
+
26
+ function setupFixture() {
27
+ const root = mkdtempSync(join(tmpdir(), 'sc-index-test-'));
28
+ mkdirSync(join(root, 'shared-context'), { recursive: true });
29
+ mkdirSync(join(root, 'shared-context', 'locked'), { recursive: true });
30
+ mkdirSync(join(root, 'shared-context', 'alice'), { recursive: true });
31
+ return root;
32
+ }
33
+
34
+ function cleanup(root) {
35
+ rmSync(root, { recursive: true, force: true });
36
+ }
37
+
38
+ console.log('# readDescription priority');
39
+
40
+ {
41
+ const root = setupFixture();
42
+ writeFileSync(
43
+ join(root, 'shared-context', 'with-fm.md'),
44
+ `---
45
+ state: locked
46
+ type: reference
47
+ description: Frontmatter description wins.
48
+ updated: 2026-04-25
49
+ ---
50
+
51
+ # Title
52
+
53
+ This is the body.
54
+ `,
55
+ );
56
+ const desc = readDescription(join(root, 'shared-context', 'with-fm.md'));
57
+ assertEq(desc, 'Frontmatter description wins.', 'frontmatter description preferred');
58
+ cleanup(root);
59
+ }
60
+
61
+ {
62
+ const root = setupFixture();
63
+ writeFileSync(
64
+ join(root, 'shared-context', 'no-fm-desc.md'),
65
+ `---
66
+ state: ephemeral
67
+ type: braindump
68
+ updated: 2026-04-25
69
+ ---
70
+
71
+ # Some Title
72
+
73
+ This is the first paragraph that should be used. It has multiple sentences.
74
+
75
+ This is a second paragraph.
76
+ `,
77
+ );
78
+ const desc = readDescription(join(root, 'shared-context', 'no-fm-desc.md'));
79
+ assertEq(desc, 'This is the first paragraph that should be used.', 'first sentence fallback');
80
+ cleanup(root);
81
+ }
82
+
83
+ {
84
+ const root = setupFixture();
85
+ writeFileSync(
86
+ join(root, 'shared-context', 'no-frontmatter.md'),
87
+ `# Bare File
88
+
89
+ Content without frontmatter at all.
90
+ `,
91
+ );
92
+ const desc = readDescription(join(root, 'shared-context', 'no-frontmatter.md'));
93
+ assertEq(desc, 'Content without frontmatter at all.', 'works without frontmatter');
94
+ cleanup(root);
95
+ }
96
+
97
+ {
98
+ const root = setupFixture();
99
+ writeFileSync(join(root, 'shared-context', 'empty-body.md'), `---
100
+ type: index
101
+ ---
102
+ `);
103
+ const desc = readDescription(join(root, 'shared-context', 'empty-body.md'));
104
+ assertEq(desc, 'empty body', 'filename slug fallback');
105
+ cleanup(root);
106
+ }
107
+
108
+ console.log('# buildEntries grouping & sort');
109
+
110
+ {
111
+ const root = setupFixture();
112
+ writeFileSync(
113
+ join(root, 'shared-context', 'locked', 'project-status.md'),
114
+ `---
115
+ description: Project status here.
116
+ ---
117
+
118
+ body
119
+ `,
120
+ );
121
+ writeFileSync(
122
+ join(root, 'shared-context', 'locked', 'naming.md'),
123
+ `---
124
+ description: Naming convention.
125
+ ---
126
+
127
+ body
128
+ `,
129
+ );
130
+ writeFileSync(
131
+ join(root, 'shared-context', 'inventory.md'),
132
+ `---
133
+ description: Top-level inventory.
134
+ ---
135
+
136
+ body
137
+ `,
138
+ );
139
+ writeFileSync(
140
+ join(root, 'shared-context', 'alice', 'notes.md'),
141
+ `---
142
+ description: Alice notes.
143
+ ---
144
+
145
+ body
146
+ `,
147
+ );
148
+
149
+ const entries = buildEntries(root);
150
+ assertEq(entries.length, 4, '4 entries');
151
+ assertEq(entries[0].group, 'locked', 'locked first');
152
+ assertEq(entries[0].sortKey, 'naming.md', 'alphabetical within locked (naming before project)');
153
+ assertEq(entries[1].sortKey, 'project-status.md', 'second locked entry');
154
+ assertEq(entries[2].group, '__root__', 'root group second');
155
+ assertEq(entries[3].group, 'alice', 'user dir last');
156
+
157
+ cleanup(root);
158
+ }
159
+
160
+ console.log('# renderIndex output');
161
+
162
+ {
163
+ const entries = [
164
+ { group: 'locked', sortKey: 'a.md', relativePath: 'locked/a.md', description: 'A.' },
165
+ { group: '__root__', sortKey: 'b.md', relativePath: 'b.md', description: 'B.' },
166
+ { group: 'alice', sortKey: 'c.md', relativePath: 'alice/c.md', description: 'C.' },
167
+ ];
168
+ const output = renderIndex(entries, '2026-04-25T00:00:00Z');
169
+ assert(output.includes('## Locked (team truths, always loaded)'), 'has locked heading');
170
+ assert(output.includes('## Team-shared (root)'), 'has team-shared heading');
171
+ assert(output.includes('## alice'), 'has user dir heading');
172
+ assert(output.includes('- [locked/a.md](locked/a.md) — A.'), 'links use relative path');
173
+ assert(output.includes('generated: 2026-04-25T00:00:00Z'), 'frontmatter has generated timestamp');
174
+ }
175
+
176
+ {
177
+ const output = renderIndex([], '2026-04-25T00:00:00Z');
178
+ assert(output.includes('_(no shared-context files yet)_'), 'empty state');
179
+ }
180
+
181
+ console.log('# fingerprint ignores generated line');
182
+
183
+ {
184
+ const a = `---
185
+ type: index
186
+ generated: 2026-04-25T00:00:00Z
187
+ ---
188
+
189
+ body
190
+ `;
191
+ const b = `---
192
+ type: index
193
+ generated: 2026-04-26T00:00:00Z
194
+ ---
195
+
196
+ body
197
+ `;
198
+ assertEq(fingerprint(a), fingerprint(b), 'different generated, same body → same fingerprint');
199
+ }
200
+
201
+ {
202
+ const a = `---
203
+ generated: 2026-04-25T00:00:00Z
204
+ ---
205
+
206
+ body one
207
+ `;
208
+ const b = `---
209
+ generated: 2026-04-25T00:00:00Z
210
+ ---
211
+
212
+ body two
213
+ `;
214
+ assert(fingerprint(a) !== fingerprint(b), 'different body → different fingerprint');
215
+ }
216
+
217
+ console.log('# missing shared-context dir');
218
+
219
+ {
220
+ const root = mkdtempSync(join(tmpdir(), 'sc-index-empty-'));
221
+ const entries = buildEntries(root);
222
+ assertEq(entries, [], 'no shared-context dir yields empty entries');
223
+ cleanup(root);
224
+ }
225
+
226
+ console.log('# .indexignore prefix excludes');
227
+
228
+ {
229
+ const root = setupFixture();
230
+ mkdirSync(join(root, 'shared-context', 'archive'), { recursive: true });
231
+ writeFileSync(
232
+ join(root, 'shared-context', 'archive', 'old1.md'),
233
+ `---
234
+ description: Archived 1.
235
+ ---
236
+ body
237
+ `,
238
+ );
239
+ writeFileSync(
240
+ join(root, 'shared-context', 'archive', 'old2.md'),
241
+ `---
242
+ description: Archived 2.
243
+ ---
244
+ body
245
+ `,
246
+ );
247
+ writeFileSync(
248
+ join(root, 'shared-context', 'live.md'),
249
+ `---
250
+ description: Live file.
251
+ ---
252
+ body
253
+ `,
254
+ );
255
+ writeFileSync(join(root, 'shared-context', '.indexignore'), 'archive/\n# comment\n\n');
256
+
257
+ const entries = buildEntries(root);
258
+ assertEq(entries.length, 1, 'archive/ excluded by .indexignore');
259
+ assertEq(entries[0].relativePath, 'live.md', 'only live.md survives');
260
+ cleanup(root);
261
+ }
262
+
263
+ {
264
+ const root = setupFixture();
265
+ writeFileSync(
266
+ join(root, 'shared-context', 'a.md'),
267
+ `---
268
+ description: A.
269
+ ---
270
+ body
271
+ `,
272
+ );
273
+ writeFileSync(
274
+ join(root, 'shared-context', 'b.md'),
275
+ `---
276
+ description: B.
277
+ ---
278
+ body
279
+ `,
280
+ );
281
+ writeFileSync(join(root, 'shared-context', '.indexignore'), 'a.md\n');
282
+ const entries = buildEntries(root);
283
+ assertEq(entries.length, 1, 'specific file excluded by .indexignore');
284
+ assertEq(entries[0].relativePath, 'b.md', 'only b.md survives');
285
+ cleanup(root);
286
+ }
287
+
288
+ console.log('# index.md is excluded from its own entries');
289
+
290
+ {
291
+ const root = setupFixture();
292
+ writeFileSync(
293
+ join(root, 'shared-context', 'index.md'),
294
+ `---
295
+ type: index
296
+ ---
297
+
298
+ # index
299
+ `,
300
+ );
301
+ writeFileSync(
302
+ join(root, 'shared-context', 'real.md'),
303
+ `---
304
+ description: Real file.
305
+ ---
306
+
307
+ body
308
+ `,
309
+ );
310
+ const entries = buildEntries(root);
311
+ assertEq(entries.length, 1, 'only the non-index file');
312
+ assertEq(entries[0].relativePath, 'real.md', 'index.md excluded');
313
+ cleanup(root);
314
+ }
315
+
316
+ console.log('');
317
+ console.log(`${passed} passed, ${failed} failed`);
318
+ process.exit(failed > 0 ? 1 : 0);
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ // Idempotent migrator: ensures CLAUDE.md includes @local-only-template-freshness.md.
3
+ // Appends one line at end if missing. Preserves the rest of the file byte-for-byte.
4
+ //
5
+ // Run standalone: node .claude/scripts/migrate-claude-md-freshness-include.mjs
6
+ // Or import { runMigration } and call programmatically.
7
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
8
+ import { join, dirname, resolve } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const INCLUDE_LINE = '@local-only-template-freshness.md';
12
+
13
+ export function runMigration({ workspaceRoot }) {
14
+ const path = join(workspaceRoot, 'CLAUDE.md');
15
+ if (!existsSync(path)) return { action: 'skipped', reason: 'no-claude-md' };
16
+ const before = readFileSync(path, 'utf-8');
17
+ if (before.includes(INCLUDE_LINE)) return { action: 'unchanged' };
18
+ const after = before.endsWith('\n') ? before + INCLUDE_LINE + '\n' : before + '\n' + INCLUDE_LINE + '\n';
19
+ writeFileSync(path, after);
20
+ return { action: 'appended' };
21
+ }
22
+
23
+ // CLI entry point — workspace root is two levels up from this file
24
+ // (.claude/scripts/migrate-... → workspace root).
25
+ if (import.meta.url === `file://${process.argv[1]}`) {
26
+ const here = dirname(fileURLToPath(import.meta.url));
27
+ const root = resolve(here, '..', '..');
28
+ const result = runMigration({ workspaceRoot: root });
29
+ console.log(JSON.stringify(result));
30
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // Unit tests for migrate-claude-md-freshness-include.mjs
3
+ // Run: node template/.claude/scripts/migrate-claude-md-freshness-include.test.mjs
4
+ import { runMigration } from './migrate-claude-md-freshness-include.mjs';
5
+ import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { tmpdir } from 'os';
8
+
9
+ let failed = 0;
10
+ let passed = 0;
11
+ function assertEq(actual, expected, msg) {
12
+ const a = JSON.stringify(actual);
13
+ const e = JSON.stringify(expected);
14
+ if (a === e) { passed++; } else {
15
+ failed++;
16
+ console.error(` FAIL: ${msg}\n expected: ${e}\n actual: ${a}`);
17
+ }
18
+ }
19
+
20
+ function withTemp(fn) {
21
+ const dir = mkdtempSync(join(tmpdir(), 'claude-md-mig-'));
22
+ try { fn(dir); } finally { rmSync(dir, { recursive: true, force: true }); }
23
+ }
24
+
25
+ console.log('# migrate-claude-md-freshness-include');
26
+
27
+ // CLAUDE.md missing → no-op
28
+ withTemp((dir) => {
29
+ const result = runMigration({ workspaceRoot: dir });
30
+ assertEq(result.action, 'skipped', 'no CLAUDE.md is skipped');
31
+ });
32
+
33
+ // CLAUDE.md missing the include → appended
34
+ withTemp((dir) => {
35
+ const path = join(dir, 'CLAUDE.md');
36
+ writeFileSync(path, '# Workspace\n@workspace.json\n');
37
+ const result = runMigration({ workspaceRoot: dir });
38
+ assertEq(result.action, 'appended', 'missing line is appended');
39
+ const after = readFileSync(path, 'utf-8');
40
+ assertEq(after.includes('@local-only-template-freshness.md'), true, 'line present after migration');
41
+ });
42
+
43
+ // CLAUDE.md already has the include → no-op
44
+ withTemp((dir) => {
45
+ const path = join(dir, 'CLAUDE.md');
46
+ const original = '# Workspace\n@workspace.json\n@local-only-template-freshness.md\n';
47
+ writeFileSync(path, original);
48
+ const result = runMigration({ workspaceRoot: dir });
49
+ assertEq(result.action, 'unchanged', 'already-present line is unchanged');
50
+ assertEq(readFileSync(path, 'utf-8'), original, 'file content unchanged');
51
+ });
52
+
53
+ console.log(`\n${passed} passed, ${failed} failed`);
54
+ process.exit(failed > 0 ? 1 : 0);
@@ -15,6 +15,12 @@
15
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
  }