worclaude 2.5.0 → 2.5.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ All notable changes to worclaude are documented in this file. Format loosely fol
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.5.1] — 2026-04-21
8
+
9
+ Patch release fixing a user-visible bug found while dogfooding the v2.5.0 upgrade: reference-copy template files (the copies worclaude writes when your customized file and the shipped template both change) were being placed as `.workflow-ref.md` siblings next to the live file — including inside `.claude/commands/`, where Claude Code's command loader discovered them as phantom slash commands like `/sync.workflow-ref`, and inside `.claude/agents/`, where they could shadow live agents. Reference copies now live in a dedicated `.claude/workflow-ref/` tree that Claude Code does not scan. Installs upgrading from pre-v2.5.1 have their legacy reference files automatically relocated on the next `worclaude upgrade`.
10
+
11
+ ### Fixed
12
+
13
+ - **Phantom slash commands from reference-copy siblings** (PR #101) — ref files are now written under `.claude/workflow-ref/<original-path>/` with the original filename preserved, instead of as `<name>.workflow-ref.md` siblings next to the live file. `.claude/commands/` and `.claude/agents/` are no longer polluted by `.workflow-ref` artifacts, so Claude Code's command and subagent loaders don't register them as live. `diff .claude/workflow-ref/commands/sync.md .claude/commands/sync.md` now reads cleanly.
14
+
15
+ ### Added
16
+
17
+ - **`migrateWorkflowRefLocation()` in `src/core/migration.js`** (PR #101) — runs unconditionally at the start of every `worclaude upgrade`. Sweeps legacy `*.workflow-ref.md` siblings under `.claude/{commands,agents,skills}/` (plus root-level `CLAUDE.md.workflow-ref.md` and `AGENTS.md.workflow-ref`) into the new `.claude/workflow-ref/<original-path>/` tree. Idempotent (skips when target already exists, never overwrites). Scoped scan avoids re-stat'ing `.claude/sessions/` on every upgrade. Deliberately not version-gated — semver-gating this kind of cleanup migration creates a new bug class for users who downgrade then re-upgrade.
18
+ - **Shared helpers in `src/core/file-categorizer.js`** (PR #101) — `WORKFLOW_REF_DIR` constant, `workflowRefRelPath(keyOrRelPath)` returning project-root-relative paths for `scaffoldFile`, `resolveRefPath(keyOrRelPath, projectRoot)` for absolute writes, and `isWorkflowRefFile(absPath, claudeDir)` predicate that unifies new-location-or-legacy-suffix detection so `status`, `doctor`, and `remover` stay in sync across the transition window.
19
+
20
+ ### Changed
21
+
22
+ - **`worclaude upgrade` report output** (PR #101) — "Conflicts: N files (saved as .workflow-ref.md)" → "Conflicts: N files (saved under .claude/workflow-ref/)". New "Relocated: N legacy ref file(s) → .claude/workflow-ref/" line surfaces the one-time migration when it fires. Final hint reads "Review files under .claude/workflow-ref/ and merge what's useful."
23
+ - **`worclaude init` and `worclaude status` output** (PR #101) — Scenario B conflict block, Next Steps, and Pending Review list all updated to reference the new location. Status continues to surface legacy `*.workflow-ref.md` siblings anywhere under `.claude/` so users upgrading from pre-v2.5.1 still see what they need to resolve before the next upgrade migrates them.
24
+ - **`src/core/remover.js`** (PR #101) — the entire `.claude/workflow-ref/` subtree is classified as safe-to-delete on `worclaude delete`; the empty-subdir cleanup list extended to include `workflow-ref`.
25
+
26
+ ### Docs
27
+
28
+ - **Upgrading guide** (PR #101) — 7 inline references updated plus a new "Upgrading from pre-v2.5.1 installs" subsection explaining what the automatic relocation does and how the skip-on-collision rule works.
29
+ - **Existing-projects guide** (PR #101) — Tier 2 "Safe Alongside" section rewritten to describe the new `.claude/workflow-ref/` layout; post-merge report sample updated; the "Reviewing reference files" section now shows the clean `diff workflow-ref/commands/sync.md commands/sync.md` idiom.
30
+ - **Commands reference, agents reference, SPEC.md, project-patterns skill** (PR #101) — all references to `.workflow-ref.md` sibling layout updated; `docs/spec/SPEC.md` edited on the feature branch as a shared-state override (PR #79 precedent: the SPEC update describes the very behavior the PR adds).
31
+
32
+ ### Tests
33
+
34
+ - **Relocation migration coverage** (PR #101) — 8 new cases in `tests/core/migration.test.js` covering command/agent/skill subdir sweeps, root-level `CLAUDE.md.workflow-ref.md` / `AGENTS.md.workflow-ref` handling, target-collision skip behavior, idempotency on repeated runs, no-op on projects with files already at the new location, and clean-project no-ops. Regression assertions in `tests/commands/upgrade.test.js`, `tests/commands/init.test.js`, and `tests/core/merger.test.js` explicitly verify that `.workflow-ref.md` siblings are NEVER created inside `.claude/commands/` or `.claude/agents/`, so the phantom-command class of bug cannot silently return. 575/575 pass (567 prior + 8 new).
35
+
7
36
  ## [2.5.0] — 2026-04-21
8
37
 
9
38
  First release under the new per-PR bump declaration workflow. Shifts release mechanism from "every `/sync` publishes" to "every PR declares a bump, `/sync` aggregates, and only user-visible work ships." PR authors now declare `Version bump: {major|minor|patch|none}` in their PR body; `/sync` picks the highest declared bump since the last tag using precedence `major > minor > patch > none`. All-`none` batches update shared-state files on develop but never cut a release — internal-only work (docs, CI, tests) accumulates without triggering noisy publishes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worclaude",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "The Workflow Layer for Claude Code — scaffold agents, commands, skills, hooks, and memory into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@ import {
11
11
  TEMPLATE_SKILLS,
12
12
  } from '../data/agents.js';
13
13
  import { hasClaudeMdMemoryGuidance, readClaudeMd } from '../core/drift-checks.js';
14
- import { resolveKeyPath } from '../core/file-categorizer.js';
14
+ import { resolveKeyPath, isWorkflowRefFile } from '../core/file-categorizer.js';
15
15
  import * as display from '../utils/display.js';
16
16
 
17
17
  // Check categories
@@ -202,7 +202,7 @@ async function checkClaudeMdMemoryGuidance(projectRoot) {
202
202
  result(
203
203
  WARN,
204
204
  'CLAUDE.md memory guidance',
205
- 'CLAUDE.md lacks memory-architecture guidance. Run worclaude upgrade to write a CLAUDE.md.workflow-ref.md sidecar with suggested additions.'
205
+ 'CLAUDE.md lacks memory-architecture guidance. Run worclaude upgrade to write a sidecar under .claude/workflow-ref/CLAUDE.md with suggested additions.'
206
206
  ),
207
207
  ];
208
208
  }
@@ -828,10 +828,8 @@ async function checkPendingReviewFiles(projectRoot) {
828
828
  const claudeDir = path.join(projectRoot, '.claude');
829
829
  const allFiles = await listFilesRecursive(claudeDir);
830
830
  for (const fp of allFiles) {
831
- const rel = path.relative(claudeDir, fp).split(path.sep).join('/');
832
- if (rel.endsWith('.workflow-ref.md')) {
833
- pending.push(rel);
834
- }
831
+ if (!isWorkflowRefFile(fp, claudeDir)) continue;
832
+ pending.push(path.relative(claudeDir, fp).split(path.sep).join('/'));
835
833
  }
836
834
  } catch {
837
835
  // .claude dir might not exist
@@ -528,7 +528,7 @@ function displayMergeReport(report, backupPath) {
528
528
  }
529
529
  if (report.added.skills.length > 0) {
530
530
  display.barLine(
531
- ` ${display.green('✓')} ${report.added.skills.length} skills added${report.conflicts.skills.length > 0 ? ` (${report.conflicts.skills.length} conflicts saved as .workflow-ref.md)` : ''}`
531
+ ` ${display.green('✓')} ${report.added.skills.length} skills added${report.conflicts.skills.length > 0 ? ` (${report.conflicts.skills.length} conflicts saved under .claude/workflow-ref/)` : ''}`
532
532
  );
533
533
  }
534
534
  if (report.added.permissions > 0) {
@@ -549,10 +549,11 @@ function displayMergeReport(report, backupPath) {
549
549
  ...report.conflicts.commands,
550
550
  ];
551
551
  if (allConflicts.length > 0) {
552
- display.barLine(`${display.yellow('~')} Conflicts (saved alongside for review):`);
552
+ display.barLine(
553
+ `${display.yellow('~')} Conflicts (template copy saved under .claude/workflow-ref/):`
554
+ );
553
555
  for (const file of allConflicts) {
554
- const refName = file.replace('.md', '.workflow-ref.md');
555
- display.barLine(` ${display.yellow('⚠')} ${file} → ${refName}`);
556
+ display.barLine(` ${display.yellow('')} ${file}`);
556
557
  }
557
558
  display.newline();
558
559
  }
@@ -606,12 +607,14 @@ function displayMergeReport(report, backupPath) {
606
607
  display.divider('NEXT');
607
608
  display.newline();
608
609
  if (allConflicts.length > 0) {
609
- console.log(` ${display.white('1.')} Review .workflow-ref.md files and merge what's useful`);
610
+ console.log(
611
+ ` ${display.white('1.')} Review files under .claude/workflow-ref/ and merge what's useful`
612
+ );
610
613
  }
611
614
  if (report.claudeMdHandling === 'suggestions-generated') {
612
615
  console.log(` ${display.white('2.')} Review CLAUDE.md.workflow-suggestions`);
613
616
  console.log(
614
- ` ${display.white('3.')} Delete .workflow-ref.md and .workflow-suggestions files when done`
617
+ ` ${display.white('3.')} Delete .claude/workflow-ref/ and .workflow-suggestions when done`
615
618
  );
616
619
  }
617
620
  console.log(
@@ -3,6 +3,7 @@ import { requireWorkflowMeta, getPackageVersion } from '../core/config.js';
3
3
  import { hashFile } from '../utils/hash.js';
4
4
  import { fileExists, readFile, listFilesRecursive } from '../utils/file.js';
5
5
  import { getLatestNpmVersion } from '../utils/npm.js';
6
+ import { isWorkflowRefFile } from '../core/file-categorizer.js';
6
7
  import { TECH_STACKS } from '../data/agents.js';
7
8
  import * as display from '../utils/display.js';
8
9
 
@@ -99,16 +100,17 @@ export async function statusCommand() {
99
100
  display.newline();
100
101
  }
101
102
 
102
- // Pending review files
103
+ // Pending review files — anything isWorkflowRefFile() recognizes. That
104
+ // covers the v2.5.1+ .claude/workflow-ref/ tree plus legacy .workflow-ref.md
105
+ // siblings, so users mid-migration still see what they need to resolve.
103
106
  const pendingReview = [];
104
107
  try {
105
108
  const claudeDir = path.join(projectRoot, '.claude');
106
109
  const allFiles = await listFilesRecursive(claudeDir);
107
110
  for (const fp of allFiles) {
111
+ if (!isWorkflowRefFile(fp, claudeDir)) continue;
108
112
  const rel = path.relative(claudeDir, fp).split(path.sep).join('/');
109
- if (rel.endsWith('.workflow-ref.md')) {
110
- pendingReview.push(`.claude/${rel}`);
111
- }
113
+ pendingReview.push(`.claude/${rel}`);
112
114
  }
113
115
  } catch {
114
116
  // .claude dir might not exist
@@ -5,7 +5,7 @@ import inquirer from 'inquirer';
5
5
  import ora from 'ora';
6
6
  import { requireWorkflowMeta, writeWorkflowMeta, getPackageVersion } from '../core/config.js';
7
7
  import { createBackup } from '../core/backup.js';
8
- import { categorizeFiles, resolveKeyPath } from '../core/file-categorizer.js';
8
+ import { categorizeFiles, resolveKeyPath, resolveRefPath } from '../core/file-categorizer.js';
9
9
  import { buildSettingsJson, mergeSettingsPermissionsAndHooks } from '../core/merger.js';
10
10
  import { readTemplate, substituteVariables, updateGitignore } from '../core/scaffolder.js';
11
11
  import { buildAgentsMdVariables } from '../core/variables.js';
@@ -19,7 +19,12 @@ import { writeFile, readFile, fileExists } from '../utils/file.js';
19
19
  import { hashFile } from '../utils/hash.js';
20
20
  import { getLatestNpmVersion } from '../utils/npm.js';
21
21
  import * as display from '../utils/display.js';
22
- import { semverLessThan, migrateSkillFormat, patchAgentDescriptions } from '../core/migration.js';
22
+ import {
23
+ semverLessThan,
24
+ migrateSkillFormat,
25
+ patchAgentDescriptions,
26
+ migrateWorkflowRefLocation,
27
+ } from '../core/migration.js';
23
28
 
24
29
  const CONFLICT_CHECK_TYPES = new Set(['hook', 'root-file']);
25
30
 
@@ -41,12 +46,6 @@ function selfUpdate(latestVersion) {
41
46
  }
42
47
  }
43
48
 
44
- function sidecarPathFor(dest) {
45
- const ext = path.extname(dest);
46
- const base = ext ? dest.slice(0, dest.length - ext.length) : dest;
47
- return `${base}.workflow-ref${ext}`;
48
- }
49
-
50
49
  async function renderTemplate({ templatePath, type }, variables) {
51
50
  const templateContent = await readTemplate(templatePath);
52
51
  return type === 'root-file' ? substituteVariables(templateContent, variables) : templateContent;
@@ -57,8 +56,8 @@ async function writeTemplateToDest(entry, dest, variables) {
57
56
  await writeFile(dest, await renderTemplate(entry, variables));
58
57
  }
59
58
 
60
- async function writeSidecarFor(entry, dest, variables) {
61
- const sidecarPath = sidecarPathFor(dest);
59
+ async function writeSidecarFor(entry, projectRoot, variables) {
60
+ const sidecarPath = resolveRefPath(entry.key, projectRoot);
62
61
  await writeFile(sidecarPath, await renderTemplate(entry, variables));
63
62
  return sidecarPath;
64
63
  }
@@ -122,7 +121,7 @@ function renderRepairPreview(plan) {
122
121
  }
123
122
  if (plan.claudeMdNeedsSidecar) {
124
123
  sidecarLines.push(
125
- ' ~ CLAUDE.md memory guidance missing (will write CLAUDE.md.workflow-ref.md)'
124
+ ' ~ CLAUDE.md memory guidance missing (will write .claude/workflow-ref/CLAUDE.md sidecar)'
126
125
  );
127
126
  }
128
127
  if (sidecarLines.length > 0) {
@@ -197,7 +196,7 @@ async function applyRepairPass(projectRoot, plan, variables) {
197
196
  if (await fileExists(dest)) {
198
197
  const matches = await diskContentMatchesTemplate(entry, dest, variables);
199
198
  if (!matches) {
200
- const sidecarPath = await writeSidecarFor(entry, dest, variables);
199
+ const sidecarPath = await writeSidecarFor(entry, projectRoot, variables);
201
200
  result.migrationConflicts.push({ key: entry.key, dest, sidecarPath });
202
201
  continue;
203
202
  }
@@ -282,7 +281,7 @@ async function runRepairOnlyFlow({ projectRoot, meta, plan, variables, dryRun, y
282
281
  }
283
282
  if (result.migrationConflicts.length > 0) {
284
283
  display.barLine(
285
- `Conflicts: ${result.migrationConflicts.length} files ${display.dimColor('(saved as .workflow-ref)')}`
284
+ `Conflicts: ${result.migrationConflicts.length} files ${display.dimColor('(saved under .claude/workflow-ref/)')}`
286
285
  );
287
286
  }
288
287
  if (result.createdDirs.length > 0) {
@@ -295,7 +294,7 @@ async function runRepairOnlyFlow({ projectRoot, meta, plan, variables, dryRun, y
295
294
 
296
295
  if (result.migrationConflicts.length > 0 || result.sidecars.length > 0) {
297
296
  display.newline();
298
- display.barLine(`Review .workflow-ref files and merge what's useful.`);
297
+ display.barLine(`Review files under .claude/workflow-ref/ and merge what's useful.`);
299
298
  }
300
299
  } catch (err) {
301
300
  spinner.fail('Repair failed.');
@@ -440,17 +439,25 @@ export async function upgradeCommand(options = {}) {
440
439
  spinner.start('Applying updates...');
441
440
  }
442
441
 
442
+ // v2.5.1 migration: relocate legacy ref files out of Claude-Code-scanned
443
+ // dirs. Runs unconditionally since it's idempotent and the fix applies
444
+ // to any project with legacy refs, regardless of source version.
445
+ const refRelocationReport = await migrateWorkflowRefLocation(projectRoot);
446
+ if (refRelocationReport.moved > 0) {
447
+ spinner.text = `Relocated ${refRelocationReport.moved} legacy ref file(s)...`;
448
+ }
449
+
443
450
  // Auto-update files
444
451
  for (const { key, templatePath } of categories.autoUpdate) {
445
452
  const content = await readTemplate(templatePath);
446
453
  await writeFile(resolveKeyPath(key, projectRoot), content);
447
454
  }
448
455
 
449
- // Conflict files: save as .workflow-ref.md
456
+ // Conflict files: save under .claude/workflow-ref/ so they never collide
457
+ // with Claude Code's command/agent discovery.
450
458
  for (const { key, templatePath } of categories.conflict) {
451
459
  const content = await readTemplate(templatePath);
452
- const refKey = key.replace(/\.md$/, '.workflow-ref.md');
453
- await writeFile(resolveKeyPath(refKey, projectRoot), content);
460
+ await writeFile(resolveRefPath(key, projectRoot), content);
454
461
  }
455
462
 
456
463
  // New files (non-migration types handled here; migration types were
@@ -510,7 +517,7 @@ export async function upgradeCommand(options = {}) {
510
517
  }
511
518
  if (repairResult.migrationConflicts.length > 0) {
512
519
  display.barLine(
513
- `Migration conflicts: ${repairResult.migrationConflicts.length} ${display.dimColor('(saved as .workflow-ref)')}`
520
+ `Migration conflicts: ${repairResult.migrationConflicts.length} ${display.dimColor('(saved under .claude/workflow-ref/)')}`
514
521
  );
515
522
  }
516
523
  if (categories.autoUpdate.length > 0) {
@@ -518,7 +525,12 @@ export async function upgradeCommand(options = {}) {
518
525
  }
519
526
  if (categories.conflict.length > 0) {
520
527
  display.barLine(
521
- `Conflicts: ${categories.conflict.length} files ${display.dimColor('(saved as .workflow-ref.md)')}`
528
+ `Conflicts: ${categories.conflict.length} files ${display.dimColor('(saved under .claude/workflow-ref/)')}`
529
+ );
530
+ }
531
+ if (refRelocationReport.moved > 0) {
532
+ display.barLine(
533
+ `Relocated: ${refRelocationReport.moved} legacy ref file(s) ${display.dimColor('→ .claude/workflow-ref/')}`
522
534
  );
523
535
  }
524
536
  if (plan.templateNewFiles.length > 0) {
@@ -558,7 +570,7 @@ export async function upgradeCommand(options = {}) {
558
570
  repairResult.sidecars.length > 0
559
571
  ) {
560
572
  display.newline();
561
- display.barLine(`Review .workflow-ref files and merge what's useful.`);
573
+ display.barLine(`Review files under .claude/workflow-ref/ and merge what's useful.`);
562
574
  }
563
575
  } catch (err) {
564
576
  spinner.fail('Upgrade failed.');
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
3
  import { fileExists, readFile, writeFile } from '../utils/file.js';
4
+ import { resolveRefPath } from './file-categorizer.js';
4
5
 
5
6
  export const MEMORY_GUIDANCE_KEYWORDS = [
6
7
  'memory architecture',
@@ -47,7 +48,7 @@ export function buildMemoryGuidanceSidecar() {
47
48
  }
48
49
 
49
50
  export async function writeMemoryGuidanceSidecar(projectRoot) {
50
- const dest = path.join(projectRoot, 'CLAUDE.md.workflow-ref.md');
51
+ const dest = resolveRefPath('root/CLAUDE.md', projectRoot);
51
52
  await writeFile(dest, buildMemoryGuidanceSidecar());
52
53
  return dest;
53
54
  }
@@ -36,6 +36,7 @@ export function isAlwaysScaffolded(entry, meta) {
36
36
  }
37
37
 
38
38
  export const ROOT_KEY_PREFIX = 'root/';
39
+ export const WORKFLOW_REF_DIR = 'workflow-ref';
39
40
 
40
41
  export function resolveKeyPath(key, projectRoot) {
41
42
  if (key.startsWith(ROOT_KEY_PREFIX)) {
@@ -45,6 +46,43 @@ export function resolveKeyPath(key, projectRoot) {
45
46
  return path.join(projectRoot, '.claude', ...key.split('/'));
46
47
  }
47
48
 
49
+ /**
50
+ * Project-root-relative path for a ref file. Useful for scaffoldFile() which
51
+ * takes a destRelativePath. Returns e.g. ".claude/workflow-ref/commands/sync.md".
52
+ * Accepts a workflow key ("commands/sync.md", "root/CLAUDE.md") or a plain
53
+ * relative path. Root-level files (CLAUDE.md, AGENTS.md) land directly
54
+ * under workflow-ref/, not under workflow-ref/root/.
55
+ */
56
+ export function workflowRefRelPath(keyOrRelPath) {
57
+ const rel = keyOrRelPath.startsWith(ROOT_KEY_PREFIX)
58
+ ? keyOrRelPath.slice(ROOT_KEY_PREFIX.length)
59
+ : keyOrRelPath;
60
+ return path.join('.claude', WORKFLOW_REF_DIR, ...rel.split('/'));
61
+ }
62
+
63
+ /**
64
+ * Resolve an absolute ref file destination for a given relative path.
65
+ * Wraps workflowRefRelPath with projectRoot.
66
+ */
67
+ export function resolveRefPath(keyOrRelPath, projectRoot) {
68
+ return path.join(projectRoot, workflowRefRelPath(keyOrRelPath));
69
+ }
70
+
71
+ /**
72
+ * Predicate for "is this file a reference copy?". Matches both the v2.5.1+
73
+ * layout (anywhere under .claude/workflow-ref/) and the legacy sibling
74
+ * convention (.workflow-ref.md suffix). Used by status, doctor, and remover
75
+ * so they stay in sync across the two location schemes.
76
+ *
77
+ * @param {string} absPath — absolute path on disk
78
+ * @param {string} claudeDir — absolute path to the project's .claude/ directory
79
+ */
80
+ export function isWorkflowRefFile(absPath, claudeDir) {
81
+ const refDirPrefix = path.join(claudeDir, WORKFLOW_REF_DIR) + path.sep;
82
+ if (absPath.startsWith(refDirPrefix)) return true;
83
+ return absPath.endsWith('.workflow-ref.md');
84
+ }
85
+
48
86
  /**
49
87
  * Build a map of all workflow template files to their hash keys, template paths, and hashes.
50
88
  * Hash keys match the format stored in workflow-meta.json (relative to .claude/).
@@ -10,6 +10,7 @@ import {
10
10
  scaffoldPluginJson,
11
11
  scaffoldMemoryDocs,
12
12
  } from './scaffolder.js';
13
+ import { workflowRefRelPath } from './file-categorizer.js';
13
14
  import { promptHookConflict } from '../prompts/conflict-resolution.js';
14
15
  import {
15
16
  detectMissingSections,
@@ -124,10 +125,11 @@ async function mergeSkills(projectRoot, existingScan, variables, report, selecti
124
125
  const existsAsFlat = existingScan.existingSkills.includes(`${skill.name}.md`);
125
126
 
126
127
  if (existsAsDir || existsAsFlat) {
127
- // Tier 2: conflict — save as .workflow-ref.md
128
+ // Tier 2: conflict — save under .claude/workflow-ref/ so the live
129
+ // SKILL.md stays authoritative and the ref cannot shadow it.
128
130
  await scaffoldFile(
129
131
  skill.templatePath,
130
- path.join('.claude', 'skills', skill.name, 'SKILL.workflow-ref.md'),
132
+ workflowRefRelPath(`skills/${skill.name}/SKILL.md`),
131
133
  skill.vars,
132
134
  projectRoot
133
135
  );
@@ -147,7 +149,7 @@ async function mergeSkills(projectRoot, existingScan, variables, report, selecti
147
149
 
148
150
  if (routingExistsAsDir || routingExistsAsFlat) {
149
151
  await writeFile(
150
- path.join(projectRoot, skillsDir, 'agent-routing', 'SKILL.workflow-ref.md'),
152
+ path.join(projectRoot, workflowRefRelPath('skills/agent-routing/SKILL.md')),
151
153
  routingContent
152
154
  );
153
155
  report.conflicts.skills.push('agent-routing');
@@ -166,7 +168,7 @@ async function mergeAgents(projectRoot, existingScan, selectedAgents, report) {
166
168
  if (existingScan.existingAgents.includes(filename)) {
167
169
  await scaffoldFile(
168
170
  `agents/universal/${agent}.md`,
169
- path.join(destDir, `${agent}.workflow-ref.md`),
171
+ workflowRefRelPath(`agents/${filename}`),
170
172
  {},
171
173
  projectRoot
172
174
  );
@@ -191,7 +193,7 @@ async function mergeAgents(projectRoot, existingScan, selectedAgents, report) {
191
193
  if (existingScan.existingAgents.includes(filename)) {
192
194
  await scaffoldFile(
193
195
  `agents/optional/${category}/${agent}.md`,
194
- path.join(destDir, `${agent}.workflow-ref.md`),
196
+ workflowRefRelPath(`agents/${filename}`),
195
197
  {},
196
198
  projectRoot
197
199
  );
@@ -216,7 +218,7 @@ async function mergeCommands(projectRoot, existingScan, report) {
216
218
  if (existingScan.existingCommands.includes(filename)) {
217
219
  await scaffoldFile(
218
220
  `commands/${cmd}.md`,
219
- path.join(destDir, `${cmd}.workflow-ref.md`),
221
+ workflowRefRelPath(`commands/${filename}`),
220
222
  {},
221
223
  projectRoot
222
224
  );
@@ -431,7 +433,12 @@ async function mergeAgentsMd(projectRoot, existingScan, variables, report) {
431
433
  await scaffoldFile('core/agents-md.md', 'AGENTS.md', variables, projectRoot);
432
434
  report.agentsMdHandling = 'created';
433
435
  } else {
434
- await scaffoldFile('core/agents-md.md', 'AGENTS.md.workflow-ref', variables, projectRoot);
436
+ await scaffoldFile(
437
+ 'core/agents-md.md',
438
+ workflowRefRelPath('root/AGENTS.md'),
439
+ variables,
440
+ projectRoot
441
+ );
435
442
  report.agentsMdHandling = 'saved-alongside';
436
443
  }
437
444
  }
@@ -1,7 +1,16 @@
1
1
  import path from 'node:path';
2
2
  import { AGENT_CATALOG } from '../data/agents.js';
3
- import { readFile, writeFile, dirExists, moveFile, listFiles } from '../utils/file.js';
3
+ import {
4
+ fileExists,
5
+ readFile,
6
+ writeFile,
7
+ dirExists,
8
+ moveFile,
9
+ listFiles,
10
+ listFilesRecursive,
11
+ } from '../utils/file.js';
4
12
  import { hashFile } from '../utils/hash.js';
13
+ import { WORKFLOW_REF_DIR } from './file-categorizer.js';
5
14
 
6
15
  // --- Version comparison ---
7
16
 
@@ -142,3 +151,70 @@ export async function patchAgentDescriptions(projectRoot, meta, promptFn) {
142
151
 
143
152
  return report;
144
153
  }
154
+
155
+ // --- Workflow-ref relocation (v2.5.1) ---
156
+
157
+ const LEGACY_ROOT_REFS = ['CLAUDE.md.workflow-ref.md', 'AGENTS.md.workflow-ref'];
158
+
159
+ // Legacy ref files could only live inside these subdirs. Scoping the scan
160
+ // here instead of walking all of .claude/ matters because `sessions/` and
161
+ // `learnings/` can accumulate hundreds of files that will never be refs.
162
+ const LEGACY_REF_SUBDIRS = ['commands', 'agents', 'skills'];
163
+
164
+ function legacyClaudeRefTarget(relKey) {
165
+ // relKey is relative to .claude/, e.g. "commands/sync.workflow-ref.md".
166
+ // Lookahead `(?=\.[^.]+$)` ensures ".workflow-ref" is stripped only when
167
+ // followed by a final extension — so "sync.workflow-ref.md" → "sync.md",
168
+ // but a hypothetical "sync.workflow-ref" (no final ext) wouldn't match.
169
+ const parts = relKey.split('/');
170
+ const name = parts.pop();
171
+ parts.push(name.replace(/\.workflow-ref(?=\.[^.]+$)/, ''));
172
+ return path.join(WORKFLOW_REF_DIR, ...parts);
173
+ }
174
+
175
+ export async function migrateWorkflowRefLocation(projectRoot) {
176
+ const report = { moved: 0, names: [], skipped: [] };
177
+ const claudeDir = path.join(projectRoot, '.claude');
178
+ const refDir = path.join(claudeDir, WORKFLOW_REF_DIR);
179
+
180
+ // 1. Scan only the subdirs where legacy `.workflow-ref.md` siblings could
181
+ // live. Walking all of .claude/ would re-stat hundreds of session files
182
+ // on every upgrade for no benefit.
183
+ for (const subdir of LEGACY_REF_SUBDIRS) {
184
+ const scanRoot = path.join(claudeDir, subdir);
185
+ if (!(await dirExists(scanRoot))) continue;
186
+
187
+ const allFiles = await listFilesRecursive(scanRoot);
188
+ for (const fp of allFiles) {
189
+ const rel = path.relative(claudeDir, fp).split(path.sep).join('/');
190
+ if (!rel.endsWith('.workflow-ref.md')) continue;
191
+
192
+ const target = path.join(claudeDir, legacyClaudeRefTarget(rel));
193
+ if (await fileExists(target)) {
194
+ report.skipped.push(rel);
195
+ continue;
196
+ }
197
+ await moveFile(fp, target);
198
+ report.moved++;
199
+ report.names.push(rel);
200
+ }
201
+ }
202
+
203
+ // 2. Legacy root-level refs (CLAUDE.md.workflow-ref.md, AGENTS.md.workflow-ref).
204
+ for (const legacyName of LEGACY_ROOT_REFS) {
205
+ const src = path.join(projectRoot, legacyName);
206
+ if (!(await fileExists(src))) continue;
207
+
208
+ const original = legacyName.replace(/\.workflow-ref(?:\.md)?$/, '');
209
+ const target = path.join(refDir, original);
210
+ if (await fileExists(target)) {
211
+ report.skipped.push(legacyName);
212
+ continue;
213
+ }
214
+ await moveFile(src, target);
215
+ report.moved++;
216
+ report.names.push(legacyName);
217
+ }
218
+
219
+ return report;
220
+ }
@@ -9,6 +9,7 @@ import {
9
9
  listFilesRecursive,
10
10
  removeDirectory,
11
11
  } from '../utils/file.js';
12
+ import { WORKFLOW_REF_DIR } from './file-categorizer.js';
12
13
 
13
14
  /**
14
15
  * Classify .claude/ files into safe-to-delete, modified, missing, and user-owned.
@@ -55,8 +56,9 @@ export async function classifyClaudeFiles(projectRoot, meta) {
55
56
  const relKey = path.relative(claudeDir, fp).split(path.sep).join('/');
56
57
  if (allTrackedKeys.has(relKey)) continue;
57
58
 
58
- // .workflow-ref.md files are upgrade artifacts — safe to delete
59
- if (relKey.endsWith('.workflow-ref.md')) {
59
+ // Upgrade artifacts — safe to delete: anything under workflow-ref/ (new
60
+ // location) or the legacy `.workflow-ref.md` sibling suffix.
61
+ if (relKey.startsWith(`${WORKFLOW_REF_DIR}/`) || relKey.endsWith('.workflow-ref.md')) {
60
62
  safeToDelete.push(relKey);
61
63
  } else {
62
64
  userOwned.push(relKey);
@@ -115,7 +117,7 @@ export async function removeTrackedFiles(projectRoot, fileKeys) {
115
117
  }
116
118
 
117
119
  // Clean up empty subdirectories
118
- for (const subdir of ['agents', 'commands', 'skills']) {
120
+ for (const subdir of ['agents', 'commands', 'skills', WORKFLOW_REF_DIR]) {
119
121
  const dirPath = path.join(claudeDir, subdir);
120
122
  if (await dirExists(dirPath)) {
121
123
  const remaining = await listFilesRecursive(dirPath);