worclaude 2.4.4 → 2.4.6

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,63 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { fileExists, readFile, writeFile } from '../utils/file.js';
4
+
5
+ export const MEMORY_GUIDANCE_KEYWORDS = [
6
+ 'memory architecture',
7
+ 'native memory',
8
+ '.claude/learnings',
9
+ '[LEARN]',
10
+ '/learn',
11
+ ];
12
+
13
+ export function hasClaudeMdMemoryGuidance(content) {
14
+ if (typeof content !== 'string' || content.length === 0) return false;
15
+ const lower = content.toLowerCase();
16
+ return MEMORY_GUIDANCE_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
17
+ }
18
+
19
+ export async function ensureLearningsDir(projectRoot) {
20
+ const dir = path.join(projectRoot, '.claude', 'learnings');
21
+ const gitkeep = path.join(dir, '.gitkeep');
22
+ if (await fs.pathExists(gitkeep)) return false;
23
+ await fs.ensureDir(dir);
24
+ await writeFile(gitkeep, '');
25
+ return true;
26
+ }
27
+
28
+ export function buildMemoryGuidanceSidecar() {
29
+ const preamble = [
30
+ '# CLAUDE.md — Memory Architecture Suggestion',
31
+ '',
32
+ 'Your CLAUDE.md does not reference the workflow memory architecture. Auto-learnings may',
33
+ 'pollute the main file. Paste the snippet below into CLAUDE.md (near the top, under an',
34
+ 'existing "Conventions" or "Session Protocol" section), then delete this sidecar file.',
35
+ '',
36
+ '---',
37
+ '',
38
+ '## Memory Architecture',
39
+ '',
40
+ '- Auto-memory: `.claude/learnings/` (captured by hooks; reviewed via `/learn`).',
41
+ '- CLAUDE.md stays lean — it is shared with teammates. Long-form notes belong in',
42
+ ' `docs/memory/` or the learnings directory.',
43
+ '- The `[LEARN]` marker in tool output flags moments worth capturing.',
44
+ '',
45
+ ];
46
+ return preamble.join('\n');
47
+ }
48
+
49
+ export async function writeMemoryGuidanceSidecar(projectRoot) {
50
+ const dest = path.join(projectRoot, 'CLAUDE.md.workflow-ref.md');
51
+ await writeFile(dest, buildMemoryGuidanceSidecar());
52
+ return dest;
53
+ }
54
+
55
+ export async function readClaudeMd(projectRoot) {
56
+ const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
57
+ if (!(await fileExists(claudeMdPath))) return null;
58
+ try {
59
+ return await readFile(claudeMdPath);
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
+ import fs from 'fs-extra';
2
3
  import { hashContent, hashFile } from '../utils/hash.js';
3
- import { readTemplate } from './scaffolder.js';
4
+ import { readTemplate, getTemplatesDir } from './scaffolder.js';
4
5
  import { fileExists, listFilesRecursive } from '../utils/file.js';
5
6
  import {
6
7
  UNIVERSAL_AGENTS,
@@ -10,6 +11,40 @@ import {
10
11
  TEMPLATE_SKILLS,
11
12
  } from '../data/agents.js';
12
13
 
14
+ const ALWAYS_SCAFFOLDED_TYPES = new Set([
15
+ 'universal-agent',
16
+ 'command',
17
+ 'universal-skill',
18
+ 'hook',
19
+ 'root-file',
20
+ ]);
21
+
22
+ /**
23
+ * Predicate: is this template entry one that should be restored when missing on disk?
24
+ * - universal-agent, command, universal-skill, hook, root-file: always scaffolded
25
+ * - optional-agent: only if the agent name is in meta.optionalAgents
26
+ * - template-skill: excluded (needs variable substitution; stored hash won't match)
27
+ */
28
+ export function isAlwaysScaffolded(entry, meta) {
29
+ if (!entry) return false;
30
+ if (ALWAYS_SCAFFOLDED_TYPES.has(entry.type)) return true;
31
+ if (entry.type === 'optional-agent') {
32
+ const agentName = entry.agentName;
33
+ return Boolean(agentName && meta?.optionalAgents?.includes(agentName));
34
+ }
35
+ return false;
36
+ }
37
+
38
+ export const ROOT_KEY_PREFIX = 'root/';
39
+
40
+ export function resolveKeyPath(key, projectRoot) {
41
+ if (key.startsWith(ROOT_KEY_PREFIX)) {
42
+ const rel = key.slice(ROOT_KEY_PREFIX.length);
43
+ return path.join(projectRoot, ...rel.split('/'));
44
+ }
45
+ return path.join(projectRoot, '.claude', ...key.split('/'));
46
+ }
47
+
13
48
  /**
14
49
  * Build a map of all workflow template files to their hash keys, template paths, and hashes.
15
50
  * Hash keys match the format stored in workflow-meta.json (relative to .claude/).
@@ -30,7 +65,12 @@ export async function buildTemplateHashMap() {
30
65
  const key = `agents/${name}.md`;
31
66
  const templatePath = `agents/optional/${info.category}/${name}.md`;
32
67
  const content = await readTemplate(templatePath);
33
- map[key] = { templatePath, hash: hashContent(content), type: 'optional-agent' };
68
+ map[key] = {
69
+ templatePath,
70
+ hash: hashContent(content),
71
+ type: 'optional-agent',
72
+ agentName: name,
73
+ };
34
74
  }
35
75
 
36
76
  // Commands: key = commands/{name}.md, templatePath = commands/{name}.md
@@ -58,6 +98,34 @@ export async function buildTemplateHashMap() {
58
98
  map[key] = { templatePath, hash: hashContent(content), type: 'template-skill' };
59
99
  }
60
100
 
101
+ // Hook scripts: walked from templates/hooks/ so new hooks flow through automatically.
102
+ // Key = hooks/{name}, templatePath = hooks/{name}
103
+ const hooksDir = path.join(getTemplatesDir(), 'hooks');
104
+ if (await fs.pathExists(hooksDir)) {
105
+ const entries = await fs.readdir(hooksDir);
106
+ for (const entry of entries) {
107
+ if (!entry.endsWith('.cjs') && !entry.endsWith('.js')) continue;
108
+ const key = `hooks/${entry}`;
109
+ const templatePath = `hooks/${entry}`;
110
+ const content = await readTemplate(templatePath);
111
+ map[key] = { templatePath, hash: hashContent(content), type: 'hook' };
112
+ }
113
+ }
114
+
115
+ // Root-level files: key = root/<path>, templatePath points into templates/
116
+ // AGENTS.md needs variable substitution at scaffold time; the raw-template hash
117
+ // is used only to detect drift against a previously-substituted install hash,
118
+ // and a mismatch routes us down the user-modified path (no false overwrite).
119
+ {
120
+ const templatePath = 'core/agents-md.md';
121
+ const content = await readTemplate(templatePath);
122
+ map['root/AGENTS.md'] = {
123
+ templatePath,
124
+ hash: hashContent(content),
125
+ type: 'root-file',
126
+ };
127
+ }
128
+
61
129
  return map;
62
130
  }
63
131
 
@@ -74,7 +142,8 @@ export async function categorizeFiles(projectRoot, meta) {
74
142
  conflict: [],
75
143
  newFiles: [],
76
144
  unchanged: [],
77
- deleted: [],
145
+ missingExpected: [],
146
+ missingUntracked: [],
78
147
  userAdded: [],
79
148
  modified: [],
80
149
  outdated: [],
@@ -83,14 +152,31 @@ export async function categorizeFiles(projectRoot, meta) {
83
152
  // Track which keys we've processed
84
153
  const processedKeys = new Set();
85
154
 
155
+ // Shared routing for files whose outdated-detection step is skipped (no
156
+ // template entry, or template has variable placeholders we can't hash
157
+ // against). Classification reduces to "did the user touch it?"
158
+ const recordByUserModification = (key, userModified) => {
159
+ if (userModified) result.modified.push({ key });
160
+ else result.unchanged.push({ key });
161
+ };
162
+
86
163
  // 1. Process each file in stored hashes
87
164
  for (const [key, storedHash] of Object.entries(storedHashes)) {
88
165
  processedKeys.add(key);
89
- const filePath = path.join(claudeDir, ...key.split('/'));
166
+ const filePath = resolveKeyPath(key, projectRoot);
90
167
 
91
168
  // Check if file still exists on disk
92
169
  if (!(await fileExists(filePath))) {
93
- result.deleted.push({ key });
170
+ const templateEntry = templateMap[key];
171
+ if (isAlwaysScaffolded(templateEntry, meta)) {
172
+ result.missingExpected.push({
173
+ key,
174
+ templatePath: templateEntry.templatePath,
175
+ type: templateEntry.type,
176
+ });
177
+ } else {
178
+ result.missingUntracked.push({ key });
179
+ }
94
180
  continue;
95
181
  }
96
182
 
@@ -102,22 +188,17 @@ export async function categorizeFiles(projectRoot, meta) {
102
188
  const templateEntry = templateMap[key];
103
189
 
104
190
  if (!templateEntry) {
105
- // File is in stored hashes but not in template map — treat as tracked file
106
- if (userModified) {
107
- result.modified.push({ key });
108
- } else {
109
- result.unchanged.push({ key });
110
- }
191
+ // File is in stored hashes but not in template map — treat as tracked file.
192
+ recordByUserModification(key, userModified);
111
193
  continue;
112
194
  }
113
195
 
114
- // Skip template skills from outdated detection (raw vs substituted hash mismatch)
115
- if (templateEntry.type === 'template-skill') {
116
- if (userModified) {
117
- result.modified.push({ key });
118
- } else {
119
- result.unchanged.push({ key });
120
- }
196
+ // Skip template skills and root-files from outdated detection
197
+ // both contain {variable} placeholders, so raw-template hash won't
198
+ // match the installed (substituted) hash. Restoration paths still
199
+ // catch them when missing; we just can't auto-update them here.
200
+ if (templateEntry.type === 'template-skill' || templateEntry.type === 'root-file') {
201
+ recordByUserModification(key, userModified);
121
202
  continue;
122
203
  }
123
204
 
@@ -125,11 +206,7 @@ export async function categorizeFiles(projectRoot, meta) {
125
206
 
126
207
  if (!templateChanged) {
127
208
  // Template unchanged — only report user modifications
128
- if (userModified) {
129
- result.modified.push({ key });
130
- } else {
131
- result.unchanged.push({ key });
132
- }
209
+ recordByUserModification(key, userModified);
133
210
  } else {
134
211
  // Template was updated in new version
135
212
  result.outdated.push({ key, templatePath: templateEntry.templatePath });
@@ -141,22 +218,13 @@ export async function categorizeFiles(projectRoot, meta) {
141
218
  }
142
219
  }
143
220
 
144
- // 2. Find new files (in template map but not in stored hashes)
221
+ // 2. Find new files (in template map but not in stored hashes).
222
+ // Gate is the same predicate used for missingExpected: always-scaffolded types,
223
+ // selected optional agents only, template skills excluded (need substitution).
145
224
  for (const [key, entry] of Object.entries(templateMap)) {
146
225
  if (processedKeys.has(key)) continue;
147
-
148
- // Skip optional agents the user didn't select
149
- if (entry.type === 'optional-agent') {
150
- const agentName = key.replace('agents/', '').replace('.md', '');
151
- if (!meta.optionalAgents?.includes(agentName)) {
152
- continue;
153
- }
154
- }
155
-
156
- // Skip template skills (would need variable substitution)
157
- if (entry.type === 'template-skill') continue;
158
-
159
- result.newFiles.push({ key, templatePath: entry.templatePath });
226
+ if (!isAlwaysScaffolded(entry, meta)) continue;
227
+ result.newFiles.push({ key, templatePath: entry.templatePath, type: entry.type });
160
228
  }
161
229
 
162
230
  // 3. Find user-added files (on disk but not in stored hashes or template map)
@@ -0,0 +1,196 @@
1
+ import path from 'node:path';
2
+ import { readFile, fileExists } from '../utils/file.js';
3
+ import { TECH_STACKS } from '../data/agents.js';
4
+
5
+ export const LANGUAGE_COMMANDS = {
6
+ python: {
7
+ heading: 'Python',
8
+ commands: [
9
+ 'python -m pytest # Run tests',
10
+ 'ruff check . # Lint',
11
+ 'ruff format . # Format',
12
+ ],
13
+ },
14
+ node: {
15
+ heading: 'Node.js / TypeScript',
16
+ commands: [
17
+ 'npm test # Run tests',
18
+ 'npx eslint . # Lint',
19
+ 'npx prettier --write . # Format',
20
+ ],
21
+ },
22
+ java: {
23
+ heading: 'Java',
24
+ commands: [
25
+ 'mvn test # Run tests',
26
+ 'mvn checkstyle:check # Lint',
27
+ 'mvn spotless:apply # Format',
28
+ ],
29
+ },
30
+ csharp: {
31
+ heading: 'C# / .NET',
32
+ commands: [
33
+ 'dotnet test # Run tests',
34
+ 'dotnet format --verify-no-changes # Lint',
35
+ 'dotnet format # Format',
36
+ ],
37
+ },
38
+ cpp: {
39
+ heading: 'C / C++',
40
+ commands: [
41
+ 'cmake --build build && ctest # Build & test',
42
+ 'clang-tidy src/*.cpp # Lint',
43
+ 'clang-format -i src/*.[ch]pp # Format',
44
+ ],
45
+ },
46
+ go: {
47
+ heading: 'Go',
48
+ commands: [
49
+ 'go test ./... # Run tests',
50
+ 'golangci-lint run # Lint',
51
+ 'gofmt -w . # Format',
52
+ ],
53
+ },
54
+ php: {
55
+ heading: 'PHP',
56
+ commands: [
57
+ 'vendor/bin/phpunit # Run tests',
58
+ 'vendor/bin/phpstan analyse # Lint',
59
+ 'vendor/bin/php-cs-fixer fix . # Format',
60
+ ],
61
+ },
62
+ ruby: {
63
+ heading: 'Ruby',
64
+ commands: [
65
+ 'bundle exec rspec # Run tests',
66
+ 'rubocop # Lint',
67
+ 'rubocop -A # Format',
68
+ ],
69
+ },
70
+ kotlin: {
71
+ heading: 'Kotlin',
72
+ commands: [
73
+ 'gradle test # Run tests',
74
+ 'detekt # Lint',
75
+ 'ktlint -F # Format',
76
+ ],
77
+ },
78
+ swift: {
79
+ heading: 'Swift',
80
+ commands: [
81
+ 'swift test # Run tests',
82
+ 'swiftlint # Lint',
83
+ 'swift-format format -r . -i # Format',
84
+ ],
85
+ },
86
+ rust: {
87
+ heading: 'Rust',
88
+ commands: [
89
+ 'cargo test # Run tests',
90
+ 'cargo clippy # Lint',
91
+ 'cargo fmt # Format',
92
+ ],
93
+ },
94
+ dart: {
95
+ heading: 'Dart / Flutter',
96
+ commands: [
97
+ 'dart test # Run tests',
98
+ 'dart analyze # Lint',
99
+ 'dart format . # Format',
100
+ ],
101
+ },
102
+ scala: {
103
+ heading: 'Scala',
104
+ commands: [
105
+ 'sbt test # Run tests',
106
+ 'sbt scalafix # Lint',
107
+ 'scalafmt # Format',
108
+ ],
109
+ },
110
+ elixir: {
111
+ heading: 'Elixir',
112
+ commands: [
113
+ 'mix test # Run tests',
114
+ 'mix credo # Lint',
115
+ 'mix format # Format',
116
+ ],
117
+ },
118
+ zig: {
119
+ heading: 'Zig',
120
+ commands: [
121
+ 'zig build test # Run tests',
122
+ 'zig build # Build (lint via compiler)',
123
+ 'zig fmt . # Format',
124
+ ],
125
+ },
126
+ };
127
+
128
+ export function buildCommandsBlock(languages, useDocker) {
129
+ const lines = ['```bash'];
130
+ for (const lang of languages) {
131
+ const entry = LANGUAGE_COMMANDS[lang];
132
+ if (!entry) continue;
133
+ if (lines.length > 1) lines.push('');
134
+ lines.push(`# ${entry.heading}`);
135
+ lines.push(...entry.commands);
136
+ }
137
+ if (useDocker) {
138
+ if (lines.length > 1) lines.push('');
139
+ lines.push('# Docker');
140
+ lines.push('docker compose up -d # Start services');
141
+ lines.push('docker compose down # Stop services');
142
+ }
143
+ if (lines.length === 1) {
144
+ lines.push('# Add your project-specific commands here');
145
+ }
146
+ lines.push('```');
147
+ return lines.join('\n');
148
+ }
149
+
150
+ function buildTechStackText(languages, useDocker) {
151
+ const lines = languages
152
+ .filter((l) => l !== 'other')
153
+ .map((l) => {
154
+ const entry = TECH_STACKS.find((s) => s.value === l);
155
+ return `- ${entry ? entry.name : l}`;
156
+ });
157
+ if (languages.includes('other') && lines.length === 0) {
158
+ lines.push('- Not specified');
159
+ }
160
+ if (useDocker) lines.push('- Docker');
161
+ return lines.join('\n');
162
+ }
163
+
164
+ async function readPackageJsonFields(projectRoot) {
165
+ const pkgPath = path.join(projectRoot, 'package.json');
166
+ if (!(await fileExists(pkgPath))) return {};
167
+ try {
168
+ const content = await readFile(pkgPath);
169
+ const pkg = JSON.parse(content);
170
+ return { name: pkg.name, description: pkg.description };
171
+ } catch {
172
+ return {};
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Reconstruct template variables from workflow-meta.json for repair flows.
178
+ * Best-effort: pulls projectName/description from package.json, tech stack
179
+ * from meta.techStack, commands block from meta.techStack + meta.useDocker.
180
+ */
181
+ export async function buildAgentsMdVariables(meta, projectRoot) {
182
+ const languages = meta.techStack || [];
183
+ const useDocker = meta.useDocker || false;
184
+ const pkg = await readPackageJsonFields(projectRoot);
185
+ const projectName = pkg.name || path.basename(projectRoot);
186
+ const description = pkg.description || 'A project scaffolded with Worclaude';
187
+ const techStackText = buildTechStackText(languages, useDocker);
188
+ const commandsText = buildCommandsBlock(languages, useDocker);
189
+ return {
190
+ project_name: projectName,
191
+ description,
192
+ tech_stack_filled_during_init: techStackText,
193
+ commands_filled_during_init: commandsText,
194
+ timestamp: new Date().toISOString(),
195
+ };
196
+ }
package/src/index.js CHANGED
@@ -29,7 +29,10 @@ program
29
29
  program
30
30
  .command('upgrade')
31
31
  .description('Update workflow components to the latest version')
32
- .action(upgradeCommand);
32
+ .option('--dry-run', 'Preview changes without writing')
33
+ .option('--yes', 'Skip confirmation prompts')
34
+ .option('--repair-only', 'Restore missing files without applying template updates')
35
+ .action((options) => upgradeCommand(options));
33
36
 
34
37
  program
35
38
  .command('status')