worclaude 1.7.0 → 1.9.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 (32) hide show
  1. package/README.md +11 -7
  2. package/package.json +1 -1
  3. package/src/commands/doctor.js +319 -0
  4. package/src/commands/init.js +8 -13
  5. package/src/commands/upgrade.js +6 -11
  6. package/src/core/config.js +19 -1
  7. package/src/core/merger.js +34 -16
  8. package/src/core/remover.js +6 -1
  9. package/src/core/scaffolder.js +20 -5
  10. package/src/data/agent-registry.js +28 -2
  11. package/src/data/agents.js +36 -4
  12. package/src/index.js +6 -0
  13. package/templates/agents/optional/docs/doc-writer.md +11 -0
  14. package/templates/agents/optional/quality/build-fixer.md +63 -0
  15. package/templates/agents/optional/quality/e2e-runner.md +95 -0
  16. package/templates/agents/optional/quality/performance-auditor.md +13 -0
  17. package/templates/agents/optional/quality/refactorer.md +19 -0
  18. package/templates/agents/universal/build-validator.md +45 -7
  19. package/templates/agents/universal/code-simplifier.md +62 -9
  20. package/templates/agents/universal/plan-reviewer.md +65 -13
  21. package/templates/agents/universal/test-writer.md +85 -9
  22. package/templates/agents/universal/verify-app.md +66 -8
  23. package/templates/commands/build-fix.md +36 -0
  24. package/templates/commands/commit-push-pr.md +31 -9
  25. package/templates/commands/end.md +7 -3
  26. package/templates/commands/refactor-clean.md +52 -0
  27. package/templates/commands/start.md +67 -7
  28. package/templates/commands/test-coverage.md +53 -0
  29. package/templates/core/claude-md.md +1 -0
  30. package/templates/settings/base.json +18 -2
  31. package/templates/skills/universal/context-management.md +55 -0
  32. package/templates/skills/universal/security-checklist.md +111 -0
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  [Full Documentation](https://sefaertunc.github.io/Worclaude/) · [Interactive Demo](https://sefaertunc.github.io/Worclaude/demo/) · [npm](https://www.npmjs.com/package/worclaude)
12
12
 
13
- Worclaude scaffolds a complete Claude Code workflow into any project in seconds. It implements all [tips by Boris Cherny](https://www.howborisusesclaudecode.com/) — the creator of Claude Code at Anthropic — as a reusable, upgradable scaffold. One `init` command gives you 23 agents, 12 slash commands, 13 skills, hooks, permissions, and a CLAUDE.md template tuned for your tech stack. Whether you're starting fresh or adding structure to an existing project, Worclaude handles the setup so you can focus on building.
13
+ Worclaude scaffolds a complete Claude Code workflow into any project in seconds. It implements all [tips by Boris Cherny](https://www.howborisusesclaudecode.com/) — the creator of Claude Code at Anthropic — as a reusable, upgradable scaffold. One `init` command gives you 25 agents, 16 slash commands, 14 skills, hooks, permissions, and a CLAUDE.md template tuned for your tech stack. Whether you're starting fresh or adding structure to an existing project, Worclaude handles the setup so you can focus on building.
14
14
 
15
15
  ---
16
16
 
@@ -18,25 +18,27 @@ Worclaude scaffolds a complete Claude Code workflow into any project in seconds.
18
18
 
19
19
  `worclaude init` installs a production-ready Claude Code workflow:
20
20
 
21
- **Agents (23 total)**
21
+ **Agents (25 total)**
22
22
 
23
23
  - 5 universal: plan-reviewer, code-simplifier, test-writer, build-validator, verify-app
24
- - 18 optional across 6 categories: Backend, Frontend, DevOps, Quality, Documentation, Data/AI
24
+ - 20 optional across 6 categories: Backend, Frontend, DevOps, Quality, Documentation, Data/AI
25
25
 
26
- **Slash Commands (12)**
27
- `/start` `/end` `/commit-push-pr` `/review-plan` `/techdebt` `/verify` `/compact-safe` `/status` `/update-claude-md` `/setup` `/sync` `/conflict-resolver`
26
+ **Slash Commands (16)**
27
+ `/start` `/end` `/commit-push-pr` `/review-plan` `/techdebt` `/verify` `/compact-safe` `/status` `/update-claude-md` `/setup` `/sync` `/conflict-resolver` `/review-changes` `/build-fix` `/refactor-clean` `/test-coverage`
28
28
 
29
- **Skills (13)**
29
+ **Skills (14)**
30
30
 
31
- - 9 universal knowledge files (testing, git conventions, context management, and more)
31
+ - 10 universal knowledge files (testing, git conventions, context management, security, and more)
32
32
  - 3 project-specific templates filled in by `/setup`
33
33
  - 1 generated agent routing guide (dynamically built from your agent selection)
34
34
 
35
35
  **Hooks**
36
36
 
37
+ - SessionStart context injection (auto-loads CLAUDE.md, PROGRESS.md, and last session on launch)
37
38
  - PostToolUse formatter (auto-formats on every write)
38
39
  - PostCompact re-injection (re-reads key files after compaction)
39
40
  - Stop notifications (desktop alert when Claude finishes)
41
+ - Hook profiles (`WORCLAUDE_HOOK_PROFILE`) — minimal, standard, or strict
40
42
 
41
43
  **Configuration**
42
44
 
@@ -70,6 +72,8 @@ For parallel tasks, run Claude with worktrees: `claude --worktree --tmux`
70
72
  | `worclaude backup` | Create timestamped backup of workflow files |
71
73
  | `worclaude restore` | Restore from a previous backup |
72
74
  | `worclaude diff` | Compare current setup vs latest version |
75
+ | `worclaude delete` | Remove worclaude workflow from project |
76
+ | `worclaude doctor` | Validate workflow installation health |
73
77
 
74
78
  The `init` command detects existing setups and merges intelligently — no data is overwritten without your confirmation. Use `upgrade` to pull in new features while preserving your customizations.
75
79
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worclaude",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "CLI tool that scaffolds a comprehensive Claude Code workflow into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,319 @@
1
+ import path from 'node:path';
2
+ import { readWorkflowMeta, workflowMetaExists, getPackageVersion } from '../core/config.js';
3
+ import { hashFile } from '../utils/hash.js';
4
+ import { fileExists, readFile, listFilesRecursive } from '../utils/file.js';
5
+ import {
6
+ UNIVERSAL_AGENTS,
7
+ COMMAND_FILES,
8
+ UNIVERSAL_SKILLS,
9
+ TEMPLATE_SKILLS,
10
+ } from '../data/agents.js';
11
+ import * as display from '../utils/display.js';
12
+
13
+ // Check categories
14
+ const PASS = 'pass';
15
+ const WARN = 'warn';
16
+ const FAIL = 'fail';
17
+
18
+ function result(status, label, detail) {
19
+ return { status, label, detail };
20
+ }
21
+
22
+ function printResult(r) {
23
+ const icon =
24
+ r.status === PASS
25
+ ? display.green('✓')
26
+ : r.status === WARN
27
+ ? display.yellow('⚠')
28
+ : display.red('✗');
29
+ const text = r.status === PASS ? display.dimColor(r.label) : display.white(r.label);
30
+ console.log(` ${icon} ${text}`);
31
+ if (r.detail && r.status !== PASS) {
32
+ console.log(` ${display.dimColor(r.detail)}`);
33
+ }
34
+ }
35
+
36
+ async function checkWorkflowMeta(projectRoot) {
37
+ if (!(await workflowMetaExists(projectRoot))) {
38
+ return result(FAIL, 'workflow-meta.json', 'Missing — run `worclaude init` to create');
39
+ }
40
+ const meta = await readWorkflowMeta(projectRoot);
41
+ if (!meta) {
42
+ return result(FAIL, 'workflow-meta.json', 'Exists but contains invalid JSON');
43
+ }
44
+ if (!meta.version || !meta.projectTypes || !meta.techStack) {
45
+ return result(
46
+ WARN,
47
+ 'workflow-meta.json',
48
+ 'Missing required fields (version, projectTypes, or techStack)'
49
+ );
50
+ }
51
+ return result(PASS, 'workflow-meta.json', null);
52
+ }
53
+
54
+ async function checkClaudeMd(projectRoot) {
55
+ const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
56
+ if (!(await fileExists(claudeMdPath))) {
57
+ return result(FAIL, 'CLAUDE.md', 'Missing — run `worclaude init` to create');
58
+ }
59
+ try {
60
+ const content = await readFile(claudeMdPath);
61
+ const lines = content.split('\n').length;
62
+ if (lines < 10) {
63
+ return result(
64
+ WARN,
65
+ 'CLAUDE.md',
66
+ `Only ${lines} lines — may be a stub. Run /setup to fill it in.`
67
+ );
68
+ }
69
+ return result(PASS, 'CLAUDE.md', null);
70
+ } catch {
71
+ return result(FAIL, 'CLAUDE.md', 'Exists but could not be read');
72
+ }
73
+ }
74
+
75
+ async function checkSettingsJson(projectRoot) {
76
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
77
+ if (!(await fileExists(settingsPath))) {
78
+ return result(FAIL, 'settings.json', 'Missing — run `worclaude init` to create');
79
+ }
80
+ try {
81
+ const raw = await readFile(settingsPath);
82
+ const settings = JSON.parse(raw);
83
+
84
+ const issues = [];
85
+ if (!settings.permissions?.allow || settings.permissions.allow.length === 0) {
86
+ issues.push('no permissions configured');
87
+ }
88
+ if (!settings.hooks || Object.keys(settings.hooks).length === 0) {
89
+ issues.push('no hooks configured');
90
+ }
91
+ if (!settings.hooks?.PostCompact) {
92
+ issues.push('missing PostCompact hook (context recovery after compaction)');
93
+ }
94
+ if (!settings.hooks?.SessionStart) {
95
+ issues.push('missing SessionStart hook (session persistence)');
96
+ }
97
+
98
+ if (issues.length > 0) {
99
+ return result(WARN, 'settings.json', issues.join('; '));
100
+ }
101
+ return result(PASS, 'settings.json', null);
102
+ } catch {
103
+ return result(FAIL, 'settings.json', 'Contains invalid JSON');
104
+ }
105
+ }
106
+
107
+ async function checkAgents(projectRoot, meta) {
108
+ const agentsDir = path.join(projectRoot, '.claude', 'agents');
109
+ const results = [];
110
+
111
+ // Check universal agents
112
+ for (const agent of UNIVERSAL_AGENTS) {
113
+ const agentPath = path.join(agentsDir, `${agent}.md`);
114
+ if (!(await fileExists(agentPath))) {
115
+ results.push(result(FAIL, `agents/${agent}.md`, 'Missing universal agent'));
116
+ }
117
+ }
118
+
119
+ // Check selected optional agents from meta
120
+ if (meta?.optionalAgents) {
121
+ for (const agent of meta.optionalAgents) {
122
+ const agentPath = path.join(agentsDir, `${agent}.md`);
123
+ if (!(await fileExists(agentPath))) {
124
+ results.push(result(WARN, `agents/${agent}.md`, 'Selected optional agent is missing'));
125
+ }
126
+ }
127
+ }
128
+
129
+ if (results.length === 0) {
130
+ const totalExpected = UNIVERSAL_AGENTS.length + (meta?.optionalAgents?.length || 0);
131
+ results.push(result(PASS, `agents/ (${totalExpected} expected, all present)`, null));
132
+ }
133
+
134
+ return results;
135
+ }
136
+
137
+ async function checkCommands(projectRoot) {
138
+ const commandsDir = path.join(projectRoot, '.claude', 'commands');
139
+ const missing = [];
140
+
141
+ for (const cmd of COMMAND_FILES) {
142
+ const cmdPath = path.join(commandsDir, `${cmd}.md`);
143
+ if (!(await fileExists(cmdPath))) {
144
+ missing.push(cmd);
145
+ }
146
+ }
147
+
148
+ if (missing.length === 0) {
149
+ return [result(PASS, `commands/ (${COMMAND_FILES.length} expected, all present)`, null)];
150
+ }
151
+ return missing.map((cmd) => result(WARN, `commands/${cmd}.md`, 'Missing command'));
152
+ }
153
+
154
+ async function checkSkills(projectRoot) {
155
+ const skillsDir = path.join(projectRoot, '.claude', 'skills');
156
+ const missing = [];
157
+ const allExpected = [...UNIVERSAL_SKILLS, ...TEMPLATE_SKILLS, 'agent-routing'];
158
+
159
+ for (const skill of allExpected) {
160
+ const skillPath = path.join(skillsDir, `${skill}.md`);
161
+ if (!(await fileExists(skillPath))) {
162
+ missing.push(skill);
163
+ }
164
+ }
165
+
166
+ if (missing.length === 0) {
167
+ return [result(PASS, `skills/ (${allExpected.length} expected, all present)`, null)];
168
+ }
169
+ return missing.map((s) => result(WARN, `skills/${s}.md`, 'Missing skill'));
170
+ }
171
+
172
+ async function checkHashIntegrity(projectRoot, meta) {
173
+ if (!meta?.fileHashes || Object.keys(meta.fileHashes).length === 0) {
174
+ return [result(WARN, 'File integrity', 'No file hashes in workflow-meta.json — cannot verify')];
175
+ }
176
+
177
+ let modified = 0;
178
+ let missing = 0;
179
+ let intact = 0;
180
+
181
+ for (const [relPath, storedHash] of Object.entries(meta.fileHashes)) {
182
+ const fullPath = path.join(projectRoot, '.claude', ...relPath.split('/'));
183
+ if (!(await fileExists(fullPath))) {
184
+ missing++;
185
+ } else {
186
+ const currentHash = await hashFile(fullPath);
187
+ if (currentHash !== storedHash) {
188
+ modified++;
189
+ } else {
190
+ intact++;
191
+ }
192
+ }
193
+ }
194
+
195
+ const total = Object.keys(meta.fileHashes).length;
196
+ const results = [];
197
+
198
+ if (missing > 0) {
199
+ results.push(result(FAIL, `File integrity: ${missing}/${total} files missing`, null));
200
+ }
201
+ if (modified > 0) {
202
+ results.push(
203
+ result(PASS, `File integrity: ${modified}/${total} files customized (expected)`, null)
204
+ );
205
+ }
206
+ if (results.length === 0) {
207
+ results.push(
208
+ result(PASS, `File integrity: all ${total} files present (${intact} intact)`, null)
209
+ );
210
+ }
211
+
212
+ return results;
213
+ }
214
+
215
+ async function checkSessions(projectRoot) {
216
+ const sessionsDir = path.join(projectRoot, '.claude', 'sessions');
217
+ if (!(await fileExists(sessionsDir))) {
218
+ return result(
219
+ WARN,
220
+ 'sessions/',
221
+ "Directory missing — session persistence won't work. Run `worclaude init` or create .claude/sessions/ manually."
222
+ );
223
+ }
224
+ return result(PASS, 'sessions/', null);
225
+ }
226
+
227
+ async function checkDocSpecs(projectRoot) {
228
+ const results = [];
229
+ const progressPath = path.join(projectRoot, 'docs', 'spec', 'PROGRESS.md');
230
+ const specPath = path.join(projectRoot, 'docs', 'spec', 'SPEC.md');
231
+
232
+ if (!(await fileExists(progressPath))) {
233
+ results.push(
234
+ result(WARN, 'docs/spec/PROGRESS.md', 'Missing — /start and /sync depend on this')
235
+ );
236
+ }
237
+ if (!(await fileExists(specPath))) {
238
+ results.push(result(WARN, 'docs/spec/SPEC.md', 'Missing — plan-reviewer references this'));
239
+ }
240
+
241
+ if (results.length === 0) {
242
+ results.push(result(PASS, 'docs/spec/ (PROGRESS.md + SPEC.md present)', null));
243
+ }
244
+ return results;
245
+ }
246
+
247
+ async function checkPendingReviewFiles(projectRoot) {
248
+ const pending = [];
249
+ try {
250
+ const claudeDir = path.join(projectRoot, '.claude');
251
+ const allFiles = await listFilesRecursive(claudeDir);
252
+ for (const fp of allFiles) {
253
+ const rel = path.relative(claudeDir, fp).split(path.sep).join('/');
254
+ if (rel.endsWith('.workflow-ref.md')) {
255
+ pending.push(rel);
256
+ }
257
+ }
258
+ } catch {
259
+ // .claude dir might not exist
260
+ }
261
+
262
+ const suggestionsPath = path.join(projectRoot, 'CLAUDE.md.workflow-suggestions');
263
+ if (await fileExists(suggestionsPath)) {
264
+ pending.push('CLAUDE.md.workflow-suggestions');
265
+ }
266
+
267
+ if (pending.length === 0) {
268
+ return [result(PASS, 'No pending review files', null)];
269
+ }
270
+ return pending.map((f) => result(WARN, `Pending review: ${f}`, 'Merge or delete this file'));
271
+ }
272
+
273
+ export async function doctorCommand() {
274
+ const projectRoot = process.cwd();
275
+ const version = await getPackageVersion();
276
+
277
+ display.newline();
278
+ display.sectionHeader('WORCLAUDE DOCTOR');
279
+ display.dim(`CLI version: v${version}`);
280
+ display.newline();
281
+
282
+ // Core files
283
+ display.barLine(display.white('Core Files'));
284
+ const metaResult = await checkWorkflowMeta(projectRoot);
285
+ printResult(metaResult);
286
+
287
+ const meta = await readWorkflowMeta(projectRoot);
288
+
289
+ printResult(await checkClaudeMd(projectRoot));
290
+ printResult(await checkSettingsJson(projectRoot));
291
+ printResult(await checkSessions(projectRoot));
292
+ display.newline();
293
+
294
+ // Components
295
+ display.barLine(display.white('Components'));
296
+ for (const r of await checkAgents(projectRoot, meta)) printResult(r);
297
+ for (const r of await checkCommands(projectRoot)) printResult(r);
298
+ for (const r of await checkSkills(projectRoot)) printResult(r);
299
+ display.newline();
300
+
301
+ // Docs
302
+ display.barLine(display.white('Documentation'));
303
+ for (const r of await checkDocSpecs(projectRoot)) printResult(r);
304
+ display.newline();
305
+
306
+ // Integrity
307
+ display.barLine(display.white('Integrity'));
308
+ for (const r of await checkHashIntegrity(projectRoot, meta)) printResult(r);
309
+ for (const r of await checkPendingReviewFiles(projectRoot)) printResult(r);
310
+ display.newline();
311
+
312
+ // Summary
313
+ if (metaResult.status === FAIL) {
314
+ display.error('Workflow is not installed. Run `worclaude init` to set up.');
315
+ } else {
316
+ display.success('Doctor complete. Review any warnings above.');
317
+ }
318
+ display.newline();
319
+ }
@@ -3,13 +3,13 @@ import inquirer from 'inquirer';
3
3
  import ora from 'ora';
4
4
  import { scaffoldFile, updateGitignore } from '../core/scaffolder.js';
5
5
  import {
6
+ computeFileHashes,
6
7
  createWorkflowMeta,
7
8
  getPackageVersion,
8
9
  readWorkflowMeta,
9
10
  writeWorkflowMeta,
10
11
  } from '../core/config.js';
11
- import { fileExists, writeFile, listFilesRecursive } from '../utils/file.js';
12
- import { hashFile } from '../utils/hash.js';
12
+ import { fileExists, writeFile } from '../utils/file.js';
13
13
  import * as display from '../utils/display.js';
14
14
  import { promptProjectType } from '../prompts/project-type.js';
15
15
  import { promptTechStack } from '../prompts/tech-stack.js';
@@ -349,17 +349,7 @@ function buildTemplateVariables(selections) {
349
349
  }
350
350
 
351
351
  async function computeAndWriteWorkflowMeta(projectRoot, selections, version) {
352
- const fileHashes = {};
353
- const claudeFiles = await listFilesRecursive(path.join(projectRoot, '.claude'));
354
- for (const filePath of claudeFiles) {
355
- const relativePath = path
356
- .relative(path.join(projectRoot, '.claude'), filePath)
357
- .split(path.sep)
358
- .join('/');
359
- if (relativePath !== 'workflow-meta.json' && relativePath !== 'settings.json') {
360
- fileHashes[relativePath] = await hashFile(filePath);
361
- }
362
- }
352
+ const fileHashes = await computeFileHashes(projectRoot);
363
353
 
364
354
  const meta = createWorkflowMeta({
365
355
  version,
@@ -478,6 +468,10 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
478
468
  }
479
469
  spinner.text = 'Created docs/spec/';
480
470
 
471
+ // Create sessions directory for session persistence
472
+ await writeFile(path.join(projectRoot, '.claude', 'sessions', '.gitkeep'), '');
473
+ spinner.text = 'Created .claude/sessions/';
474
+
481
475
  await computeAndWriteWorkflowMeta(projectRoot, selections, version);
482
476
  spinner.text = 'Created .claude/workflow-meta.json';
483
477
 
@@ -501,6 +495,7 @@ function displayFreshSuccess(selections, skipped) {
501
495
  display.success(`.claude/agents/${display.dimColor(` ${totalAgents} agents`)}`);
502
496
  display.success(`.claude/commands/${display.dimColor(` ${COMMAND_FILES.length} commands`)}`);
503
497
  display.success(`.claude/skills/${display.dimColor(` ${totalSkills} skills`)}`);
498
+ display.success('.claude/sessions/');
504
499
  display.success('.mcp.json');
505
500
  display.success('.gitignore');
506
501
  if (skipped.progressMd) {
@@ -3,6 +3,7 @@ import { execSync } from 'node:child_process';
3
3
  import inquirer from 'inquirer';
4
4
  import ora from 'ora';
5
5
  import {
6
+ computeFileHashes,
6
7
  readWorkflowMeta,
7
8
  workflowMetaExists,
8
9
  writeWorkflowMeta,
@@ -12,8 +13,7 @@ import { createBackup } from '../core/backup.js';
12
13
  import { categorizeFiles } from '../core/file-categorizer.js';
13
14
  import { buildSettingsJson, mergeSettingsPermissionsAndHooks } from '../core/merger.js';
14
15
  import { readTemplate, updateGitignore } from '../core/scaffolder.js';
15
- import { hashFile } from '../utils/hash.js';
16
- import { writeFile, fileExists, listFilesRecursive } from '../utils/file.js';
16
+ import { writeFile, fileExists } from '../utils/file.js';
17
17
  import { getLatestNpmVersion } from '../utils/npm.js';
18
18
  import * as display from '../utils/display.js';
19
19
 
@@ -211,16 +211,11 @@ export async function upgradeCommand() {
211
211
  spinner.text = 'Settings merged...';
212
212
  }
213
213
 
214
+ // Ensure sessions directory exists for session persistence
215
+ await writeFile(path.join(projectRoot, '.claude', 'sessions', '.gitkeep'), '');
216
+
214
217
  // Recompute file hashes
215
- const fileHashes = {};
216
- const claudeDir = path.join(projectRoot, '.claude');
217
- const allFiles = await listFilesRecursive(claudeDir);
218
- for (const filePath of allFiles) {
219
- const relKey = path.relative(claudeDir, filePath).split(path.sep).join('/');
220
- if (relKey !== 'workflow-meta.json' && relKey !== 'settings.json') {
221
- fileHashes[relKey] = await hashFile(filePath);
222
- }
223
- }
218
+ const fileHashes = await computeFileHashes(projectRoot);
224
219
 
225
220
  // Ensure .gitignore has worclaude entries
226
221
  await updateGitignore(projectRoot);
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { readFile, writeFile, fileExists } from '../utils/file.js';
4
+ import { readFile, writeFile, fileExists, listFilesRecursive } from '../utils/file.js';
5
+ import { hashFile } from '../utils/hash.js';
5
6
 
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
8
  const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
@@ -57,3 +58,20 @@ export async function writeWorkflowMeta(projectRoot, meta) {
57
58
  const metaPath = path.join(projectRoot, '.claude', 'workflow-meta.json');
58
59
  await writeFile(metaPath, JSON.stringify(meta, null, 2));
59
60
  }
61
+
62
+ export async function computeFileHashes(projectRoot) {
63
+ const claudeDir = path.join(projectRoot, '.claude');
64
+ const allFiles = await listFilesRecursive(claudeDir);
65
+ const fileHashes = {};
66
+ for (const filePath of allFiles) {
67
+ const relKey = path.relative(claudeDir, filePath).split(path.sep).join('/');
68
+ if (
69
+ relKey !== 'workflow-meta.json' &&
70
+ relKey !== 'settings.json' &&
71
+ !relKey.startsWith('sessions/')
72
+ ) {
73
+ fileHashes[relKey] = await hashFile(filePath);
74
+ }
75
+ }
76
+ return fileHashes;
77
+ }
@@ -238,36 +238,50 @@ export async function mergeSettingsPermissionsAndHooks(projectRoot, workflowSett
238
238
  if (!existing.hooks[category]) existing.hooks[category] = [];
239
239
 
240
240
  const existingEntries = existing.hooks[category];
241
- const existingMatchers = new Map(existingEntries.map((h) => [h.matcher, h]));
241
+ const existingByMatcher = new Map();
242
+ for (const entry of existingEntries) {
243
+ if (!existingByMatcher.has(entry.matcher)) {
244
+ existingByMatcher.set(entry.matcher, []);
245
+ }
246
+ existingByMatcher.get(entry.matcher).push(entry);
247
+ }
248
+ const matched = new Set();
242
249
 
243
250
  for (const workflowEntry of workflowHooks[category]) {
244
- if (existingMatchers.has(workflowEntry.matcher)) {
245
- const existingEntry = existingMatchers.get(workflowEntry.matcher);
246
-
247
- // If hooks are identical, skip no conflict to resolve
248
- const existingCmd = existingEntry.hooks?.[0]?.command || '';
249
- const workflowCmd = workflowEntry.hooks?.[0]?.command || '';
250
- if (existingCmd === workflowCmd) {
251
- continue;
252
- }
251
+ const candidates = existingByMatcher.get(workflowEntry.matcher) || [];
252
+ const workflowCmd = workflowEntry.hooks?.[0]?.command || '';
253
+
254
+ // Try exact match first (identical command = skip)
255
+ const exactMatch = candidates.find(
256
+ (c) => !matched.has(c) && (c.hooks?.[0]?.command || '') === workflowCmd
257
+ );
258
+ if (exactMatch) {
259
+ matched.add(exactMatch);
260
+ continue;
261
+ }
262
+
263
+ // Try unmatched candidate with same matcher (conflict)
264
+ const conflictCandidate = candidates.find((c) => !matched.has(c));
265
+ if (conflictCandidate) {
266
+ matched.add(conflictCandidate);
253
267
 
254
268
  // Tier 3: conflict — ask user
255
- const resolution = await promptHookConflict(category, existingEntry, workflowEntry);
269
+ const resolution = await promptHookConflict(category, conflictCandidate, workflowEntry);
256
270
 
257
271
  if (resolution === 'replace') {
258
- const idx = existingEntries.indexOf(existingEntry);
272
+ const idx = existingEntries.indexOf(conflictCandidate);
259
273
  existingEntries[idx] = workflowEntry;
260
274
  report.hookConflicts.push(
261
275
  `${category} "${workflowEntry.matcher}": replaced with workflow hook`
262
276
  );
263
277
  } else if (resolution === 'chain') {
264
- const idx = existingEntries.indexOf(existingEntry);
278
+ const idx = existingEntries.indexOf(conflictCandidate);
265
279
  existingEntries[idx] = {
266
- matcher: existingEntry.matcher,
280
+ matcher: conflictCandidate.matcher,
267
281
  hooks: [
268
282
  {
269
283
  type: 'command',
270
- command: `${existingEntry.hooks[0].command} && ${workflowEntry.hooks[0].command}`,
284
+ command: `${conflictCandidate.hooks[0].command} && ${workflowEntry.hooks[0].command}`,
271
285
  },
272
286
  ],
273
287
  };
@@ -276,7 +290,7 @@ export async function mergeSettingsPermissionsAndHooks(projectRoot, workflowSett
276
290
  report.hookConflicts.push(`${category} "${workflowEntry.matcher}": kept existing hook`);
277
291
  }
278
292
  } else {
279
- // Tier 1: no conflict — append
293
+ // Tier 1: no match — append
280
294
  existingEntries.push(workflowEntry);
281
295
  report.added.hooks++;
282
296
  }
@@ -422,6 +436,10 @@ export async function performMerge(
422
436
  if (spinner) spinner.start();
423
437
 
424
438
  await mergeMcpJson(projectRoot, existingScan);
439
+
440
+ // Ensure sessions directory exists for session persistence
441
+ await writeFile(path.join(projectRoot, '.claude', 'sessions', '.gitkeep'), '');
442
+
425
443
  await mergeDocSpecs(projectRoot, existingScan, variables, selections, report);
426
444
 
427
445
  // Stop spinner before CLAUDE.md merge — interactive prompts for section selection
@@ -181,7 +181,12 @@ export async function cleanGitignore(projectRoot) {
181
181
  const content = await readFile(gitignorePath);
182
182
  const lines = content.split(/\r?\n/);
183
183
 
184
- const REMOVE_LINES = new Set(['# Worclaude (generated workflow files)', '.claude/']);
184
+ const REMOVE_LINES = new Set([
185
+ '# Worclaude (generated workflow files)',
186
+ '.claude/',
187
+ '.claude/sessions/',
188
+ '.claude/workflow-meta.json',
189
+ ]);
185
190
 
186
191
  const filtered = lines.filter((line) => !REMOVE_LINES.has(line.trim()));
187
192
 
@@ -46,7 +46,7 @@ export async function scaffoldDirectory(templateDir, destDir, variables, project
46
46
 
47
47
  export async function updateGitignore(projectDir) {
48
48
  const gitignorePath = path.join(projectDir, '.gitignore');
49
- const entries = ['.claude/', '.claude-backup-*/'];
49
+ const entries = ['.claude/sessions/', '.claude/workflow-meta.json', '.claude-backup-*/'];
50
50
  const header = '# Worclaude (generated workflow files)';
51
51
 
52
52
  let content = '';
@@ -56,12 +56,27 @@ export async function updateGitignore(projectDir) {
56
56
  if (err.code !== 'ENOENT') throw err;
57
57
  }
58
58
 
59
+ // Migrate: remove old blanket .claude/ entry (and its header) if present
60
+ const lines = content.split(/\r?\n/);
61
+ const hasBlanketEntry = lines.some((l) => l.trim() === '.claude/');
62
+ if (hasBlanketEntry) {
63
+ const filtered = lines.filter((l) => {
64
+ const t = l.trim();
65
+ return t !== '.claude/' && t !== header;
66
+ });
67
+ content = filtered.join('\n');
68
+ }
69
+
59
70
  const missing = entries.filter((entry) => !content.includes(entry));
60
- if (missing.length === 0) return false;
71
+ if (missing.length === 0 && !hasBlanketEntry) return false;
72
+
73
+ if (missing.length > 0) {
74
+ const needsNewline = content.length > 0 && !content.endsWith('\n');
75
+ const addition = (needsNewline ? '\n' : '') + '\n' + header + '\n' + missing.join('\n') + '\n';
76
+ content += addition;
77
+ }
61
78
 
62
- const needsNewline = content.length > 0 && !content.endsWith('\n');
63
- const addition = (needsNewline ? '\n' : '') + '\n' + header + '\n' + missing.join('\n') + '\n';
64
- await fs.appendFile(gitignorePath, addition);
79
+ await fs.writeFile(gitignorePath, content);
65
80
  return true;
66
81
  }
67
82