worclaude 2.4.13 → 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 +47 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/commands/doctor.js +4 -6
- package/src/commands/init.js +9 -6
- package/src/commands/status.js +6 -4
- package/src/commands/upgrade.js +32 -20
- package/src/core/drift-checks.js +2 -1
- package/src/core/file-categorizer.js +38 -0
- package/src/core/merger.js +14 -7
- package/src/core/migration.js +77 -1
- package/src/core/remover.js +5 -3
- package/templates/commands/commit-push-pr.md +24 -1
- package/templates/commands/sync.md +177 -10
- package/templates/skills/universal/git-conventions.md +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ 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
|
+
|
|
36
|
+
## [2.5.0] — 2026-04-21
|
|
37
|
+
|
|
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.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **Per-PR `Version bump:` declaration** (PR #99) — new step 6 in `/commit-push-pr` asks authors to declare `major`/`minor`/`patch`/`none` in the PR body; revert PRs match the bump of the PR being reverted; ambiguous cases ASK the user. The declaration is pasted into both `templates/commands/` (scaffolded into user projects) and `.claude/commands/` (Worclaude's own runtime), kept byte-identical by convention.
|
|
43
|
+
- **`/sync` aggregation rewrite** (PR #99) — replaces the single-step version bump with bootstrap (no-tag → yes/custom/cancel prompt, broadened semver regex accepts pre-release/build metadata, push-failure recovery), aggregation (`gh pr list --limit 500`, `%as` date format to avoid GitHub-search incompatibility with `%ai`, release-PR filter via `headRefName=develop`+`baseRefName=main`, missing-declarations treated as `none`+warning carried through to release PR body and CHANGELOG), ship/wait confirmation (always prompts including for `major`), CHANGELOG append with section defaults mapped from bump type (major/minor → Added, patch → Fixed) and content-driven placement across `### Added`/`### Changed`/`### Fixed`/`### Tests`/`### Docs`.
|
|
44
|
+
- **`tests/templates/version-bump-consistency.test.js`** (PR #99) — new 8-case Vitest asserts `Version bump:` appears literally in all 8 authoritative files (`CLAUDE.md`, both tree copies of `commit-push-pr.md`, `sync.md`, `git-conventions`, and `.github/pull_request_template.md`). Catches rename drift (e.g., `Version bump:` → `Release bump:` in one file without the others). Uses `import.meta.dirname`-derived `REPO_ROOT` matching the `v2-audit` test convention.
|
|
45
|
+
- **`.github/pull_request_template.md`** — required `Version bump:` field with HTML-comment placeholder.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **CLAUDE.md Rule #13** reworded: "every merge to `main` gets at least a patch bump" → "every merge to `main` IS a release." Internal-only `none`-only batches now update shared-state files on develop but never reach `main`.
|
|
50
|
+
- **`.claude/skills/git-conventions/SKILL.md`** — fixed two pre-existing statements that contradicted the new policy: line 117 "every merge to main gets at least a patch bump" replaced with release-carries-bump wording; line 124 table row for docs/CI/tests changed from `patch` to `none (no release)`. `templates/skills/universal/git-conventions.md` already said "no bump" so no contradiction-fix was needed there; both files now have the new `### Per-PR bump declarations` + `### Edge cases` subsections appended identically. Unrelated wording divergence between the two trees left alone.
|
|
51
|
+
- **`README.md`** — short "Batched releases" bullet in the "Why Worclaude" section.
|
|
52
|
+
- ⚠ **PR #98 (`fix(upstream): advance state on SKIP_ISSUE`)** — no `Version bump:` declaration (merged 2026-04-21 14:58 UTC, before PR #99 introduced the workflow). Treated as `none` with warning per bootstrap handling. Under-documentation made visible here rather than silently lost. The fix itself advances `.github/upstream-state.json` whenever Claude returns `SKIP_ISSUE` so no-new-item runs don't re-evaluate the same feed indefinitely.
|
|
53
|
+
|
|
7
54
|
## [2.4.12] — 2026-04-20
|
|
8
55
|
|
|
9
56
|
Internal fix release — first post-v2.4.11 `upstream-check` run on `main` (workflow run 24693290867) failed with `error_max_turns / num_turns: 16` at the Claude cross-reference step. Not a parser issue: the v2.4.11 format-drift fix still works; Claude simply couldn't fit the workload into 15 turns. The prompt requires ~9 `Read` calls (feed inputs + `.claude/commands/upstream-check.md` + cross-reference against agents/commands/hooks templates, `src/data/agents.js`, `src/data/agent-registry.js`, `docs/spec/BACKLOG-v2.1.md`, `CLAUDE.md`) before the final response — each Read burns a turn, so 15 was tight by luck, not by design.
|
package/README.md
CHANGED
|
@@ -144,6 +144,7 @@ See the [full command reference](https://sefaertunc.github.io/Worclaude/referenc
|
|
|
144
144
|
- **Hook profiles.** Dial strictness up or down via one environment variable. `minimal` for CI, `standard` for daily work, `strict` for type-heavy projects.
|
|
145
145
|
- **Smart merge.** Detects existing Claude Code setups and merges additively — existing files never overwritten without confirmation. Three-tier strategy: additive for missing content, safe-alongside for conflicts, interactive for CLAUDE.md.
|
|
146
146
|
- **Self-healing doctor.** Catches drift, stale hashes, deprecated models, broken learnings — before they bite.
|
|
147
|
+
- **Batched releases.** Every PR declares `Version bump: {major|minor|patch|none}` in its body; `/sync` aggregates declarations across merged PRs and only cuts a release when at least one rises above `none`. Internal-only work (docs, CI, tests) accumulates on `develop` without triggering noisy publishes.
|
|
147
148
|
|
|
148
149
|
---
|
|
149
150
|
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
832
|
-
|
|
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
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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(
|
|
552
|
+
display.barLine(
|
|
553
|
+
`${display.yellow('~')} Conflicts (template copy saved under .claude/workflow-ref/):`
|
|
554
|
+
);
|
|
553
555
|
for (const file of allConflicts) {
|
|
554
|
-
|
|
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(
|
|
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
|
|
617
|
+
` ${display.white('3.')} Delete .claude/workflow-ref/ and .workflow-suggestions when done`
|
|
615
618
|
);
|
|
616
619
|
}
|
|
617
620
|
console.log(
|
package/src/commands/status.js
CHANGED
|
@@ -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
|
-
|
|
110
|
-
pendingReview.push(`.claude/${rel}`);
|
|
111
|
-
}
|
|
113
|
+
pendingReview.push(`.claude/${rel}`);
|
|
112
114
|
}
|
|
113
115
|
} catch {
|
|
114
116
|
// .claude dir might not exist
|
package/src/commands/upgrade.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
61
|
-
const sidecarPath =
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.');
|
package/src/core/drift-checks.js
CHANGED
|
@@ -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 =
|
|
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/).
|
package/src/core/merger.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
package/src/core/migration.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { AGENT_CATALOG } from '../data/agents.js';
|
|
3
|
-
import {
|
|
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
|
+
}
|
package/src/core/remover.js
CHANGED
|
@@ -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
|
-
//
|
|
59
|
-
|
|
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);
|
|
@@ -45,12 +45,35 @@ files (see git-conventions.md for the canonical list).
|
|
|
45
45
|
3. Write a clear, conventional commit message
|
|
46
46
|
4. Push to the current branch
|
|
47
47
|
5. Create a PR targeting develop: gh pr create --base develop
|
|
48
|
-
6.
|
|
48
|
+
6. Determine the version bump level for this PR. Read the Versioning Policy
|
|
49
|
+
in the project's git-conventions document to decide: `major`, `minor`,
|
|
50
|
+
`patch`, or `none`.
|
|
51
|
+
- `major` — breaking change to public API, CLI, or scaffold contract
|
|
52
|
+
- `minor` — new feature, command, agent, or flag
|
|
53
|
+
- `patch` — bug fix or user-visible behavior change with no new surface
|
|
54
|
+
- `none` — docs, CI, tests, internal refactor (nothing consumers notice)
|
|
55
|
+
|
|
56
|
+
For revert PRs: declare the same bump level as the PR being reverted.
|
|
57
|
+
|
|
58
|
+
If the change is ambiguous, ASK THE USER. Do not guess.
|
|
59
|
+
|
|
60
|
+
The PR description MUST include this line on its own, verbatim:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Version bump: {major|minor|patch|none}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`/sync` parses this string exactly — other phrasings will be ignored.
|
|
67
|
+
7. Include in PR description: title, changes, testing done, reviewer notes
|
|
49
68
|
|
|
50
69
|
## On develop
|
|
51
70
|
|
|
52
71
|
Only used for release merges after /sync has been run.
|
|
53
72
|
|
|
73
|
+
Versioning happens in `/sync`, not here. The release PR body is pre-written
|
|
74
|
+
by `/sync` with the aggregated bump summary and the list of feature PRs
|
|
75
|
+
included in the release.
|
|
76
|
+
|
|
54
77
|
1. Write a session summary to .claude/sessions/:
|
|
55
78
|
- Filename: YYYY-MM-DD-HHMM-{short-branch-name}.md
|
|
56
79
|
- Same format as the feature branch session summary above
|
|
@@ -35,23 +35,190 @@ and tell the user to run /conflict-resolver first.
|
|
|
35
35
|
docs/spec/SPEC.md to reflect the current state.
|
|
36
36
|
If nothing changed spec-wise, leave it alone.
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Bootstrap: ensure a version tag exists
|
|
39
39
|
|
|
40
|
-
6.
|
|
41
|
-
|
|
40
|
+
6. Check for a version tag:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If no tag exists, prompt the user. Do NOT silently auto-tag:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
No version tag found. /sync needs a starting tag to compute release scope.
|
|
50
|
+
|
|
51
|
+
Proposed: tag current HEAD as v0.1.0
|
|
52
|
+
|
|
53
|
+
- yes → create v0.1.0 tag at HEAD and continue
|
|
54
|
+
- custom → specify your own starting version (e.g., v1.0.0 if this
|
|
55
|
+
project had releases before adopting this workflow)
|
|
56
|
+
- cancel → stop; tag manually before re-running
|
|
57
|
+
|
|
58
|
+
Choose (yes / custom / cancel):
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
On "yes": `git tag v0.1.0 && git push origin v0.1.0`, then set
|
|
62
|
+
`LAST_TAG="v0.1.0"`.
|
|
63
|
+
|
|
64
|
+
On "custom": prompt for the version string. Accept any string matching
|
|
65
|
+
`^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$` (semver with
|
|
66
|
+
optional pre-release and build metadata, e.g. `v1.0.0-rc.1`, `v2.0.0+build.5`).
|
|
67
|
+
Reject non-matching input with a clear message and re-prompt. Then tag
|
|
68
|
+
and push as above.
|
|
69
|
+
|
|
70
|
+
On "cancel": exit cleanly with "Tag the current state manually before
|
|
71
|
+
re-running /sync."
|
|
72
|
+
|
|
73
|
+
If `git push origin {tag}` fails (fork clone, expired credentials,
|
|
74
|
+
network): the local tag is already created and valid. Report:
|
|
75
|
+
"Local tag created but push failed — retry with `git push origin {tag}`
|
|
76
|
+
manually before opening a release PR." Then exit cleanly.
|
|
77
|
+
|
|
78
|
+
After bootstrap, proceed to step 7. Expect "Nothing publishable since
|
|
79
|
+
{tag}" if no PRs have been merged yet — that is correct behavior for
|
|
80
|
+
a first-release bootstrap, not an error.
|
|
81
|
+
|
|
82
|
+
## Aggregate version bumps from merged PRs
|
|
83
|
+
|
|
84
|
+
7. Collect `Version bump:` declarations from all PRs merged into develop
|
|
85
|
+
since the last version tag. Use `%as` for the date format (strict
|
|
86
|
+
YYYY-MM-DD; `%ai` breaks GitHub search due to space separator and
|
|
87
|
+
timezone offset). Pass `--limit 500` to avoid the `gh pr list` default
|
|
88
|
+
cap of 30, which would silently truncate on repos with infrequent
|
|
89
|
+
tagging:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
SINCE=$(git log -1 --format=%as "$LAST_TAG")
|
|
93
|
+
gh pr list --state merged --base develop \
|
|
94
|
+
--limit 500 \
|
|
95
|
+
--search "merged:>=$SINCE" \
|
|
96
|
+
--json number,title,body,headRefName,baseRefName
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Filter out release PRs — any PR with `headRefName: develop` AND
|
|
100
|
+
`baseRefName: main` is a release PR from a prior `/sync` run, not an
|
|
101
|
+
input. Skip those.
|
|
102
|
+
|
|
103
|
+
For each remaining PR, extract the `Version bump:` line from the body.
|
|
104
|
+
Valid values: `major`, `minor`, `patch`, `none`.
|
|
105
|
+
|
|
106
|
+
Missing declarations: treat as `none` and add to a warning list
|
|
107
|
+
(carried through to step 9's summary and step 10's CHANGELOG entry).
|
|
108
|
+
Do NOT guess a higher value. Do NOT stop.
|
|
109
|
+
|
|
110
|
+
8. Compute the release bump using precedence: `major > minor > patch > none`.
|
|
111
|
+
- If the highest is `none`: update PROGRESS.md and SPEC.md if needed,
|
|
112
|
+
commit those, push, and stop. Do NOT bump the version. Do NOT open
|
|
113
|
+
a PR to main. Report:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Nothing publishable since {last-tag}. Shared state updated.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- If the highest is `patch`, `minor`, or `major`: proceed to step 9.
|
|
120
|
+
|
|
121
|
+
9. Summarize the release group for the user and ask for confirmation.
|
|
122
|
+
Always prompt, including for `major` — that's exactly when the human
|
|
123
|
+
sanity-check is most valuable.
|
|
124
|
+
|
|
125
|
+
If the warnings list from step 7 is empty, omit the warnings section
|
|
126
|
+
entirely — do NOT render an empty block or a "no warnings" line.
|
|
127
|
+
|
|
128
|
+
Summary format (warnings section included only when warnings exist):
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Release group since {last-tag}:
|
|
132
|
+
- #{num} {title} — {bump}
|
|
133
|
+
- #{num} {title} — {bump}
|
|
134
|
+
- ...
|
|
135
|
+
|
|
136
|
+
⚠ PRs without Version bump: declaration (treated as none):
|
|
137
|
+
- #{num} {title}
|
|
138
|
+
- ...
|
|
139
|
+
|
|
140
|
+
Proposed version: {old} → {new} ({highest-bump})
|
|
141
|
+
|
|
142
|
+
Ship now, or wait for more work to land? (ship/wait)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
If the user says "wait", stop without bumping. They will re-run `/sync`
|
|
146
|
+
later when more PRs have merged.
|
|
147
|
+
|
|
148
|
+
10. On "ship":
|
|
149
|
+
|
|
150
|
+
a. Update the `version` field in `package.json` to the new version.
|
|
151
|
+
|
|
152
|
+
b. Append to `CHANGELOG.md` at the top of the entries list (after the
|
|
153
|
+
`# Changelog` header and the `## [Unreleased]` marker if present).
|
|
154
|
+
If `CHANGELOG.md` does not exist, create it with this header:
|
|
155
|
+
|
|
156
|
+
```markdown
|
|
157
|
+
# Changelog
|
|
158
|
+
|
|
159
|
+
All notable changes to this project are documented in this file.
|
|
160
|
+
Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
161
|
+
versions follow [semver](https://semver.org/).
|
|
162
|
+
|
|
163
|
+
## [Unreleased]
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The new entry format:
|
|
167
|
+
|
|
168
|
+
```markdown
|
|
169
|
+
## [{new-version}] — {YYYY-MM-DD}
|
|
170
|
+
|
|
171
|
+
{one-paragraph prose summary of what this release does — synthesize
|
|
172
|
+
from the merged PR titles and bodies; no bullet list in the summary}
|
|
173
|
+
|
|
174
|
+
### {Section}
|
|
175
|
+
|
|
176
|
+
- {bullet per PR mapped to its section, including PR number}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Section mapping — the declared bump determines the MINIMUM section,
|
|
180
|
+
but each PR's content governs its actual placement. PRs may be filed
|
|
181
|
+
under any of `### Added` / `### Changed` / `### Fixed` / `### Tests` /
|
|
182
|
+
`### Docs` as content warrants. Do not force a mapping that fights
|
|
183
|
+
the content.
|
|
184
|
+
|
|
185
|
+
Defaults when content is ambiguous:
|
|
186
|
+
- `major`, `minor` → default `### Added` (or `### Changed` for
|
|
187
|
+
breaking changes)
|
|
188
|
+
- `patch` → default `### Fixed` (but `### Added` is fine for
|
|
189
|
+
patch-level additive surface like new optional flags)
|
|
190
|
+
- `none` with warning → `### Changed` with ⚠ prefix noting
|
|
191
|
+
"no Version bump declaration — under-documented"
|
|
192
|
+
|
|
193
|
+
If a release mixes levels, each PR goes under its own section.
|
|
194
|
+
Multiple sections per release entry are standard.
|
|
195
|
+
|
|
196
|
+
The warning list from step 9 MUST appear in the CHANGELOG entry as
|
|
197
|
+
⚠-prefixed bullets under `### Changed` — not just in the transient
|
|
198
|
+
prompt. This is how under-documentation becomes permanent record.
|
|
42
199
|
|
|
43
200
|
## Verify
|
|
44
201
|
|
|
45
|
-
|
|
46
|
-
|
|
202
|
+
11. Run /verify to confirm tests and lint pass.
|
|
203
|
+
If anything fails, fix it before proceeding.
|
|
47
204
|
|
|
48
205
|
## Commit, push, and PR
|
|
49
206
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
207
|
+
12. git add -A
|
|
208
|
+
13. git commit -m "chore: sync progress, spec, and version to [new version]"
|
|
209
|
+
Use exactly this message format — no trailers or Co-Authored-By lines.
|
|
210
|
+
14. git push origin develop
|
|
211
|
+
15. Create the PR to main. Use the release group summary from step 9 as the
|
|
212
|
+
PR body verbatim (including any warnings block) — that becomes the
|
|
213
|
+
release notes:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
gh pr create --base main --title "release: v{new-version}" --body "{step-9-summary}"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The maintainer then manually creates a GitHub Release against main
|
|
220
|
+
with tag vX.Y.Z — that triggers the release.yml workflow which
|
|
221
|
+
publishes to npm with provenance. /sync does not publish.
|
|
55
222
|
|
|
56
223
|
## Trigger Phrases
|
|
57
224
|
- "sync progress"
|
|
@@ -135,6 +135,55 @@ Follow [semver](https://semver.org/) when the project publishes releases:
|
|
|
135
135
|
|
|
136
136
|
**Rule of thumb:** If the change affects what users see, install, or depend on, it needs a version bump. If it only affects the project's internal development workflow, it does not.
|
|
137
137
|
|
|
138
|
+
### Per-PR bump declarations
|
|
139
|
+
|
|
140
|
+
Every PR targeting `develop` declares its intended version bump in the PR
|
|
141
|
+
description:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
Version bump: {major|minor|patch|none}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- `major` — breaking change to public API, CLI, or scaffold contract
|
|
148
|
+
- `minor` — new feature, command, agent, or flag
|
|
149
|
+
- `patch` — bug fix or user-visible behavior change with no new surface
|
|
150
|
+
- `none` — docs, CI, tests, internal refactor (nothing consumers notice)
|
|
151
|
+
|
|
152
|
+
`/sync` aggregates declarations from all PRs merged since the last version
|
|
153
|
+
tag and picks the highest: `major > minor > patch > none`. If all merged
|
|
154
|
+
PRs are `none`, no release is cut — `/sync` updates shared-state files but
|
|
155
|
+
does not bump the version or open a PR to `main`.
|
|
156
|
+
|
|
157
|
+
This is how release batching works: internal-only work accumulates on
|
|
158
|
+
develop without triggering publishes, and multiple user-facing PRs group
|
|
159
|
+
into a single well-documented release.
|
|
160
|
+
|
|
161
|
+
### Edge cases
|
|
162
|
+
|
|
163
|
+
- **Release PRs (develop → main):** `/sync` excludes these from scanning.
|
|
164
|
+
They ARE the release, not an input to one. Known limitation: any
|
|
165
|
+
develop→main PR is excluded regardless of content. Safe in practice
|
|
166
|
+
because that direction is only used for releases.
|
|
167
|
+
- **Revert PRs:** Declare the same bump level as the PR being reverted.
|
|
168
|
+
Reverting a `patch` is itself a `patch`. A `patch` + matching revert
|
|
169
|
+
in the same release group produces a net-zero user-visible change but
|
|
170
|
+
still bumps the version — something shipped internally and was
|
|
171
|
+
un-shipped, which is part of the release's history. Do not cancel
|
|
172
|
+
them out.
|
|
173
|
+
- **Missing declarations:** Treated as `none` with a warning that
|
|
174
|
+
propagates to the release PR body and CHANGELOG entry. Do not manually
|
|
175
|
+
backfill old PR bodies — let them flow through as `none` so
|
|
176
|
+
under-documentation is visible rather than silent.
|
|
177
|
+
- **No tags yet (first release):** `/sync` prompts to create a starting
|
|
178
|
+
tag (default `v0.1.0`, customizable, cancelable). The tool does NOT
|
|
179
|
+
silently invent a tag.
|
|
180
|
+
- **Hotfix merged directly to main:** Bypasses `/sync` entirely. Requires
|
|
181
|
+
manual version management. Known limitation; out of scope.
|
|
182
|
+
|
|
183
|
+
**Optional:** GitHub labels (`release:major`, `release:minor`, etc.) can
|
|
184
|
+
replace body-text declarations for maintainers who prefer them. Deferred
|
|
185
|
+
to a future phase; not scaffolded by default.
|
|
186
|
+
|
|
138
187
|
## Gotchas
|
|
139
188
|
|
|
140
189
|
- Never force-push to main/master. Force-push to feature branches only when you
|